diff --git a/api/schema.graphqls b/api/schema.graphqls index 82c9488..1ad8511 100644 --- a/api/schema.graphqls +++ b/api/schema.graphqls @@ -160,6 +160,7 @@ type Count { type User { username: String! name: String! + project: String email: String! } diff --git a/cmd/cc-backend/main.go b/cmd/cc-backend/main.go index 7c41d73..243d7a4 100644 --- a/cmd/cc-backend/main.go +++ b/cmd/cc-backend/main.go @@ -132,7 +132,7 @@ func main() { } if err := authentication.AddUser(&auth.User{ - Username: parts[0], Password: parts[2], Roles: strings.Split(parts[1], ","), + Username: parts[0], Project: "", Password: parts[2], Roles: strings.Split(parts[1], ","), }); err != nil { log.Fatal(err) } diff --git a/internal/api/rest.go b/internal/api/rest.go index dbc778c..8559954 100644 --- a/internal/api/rest.go +++ b/internal/api/rest.go @@ -822,17 +822,26 @@ func (api *RestApi) createUser(rw http.ResponseWriter, r *http.Request) { return } - username, password, role, name, email := r.FormValue("username"), r.FormValue("password"), r.FormValue("role"), r.FormValue("name"), r.FormValue("email") + username, password, role, name, email, project := r.FormValue("username"), r.FormValue("password"), r.FormValue("role"), r.FormValue("name"), r.FormValue("email"), r.FormValue("project") if len(password) == 0 && role != auth.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.RoleManager { + http.Error(rw, "only managers require a project (can be changed later)", http.StatusBadRequest) + return + } else if (len(project) == 0 && role == auth.RoleManager) { + http.Error(rw, "managers require a project to manage (can be changed later)", http.StatusBadRequest) + return + } + if err := api.Authentication.AddUser(&auth.User{ Username: username, Name: name, Password: password, Email: email, + Project: project, Roles: []string{role}}); err != nil { http.Error(rw, err.Error(), http.StatusUnprocessableEntity) return @@ -880,6 +889,8 @@ func (api *RestApi) updateUser(rw http.ResponseWriter, r *http.Request) { // Get Values newrole := r.FormValue("add-role") delrole := r.FormValue("remove-role") + newproj := r.FormValue("add-project") + delproj := r.FormValue("remove-project") // TODO: Handle anything but roles... if newrole != "" { @@ -894,8 +905,20 @@ func (api *RestApi) updateUser(rw http.ResponseWriter, r *http.Request) { return } rw.Write([]byte("Remove Role Success")) + } else if newproj != "" { + if err := api.Authentication.AddProject(r.Context(), mux.Vars(r)["id"], newproj); err != nil { + http.Error(rw, err.Error(), http.StatusUnprocessableEntity) + return + } + rw.Write([]byte("Set Project Success")) + } else if delproj != "" { + if err := api.Authentication.RemoveProject(r.Context(), mux.Vars(r)["id"], delproj); err != nil { + http.Error(rw, err.Error(), http.StatusUnprocessableEntity) + return + } + rw.Write([]byte("Reset Project Success")) } else { - http.Error(rw, "Not Add or Del?", http.StatusInternalServerError) + http.Error(rw, "Not Add or Del [role|project]?", http.StatusInternalServerError) } } diff --git a/internal/auth/auth.go b/internal/auth/auth.go index b2e781d..a8946c9 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -21,12 +21,12 @@ import ( const ( RoleAdmin string = "admin" RoleSupport string = "support" - RoleApi string = "api" + RoleManager string = "manager" RoleUser string = "user" - RoleProject string = "project" + RoleApi string = "api" ) -var validRoles = [5]string{RoleAdmin, RoleSupport, RoleApi, RoleUser, RoleProject} +var validRoles = [5]string{RoleUser, RoleManager, RoleSupport, RoleAdmin, RoleApi} const ( AuthViaLocalPassword int8 = 0 @@ -105,6 +105,31 @@ func (u *User) HasNotRoles(queryroles []string) bool { } } +// Find highest role, returns integer +func (u *User) GetAuthLevel() int { + if (u.HasRole(RoleAdmin)) { + return 5 + } else if (u.HasRole(RoleSupport)) { + return 4 + } else if (u.HasRole(RoleManager)) { + return 3 + } else if (u.HasRole(RoleUser)) { + return 2 + } else if (u.HasRole(RoleApi)) { + return 1 + } else { + return 0 + } +} + +func (u *User) HasProject(project string) bool { + if (u.Project != "" && u.Project == project) { + return true + } else { + return false + } +} + func IsValidRole(role string) bool { for _, r := range validRoles { if r == role { @@ -156,7 +181,8 @@ func Init(db *sqlx.DB, ldap tinyint NOT NULL DEFAULT 0, /* col called "ldap" for historic reasons, fills the "AuthSource" */ name varchar(255) DEFAULT NULL, roles varchar(255) NOT NULL DEFAULT "[]", - email varchar(255) DEFAULT NULL);`) + email varchar(255) DEFAULT NULL, + project varchar(255) DEFAULT NULL);`) if err != nil { return nil, err } @@ -214,9 +240,11 @@ func (auth *Authentication) AuthViaSession( } username, _ := session.Values["username"].(string) + project, _ := session.Values["project"].(string) roles, _ := session.Values["roles"].([]string) return &User{ Username: username, + Project: project, Roles: roles, AuthSource: -1, }, nil @@ -261,6 +289,7 @@ func (auth *Authentication) Login( session.Options.MaxAge = int(auth.SessionMaxAge.Seconds()) } session.Values["username"] = user.Username + session.Values["project"] = user.Project session.Values["roles"] = user.Roles if err := auth.sessionStore.Save(r, rw, session); err != nil { log.Errorf("session save failed: %s", err.Error()) @@ -268,7 +297,7 @@ func (auth *Authentication) Login( return } - log.Infof("login successfull: user: %#v (roles: %v)", user.Username, user.Roles) + log.Infof("login successfull: user: %#v (roles: %v, project: %v)", user.Username, user.Roles, user.Project) ctx := context.WithValue(r.Context(), ContextUserKey, user) onsuccess.ServeHTTP(rw, r.WithContext(ctx)) return diff --git a/internal/auth/users.go b/internal/auth/users.go index edcf9bb..4b0e274 100644 --- a/internal/auth/users.go +++ b/internal/auth/users.go @@ -21,16 +21,17 @@ import ( func (auth *Authentication) GetUser(username string) (*User, error) { user := &User{Username: username} - var hashedPassword, name, rawRoles, email sql.NullString - if err := sq.Select("password", "ldap", "name", "roles", "email").From("user"). + var hashedPassword, name, rawRoles, email, project sql.NullString + if err := sq.Select("password", "ldap", "name", "roles", "email", "project").From("user"). Where("user.username = ?", username).RunWith(auth.db). - QueryRow().Scan(&hashedPassword, &user.AuthSource, &name, &rawRoles, &email); err != nil { + QueryRow().Scan(&hashedPassword, &user.AuthSource, &name, &rawRoles, &email, &project); err != nil { return nil, err } user.Password = hashedPassword.String user.Name = name.String user.Email = email.String + user.Project = project.String if rawRoles.Valid { if err := json.Unmarshal([]byte(rawRoles.String), &user.Roles); err != nil { return nil, err @@ -54,6 +55,10 @@ func (auth *Authentication) AddUser(user *User) error { cols = append(cols, "email") vals = append(vals, user.Email) } + if user.Project != "" { + cols = append(cols, "project") + vals = append(vals, user.Project) + } if user.Password != "" { password, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost) if err != nil { @@ -67,7 +72,7 @@ func (auth *Authentication) AddUser(user *User) error { return err } - log.Infof("new user %#v created (roles: %s, auth-source: %d)", user.Username, rolesJson, user.AuthSource) + log.Infof("new user %#v created (roles: %s, auth-source: %d, project: %s)", user.Username, rolesJson, user.AuthSource, user.Project) return nil } @@ -79,7 +84,7 @@ func (auth *Authentication) DelUser(username string) error { func (auth *Authentication) ListUsers(specialsOnly bool) ([]*User, error) { - q := sq.Select("username", "name", "email", "roles").From("user") + q := sq.Select("username", "name", "email", "roles", "project").From("user") if specialsOnly { q = q.Where("(roles != '[\"user\"]' AND roles != '[]')") } @@ -94,8 +99,8 @@ func (auth *Authentication) ListUsers(specialsOnly bool) ([]*User, error) { for rows.Next() { rawroles := "" user := &User{} - var name, email sql.NullString - if err := rows.Scan(&user.Username, &name, &email, &rawroles); err != nil { + var name, email, project sql.NullString + if err := rows.Scan(&user.Username, &name, &email, &rawroles, &project); err != nil { return nil, err } @@ -105,6 +110,7 @@ func (auth *Authentication) ListUsers(specialsOnly bool) ([]*User, error) { user.Name = name.String user.Email = email.String + user.Project = project.String users = append(users, user) } return users, nil @@ -145,6 +151,10 @@ func (auth *Authentication) RemoveRole(ctx context.Context, username string, rol return fmt.Errorf("invalid user role: %#v", role) } + if (role == RoleManager && len(user.Project) != 0) { + return fmt.Errorf("Cannot remove role 'manager' while user %#v still has an assigned project!", username) + } + var exists bool var newroles []string for _, r := range user.Roles { @@ -166,9 +176,53 @@ func (auth *Authentication) RemoveRole(ctx context.Context, username string, rol } } +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 '%#v' is not a manager!", username) + } + + if user.HasProject(project) { + return fmt.Errorf("user '%#v' already manages project '%#v'", username, project) + } + + if _, err := sq.Update("user").Set("project", project).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) + } + + if _, err := sq.Update("user").Set("project", "").Where("user.username = ?", username).Where("user.project = ?", project).RunWith(auth.db).Exec(); err != nil { + return err + } + return nil +} + func FetchUser(ctx context.Context, db *sqlx.DB, username string) (*model.User, error) { me := GetUser(ctx) - if me != nil && me.Username != username && me.HasNotRoles([]string{RoleAdmin, RoleSupport}) { + if me != nil && me.Username != username && me.HasNotRoles([]string{RoleAdmin, RoleSupport, RoleManager}) { return nil, errors.New("forbidden") } @@ -185,5 +239,6 @@ func FetchUser(ctx context.Context, db *sqlx.DB, username string) (*model.User, user.Name = name.String user.Email = email.String + // user.Project = project.String return user, nil } diff --git a/internal/graph/schema.resolvers.go b/internal/graph/schema.resolvers.go index 61d8643..941a9b2 100644 --- a/internal/graph/schema.resolvers.go +++ b/internal/graph/schema.resolvers.go @@ -152,7 +152,7 @@ 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([]string{auth.RoleAdmin, auth.RoleSupport}){ + if user := auth.GetUser(ctx); user != nil && job.User != user.Username && user.HasNotRoles([]string{auth.RoleAdmin, auth.RoleSupport, auth.RoleManager}){ return nil, errors.New("you are not allowed to see this job") } diff --git a/internal/graph/stats.go b/internal/graph/stats.go index ed74592..f7d08ef 100644 --- a/internal/graph/stats.go +++ b/internal/graph/stats.go @@ -39,9 +39,9 @@ func (r *queryResolver) jobsStatistics(ctx context.Context, filter []*model.JobF for _, cluster := range archive.Clusters { for _, subcluster := range cluster.SubClusters { corehoursCol := fmt.Sprintf("CAST(ROUND(SUM(job.duration * job.num_nodes * %d * %d) / 3600) as int)", subcluster.SocketsPerNode, subcluster.CoresPerSocket) - var query sq.SelectBuilder + var rawQuery sq.SelectBuilder if groupBy == nil { - query = sq.Select( + rawQuery = sq.Select( "''", "COUNT(job.id)", "CAST(ROUND(SUM(job.duration) / 3600) as int)", @@ -49,7 +49,7 @@ func (r *queryResolver) jobsStatistics(ctx context.Context, filter []*model.JobF ).From("job") } else { col := groupBy2column[*groupBy] - query = sq.Select( + rawQuery = sq.Select( col, "COUNT(job.id)", "CAST(ROUND(SUM(job.duration) / 3600) as int)", @@ -57,11 +57,16 @@ func (r *queryResolver) jobsStatistics(ctx context.Context, filter []*model.JobF ).From("job").GroupBy(col) } - query = query. + rawQuery = rawQuery. Where("job.cluster = ?", cluster.Name). Where("job.subcluster = ?", subcluster.Name) - query = repository.SecurityCheck(ctx, query) + query, qerr := repository.SecurityCheck(ctx, rawQuery) + + if qerr != nil { + return nil, qerr + } + for _, f := range filter { query = repository.BuildWhereClause(f, query) } @@ -97,8 +102,13 @@ func (r *queryResolver) jobsStatistics(ctx context.Context, filter []*model.JobF } if groupBy == nil { - query := sq.Select("COUNT(job.id)").From("job").Where("job.duration < ?", ShortJobDuration) - query = repository.SecurityCheck(ctx, query) + + query, qerr := repository.SecurityCheck(ctx, sq.Select("COUNT(job.id)").From("job").Where("job.duration < ?", ShortJobDuration)) + + if qerr != nil { + return nil, qerr + } + for _, f := range filter { query = repository.BuildWhereClause(f, query) } @@ -107,8 +117,13 @@ func (r *queryResolver) jobsStatistics(ctx context.Context, filter []*model.JobF } } else { col := groupBy2column[*groupBy] - query := sq.Select(col, "COUNT(job.id)").From("job").Where("job.duration < ?", ShortJobDuration) - query = repository.SecurityCheck(ctx, query) + + query, qerr := repository.SecurityCheck(ctx, sq.Select(col, "COUNT(job.id)").From("job").Where("job.duration < ?", ShortJobDuration)) + + if qerr != nil { + return nil, qerr + } + for _, f := range filter { query = repository.BuildWhereClause(f, query) } @@ -170,8 +185,13 @@ func (r *queryResolver) jobsStatistics(ctx context.Context, filter []*model.JobF // `value` must be the column grouped by, but renamed to "value". `id` and `col` can optionally be used // to add a condition to the query of the kind "