diff --git a/configuration/config.go b/configuration/config.go index db5db54..0dfeece 100644 --- a/configuration/config.go +++ b/configuration/config.go @@ -39,56 +39,70 @@ func InitConfig() error { } var ( - dbHost = flag.String("db-host", "/var/run/postgresql", "Host of the PostgreSQL database (default is /var/run/postgresql for localhost DB on Ubuntu systems)") - dbName = flag.String("db-name", "villasdb", "Name of the database to use (default is villasdb)") - dbUser = flag.String("db-user", "", "Username of database connection (default is )") - dbPass = flag.String("db-pass", "", "Password of database connection (default is )") - dbSSLMode = flag.String("db-ssl-mode", "disable", "SSL mode of DB (default is disable)") // TODO: change default for production - amqpHost = flag.String("amqp-host", "", "If set, use this as host for AMQP broker (default is disabled)") - amqpUser = flag.String("amqp-user", "", "Username for AMQP broker") - amqpPass = flag.String("amqp-pass", "", "Password for AMQP broker") - configFile = flag.String("config", "", "Path to YAML configuration file") - mode = flag.String("mode", "release", "Select debug/release/test mode (default is release)") - port = flag.String("port", "4000", "Port of the backend (default is 4000)") - baseHost = flag.String("base-host", "localhost:4000", "The host at which the backend is hosted (default: localhost)") - basePath = flag.String("base-path", "/api/v2", "The path at which the API routes are located (default /api/v2)") - adminUser = flag.String("admin-user", "", "Initial admin username") - adminPass = flag.String("admin-pass", "", "Initial admin password") - adminMail = flag.String("admin-mail", "", "Initial admin mail address") - s3Bucket = flag.String("s3-bucket", "", "S3 Bucket for uploading files") - s3Endpoint = flag.String("s3-endpoint", "", "Endpoint of S3 API for file uploads") - s3Region = flag.String("s3-region", "default", "S3 Region for file uploads") - s3NoSSL = flag.Bool("s3-nossl", false, "Use encrypted connections to the S3 API") - s3PathStyle = flag.Bool("s3-pathstyle", false, "Use path-style S3 API") - jwtSecret = flag.String("jwt-secret", "This should NOT be here!!@33$8&", "The JSON Web Token secret") - jwtExpiresAfter = flag.String("jwt-expires-after", "168h" /* 1 week */, "The time after which the JSON Web Token expires") - testDataPath = flag.String("test-data-path", "database/testdata.json", "The path to the test data json file (used in test mode)") - authExternal = flag.Bool("auth.external", false, "Use external authentication via X-Forwarded-User header (e.g. OAuth2 Proxy)") + dbHost = flag.String("db-host", "/var/run/postgresql", "Host of the PostgreSQL database (default is /var/run/postgresql for localhost DB on Ubuntu systems)") + dbName = flag.String("db-name", "villasdb", "Name of the database to use (default is villasdb)") + dbUser = flag.String("db-user", "", "Username of database connection (default is )") + dbPass = flag.String("db-pass", "", "Password of database connection (default is )") + dbSSLMode = flag.String("db-ssl-mode", "disable", "SSL mode of DB (default is disable)") // TODO: change default for production + amqpHost = flag.String("amqp-host", "", "If set, use this as host for AMQP broker (default is disabled)") + amqpUser = flag.String("amqp-user", "", "Username for AMQP broker") + amqpPass = flag.String("amqp-pass", "", "Password for AMQP broker") + configFile = flag.String("config", "", "Path to YAML configuration file") + mode = flag.String("mode", "release", "Select debug/release/test mode (default is release)") + port = flag.String("port", "4000", "Port of the backend (default is 4000)") + baseHost = flag.String("base-host", "localhost:4000", "The host at which the backend is hosted (default: localhost)") + basePath = flag.String("base-path", "/api/v2", "The path at which the API routes are located (default /api/v2)") + adminUser = flag.String("admin-user", "", "Initial admin username") + adminPass = flag.String("admin-pass", "", "Initial admin password") + adminMail = flag.String("admin-mail", "", "Initial admin mail address") + s3Bucket = flag.String("s3-bucket", "", "S3 Bucket for uploading files") + s3Endpoint = flag.String("s3-endpoint", "", "Endpoint of S3 API for file uploads") + s3Region = flag.String("s3-region", "default", "S3 Region for file uploads") + s3NoSSL = flag.Bool("s3-nossl", false, "Use encrypted connections to the S3 API") + s3PathStyle = flag.Bool("s3-pathstyle", false, "Use path-style S3 API") + jwtSecret = flag.String("jwt-secret", "This should NOT be here!!@33$8&", "The JSON Web Token secret") + jwtExpiresAfter = flag.String("jwt-expires-after", "168h" /* 1 week */, "The time after which the JSON Web Token expires") + authExternal = flag.Bool("auth-external", false, "Use external authentication via X-Forwarded-User header (e.g. OAuth2 Proxy)") + authExternalAuthorizeURL = flag.String("authexternal-authorize-url", "/oauth2/start", "A URL to initiate external login procedure") + authExternalProviderName = flag.String("auth-external-provider-name", "JupyterHub", "A name of the external authentication provider") + authLogoutURL = flag.String("auth-logout-url", "/oauth2/sign_out?rd=https%3A%2F%2Fjupyter.k8s.eonerc.rwth-aachen.de%2Fhub%2Flogout", "The URL to redirect the user to log out") + title = flag.String("title", "VILLASweb", "Title shown in the frontend") + subTitle = flag.String("sub-title", "", "Sub-title shown in the frontend") + contactName = flag.String("contact-name", "Steffen Vogel", "Name of the administrative contact") + contactMail = flag.String("contact-mail", "svogel2@eonerc.rwth-aachen.de", "EMail of the administrative contact") + testDataPath = flag.String("test-data-path", "database/testdata.json", "The path to the test data json file (used in test mode)") ) flag.Parse() static := map[string]string{ - "db.host": *dbHost, - "db.name": *dbName, - "db.user": *dbUser, - "db.pass": *dbPass, - "db.ssl": *dbSSLMode, - "amqp.host": *amqpHost, - "amqp.user": *amqpUser, - "amqp.pass": *amqpPass, - "mode": *mode, - "port": *port, - "base.host": *baseHost, - "base.path": *basePath, - "admin.user": *adminUser, - "admin.pass": *adminPass, - "admin.mail": *adminMail, - "s3.bucket": *s3Bucket, - "s3.endpoint": *s3Endpoint, - "s3.region": *s3Region, - "jwt.secret": *jwtSecret, - "jwt.expires-after": *jwtExpiresAfter, - "test.datapath": *testDataPath, + "db.host": *dbHost, + "db.name": *dbName, + "db.user": *dbUser, + "db.pass": *dbPass, + "db.ssl": *dbSSLMode, + "amqp.host": *amqpHost, + "amqp.user": *amqpUser, + "amqp.pass": *amqpPass, + "mode": *mode, + "port": *port, + "base.host": *baseHost, + "base.path": *basePath, + "admin.user": *adminUser, + "admin.pass": *adminPass, + "admin.mail": *adminMail, + "s3.bucket": *s3Bucket, + "s3.endpoint": *s3Endpoint, + "s3.region": *s3Region, + "jwt.secret": *jwtSecret, + "jwt.expires-after": *jwtExpiresAfter, + "auth.external.authorize-url": *authExternalAuthorizeURL, + "auth.external.provider-name": *authExternalProviderName, + "auth.logout-url": *authLogoutURL, + "title": *title, + "sub-title": *subTitle, + "contact.name": *contactName, + "contact.mail": *contactMail, + "test.datapath": *testDataPath, } if *s3NoSSL == true { @@ -104,16 +118,16 @@ func InitConfig() error { } if *authExternal == true { - static["auth.external"] = "true" + static["auth.external.enabled"] = "true" } else { - static["auth.external"] = "false" + static["auth.external.enabled"] = "false" } mappings := map[string]string{} - for name, value := range static { - envName := strings.Replace(name, ".", "_") - envName := strings.Replace(envName, "-", "_") - envName := strings.ToUpper(envName) + for name := range static { + envName := strings.ReplaceAll(name, ".", "_") + envName = strings.ReplaceAll(envName, "-", "_") + envName = strings.ToUpper(envName) mappings[envName] = name } diff --git a/routes/config/config_endpoint.go b/routes/config/config_endpoint.go index 7186fdb..99370b2 100644 --- a/routes/config/config_endpoint.go +++ b/routes/config/config_endpoint.go @@ -39,7 +39,6 @@ type ConfigAuthenticationExternal struct { type ConfigAuthentication struct { External ConfigAuthenticationExternal `json:"external"` - LoginURL string `json:"login_url"` LogoutURL string `json:"logout_url"` } @@ -76,8 +75,8 @@ func getConfig(c *gin.Context) { resp.BasePath, _ = cfg.String("base.path") resp.Authentication.LogoutURL, _ = cfg.String("auth.logout-url") resp.Authentication.External.Enabled, _ = cfg.Bool("auth.external") - resp.Authentication.External.AuthorizeURL, _ = cfg.String("auth.external-authorize-url") - resp.Authentication.External.ProviderName, _ = cfg.String("auth.external-provider-name") + resp.Authentication.External.AuthorizeURL, _ = cfg.String("auth.external.authorize-url") + resp.Authentication.External.ProviderName, _ = cfg.String("auth.external.provider-name") resp.Title, _ = cfg.String("title") resp.SubTitle, _ = cfg.String("sub-title") resp.Contact.Name, _ = cfg.String("contact.name") diff --git a/routes/user/authenticate_endpoint.go b/routes/user/authenticate_endpoint.go index 0d579df..d8a4fc3 100644 --- a/routes/user/authenticate_endpoint.go +++ b/routes/user/authenticate_endpoint.go @@ -41,7 +41,7 @@ type tokenClaims struct { func RegisterAuthenticate(r *gin.RouterGroup) { r.GET("", authenticated) - r.POST("", authenticate) + r.POST("/:mechanism", authenticate) } // authenticated godoc @@ -105,48 +105,57 @@ func authenticated(c *gin.Context) { // @Produce json // @Tags authentication // @Param inputUser body user.loginRequest true "loginRequest of user" +// @Param mechanism path string true "Login mechanism" Enums(internal, external) // @Success 200 {object} api.ResponseAuthenticate "JSON web token, success status, message and authenticated user object" // @Failure 401 {object} api.ResponseError "Unauthorized" // @Failure 500 {object} api.ResponseError "Internal server error." -// @Router /authenticate [post] +// @Router /authenticate{mechanism} [post] func authenticate(c *gin.Context) { - var user *User + var user *User = nil - authExternal, err := configuration.GlobalConfig.Bool("auth.external") - if err != nil { - helper.UnauthorizedError(c, "Backend configuration error") - return - } - - if err != nil || !authExternal { - user = authenticateStandard(c) - } else { - user = authenticateExternal(c) + switch c.Param("mechanism") { + case "internal": + user = authenticateInternal(c) + case "external": + authExternal, err := configuration.GlobalConfig.Bool("auth.external") + if err == nil && authExternal { + user = authenticateExternal(c) + } else { + helper.BadRequestError(c, "External authentication is not activated") + } + default: + helper.BadRequestError(c, "Invalid authentication mechanism") } if user == nil { return } + // Check if this is an active user + if !user.Active { + helper.UnauthorizedError(c, "User is not active") + return + } + expiresStr, err := configuration.GlobalConfig.String("jwt.expires-after") if err != nil { - helper.UnauthorizedError(c, "Backend configuration error") + helper.InternalServerError(c, "Invalid backend configuration: jwt.expires-after") return } expiresDuration, err := time.ParseDuration(expiresStr) if err != nil { - helper.UnauthorizedError(c, "Backend configuration error") + helper.InternalServerError(c, "Invalid backend configuration: jwt.expires-after") return } secret, err := configuration.GlobalConfig.String("jwt.secret") if err != nil { - helper.UnauthorizedError(c, "Backend configuration error") + helper.InternalServerError(c, "Invalid backend configuration: jwt.secret") return } - // create authentication token + // Create authentication token claims := tokenClaims{ user.ID, user.Role, @@ -161,7 +170,7 @@ func authenticate(c *gin.Context) { tokenString, err := token.SignedString([]byte(secret)) if err != nil { - helper.InternalServerError(c, err.Error()) + helper.InternalServerError(c, "Invalid backend configuration: jwt.secret") return } @@ -173,7 +182,7 @@ func authenticate(c *gin.Context) { }) } -func authenticateStandard(c *gin.Context) *User { +func authenticateInternal(c *gin.Context) *User { // Bind the response (context) with the loginRequest struct var credentials loginRequest if err := c.ShouldBindJSON(&credentials); err != nil { @@ -195,12 +204,6 @@ func authenticateStandard(c *gin.Context) *User { return nil } - // Check if this is an active user - if !user.Active { - helper.UnauthorizedError(c, "User is not active") - return nil - } - // Validate the password err = user.validatePassword(credentials.Password) if err != nil {