From 87ce4f63d48a331cf44ea1450224967363a48d9c Mon Sep 17 00:00:00 2001 From: Jan Eitzinger Date: Thu, 17 Aug 2023 10:29:00 +0200 Subject: [PATCH] Refactor auth module Separate parts Add user repository Add user schema --- cmd/cc-backend/main.go | 23 +- internal/api/rest.go | 97 +++-- internal/auth/auth.go | 73 +--- internal/auth/jwt.go | 18 +- internal/auth/jwtCookieSession.go | 20 +- internal/auth/jwtSession.go | 22 +- internal/auth/ldap.go | 12 +- internal/auth/local.go | 9 +- internal/auth/users.go | 289 ------------- internal/graph/schema.resolvers.go | 15 +- internal/repository/job.go | 11 +- internal/repository/query.go | 9 +- internal/repository/stats.go | 3 +- internal/repository/tags.go | 7 +- internal/repository/user.go | 392 +++++++++++++----- internal/repository/userConfig.go | 137 ++++++ .../{user_test.go => userConfig_test.go} | 4 +- internal/routerConfig/routes.go | 35 +- pkg/schema/config.go | 11 +- internal/auth/roles.go => pkg/schema/user.go | 41 +- .../auth_test.go => pkg/schema/user_test.go | 2 +- web/web.go | 7 +- 22 files changed, 637 insertions(+), 600 deletions(-) delete mode 100644 internal/auth/users.go create mode 100644 internal/repository/userConfig.go rename internal/repository/{user_test.go => userConfig_test.go} (94%) rename internal/auth/roles.go => pkg/schema/user.go (82%) rename internal/auth/auth_test.go => pkg/schema/user_test.go (99%) diff --git a/cmd/cc-backend/main.go b/cmd/cc-backend/main.go index 8164792..60c5edd 100644 --- a/cmd/cc-backend/main.go +++ b/cmd/cc-backend/main.go @@ -228,14 +228,16 @@ func main() { log.Fatal("invalid argument format for user creation") } - if err := authentication.AddUser(&auth.User{ + ur := repository.GetUserRepository() + if err := ur.AddUser(&schema.User{ Username: parts[0], Projects: make([]string, 0), Password: parts[2], Roles: strings.Split(parts[1], ","), }); err != nil { log.Fatalf("adding '%s' user authentication failed: %v", parts[0], err) } } if flagDelUser != "" { - if err := authentication.DelUser(flagDelUser); err != nil { + ur := repository.GetUserRepository() + if err := ur.DelUser(flagDelUser); err != nil { log.Fatalf("deleting user failed: %v", err) } } @@ -252,12 +254,13 @@ func main() { } if flagGenJWT != "" { - user, err := authentication.GetUser(flagGenJWT) + ur := repository.GetUserRepository() + user, err := ur.GetUser(flagGenJWT) if err != nil { log.Fatalf("could not get user from JWT: %v", err) } - if !user.HasRole(auth.RoleApi) { + if !user.HasRole(schema.RoleApi) { log.Warnf("user '%s' does not have the API role", user.Username) } @@ -327,15 +330,15 @@ func main() { r.HandleFunc("/login", func(rw http.ResponseWriter, r *http.Request) { rw.Header().Add("Content-Type", "text/html; charset=utf-8") - web.RenderTemplate(rw, r, "login.tmpl", &web.Page{Title: "Login", Build: buildInfo}) + web.RenderTemplate(rw, "login.tmpl", &web.Page{Title: "Login", Build: buildInfo}) }).Methods(http.MethodGet) r.HandleFunc("/imprint", func(rw http.ResponseWriter, r *http.Request) { rw.Header().Add("Content-Type", "text/html; charset=utf-8") - web.RenderTemplate(rw, r, "imprint.tmpl", &web.Page{Title: "Imprint", Build: buildInfo}) + web.RenderTemplate(rw, "imprint.tmpl", &web.Page{Title: "Imprint", Build: buildInfo}) }) r.HandleFunc("/privacy", func(rw http.ResponseWriter, r *http.Request) { rw.Header().Add("Content-Type", "text/html; charset=utf-8") - web.RenderTemplate(rw, r, "privacy.tmpl", &web.Page{Title: "Privacy", Build: buildInfo}) + web.RenderTemplate(rw, "privacy.tmpl", &web.Page{Title: "Privacy", Build: buildInfo}) }) // Some routes, such as /login or /query, should only be accessible to a user that is logged in. @@ -351,7 +354,7 @@ func main() { func(rw http.ResponseWriter, r *http.Request, err error) { rw.Header().Add("Content-Type", "text/html; charset=utf-8") rw.WriteHeader(http.StatusUnauthorized) - web.RenderTemplate(rw, r, "login.tmpl", &web.Page{ + web.RenderTemplate(rw, "login.tmpl", &web.Page{ Title: "Login failed - ClusterCockpit", MsgType: "alert-warning", Message: err.Error(), @@ -362,7 +365,7 @@ func main() { r.Handle("/logout", authentication.Logout(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { rw.Header().Add("Content-Type", "text/html; charset=utf-8") rw.WriteHeader(http.StatusOK) - web.RenderTemplate(rw, r, "login.tmpl", &web.Page{ + web.RenderTemplate(rw, "login.tmpl", &web.Page{ Title: "Bye - ClusterCockpit", MsgType: "alert-info", Message: "Logout successful", @@ -378,7 +381,7 @@ func main() { // On failure: func(rw http.ResponseWriter, r *http.Request, err error) { rw.WriteHeader(http.StatusUnauthorized) - web.RenderTemplate(rw, r, "login.tmpl", &web.Page{ + web.RenderTemplate(rw, "login.tmpl", &web.Page{ Title: "Authentication failed - ClusterCockpit", MsgType: "alert-danger", Message: err.Error(), diff --git a/internal/api/rest.go b/internal/api/rest.go index 501cf3b..0716514 100644 --- a/internal/api/rest.go +++ b/internal/api/rest.go @@ -182,12 +182,12 @@ func decode(r io.Reader, val interface{}) error { } func securedCheck(r *http.Request) error { - user := auth.GetUser(r.Context()) + user := repository.GetUserFromContext(r.Context()) if user == nil { return fmt.Errorf("no user in context") } - if user.AuthType == auth.AuthToken { + if user.AuthType == schema.AuthToken { // If nothing declared in config: deny all request to this endpoint if config.Keys.ApiAllowedIPs == nil || len(config.Keys.ApiAllowedIPs) == 0 { return fmt.Errorf("missing configuration key ApiAllowedIPs") @@ -232,8 +232,10 @@ func securedCheck(r *http.Request) error { // @router /jobs/ [get] func (api *RestApi) getJobs(rw http.ResponseWriter, r *http.Request) { - if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) { - handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw) + if user := repository.GetUserFromContext(r.Context()); user != nil && + !user.HasRole(schema.RoleApi) { + + handleError(fmt.Errorf("missing role: %v", schema.GetRoleString(schema.RoleApi)), http.StatusForbidden, rw) return } @@ -374,9 +376,11 @@ func (api *RestApi) getJobs(rw http.ResponseWriter, r *http.Request) { // @security ApiKeyAuth // @router /jobs/{id} [post] func (api *RestApi) getJobById(rw http.ResponseWriter, r *http.Request) { - if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) { + if user := repository.GetUserFromContext(r.Context()); user != nil && + !user.HasRole(schema.RoleApi) { + handleError(fmt.Errorf("missing role: %v", - auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw) + schema.GetRoleString(schema.RoleApi)), http.StatusForbidden, rw) return } @@ -465,8 +469,10 @@ func (api *RestApi) getJobById(rw http.ResponseWriter, r *http.Request) { // @security ApiKeyAuth // @router /jobs/tag_job/{id} [post] func (api *RestApi) tagJob(rw http.ResponseWriter, r *http.Request) { - if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) { - handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw) + if user := repository.GetUserFromContext(r.Context()); user != nil && + !user.HasRole(schema.RoleApi) { + + handleError(fmt.Errorf("missing role: %v", schema.GetRoleString(schema.RoleApi)), http.StatusForbidden, rw) return } @@ -530,8 +536,10 @@ func (api *RestApi) tagJob(rw http.ResponseWriter, r *http.Request) { // @security ApiKeyAuth // @router /jobs/start_job/ [post] func (api *RestApi) startJob(rw http.ResponseWriter, r *http.Request) { - if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) { - handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw) + if user := repository.GetUserFromContext(r.Context()); user != nil && + !user.HasRole(schema.RoleApi) { + + handleError(fmt.Errorf("missing role: %v", schema.GetRoleString(schema.RoleApi)), http.StatusForbidden, rw) return } @@ -611,8 +619,10 @@ func (api *RestApi) startJob(rw http.ResponseWriter, r *http.Request) { // @security ApiKeyAuth // @router /jobs/stop_job/{id} [post] func (api *RestApi) stopJobById(rw http.ResponseWriter, r *http.Request) { - if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) { - handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw) + if user := repository.GetUserFromContext(r.Context()); user != nil && + !user.HasRole(schema.RoleApi) { + + handleError(fmt.Errorf("missing role: %v", schema.GetRoleString(schema.RoleApi)), http.StatusForbidden, rw) return } @@ -664,8 +674,10 @@ func (api *RestApi) stopJobById(rw http.ResponseWriter, r *http.Request) { // @security ApiKeyAuth // @router /jobs/stop_job/ [post] func (api *RestApi) stopJobByRequest(rw http.ResponseWriter, r *http.Request) { - if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) { - handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw) + if user := repository.GetUserFromContext(r.Context()); user != nil && + !user.HasRole(schema.RoleApi) { + + handleError(fmt.Errorf("missing role: %v", schema.GetRoleString(schema.RoleApi)), http.StatusForbidden, rw) return } @@ -710,8 +722,8 @@ func (api *RestApi) stopJobByRequest(rw http.ResponseWriter, r *http.Request) { // @security ApiKeyAuth // @router /jobs/delete_job/{id} [delete] func (api *RestApi) deleteJobById(rw http.ResponseWriter, r *http.Request) { - if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) { - handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw) + if user := repository.GetUserFromContext(r.Context()); user != nil && !user.HasRole(schema.RoleApi) { + handleError(fmt.Errorf("missing role: %v", schema.GetRoleString(schema.RoleApi)), http.StatusForbidden, rw) return } @@ -758,8 +770,9 @@ func (api *RestApi) deleteJobById(rw http.ResponseWriter, r *http.Request) { // @security ApiKeyAuth // @router /jobs/delete_job/ [delete] func (api *RestApi) deleteJobByRequest(rw http.ResponseWriter, r *http.Request) { - if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) { - handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw) + if user := repository.GetUserFromContext(r.Context()); user != nil && + !user.HasRole(schema.RoleApi) { + handleError(fmt.Errorf("missing role: %v", schema.GetRoleString(schema.RoleApi)), http.StatusForbidden, rw) return } @@ -814,8 +827,8 @@ func (api *RestApi) deleteJobByRequest(rw http.ResponseWriter, r *http.Request) // @security ApiKeyAuth // @router /jobs/delete_job_before/{ts} [delete] func (api *RestApi) deleteJobBefore(rw http.ResponseWriter, r *http.Request) { - if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) { - handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw) + if user := repository.GetUserFromContext(r.Context()); user != nil && !user.HasRole(schema.RoleApi) { + handleError(fmt.Errorf("missing role: %v", schema.GetRoleString(schema.RoleApi)), http.StatusForbidden, rw) return } @@ -938,8 +951,8 @@ func (api *RestApi) getJWT(rw http.ResponseWriter, r *http.Request) { rw.Header().Set("Content-Type", "text/plain") username := r.FormValue("username") - me := auth.GetUser(r.Context()) - if !me.HasRole(auth.RoleAdmin) { + me := repository.GetUserFromContext(r.Context()) + if !me.HasRole(schema.RoleAdmin) { if username != me.Username { http.Error(rw, "Only admins are allowed to sign JWTs not for themselves", http.StatusForbidden) @@ -947,7 +960,7 @@ func (api *RestApi) getJWT(rw http.ResponseWriter, r *http.Request) { } } - user, err := api.Authentication.GetUser(username) + user, err := repository.GetUserRepository().GetUser(username) if err != nil { http.Error(rw, err.Error(), http.StatusUnprocessableEntity) return @@ -970,8 +983,8 @@ func (api *RestApi) createUser(rw http.ResponseWriter, r *http.Request) { } rw.Header().Set("Content-Type", "text/plain") - me := auth.GetUser(r.Context()) - if !me.HasRole(auth.RoleAdmin) { + me := repository.GetUserFromContext(r.Context()) + if !me.HasRole(schema.RoleAdmin) { http.Error(rw, "Only admins are allowed to create new users", http.StatusForbidden) return } @@ -980,22 +993,22 @@ func (api *RestApi) createUser(rw http.ResponseWriter, r *http.Request) { r.FormValue("password"), r.FormValue("role"), r.FormValue("name"), r.FormValue("email"), r.FormValue("project") - if len(password) == 0 && role != auth.GetRoleString(auth.RoleApi) { + if len(password) == 0 && role != schema.GetRoleString(schema.RoleApi) { http.Error(rw, "Only API users are allowed to have a blank password (login will be impossible)", http.StatusBadRequest) return } - if len(project) != 0 && role != auth.GetRoleString(auth.RoleManager) { + if len(project) != 0 && role != schema.GetRoleString(schema.RoleManager) { http.Error(rw, "only managers require a project (can be changed later)", http.StatusBadRequest) return - } else if len(project) == 0 && role == auth.GetRoleString(auth.RoleManager) { + } else if len(project) == 0 && role == schema.GetRoleString(schema.RoleManager) { http.Error(rw, "managers require a project to manage (can be changed later)", http.StatusBadRequest) return } - if err := api.Authentication.AddUser(&auth.User{ + if err := repository.GetUserRepository().AddUser(&schema.User{ Username: username, Name: name, Password: password, @@ -1015,13 +1028,13 @@ func (api *RestApi) deleteUser(rw http.ResponseWriter, r *http.Request) { http.Error(rw, err.Error(), http.StatusForbidden) } - if user := auth.GetUser(r.Context()); !user.HasRole(auth.RoleAdmin) { + if user := repository.GetUserFromContext(r.Context()); !user.HasRole(schema.RoleAdmin) { http.Error(rw, "Only admins are allowed to delete a user", http.StatusForbidden) return } username := r.FormValue("username") - if err := api.Authentication.DelUser(username); err != nil { + if err := repository.GetUserRepository().DelUser(username); err != nil { http.Error(rw, err.Error(), http.StatusUnprocessableEntity) return } @@ -1035,12 +1048,12 @@ func (api *RestApi) getUsers(rw http.ResponseWriter, r *http.Request) { http.Error(rw, err.Error(), http.StatusForbidden) } - if user := auth.GetUser(r.Context()); !user.HasRole(auth.RoleAdmin) { + if user := repository.GetUserFromContext(r.Context()); !user.HasRole(schema.RoleAdmin) { http.Error(rw, "Only admins are allowed to fetch a list of users", http.StatusForbidden) return } - users, err := api.Authentication.ListUsers(r.URL.Query().Get("not-just-user") == "true") + users, err := repository.GetUserRepository().ListUsers(r.URL.Query().Get("not-just-user") == "true") if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return @@ -1055,13 +1068,13 @@ func (api *RestApi) getRoles(rw http.ResponseWriter, r *http.Request) { http.Error(rw, err.Error(), http.StatusForbidden) } - user := auth.GetUser(r.Context()) - if !user.HasRole(auth.RoleAdmin) { + user := repository.GetUserFromContext(r.Context()) + if !user.HasRole(schema.RoleAdmin) { http.Error(rw, "only admins are allowed to fetch a list of roles", http.StatusForbidden) return } - roles, err := auth.GetValidRoles(user) + roles, err := schema.GetValidRoles(user) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return @@ -1076,7 +1089,7 @@ func (api *RestApi) updateUser(rw http.ResponseWriter, r *http.Request) { http.Error(rw, err.Error(), http.StatusForbidden) } - if user := auth.GetUser(r.Context()); !user.HasRole(auth.RoleAdmin) { + if user := repository.GetUserFromContext(r.Context()); !user.HasRole(schema.RoleAdmin) { http.Error(rw, "Only admins are allowed to update a user", http.StatusForbidden) return } @@ -1089,25 +1102,25 @@ func (api *RestApi) updateUser(rw http.ResponseWriter, r *http.Request) { // TODO: Handle anything but roles... if newrole != "" { - if err := api.Authentication.AddRole(r.Context(), mux.Vars(r)["id"], newrole); err != nil { + if err := repository.GetUserRepository().AddRole(r.Context(), mux.Vars(r)["id"], newrole); err != nil { http.Error(rw, err.Error(), http.StatusUnprocessableEntity) return } rw.Write([]byte("Add Role Success")) } else if delrole != "" { - if err := api.Authentication.RemoveRole(r.Context(), mux.Vars(r)["id"], delrole); err != nil { + if err := repository.GetUserRepository().RemoveRole(r.Context(), mux.Vars(r)["id"], delrole); err != nil { http.Error(rw, err.Error(), http.StatusUnprocessableEntity) return } rw.Write([]byte("Remove Role Success")) } else if newproj != "" { - if err := api.Authentication.AddProject(r.Context(), mux.Vars(r)["id"], newproj); err != nil { + if err := repository.GetUserRepository().AddProject(r.Context(), mux.Vars(r)["id"], newproj); err != nil { http.Error(rw, err.Error(), http.StatusUnprocessableEntity) return } rw.Write([]byte("Add Project Success")) } else if delproj != "" { - if err := api.Authentication.RemoveProject(r.Context(), mux.Vars(r)["id"], delproj); err != nil { + if err := repository.GetUserRepository().RemoveProject(r.Context(), mux.Vars(r)["id"], delproj); err != nil { http.Error(rw, err.Error(), http.StatusUnprocessableEntity) return } @@ -1188,7 +1201,7 @@ func (api *RestApi) updateConfiguration(rw http.ResponseWriter, r *http.Request) fmt.Printf("REST > KEY: %#v\nVALUE: %#v\n", key, value) - if err := repository.GetUserCfgRepo().UpdateConfig(key, value, auth.GetUser(r.Context())); err != nil { + if err := repository.GetUserCfgRepo().UpdateConfig(key, value, repository.GetUserFromContext(r.Context())); err != nil { http.Error(rw, err.Error(), http.StatusUnprocessableEntity) return } diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 57bf3c4..d79f28b 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -14,65 +14,19 @@ import ( "os" "time" + "github.com/ClusterCockpit/cc-backend/internal/repository" "github.com/ClusterCockpit/cc-backend/pkg/log" + "github.com/ClusterCockpit/cc-backend/pkg/schema" "github.com/gorilla/sessions" "github.com/jmoiron/sqlx" ) -type AuthSource int - -const ( - AuthViaLocalPassword AuthSource = iota - AuthViaLDAP - AuthViaToken -) - -type AuthType int - -const ( - AuthToken AuthType = iota - AuthSession -) - -type User struct { - Username string `json:"username"` - Password string `json:"-"` - Name string `json:"name"` - Roles []string `json:"roles"` - AuthType AuthType `json:"authType"` - AuthSource AuthSource `json:"authSource"` - Email string `json:"email"` - Projects []string `json:"projects"` -} - -func (u *User) HasProject(project string) bool { - for _, p := range u.Projects { - if p == project { - return true - } - } - return false -} - -func GetUser(ctx context.Context) *User { - x := ctx.Value(ContextUserKey) - if x == nil { - return nil - } - - return x.(*User) -} - type Authenticator interface { Init(auth *Authentication, config interface{}) error - CanLogin(user *User, username string, rw http.ResponseWriter, r *http.Request) bool - Login(user *User, rw http.ResponseWriter, r *http.Request) (*User, error) + CanLogin(user *schema.User, username string, rw http.ResponseWriter, r *http.Request) bool + Login(user *schema.User, rw http.ResponseWriter, r *http.Request) (*schema.User, error) } -type ContextKey string - -const ContextUserKey ContextKey = "user" - type Authentication struct { db *sqlx.DB sessionStore *sessions.CookieStore @@ -86,7 +40,7 @@ type Authentication struct { func (auth *Authentication) AuthViaSession( rw http.ResponseWriter, - r *http.Request) (*User, error) { + r *http.Request) (*schema.User, error) { session, err := auth.sessionStore.Get(r, "session") if err != nil { log.Error("Error while getting session store") @@ -119,11 +73,11 @@ func (auth *Authentication) AuthViaSession( username, _ := session.Values["username"].(string) projects, _ := session.Values["projects"].([]string) roles, _ := session.Values["roles"].([]string) - return &User{ + return &schema.User{ Username: username, Projects: projects, Roles: roles, - AuthType: AuthSession, + AuthType: schema.AuthSession, AuthSource: -1, }, nil } @@ -196,12 +150,13 @@ func (auth *Authentication) Login( onfailure func(rw http.ResponseWriter, r *http.Request, loginErr error)) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + ur := repository.GetUserRepository() err := errors.New("no authenticator applied") username := r.FormValue("username") - dbUser := (*User)(nil) + dbUser := (*schema.User)(nil) if username != "" { - dbUser, err = auth.GetUser(username) + dbUser, err = ur.GetUser(username) if err != nil && err != sql.ErrNoRows { log.Errorf("Error while loading user '%v'", username) } @@ -211,7 +166,7 @@ func (auth *Authentication) Login( if !authenticator.CanLogin(dbUser, username, rw, r) { continue } - dbUser, err = auth.GetUser(username) + dbUser, err = ur.GetUser(username) if err != nil && err != sql.ErrNoRows { log.Errorf("Error while loading user '%v'", username) } @@ -243,7 +198,7 @@ func (auth *Authentication) Login( } if dbUser == nil { - if err := auth.AddUser(user); err != nil { + if err := ur.AddUser(user); err != nil { // TODO Add AuthSource log.Errorf("Error while adding user '%v' to auth from XX", user.Username) @@ -251,7 +206,7 @@ func (auth *Authentication) Login( } log.Infof("login successfull: user: %#v (roles: %v, projects: %v)", user.Username, user.Roles, user.Projects) - ctx := context.WithValue(r.Context(), ContextUserKey, user) + ctx := context.WithValue(r.Context(), repository.ContextUserKey, user) onsuccess.ServeHTTP(rw, r.WithContext(ctx)) return } @@ -284,7 +239,7 @@ func (auth *Authentication) Auth( } if user != nil { - ctx := context.WithValue(r.Context(), ContextUserKey, user) + ctx := context.WithValue(r.Context(), repository.ContextUserKey, user) onsuccess.ServeHTTP(rw, r.WithContext(ctx)) return } diff --git a/internal/auth/jwt.go b/internal/auth/jwt.go index 6a77fc4..0690b9b 100644 --- a/internal/auth/jwt.go +++ b/internal/auth/jwt.go @@ -13,22 +13,19 @@ import ( "strings" "time" + "github.com/ClusterCockpit/cc-backend/internal/repository" "github.com/ClusterCockpit/cc-backend/pkg/log" "github.com/ClusterCockpit/cc-backend/pkg/schema" "github.com/golang-jwt/jwt/v4" ) type JWTAuthenticator struct { - auth *Authentication - publicKey ed25519.PublicKey privateKey ed25519.PrivateKey config *schema.JWTAuthConfig } func (ja *JWTAuthenticator) Init(auth *Authentication, conf interface{}) error { - - ja.auth = auth ja.config = conf.(*schema.JWTAuthConfig) pubKey, privKey := os.Getenv("JWT_PUBLIC_KEY"), os.Getenv("JWT_PRIVATE_KEY") @@ -54,7 +51,7 @@ func (ja *JWTAuthenticator) Init(auth *Authentication, conf interface{}) error { func (ja *JWTAuthenticator) AuthViaJWT( rw http.ResponseWriter, - r *http.Request) (*User, error) { + r *http.Request) (*schema.User, error) { rawtoken := r.Header.Get("X-Auth-Token") if rawtoken == "" { @@ -90,8 +87,9 @@ func (ja *JWTAuthenticator) AuthViaJWT( var roles []string // Validate user + roles from JWT against database? - if ja.config != nil && ja.config.ForceJWTValidationViaDatabase { - user, err := ja.auth.GetUser(sub) + if ja.config != nil && ja.config.ValidateUser { + ur := repository.GetUserRepository() + user, err := ur.GetUser(sub) // Deny any logins for unknown usernames if err != nil { @@ -111,16 +109,16 @@ func (ja *JWTAuthenticator) AuthViaJWT( } } - return &User{ + return &schema.User{ Username: sub, Roles: roles, - AuthType: AuthToken, + AuthType: schema.AuthToken, AuthSource: -1, }, nil } // Generate a new JWT that can be used for authentication -func (ja *JWTAuthenticator) ProvideJWT(user *User) (string, error) { +func (ja *JWTAuthenticator) ProvideJWT(user *schema.User) (string, error) { if ja.privateKey == nil { return "", errors.New("environment variable 'JWT_PRIVATE_KEY' not set") diff --git a/internal/auth/jwtCookieSession.go b/internal/auth/jwtCookieSession.go index 8f31335..4787107 100644 --- a/internal/auth/jwtCookieSession.go +++ b/internal/auth/jwtCookieSession.go @@ -73,10 +73,10 @@ func (ja *JWTCookieSessionAuthenticator) Init(auth *Authentication, conf interfa log.Warn("cookieName for JWTs not configured (cross login via JWT cookie will fail)") return errors.New("cookieName for JWTs not configured (cross login via JWT cookie will fail)") } - if !ja.config.ForceJWTValidationViaDatabase { + if !ja.config.ValidateUser { log.Warn("forceJWTValidationViaDatabase not set to true: CC will accept users and roles defined in JWTs regardless of its own database!") } - if ja.config.TrustedExternalIssuer == "" { + if ja.config.TrustedIssuer == "" { log.Warn("trustedExternalIssuer for JWTs not configured (cross login via JWT cookie will fail)") return errors.New("trustedExternalIssuer for JWTs not configured (cross login via JWT cookie will fail)") } @@ -89,7 +89,7 @@ func (ja *JWTCookieSessionAuthenticator) Init(auth *Authentication, conf interfa } func (ja *JWTCookieSessionAuthenticator) CanLogin( - user *User, + user *schema.User, username string, rw http.ResponseWriter, r *http.Request) bool { @@ -112,9 +112,9 @@ func (ja *JWTCookieSessionAuthenticator) CanLogin( } func (ja *JWTCookieSessionAuthenticator) Login( - user *User, + user *schema.User, rw http.ResponseWriter, - r *http.Request) (*User, error) { + r *http.Request) (*schema.User, error) { jwtCookie, err := r.Cookie(ja.config.CookieName) var rawtoken string @@ -129,7 +129,7 @@ func (ja *JWTCookieSessionAuthenticator) Login( } unvalidatedIssuer, success := t.Claims.(jwt.MapClaims)["iss"].(string) - if success && unvalidatedIssuer == ja.config.TrustedExternalIssuer { + if success && unvalidatedIssuer == ja.config.TrustedIssuer { // The (unvalidated) issuer seems to be the expected one, // use public cross login key from config return ja.publicKeyCrossLogin, nil @@ -160,7 +160,7 @@ func (ja *JWTCookieSessionAuthenticator) Login( var roles []string - if ja.config.ForceJWTValidationViaDatabase { + if ja.config.ValidateUser { // Deny any logins for unknown usernames if user == nil { log.Warn("Could not find user from JWT in internal database.") @@ -191,12 +191,12 @@ func (ja *JWTCookieSessionAuthenticator) Login( http.SetCookie(rw, deletedCookie) if user == nil { - user = &User{ + user = &schema.User{ Username: sub, Name: name, Roles: roles, - AuthType: AuthSession, - AuthSource: AuthViaToken, + AuthType: schema.AuthSession, + AuthSource: schema.AuthViaToken, } } diff --git a/internal/auth/jwtSession.go b/internal/auth/jwtSession.go index 13af7c1..59250ac 100644 --- a/internal/auth/jwtSession.go +++ b/internal/auth/jwtSession.go @@ -12,21 +12,17 @@ import ( "strings" "github.com/ClusterCockpit/cc-backend/pkg/log" + "github.com/ClusterCockpit/cc-backend/pkg/schema" "github.com/golang-jwt/jwt/v4" ) type JWTSessionAuthenticator struct { - auth *Authentication - loginTokenKey []byte // HS256 key } var _ Authenticator = (*JWTSessionAuthenticator)(nil) func (ja *JWTSessionAuthenticator) Init(auth *Authentication, conf interface{}) error { - - ja.auth = auth - if pubKey := os.Getenv("CROSS_LOGIN_JWT_HS512_KEY"); pubKey != "" { bytes, err := base64.StdEncoding.DecodeString(pubKey) if err != nil { @@ -40,7 +36,7 @@ func (ja *JWTSessionAuthenticator) Init(auth *Authentication, conf interface{}) } func (ja *JWTSessionAuthenticator) CanLogin( - user *User, + user *schema.User, username string, rw http.ResponseWriter, r *http.Request) bool { @@ -49,9 +45,9 @@ func (ja *JWTSessionAuthenticator) CanLogin( } func (ja *JWTSessionAuthenticator) Login( - user *User, + user *schema.User, rw http.ResponseWriter, - r *http.Request) (*User, error) { + r *http.Request) (*schema.User, error) { rawtoken := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") if rawtoken == "" { @@ -92,14 +88,14 @@ func (ja *JWTSessionAuthenticator) Login( if rawroles, ok := claims["roles"].([]interface{}); ok { for _, rr := range rawroles { if r, ok := rr.(string); ok { - if isValidRole(r) { + if schema.IsValidRole(r) { roles = append(roles, r) } } } } else if rawroles, ok := claims["roles"]; ok { for _, r := range rawroles.([]string) { - if isValidRole(r) { + if schema.IsValidRole(r) { roles = append(roles, r) } } @@ -120,13 +116,13 @@ func (ja *JWTSessionAuthenticator) Login( // } if user == nil { - user = &User{ + user = &schema.User{ Username: sub, Name: name, Roles: roles, Projects: projects, - AuthType: AuthSession, - AuthSource: AuthViaToken, + AuthType: schema.AuthSession, + AuthSource: schema.AuthViaToken, } } diff --git a/internal/auth/ldap.go b/internal/auth/ldap.go index 9feebc1..925e967 100644 --- a/internal/auth/ldap.go +++ b/internal/auth/ldap.go @@ -67,12 +67,12 @@ func (la *LdapAuthenticator) Init( } func (la *LdapAuthenticator) CanLogin( - user *User, + user *schema.User, username string, rw http.ResponseWriter, r *http.Request) bool { - if user != nil && user.AuthSource == AuthViaLDAP { + if user != nil && user.AuthSource == schema.AuthViaLDAP { return true } else { if la.config != nil && la.config.SyncUserOnLogin { @@ -103,7 +103,7 @@ func (la *LdapAuthenticator) CanLogin( name := entry.GetAttributeValue("gecos") if _, err := la.auth.db.Exec(`INSERT INTO user (username, ldap, name, roles) VALUES (?, ?, ?, ?)`, - username, 1, name, "[\""+GetRoleString(RoleUser)+"\"]"); err != nil { + username, 1, name, "[\""+schema.GetRoleString(schema.RoleUser)+"\"]"); err != nil { log.Errorf("User '%s' new in LDAP: Insert into DB failed", username) return false } @@ -116,9 +116,9 @@ func (la *LdapAuthenticator) CanLogin( } func (la *LdapAuthenticator) Login( - user *User, + user *schema.User, rw http.ResponseWriter, - r *http.Request) (*User, error) { + r *http.Request) (*schema.User, error) { l, err := la.getLdapConnection(false) if err != nil { @@ -203,7 +203,7 @@ func (la *LdapAuthenticator) Sync() error { name := newnames[username] log.Debugf("sync: add %v (name: %v, roles: [user], ldap: true)", username, name) if _, err := la.auth.db.Exec(`INSERT INTO user (username, ldap, name, roles) VALUES (?, ?, ?, ?)`, - username, 1, name, "[\""+GetRoleString(RoleUser)+"\"]"); err != nil { + username, 1, name, "[\""+schema.GetRoleString(schema.RoleUser)+"\"]"); err != nil { log.Errorf("User '%s' new in LDAP: Insert into DB failed", username) return err } diff --git a/internal/auth/local.go b/internal/auth/local.go index 700db3a..fb1ba0f 100644 --- a/internal/auth/local.go +++ b/internal/auth/local.go @@ -9,6 +9,7 @@ import ( "net/http" "github.com/ClusterCockpit/cc-backend/pkg/log" + "github.com/ClusterCockpit/cc-backend/pkg/schema" "golang.org/x/crypto/bcrypt" ) @@ -27,18 +28,18 @@ func (la *LocalAuthenticator) Init( } func (la *LocalAuthenticator) CanLogin( - user *User, + user *schema.User, username string, rw http.ResponseWriter, r *http.Request) bool { - return user != nil && user.AuthSource == AuthViaLocalPassword + return user != nil && user.AuthSource == schema.AuthViaLocalPassword } func (la *LocalAuthenticator) Login( - user *User, + user *schema.User, rw http.ResponseWriter, - r *http.Request) (*User, error) { + r *http.Request) (*schema.User, error) { if e := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(r.FormValue("password"))); e != nil { diff --git a/internal/auth/users.go b/internal/auth/users.go deleted file mode 100644 index b69533b..0000000 --- a/internal/auth/users.go +++ /dev/null @@ -1,289 +0,0 @@ -// Copyright (C) 2022 NHR@FAU, University Erlangen-Nuremberg. -// All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. -package auth - -import ( - "context" - "database/sql" - "encoding/json" - "errors" - "fmt" - "strings" - - "github.com/ClusterCockpit/cc-backend/internal/graph/model" - "github.com/ClusterCockpit/cc-backend/pkg/log" - sq "github.com/Masterminds/squirrel" - "github.com/jmoiron/sqlx" - "golang.org/x/crypto/bcrypt" -) - -func (auth *Authentication) GetUser(username string) (*User, error) { - - user := &User{Username: username} - var hashedPassword, name, rawRoles, email, rawProjects sql.NullString - if err := sq.Select("password", "ldap", "name", "roles", "email", "projects").From("user"). - Where("user.username = ?", username).RunWith(auth.db). - QueryRow().Scan(&hashedPassword, &user.AuthSource, &name, &rawRoles, &email, &rawProjects); err != nil { - log.Warnf("Error while querying user '%v' from database", username) - return nil, err - } - - user.Password = hashedPassword.String - user.Name = name.String - user.Email = email.String - if rawRoles.Valid { - if err := json.Unmarshal([]byte(rawRoles.String), &user.Roles); err != nil { - log.Warn("Error while unmarshaling raw roles from DB") - return nil, err - } - } - if rawProjects.Valid { - if err := json.Unmarshal([]byte(rawProjects.String), &user.Projects); err != nil { - return nil, err - } - } - - return user, nil -} - -func (auth *Authentication) AddUser(user *User) error { - - rolesJson, _ := json.Marshal(user.Roles) - projectsJson, _ := json.Marshal(user.Projects) - - cols := []string{"username", "roles", "projects"} - vals := []interface{}{user.Username, string(rolesJson), string(projectsJson)} - - if user.Name != "" { - cols = append(cols, "name") - vals = append(vals, user.Name) - } - if user.Email != "" { - cols = append(cols, "email") - vals = append(vals, user.Email) - } - if user.Password != "" { - password, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost) - if err != nil { - log.Error("Error while encrypting new user password") - return err - } - cols = append(cols, "password") - vals = append(vals, string(password)) - } - - if _, err := sq.Insert("user").Columns(cols...).Values(vals...).RunWith(auth.db).Exec(); err != nil { - log.Errorf("Error while inserting new user '%v' into DB", user.Username) - return err - } - - log.Infof("new user %#v created (roles: %s, auth-source: %d, projects: %s)", user.Username, rolesJson, user.AuthSource, projectsJson) - return nil -} - -func (auth *Authentication) DelUser(username string) error { - - _, err := auth.db.Exec(`DELETE FROM user WHERE user.username = ?`, username) - log.Errorf("Error while deleting user '%s' from DB", username) - return err -} - -func (auth *Authentication) ListUsers(specialsOnly bool) ([]*User, error) { - - q := sq.Select("username", "name", "email", "roles", "projects").From("user") - if specialsOnly { - q = q.Where("(roles != '[\"user\"]' AND roles != '[]')") - } - - rows, err := q.RunWith(auth.db).Query() - if err != nil { - log.Warn("Error while querying user list") - return nil, err - } - - users := make([]*User, 0) - defer rows.Close() - for rows.Next() { - rawroles := "" - rawprojects := "" - user := &User{} - var name, email sql.NullString - if err := rows.Scan(&user.Username, &name, &email, &rawroles, &rawprojects); err != nil { - log.Warn("Error while scanning user list") - return nil, err - } - - if err := json.Unmarshal([]byte(rawroles), &user.Roles); err != nil { - log.Warn("Error while unmarshaling raw role list") - return nil, err - } - - if err := json.Unmarshal([]byte(rawprojects), &user.Projects); err != nil { - return nil, err - } - - user.Name = name.String - user.Email = email.String - users = append(users, user) - } - return users, nil -} - -func (auth *Authentication) AddRole( - ctx context.Context, - username string, - queryrole string) error { - - newRole := strings.ToLower(queryrole) - user, err := auth.GetUser(username) - if err != nil { - log.Warnf("Could not load user '%s'", username) - return err - } - - exists, valid := user.HasValidRole(newRole) - - if !valid { - return fmt.Errorf("Supplied role is no valid option : %v", newRole) - } - if exists { - return fmt.Errorf("User %v already has role %v", username, newRole) - } - - roles, _ := json.Marshal(append(user.Roles, newRole)) - if _, err := sq.Update("user").Set("roles", roles).Where("user.username = ?", username).RunWith(auth.db).Exec(); err != nil { - log.Errorf("Error while adding new role for user '%s'", user.Username) - return err - } - return nil -} - -func (auth *Authentication) RemoveRole(ctx context.Context, username string, queryrole string) error { - oldRole := strings.ToLower(queryrole) - user, err := auth.GetUser(username) - if err != nil { - log.Warnf("Could not load user '%s'", username) - return err - } - - exists, valid := user.HasValidRole(oldRole) - - if !valid { - return fmt.Errorf("Supplied role is no valid option : %v", oldRole) - } - if !exists { - return fmt.Errorf("Role already deleted for user '%v': %v", username, oldRole) - } - - if oldRole == GetRoleString(RoleManager) && len(user.Projects) != 0 { - return fmt.Errorf("Cannot remove role 'manager' while user %s still has assigned project(s) : %v", username, user.Projects) - } - - var newroles []string - for _, r := range user.Roles { - if r != oldRole { - newroles = append(newroles, r) // Append all roles not matching requested to be deleted role - } - } - - var mroles, _ = json.Marshal(newroles) - if _, err := sq.Update("user").Set("roles", mroles).Where("user.username = ?", username).RunWith(auth.db).Exec(); err != nil { - log.Errorf("Error while removing role for user '%s'", user.Username) - return err - } - return nil -} - -func (auth *Authentication) AddProject( - ctx context.Context, - username string, - project string) error { - - user, err := auth.GetUser(username) - if err != nil { - return err - } - - if !user.HasRole(RoleManager) { - return fmt.Errorf("user '%s' is not a manager!", username) - } - - if user.HasProject(project) { - return fmt.Errorf("user '%s' already manages project '%s'", username, project) - } - - projects, _ := json.Marshal(append(user.Projects, project)) - if _, err := sq.Update("user").Set("projects", projects).Where("user.username = ?", username).RunWith(auth.db).Exec(); err != nil { - return err - } - - return nil -} - -func (auth *Authentication) RemoveProject(ctx context.Context, username string, project string) error { - user, err := auth.GetUser(username) - if err != nil { - return err - } - - if !user.HasRole(RoleManager) { - return fmt.Errorf("user '%#v' is not a manager!", username) - } - - if !user.HasProject(project) { - return fmt.Errorf("user '%#v': Cannot remove project '%#v' - Does not match!", username, project) - } - - var exists bool - var newprojects []string - for _, p := range user.Projects { - if p != project { - newprojects = append(newprojects, p) // Append all projects not matching requested to be deleted project - } else { - exists = true - } - } - - if exists == true { - var result interface{} - if len(newprojects) == 0 { - result = "[]" - } else { - result, _ = json.Marshal(newprojects) - } - if _, err := sq.Update("user").Set("projects", result).Where("user.username = ?", username).RunWith(auth.db).Exec(); err != nil { - return err - } - return nil - } else { - return fmt.Errorf("user %s already does not manage project %s", username, project) - } -} - -func FetchUser(ctx context.Context, db *sqlx.DB, username string) (*model.User, error) { - me := GetUser(ctx) - if me != nil && me.Username != username && me.HasNotRoles([]Role{RoleAdmin, RoleSupport, RoleManager}) { - return nil, errors.New("forbidden") - } - - user := &model.User{Username: username} - var name, email sql.NullString - if err := sq.Select("name", "email").From("user").Where("user.username = ?", username). - RunWith(db).QueryRow().Scan(&name, &email); err != nil { - if err == sql.ErrNoRows { - /* This warning will be logged *often* for non-local users, i.e. users mentioned only in job-table or archive, */ - /* since FetchUser will be called to retrieve full name and mail for every job in query/list */ - // log.Warnf("User '%s' Not found in DB", username) - return nil, nil - } - - log.Warnf("Error while fetching user '%s'", username) - return nil, err - } - - user.Name = name.String - user.Email = email.String - return user, nil -} diff --git a/internal/graph/schema.resolvers.go b/internal/graph/schema.resolvers.go index cd24a2a..1be455c 100644 --- a/internal/graph/schema.resolvers.go +++ b/internal/graph/schema.resolvers.go @@ -11,7 +11,6 @@ import ( "strconv" "time" - "github.com/ClusterCockpit/cc-backend/internal/auth" "github.com/ClusterCockpit/cc-backend/internal/graph/generated" "github.com/ClusterCockpit/cc-backend/internal/graph/model" "github.com/ClusterCockpit/cc-backend/internal/metricdata" @@ -51,7 +50,7 @@ func (r *jobResolver) MetaData(ctx context.Context, obj *schema.Job) (interface{ // UserData is the resolver for the userData field. func (r *jobResolver) UserData(ctx context.Context, obj *schema.Job) (*model.User, error) { - return auth.FetchUser(ctx, r.DB, obj.User) + return repository.GetUserRepository().FetchUserInCtx(ctx, obj.User) } // CreateTag is the resolver for the createTag field. @@ -122,7 +121,7 @@ func (r *mutationResolver) RemoveTagsFromJob(ctx context.Context, job string, ta // UpdateConfiguration is the resolver for the updateConfiguration field. func (r *mutationResolver) UpdateConfiguration(ctx context.Context, name string, value string) (*string, error) { - if err := repository.GetUserCfgRepo().UpdateConfig(name, value, auth.GetUser(ctx)); err != nil { + if err := repository.GetUserCfgRepo().UpdateConfig(name, value, repository.GetUserFromContext(ctx)); err != nil { log.Warn("Error while updating user config") return nil, err } @@ -142,7 +141,7 @@ func (r *queryResolver) Tags(ctx context.Context) ([]*schema.Tag, error) { // User is the resolver for the user field. func (r *queryResolver) User(ctx context.Context, username string) (*model.User, error) { - return auth.FetchUser(ctx, r.DB, username) + return repository.GetUserRepository().FetchUserInCtx(ctx, username) } // AllocatedNodes is the resolver for the allocatedNodes field. @@ -178,7 +177,9 @@ func (r *queryResolver) Job(ctx context.Context, id string) (*schema.Job, error) return nil, err } - if user := auth.GetUser(ctx); user != nil && job.User != user.Username && user.HasNotRoles([]auth.Role{auth.RoleAdmin, auth.RoleSupport, auth.RoleManager}) { + if user := repository.GetUserFromContext(ctx); user != nil && + job.User != user.Username && + user.HasNotRoles([]schema.Role{schema.RoleAdmin, schema.RoleSupport, schema.RoleManager}) { return nil, errors.New("you are not allowed to see this job") } @@ -318,8 +319,8 @@ func (r *queryResolver) RooflineHeatmap(ctx context.Context, filter []*model.Job // NodeMetrics is the resolver for the nodeMetrics field. func (r *queryResolver) NodeMetrics(ctx context.Context, cluster string, nodes []string, scopes []schema.MetricScope, metrics []string, from time.Time, to time.Time) ([]*model.NodeMetrics, error) { - user := auth.GetUser(ctx) - if user != nil && !user.HasRole(auth.RoleAdmin) { + user := repository.GetUserFromContext(ctx) + if user != nil && !user.HasRole(schema.RoleAdmin) { return nil, errors.New("you need to be an administrator for this query") } diff --git a/internal/repository/job.go b/internal/repository/job.go index b712a86..449c267 100644 --- a/internal/repository/job.go +++ b/internal/repository/job.go @@ -14,7 +14,6 @@ import ( "sync" "time" - "github.com/ClusterCockpit/cc-backend/internal/auth" "github.com/ClusterCockpit/cc-backend/internal/graph/model" "github.com/ClusterCockpit/cc-backend/internal/metricdata" "github.com/ClusterCockpit/cc-backend/pkg/log" @@ -615,7 +614,7 @@ func (r *JobRepository) WaitForArchiving() { r.archivePending.Wait() } -func (r *JobRepository) FindUserOrProjectOrJobname(user *auth.User, searchterm string) (jobid string, username string, project string, jobname string) { +func (r *JobRepository) FindUserOrProjectOrJobname(user *schema.User, searchterm string) (jobid string, username string, project string, jobname string) { if _, err := strconv.Atoi(searchterm); err == nil { // Return empty on successful conversion: parent method will redirect for integer jobId return searchterm, "", "", "" } else { // Has to have letters and logged-in user for other guesses @@ -644,14 +643,14 @@ func (r *JobRepository) FindUserOrProjectOrJobname(user *auth.User, searchterm s var ErrNotFound = errors.New("no such jobname, project or user") var ErrForbidden = errors.New("not authorized") -func (r *JobRepository) FindColumnValue(user *auth.User, searchterm string, table string, selectColumn string, whereColumn string, isLike bool) (result string, err error) { +func (r *JobRepository) FindColumnValue(user *schema.User, searchterm string, table string, selectColumn string, whereColumn string, isLike bool) (result string, err error) { compareStr := " = ?" query := searchterm if isLike { compareStr = " LIKE ?" query = "%" + searchterm + "%" } - if user.HasAnyRole([]auth.Role{auth.RoleAdmin, auth.RoleSupport, auth.RoleManager}) { + if user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport, schema.RoleManager}) { theQuery := sq.Select(table+"."+selectColumn).Distinct().From(table). Where(table+"."+whereColumn+compareStr, query) @@ -676,9 +675,9 @@ func (r *JobRepository) FindColumnValue(user *auth.User, searchterm string, tabl } } -func (r *JobRepository) FindColumnValues(user *auth.User, query string, table string, selectColumn string, whereColumn string) (results []string, err error) { +func (r *JobRepository) FindColumnValues(user *schema.User, query string, table string, selectColumn string, whereColumn string) (results []string, err error) { emptyResult := make([]string, 0) - if user.HasAnyRole([]auth.Role{auth.RoleAdmin, auth.RoleSupport, auth.RoleManager}) { + if user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport, schema.RoleManager}) { rows, err := sq.Select(table+"."+selectColumn).Distinct().From(table). Where(table+"."+whereColumn+" LIKE ?", fmt.Sprint("%", query, "%")). RunWith(r.stmtCache).Query() diff --git a/internal/repository/query.go b/internal/repository/query.go index 02e5304..0501fe1 100644 --- a/internal/repository/query.go +++ b/internal/repository/query.go @@ -12,7 +12,6 @@ import ( "strings" "time" - "github.com/ClusterCockpit/cc-backend/internal/auth" "github.com/ClusterCockpit/cc-backend/internal/graph/model" "github.com/ClusterCockpit/cc-backend/pkg/log" "github.com/ClusterCockpit/cc-backend/pkg/schema" @@ -130,20 +129,20 @@ func (r *JobRepository) CountJobs( } func SecurityCheck(ctx context.Context, query sq.SelectBuilder) (sq.SelectBuilder, error) { - user := auth.GetUser(ctx) + user := GetUserFromContext(ctx) if user == nil { var qnil sq.SelectBuilder return qnil, fmt.Errorf("user context is nil!") - } else if user.HasAnyRole([]auth.Role{auth.RoleAdmin, auth.RoleSupport, auth.RoleApi}) { // Admin & Co. : All jobs + } else if user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport, schema.RoleApi}) { // Admin & Co. : All jobs return query, nil - } else if user.HasRole(auth.RoleManager) { // Manager : Add filter for managed projects' jobs only + personal jobs + } else if user.HasRole(schema.RoleManager) { // Manager : Add filter for managed projects' jobs only + personal jobs if len(user.Projects) != 0 { return query.Where(sq.Or{sq.Eq{"job.project": user.Projects}, sq.Eq{"job.user": user.Username}}), nil } else { log.Debugf("Manager-User '%s' has no defined projects to lookup! Query only personal jobs ...", user.Username) return query.Where("job.user = ?", user.Username), nil } - } else if user.HasRole(auth.RoleUser) { // User : Only personal jobs + } else if user.HasRole(schema.RoleUser) { // User : Only personal jobs return query.Where("job.user = ?", user.Username), nil } else { // Shortterm compatibility: Return User-Query if no roles: diff --git a/internal/repository/stats.go b/internal/repository/stats.go index 158d34a..8a74008 100644 --- a/internal/repository/stats.go +++ b/internal/repository/stats.go @@ -10,7 +10,6 @@ import ( "fmt" "time" - "github.com/ClusterCockpit/cc-backend/internal/auth" "github.com/ClusterCockpit/cc-backend/internal/config" "github.com/ClusterCockpit/cc-backend/internal/graph/model" "github.com/ClusterCockpit/cc-backend/pkg/log" @@ -86,7 +85,7 @@ func (r *JobRepository) buildStatsQuery( } func (r *JobRepository) getUserName(ctx context.Context, id string) string { - user := auth.GetUser(ctx) + user := GetUserFromContext(ctx) name, _ := r.FindColumnValue(user, id, "user", "name", "username", false) if name != "" { return name diff --git a/internal/repository/tags.go b/internal/repository/tags.go index 6c46352..52bc836 100644 --- a/internal/repository/tags.go +++ b/internal/repository/tags.go @@ -7,7 +7,6 @@ package repository import ( "strings" - "github.com/ClusterCockpit/cc-backend/internal/auth" "github.com/ClusterCockpit/cc-backend/pkg/archive" "github.com/ClusterCockpit/cc-backend/pkg/log" "github.com/ClusterCockpit/cc-backend/pkg/schema" @@ -68,7 +67,7 @@ func (r *JobRepository) CreateTag(tagType string, tagName string) (tagId int64, return res.LastInsertId() } -func (r *JobRepository) CountTags(user *auth.User) (tags []schema.Tag, counts map[string]int, err error) { +func (r *JobRepository) CountTags(user *schema.User) (tags []schema.Tag, counts map[string]int, err error) { tags = make([]schema.Tag, 0, 100) xrows, err := r.DB.Queryx("SELECT id, tag_type, tag_name FROM tag") if err != nil { @@ -88,10 +87,10 @@ func (r *JobRepository) CountTags(user *auth.User) (tags []schema.Tag, counts ma LeftJoin("jobtag jt ON t.id = jt.tag_id"). GroupBy("t.tag_name") - if user != nil && user.HasAnyRole([]auth.Role{auth.RoleAdmin, auth.RoleSupport}) { // ADMIN || SUPPORT: Count all jobs + if user != nil && user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport}) { // ADMIN || SUPPORT: Count all jobs log.Debug("CountTags: User Admin or Support -> Count all Jobs for Tags") // Unchanged: Needs to be own case still, due to UserRole/NoRole compatibility handling in else case - } else if user != nil && user.HasRole(auth.RoleManager) { // MANAGER: Count own jobs plus project's jobs + } else if user != nil && user.HasRole(schema.RoleManager) { // MANAGER: Count own jobs plus project's jobs // Build ("project1", "project2", ...) list of variable length directly in SQL string q = q.Where("jt.job_id IN (SELECT id FROM job WHERE job.user = ? OR job.project IN (\""+strings.Join(user.Projects, "\",\"")+"\"))", user.Username) } else if user != nil { // USER OR NO ROLE (Compatibility): Only count own jobs diff --git a/internal/repository/user.go b/internal/repository/user.go index 6a6fe62..3db7e4d 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -1,137 +1,325 @@ -// Copyright (C) 2022 NHR@FAU, University Erlangen-Nuremberg. +// Copyright (C) 2023 NHR@FAU, University Erlangen-Nuremberg. // All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package repository import ( + "context" + "database/sql" "encoding/json" + "errors" + "fmt" + "strings" "sync" - "time" - "github.com/ClusterCockpit/cc-backend/internal/auth" - "github.com/ClusterCockpit/cc-backend/internal/config" + "github.com/ClusterCockpit/cc-backend/internal/graph/model" "github.com/ClusterCockpit/cc-backend/pkg/log" - "github.com/ClusterCockpit/cc-backend/pkg/lrucache" + "github.com/ClusterCockpit/cc-backend/pkg/schema" + sq "github.com/Masterminds/squirrel" "github.com/jmoiron/sqlx" + "golang.org/x/crypto/bcrypt" ) var ( - userCfgRepoOnce sync.Once - userCfgRepoInstance *UserCfgRepo + userRepoOnce sync.Once + userRepoInstance *UserRepository ) -type UserCfgRepo struct { - DB *sqlx.DB - Lookup *sqlx.Stmt - lock sync.RWMutex - uiDefaults map[string]interface{} - cache *lrucache.Cache +type UserRepository struct { + DB *sqlx.DB + driver string } -func GetUserCfgRepo() *UserCfgRepo { - userCfgRepoOnce.Do(func() { +func GetUserRepository() *UserRepository { + userRepoOnce.Do(func() { db := GetConnection() - lookupConfigStmt, err := db.DB.Preparex(`SELECT confkey, value FROM configuration WHERE configuration.username = ?`) - if err != nil { - log.Fatalf("db.DB.Preparex() error: %v", err) - } - - userCfgRepoInstance = &UserCfgRepo{ - DB: db.DB, - Lookup: lookupConfigStmt, - uiDefaults: config.Keys.UiDefaults, - cache: lrucache.New(1024), + userRepoInstance = &UserRepository{ + DB: db.DB, + driver: db.Driver, } }) - - return userCfgRepoInstance + return userRepoInstance } -// Return the personalised UI config for the currently authenticated -// user or return the plain default config. -func (uCfg *UserCfgRepo) GetUIConfig(user *auth.User) (map[string]interface{}, error) { - if user == nil { - uCfg.lock.RLock() - copy := make(map[string]interface{}, len(uCfg.uiDefaults)) - for k, v := range uCfg.uiDefaults { - copy[k] = v - } - uCfg.lock.RUnlock() - return copy, nil - } - - data := uCfg.cache.Get(user.Username, func() (interface{}, time.Duration, int) { - uiconfig := make(map[string]interface{}, len(uCfg.uiDefaults)) - for k, v := range uCfg.uiDefaults { - uiconfig[k] = v - } - - rows, err := uCfg.Lookup.Query(user.Username) - if err != nil { - log.Warnf("Error while looking up user uiconfig for user '%v'", user.Username) - return err, 0, 0 - } - - size := 0 - defer rows.Close() - for rows.Next() { - var key, rawval string - if err := rows.Scan(&key, &rawval); err != nil { - log.Warn("Error while scanning user uiconfig values") - return err, 0, 0 - } - - var val interface{} - if err := json.Unmarshal([]byte(rawval), &val); err != nil { - log.Warn("Error while unmarshaling raw user uiconfig json") - return err, 0, 0 - } - - size += len(key) - size += len(rawval) - uiconfig[key] = val - } - - // Add global ShortRunningJobsDuration setting as plot_list_hideShortRunningJobs - uiconfig["plot_list_hideShortRunningJobs"] = config.Keys.ShortRunningJobsDuration - - return uiconfig, 24 * time.Hour, size - }) - if err, ok := data.(error); ok { - log.Error("Error in returned dataset") +func (r *UserRepository) GetUser(username string) (*schema.User, error) { + user := &schema.User{Username: username} + var hashedPassword, name, rawRoles, email, rawProjects sql.NullString + if err := sq.Select("password", "ldap", "name", "roles", "email", "projects").From("user"). + Where("user.username = ?", username).RunWith(r.DB). + QueryRow().Scan(&hashedPassword, &user.AuthSource, &name, &rawRoles, &email, &rawProjects); err != nil { + log.Warnf("Error while querying user '%v' from database", username) return nil, err } - return data.(map[string]interface{}), nil -} - -// If the context does not have a user, update the global ui configuration -// without persisting it! If there is a (authenticated) user, update only his -// configuration. -func (uCfg *UserCfgRepo) UpdateConfig( - key, value string, - user *auth.User) error { - - if user == nil { - var val interface{} - if err := json.Unmarshal([]byte(value), &val); err != nil { - log.Warn("Error while unmarshaling raw user config json") - return err + user.Password = hashedPassword.String + user.Name = name.String + user.Email = email.String + if rawRoles.Valid { + if err := json.Unmarshal([]byte(rawRoles.String), &user.Roles); err != nil { + log.Warn("Error while unmarshaling raw roles from DB") + return nil, err + } + } + if rawProjects.Valid { + if err := json.Unmarshal([]byte(rawProjects.String), &user.Projects); err != nil { + return nil, err } - - uCfg.lock.Lock() - defer uCfg.lock.Unlock() - uCfg.uiDefaults[key] = val - return nil } - if _, err := uCfg.DB.Exec(`REPLACE INTO configuration (username, confkey, value) VALUES (?, ?, ?)`, user.Username, key, value); err != nil { - log.Warnf("Error while replacing user config in DB for user '%v'", user.Username) + return user, nil +} + +func (r *UserRepository) AddUser(user *schema.User) error { + rolesJson, _ := json.Marshal(user.Roles) + projectsJson, _ := json.Marshal(user.Projects) + + cols := []string{"username", "roles", "projects"} + vals := []interface{}{user.Username, string(rolesJson), string(projectsJson)} + + if user.Name != "" { + cols = append(cols, "name") + vals = append(vals, user.Name) + } + if user.Email != "" { + cols = append(cols, "email") + vals = append(vals, user.Email) + } + if user.Password != "" { + password, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost) + if err != nil { + log.Error("Error while encrypting new user password") + return err + } + cols = append(cols, "password") + vals = append(vals, string(password)) + } + + if _, err := sq.Insert("user").Columns(cols...).Values(vals...).RunWith(r.DB).Exec(); err != nil { + log.Errorf("Error while inserting new user '%v' into DB", user.Username) return err } - uCfg.cache.Del(user.Username) + log.Infof("new user %#v created (roles: %s, auth-source: %d, projects: %s)", user.Username, rolesJson, user.AuthSource, projectsJson) return nil } + +func (r *UserRepository) DelUser(username string) error { + + _, err := r.DB.Exec(`DELETE FROM user WHERE user.username = ?`, username) + log.Errorf("Error while deleting user '%s' from DB", username) + return err +} + +func (r *UserRepository) ListUsers(specialsOnly bool) ([]*schema.User, error) { + + q := sq.Select("username", "name", "email", "roles", "projects").From("user") + if specialsOnly { + q = q.Where("(roles != '[\"user\"]' AND roles != '[]')") + } + + rows, err := q.RunWith(r.DB).Query() + if err != nil { + log.Warn("Error while querying user list") + return nil, err + } + + users := make([]*schema.User, 0) + defer rows.Close() + for rows.Next() { + rawroles := "" + rawprojects := "" + user := &schema.User{} + var name, email sql.NullString + if err := rows.Scan(&user.Username, &name, &email, &rawroles, &rawprojects); err != nil { + log.Warn("Error while scanning user list") + return nil, err + } + + if err := json.Unmarshal([]byte(rawroles), &user.Roles); err != nil { + log.Warn("Error while unmarshaling raw role list") + return nil, err + } + + if err := json.Unmarshal([]byte(rawprojects), &user.Projects); err != nil { + return nil, err + } + + user.Name = name.String + user.Email = email.String + users = append(users, user) + } + return users, nil +} + +func (r *UserRepository) AddRole( + ctx context.Context, + username string, + queryrole string) error { + + newRole := strings.ToLower(queryrole) + user, err := r.GetUser(username) + if err != nil { + log.Warnf("Could not load user '%s'", username) + return err + } + + exists, valid := user.HasValidRole(newRole) + + if !valid { + return fmt.Errorf("Supplied role is no valid option : %v", newRole) + } + if exists { + return fmt.Errorf("User %v already has role %v", username, newRole) + } + + roles, _ := json.Marshal(append(user.Roles, newRole)) + if _, err := sq.Update("user").Set("roles", roles).Where("user.username = ?", username).RunWith(r.DB).Exec(); err != nil { + log.Errorf("Error while adding new role for user '%s'", user.Username) + return err + } + return nil +} + +func (r *UserRepository) RemoveRole(ctx context.Context, username string, queryrole string) error { + oldRole := strings.ToLower(queryrole) + user, err := r.GetUser(username) + if err != nil { + log.Warnf("Could not load user '%s'", username) + return err + } + + exists, valid := user.HasValidRole(oldRole) + + if !valid { + return fmt.Errorf("Supplied role is no valid option : %v", oldRole) + } + if !exists { + return fmt.Errorf("Role already deleted for user '%v': %v", username, oldRole) + } + + if oldRole == schema.GetRoleString(schema.RoleManager) && len(user.Projects) != 0 { + return fmt.Errorf("Cannot remove role 'manager' while user %s still has assigned project(s) : %v", username, user.Projects) + } + + var newroles []string + for _, r := range user.Roles { + if r != oldRole { + newroles = append(newroles, r) // Append all roles not matching requested to be deleted role + } + } + + var mroles, _ = json.Marshal(newroles) + if _, err := sq.Update("user").Set("roles", mroles).Where("user.username = ?", username).RunWith(r.DB).Exec(); err != nil { + log.Errorf("Error while removing role for user '%s'", user.Username) + return err + } + return nil +} + +func (r *UserRepository) AddProject( + ctx context.Context, + username string, + project string) error { + + user, err := r.GetUser(username) + if err != nil { + return err + } + + if !user.HasRole(schema.RoleManager) { + return fmt.Errorf("user '%s' is not a manager!", username) + } + + if user.HasProject(project) { + return fmt.Errorf("user '%s' already manages project '%s'", username, project) + } + + projects, _ := json.Marshal(append(user.Projects, project)) + if _, err := sq.Update("user").Set("projects", projects).Where("user.username = ?", username).RunWith(r.DB).Exec(); err != nil { + return err + } + + return nil +} + +func (r *UserRepository) RemoveProject(ctx context.Context, username string, project string) error { + user, err := r.GetUser(username) + if err != nil { + return err + } + + if !user.HasRole(schema.RoleManager) { + return fmt.Errorf("user '%#v' is not a manager!", username) + } + + if !user.HasProject(project) { + return fmt.Errorf("user '%#v': Cannot remove project '%#v' - Does not match!", username, project) + } + + var exists bool + var newprojects []string + for _, p := range user.Projects { + if p != project { + newprojects = append(newprojects, p) // Append all projects not matching requested to be deleted project + } else { + exists = true + } + } + + if exists == true { + var result interface{} + if len(newprojects) == 0 { + result = "[]" + } else { + result, _ = json.Marshal(newprojects) + } + if _, err := sq.Update("user").Set("projects", result).Where("user.username = ?", username).RunWith(r.DB).Exec(); err != nil { + return err + } + return nil + } else { + return fmt.Errorf("user %s already does not manage project %s", username, project) + } +} + +type ContextKey string + +const ContextUserKey ContextKey = "user" + +func GetUserFromContext(ctx context.Context) *schema.User { + x := ctx.Value(ContextUserKey) + if x == nil { + return nil + } + + return x.(*schema.User) +} + +func (r *UserRepository) FetchUserInCtx(ctx context.Context, username string) (*model.User, error) { + me := GetUserFromContext(ctx) + if me != nil && me.Username != username && + me.HasNotRoles([]schema.Role{schema.RoleAdmin, schema.RoleSupport, schema.RoleManager}) { + return nil, errors.New("forbidden") + } + + user := &model.User{Username: username} + var name, email sql.NullString + if err := sq.Select("name", "email").From("user").Where("user.username = ?", username). + RunWith(r.DB).QueryRow().Scan(&name, &email); err != nil { + if err == sql.ErrNoRows { + /* This warning will be logged *often* for non-local users, i.e. users mentioned only in job-table or archive, */ + /* since FetchUser will be called to retrieve full name and mail for every job in query/list */ + // log.Warnf("User '%s' Not found in DB", username) + return nil, nil + } + + log.Warnf("Error while fetching user '%s'", username) + return nil, err + } + + user.Name = name.String + user.Email = email.String + return user, nil +} diff --git a/internal/repository/userConfig.go b/internal/repository/userConfig.go new file mode 100644 index 0000000..fb8c3f5 --- /dev/null +++ b/internal/repository/userConfig.go @@ -0,0 +1,137 @@ +// Copyright (C) 2022 NHR@FAU, University Erlangen-Nuremberg. +// All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. +package repository + +import ( + "encoding/json" + "sync" + "time" + + "github.com/ClusterCockpit/cc-backend/internal/config" + "github.com/ClusterCockpit/cc-backend/pkg/log" + "github.com/ClusterCockpit/cc-backend/pkg/lrucache" + "github.com/ClusterCockpit/cc-backend/pkg/schema" + "github.com/jmoiron/sqlx" +) + +var ( + userCfgRepoOnce sync.Once + userCfgRepoInstance *UserCfgRepo +) + +type UserCfgRepo struct { + DB *sqlx.DB + Lookup *sqlx.Stmt + lock sync.RWMutex + uiDefaults map[string]interface{} + cache *lrucache.Cache +} + +func GetUserCfgRepo() *UserCfgRepo { + userCfgRepoOnce.Do(func() { + db := GetConnection() + + lookupConfigStmt, err := db.DB.Preparex(`SELECT confkey, value FROM configuration WHERE configuration.username = ?`) + if err != nil { + log.Fatalf("db.DB.Preparex() error: %v", err) + } + + userCfgRepoInstance = &UserCfgRepo{ + DB: db.DB, + Lookup: lookupConfigStmt, + uiDefaults: config.Keys.UiDefaults, + cache: lrucache.New(1024), + } + }) + + return userCfgRepoInstance +} + +// Return the personalised UI config for the currently authenticated +// user or return the plain default config. +func (uCfg *UserCfgRepo) GetUIConfig(user *schema.User) (map[string]interface{}, error) { + if user == nil { + uCfg.lock.RLock() + copy := make(map[string]interface{}, len(uCfg.uiDefaults)) + for k, v := range uCfg.uiDefaults { + copy[k] = v + } + uCfg.lock.RUnlock() + return copy, nil + } + + data := uCfg.cache.Get(user.Username, func() (interface{}, time.Duration, int) { + uiconfig := make(map[string]interface{}, len(uCfg.uiDefaults)) + for k, v := range uCfg.uiDefaults { + uiconfig[k] = v + } + + rows, err := uCfg.Lookup.Query(user.Username) + if err != nil { + log.Warnf("Error while looking up user uiconfig for user '%v'", user.Username) + return err, 0, 0 + } + + size := 0 + defer rows.Close() + for rows.Next() { + var key, rawval string + if err := rows.Scan(&key, &rawval); err != nil { + log.Warn("Error while scanning user uiconfig values") + return err, 0, 0 + } + + var val interface{} + if err := json.Unmarshal([]byte(rawval), &val); err != nil { + log.Warn("Error while unmarshaling raw user uiconfig json") + return err, 0, 0 + } + + size += len(key) + size += len(rawval) + uiconfig[key] = val + } + + // Add global ShortRunningJobsDuration setting as plot_list_hideShortRunningJobs + uiconfig["plot_list_hideShortRunningJobs"] = config.Keys.ShortRunningJobsDuration + + return uiconfig, 24 * time.Hour, size + }) + if err, ok := data.(error); ok { + log.Error("Error in returned dataset") + return nil, err + } + + return data.(map[string]interface{}), nil +} + +// If the context does not have a user, update the global ui configuration +// without persisting it! If there is a (authenticated) user, update only his +// configuration. +func (uCfg *UserCfgRepo) UpdateConfig( + key, value string, + user *schema.User) error { + + if user == nil { + var val interface{} + if err := json.Unmarshal([]byte(value), &val); err != nil { + log.Warn("Error while unmarshaling raw user config json") + return err + } + + uCfg.lock.Lock() + defer uCfg.lock.Unlock() + uCfg.uiDefaults[key] = val + return nil + } + + if _, err := uCfg.DB.Exec(`REPLACE INTO configuration (username, confkey, value) VALUES (?, ?, ?)`, user.Username, key, value); err != nil { + log.Warnf("Error while replacing user config in DB for user '%v'", user.Username) + return err + } + + uCfg.cache.Del(user.Username) + return nil +} diff --git a/internal/repository/user_test.go b/internal/repository/userConfig_test.go similarity index 94% rename from internal/repository/user_test.go rename to internal/repository/userConfig_test.go index ac3b0d5..0beec7b 100644 --- a/internal/repository/user_test.go +++ b/internal/repository/userConfig_test.go @@ -9,9 +9,9 @@ import ( "path/filepath" "testing" - "github.com/ClusterCockpit/cc-backend/internal/auth" "github.com/ClusterCockpit/cc-backend/internal/config" "github.com/ClusterCockpit/cc-backend/pkg/log" + "github.com/ClusterCockpit/cc-backend/pkg/schema" _ "github.com/mattn/go-sqlite3" ) @@ -53,7 +53,7 @@ func setupUserTest(t *testing.T) *UserCfgRepo { func TestGetUIConfig(t *testing.T) { r := setupUserTest(t) - u := auth.User{Username: "demo"} + u := schema.User{Username: "demo"} cfg, err := r.GetUIConfig(&u) if err != nil { diff --git a/internal/routerConfig/routes.go b/internal/routerConfig/routes.go index 5fad301..322cbf3 100644 --- a/internal/routerConfig/routes.go +++ b/internal/routerConfig/routes.go @@ -13,11 +13,11 @@ import ( "strings" "time" - "github.com/ClusterCockpit/cc-backend/internal/auth" "github.com/ClusterCockpit/cc-backend/internal/graph/model" "github.com/ClusterCockpit/cc-backend/internal/repository" "github.com/ClusterCockpit/cc-backend/internal/util" "github.com/ClusterCockpit/cc-backend/pkg/log" + "github.com/ClusterCockpit/cc-backend/pkg/schema" "github.com/ClusterCockpit/cc-backend/web" "github.com/gorilla/mux" ) @@ -81,12 +81,11 @@ func setupJobRoute(i InfoType, r *http.Request) InfoType { } func setupUserRoute(i InfoType, r *http.Request) InfoType { - jobRepo := repository.GetJobRepository() username := mux.Vars(r)["id"] i["id"] = username i["username"] = username // TODO: If forbidden (== err exists), redirect to error page - if user, _ := auth.FetchUser(r.Context(), jobRepo.DB, username); user != nil { + if user, _ := repository.GetUserRepository().FetchUserInCtx(r.Context(), username); user != nil { i["name"] = user.Name i["email"] = user.Email } @@ -125,7 +124,7 @@ func setupAnalysisRoute(i InfoType, r *http.Request) InfoType { func setupTaglistRoute(i InfoType, r *http.Request) InfoType { jobRepo := repository.GetJobRepository() - user := auth.GetUser(r.Context()) + user := repository.GetUserFromContext(r.Context()) tags, counts, err := jobRepo.CountTags(user) tagMap := make(map[string][]map[string]interface{}) @@ -255,7 +254,7 @@ func SetupRoutes(router *mux.Router, buildInfo web.Build) { for _, route := range routes { route := route router.HandleFunc(route.Route, func(rw http.ResponseWriter, r *http.Request) { - conf, err := userCfgRepo.GetUIConfig(auth.GetUser(r.Context())) + conf, err := userCfgRepo.GetUIConfig(repository.GetUserFromContext(r.Context())) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return @@ -268,9 +267,9 @@ func SetupRoutes(router *mux.Router, buildInfo web.Build) { } // Get User -> What if NIL? - user := auth.GetUser(r.Context()) + user := repository.GetUserFromContext(r.Context()) // Get Roles - availableRoles, _ := auth.GetValidRolesMap(user) + availableRoles, _ := schema.GetValidRolesMap(user) page := web.Page{ Title: title, @@ -285,14 +284,14 @@ func SetupRoutes(router *mux.Router, buildInfo web.Build) { page.FilterPresets = buildFilterPresets(r.URL.Query()) } - web.RenderTemplate(rw, r, route.Template, &page) + web.RenderTemplate(rw, route.Template, &page) }) } } func HandleSearchBar(rw http.ResponseWriter, r *http.Request, buildInfo web.Build) { - user := auth.GetUser(r.Context()) - availableRoles, _ := auth.GetValidRolesMap(user) + user := repository.GetUserFromContext(r.Context()) + availableRoles, _ := schema.GetValidRolesMap(user) if search := r.URL.Query().Get("searchId"); search != "" { repo := repository.GetJobRepository() @@ -309,10 +308,10 @@ func HandleSearchBar(rw http.ResponseWriter, r *http.Request, buildInfo web.Buil case "arrayJobId": http.Redirect(rw, r, "/monitoring/jobs/?arrayJobId="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusFound) // All Users: Redirect to Tablequery case "username": - if user.HasAnyRole([]auth.Role{auth.RoleAdmin, auth.RoleSupport, auth.RoleManager}) { + if user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport, schema.RoleManager}) { http.Redirect(rw, r, "/monitoring/users/?user="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusFound) } else { - web.RenderTemplate(rw, r, "message.tmpl", &web.Page{Title: "Error", MsgType: "alert-danger", Message: "Missing Access Rights", User: *user, Roles: availableRoles, Build: buildInfo}) + web.RenderTemplate(rw, "message.tmpl", &web.Page{Title: "Error", MsgType: "alert-danger", Message: "Missing Access Rights", User: *user, Roles: availableRoles, Build: buildInfo}) } case "name": usernames, _ := repo.FindColumnValues(user, strings.Trim(splitSearch[1], " "), "user", "username", "name") @@ -320,14 +319,14 @@ func HandleSearchBar(rw http.ResponseWriter, r *http.Request, buildInfo web.Buil joinedNames := strings.Join(usernames, "&user=") http.Redirect(rw, r, "/monitoring/users/?user="+joinedNames, http.StatusFound) } else { - if user.HasAnyRole([]auth.Role{auth.RoleAdmin, auth.RoleSupport, auth.RoleManager}) { + if user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport, schema.RoleManager}) { http.Redirect(rw, r, "/monitoring/users/?user=NoUserNameFound", http.StatusPermanentRedirect) } else { - web.RenderTemplate(rw, r, "message.tmpl", &web.Page{Title: "Error", MsgType: "alert-danger", Message: "Missing Access Rights", User: *user, Roles: availableRoles, Build: buildInfo}) + web.RenderTemplate(rw, "message.tmpl", &web.Page{Title: "Error", MsgType: "alert-danger", Message: "Missing Access Rights", User: *user, Roles: availableRoles, Build: buildInfo}) } } default: - web.RenderTemplate(rw, r, "message.tmpl", &web.Page{Title: "Warning", MsgType: "alert-warning", Message: fmt.Sprintf("Unknown search type: %s", strings.Trim(splitSearch[0], " ")), User: *user, Roles: availableRoles, Build: buildInfo}) + web.RenderTemplate(rw, "message.tmpl", &web.Page{Title: "Warning", MsgType: "alert-warning", Message: fmt.Sprintf("Unknown search type: %s", strings.Trim(splitSearch[0], " ")), User: *user, Roles: availableRoles, Build: buildInfo}) } } else if len(splitSearch) == 1 { @@ -342,13 +341,13 @@ func HandleSearchBar(rw http.ResponseWriter, r *http.Request, buildInfo web.Buil } else if jobname != "" { http.Redirect(rw, r, "/monitoring/jobs/?jobName="+url.QueryEscape(jobname), http.StatusFound) // JobName (contains) } else { - web.RenderTemplate(rw, r, "message.tmpl", &web.Page{Title: "Info", MsgType: "alert-info", Message: "Search without result", User: *user, Roles: availableRoles, Build: buildInfo}) + web.RenderTemplate(rw, "message.tmpl", &web.Page{Title: "Info", MsgType: "alert-info", Message: "Search without result", User: *user, Roles: availableRoles, Build: buildInfo}) } } else { - web.RenderTemplate(rw, r, "message.tmpl", &web.Page{Title: "Error", MsgType: "alert-danger", Message: "Searchbar query parameters malformed", User: *user, Roles: availableRoles, Build: buildInfo}) + web.RenderTemplate(rw, "message.tmpl", &web.Page{Title: "Error", MsgType: "alert-danger", Message: "Searchbar query parameters malformed", User: *user, Roles: availableRoles, Build: buildInfo}) } } else { - web.RenderTemplate(rw, r, "message.tmpl", &web.Page{Title: "Warning", MsgType: "alert-warning", Message: "Empty search", User: *user, Roles: availableRoles, Build: buildInfo}) + web.RenderTemplate(rw, "message.tmpl", &web.Page{Title: "Warning", MsgType: "alert-warning", Message: "Empty search", User: *user, Roles: availableRoles, Build: buildInfo}) } } diff --git a/pkg/schema/config.go b/pkg/schema/config.go index 95cc641..efcd6f9 100644 --- a/pkg/schema/config.go +++ b/pkg/schema/config.go @@ -17,7 +17,9 @@ type LdapConfig struct { UserFilter string `json:"user_filter"` SyncInterval string `json:"sync_interval"` // Parsed using time.ParseDuration. SyncDelOldUsers bool `json:"sync_del_old_users"` - SyncUserOnLogin bool `json:"syncUserOnLogin"` + + // Should an non-existent user be added to the DB if user exists in ldap directory + SyncUserOnLogin bool `json:"syncUserOnLogin"` } type JWTAuthConfig struct { @@ -30,10 +32,13 @@ type JWTAuthConfig struct { // Deny login for users not in database (but defined in JWT). // Ignore user roles defined in JWTs ('roles' claim), get them from db. - ForceJWTValidationViaDatabase bool `json:"forceJWTValidationViaDatabase"` + ValidateUser bool `json:"validateUser"` // Specifies which issuer should be accepted when validating external JWTs ('iss' claim) - TrustedExternalIssuer string `json:"trustedExternalIssuer"` + TrustedIssuer string `json:"trustedIssuer"` + + // Should an non-existent user be added to the DB based on the information in the token + SyncUserOnLogin bool `json:"syncUserOnLogin"` } type IntRange struct { diff --git a/internal/auth/roles.go b/pkg/schema/user.go similarity index 82% rename from internal/auth/roles.go rename to pkg/schema/user.go index e352df1..1547d3f 100644 --- a/internal/auth/roles.go +++ b/pkg/schema/user.go @@ -2,7 +2,7 @@ // All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package auth +package schema import ( "fmt" @@ -21,6 +21,41 @@ const ( RoleError ) +type AuthSource int + +const ( + AuthViaLocalPassword AuthSource = iota + AuthViaLDAP + AuthViaToken +) + +type AuthType int + +const ( + AuthToken AuthType = iota + AuthSession +) + +type User struct { + Username string `json:"username"` + Password string `json:"-"` + Name string `json:"name"` + Roles []string `json:"roles"` + AuthType AuthType `json:"authType"` + AuthSource AuthSource `json:"authSource"` + Email string `json:"email"` + Projects []string `json:"projects"` +} + +func (u *User) HasProject(project string) bool { + for _, p := range u.Projects { + if p == project { + return true + } + } + return false +} + func GetRoleString(roleInt Role) string { return [6]string{"anonymous", "api", "user", "manager", "support", "admin"}[roleInt] } @@ -44,12 +79,12 @@ func getRoleEnum(roleStr string) Role { } } -func isValidRole(role string) bool { +func IsValidRole(role string) bool { return getRoleEnum(role) != RoleError } func (u *User) HasValidRole(role string) (hasRole bool, isValid bool) { - if isValidRole(role) { + if IsValidRole(role) { for _, r := range u.Roles { if r == role { return true, true diff --git a/internal/auth/auth_test.go b/pkg/schema/user_test.go similarity index 99% rename from internal/auth/auth_test.go rename to pkg/schema/user_test.go index 81aa4eb..cd054c3 100644 --- a/internal/auth/auth_test.go +++ b/pkg/schema/user_test.go @@ -2,7 +2,7 @@ // All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package auth +package schema import ( "testing" diff --git a/web/web.go b/web/web.go index c94c2b1..8d4ce4b 100644 --- a/web/web.go +++ b/web/web.go @@ -11,7 +11,6 @@ import ( "net/http" "strings" - "github.com/ClusterCockpit/cc-backend/internal/auth" "github.com/ClusterCockpit/cc-backend/internal/config" "github.com/ClusterCockpit/cc-backend/internal/util" "github.com/ClusterCockpit/cc-backend/pkg/log" @@ -92,8 +91,8 @@ type Page struct { Title string // Page title MsgType string // For generic use in message boxes Message string // For generic use in message boxes - User auth.User // Information about the currently logged in user (Full User Info) - Roles map[string]auth.Role // Available roles for frontend render checks + User schema.User // Information about the currently logged in user (Full User Info) + Roles map[string]schema.Role // Available roles for frontend render checks Build Build // Latest information about the application Clusters []schema.ClusterConfig // List of all clusters for use in the Header FilterPresets map[string]interface{} // For pages with the Filter component, this can be used to set initial filters. @@ -101,7 +100,7 @@ type Page struct { Config map[string]interface{} // UI settings for the currently logged in user (e.g. line width, ...) } -func RenderTemplate(rw http.ResponseWriter, r *http.Request, file string, page *Page) { +func RenderTemplate(rw http.ResponseWriter, file string, page *Page) { t, ok := templates[file] if !ok { log.Errorf("WEB/WEB > template '%s' not found", file)