From 834f9d908508a41578e84fd129c905b0acac7b7a Mon Sep 17 00:00:00 2001 From: Christoph Kluge Date: Wed, 25 Jan 2023 16:59:16 +0100 Subject: [PATCH 01/20] Add role helper functions, add project role barebone, add valid role arr - HasAnyRoles([]string): Checks if user has *one* of the roles - HasAllRoles([]string): Cheks if user has *all* of the roles - HasNotRoles([]string): Checks if user has *none* of the roles - IsValidRole(string): Checks if given string is known valid role --- internal/auth/auth.go | 64 ++++++++++++++++++++++++++++++ internal/auth/users.go | 12 +++--- internal/graph/schema.resolvers.go | 2 +- internal/repository/job.go | 4 +- internal/repository/query.go | 2 +- 5 files changed, 73 insertions(+), 11 deletions(-) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 22fa0db..b2e781d 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -23,8 +23,11 @@ const ( RoleSupport string = "support" RoleApi string = "api" RoleUser string = "user" + RoleProject string = "project" ) +var validRoles = [5]string{RoleAdmin, RoleSupport, RoleApi, RoleUser, RoleProject} + const ( AuthViaLocalPassword int8 = 0 AuthViaLDAP int8 = 1 @@ -38,6 +41,7 @@ type User struct { Roles []string `json:"roles"` AuthSource int8 `json:"via"` Email string `json:"email"` + Project string `json:"project"` Expiration time.Time } @@ -50,6 +54,66 @@ func (u *User) HasRole(role string) bool { return false } +// Role-Arrays are short: performance not impacted by nested loop +func (u *User) HasAnyRole(queryroles []string) bool { + for _, ur := range u.Roles { + for _, qr := range queryroles { + if ur == qr { + return true + } + } + } + return false +} + +// Role-Arrays are short: performance not impacted by nested loop +func (u *User) HasAllRoles(queryroles []string) bool { + target := len(queryroles) + matches := 0 + for _, ur := range u.Roles { + for _, qr := range queryroles { + if ur == qr { + matches += 1 + break + } + } + } + + if matches == target { + return true + } else { + return false + } +} + +// Role-Arrays are short: performance not impacted by nested loop +func (u *User) HasNotRoles(queryroles []string) bool { + matches := 0 + for _, ur := range u.Roles { + for _, qr := range queryroles { + if ur == qr { + matches += 1 + break + } + } + } + + if matches == 0 { + return true + } else { + return false + } +} + +func IsValidRole(role string) bool { + for _, r := range validRoles { + if r == role { + return true + } + } + return false +} + func GetUser(ctx context.Context) *User { x := ctx.Value(ContextUserKey) if x == nil { diff --git a/internal/auth/users.go b/internal/auth/users.go index aea5eb3..edcf9bb 100644 --- a/internal/auth/users.go +++ b/internal/auth/users.go @@ -120,14 +120,12 @@ func (auth *Authentication) AddRole( return err } - if role != RoleAdmin && role != RoleApi && role != RoleUser && role != RoleSupport { + if !IsValidRole(role) { return fmt.Errorf("invalid user role: %#v", role) } - for _, r := range user.Roles { - if r == role { - return fmt.Errorf("user %#v already has role %#v", username, role) - } + if user.HasRole(role) { + return fmt.Errorf("user %#v already has role %#v", username, role) } roles, _ := json.Marshal(append(user.Roles, role)) @@ -143,7 +141,7 @@ func (auth *Authentication) RemoveRole(ctx context.Context, username string, rol return err } - if role != RoleAdmin && role != RoleApi && role != RoleUser && role != RoleSupport { + if !IsValidRole(role) { return fmt.Errorf("invalid user role: %#v", role) } @@ -170,7 +168,7 @@ func (auth *Authentication) RemoveRole(ctx context.Context, username string, rol func FetchUser(ctx context.Context, db *sqlx.DB, username string) (*model.User, error) { me := GetUser(ctx) - if me != nil && !me.HasRole(RoleAdmin) && !me.HasRole(RoleSupport) && me.Username != username { + if me != nil && me.Username != username && me.HasNotRoles([]string{RoleAdmin, RoleSupport}) { return nil, errors.New("forbidden") } diff --git a/internal/graph/schema.resolvers.go b/internal/graph/schema.resolvers.go index 1aa8a04..61d8643 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 && !user.HasRole(auth.RoleAdmin) && !user.HasRole(auth.RoleSupport) && job.User != user.Username { + if user := auth.GetUser(ctx); user != nil && job.User != user.Username && user.HasNotRoles([]string{auth.RoleAdmin, auth.RoleSupport}){ return nil, errors.New("you are not allowed to see this job") } diff --git a/internal/repository/job.go b/internal/repository/job.go index f305cd0..b7bba51 100644 --- a/internal/repository/job.go +++ b/internal/repository/job.go @@ -423,7 +423,7 @@ func (r *JobRepository) FindJobOrUser(ctx context.Context, searchterm string) (j user := auth.GetUser(ctx) if id, err := strconv.Atoi(searchterm); err == nil { qb := sq.Select("job.id").From("job").Where("job.job_id = ?", id) - if user != nil && !user.HasRole(auth.RoleAdmin) && !user.HasRole(auth.RoleSupport) { + if user != nil && user.HasNotRoles([]string{auth.RoleAdmin, auth.RoleSupport}) { qb = qb.Where("job.user = ?", user.Username) } @@ -435,7 +435,7 @@ func (r *JobRepository) FindJobOrUser(ctx context.Context, searchterm string) (j } } - if user == nil || user.HasRole(auth.RoleAdmin) || user.HasRole(auth.RoleSupport) { + if user == nil || user.HasAnyRole([]string{auth.RoleAdmin, auth.RoleSupport}) { err := sq.Select("job.user").Distinct().From("job"). Where("job.user = ?", searchterm). RunWith(r.stmtCache).QueryRow().Scan(&username) diff --git a/internal/repository/query.go b/internal/repository/query.go index fad6091..a58af17 100644 --- a/internal/repository/query.go +++ b/internal/repository/query.go @@ -94,7 +94,7 @@ func (r *JobRepository) CountJobs( func SecurityCheck(ctx context.Context, query sq.SelectBuilder) sq.SelectBuilder { user := auth.GetUser(ctx) - if user == nil || user.HasRole(auth.RoleAdmin) || user.HasRole(auth.RoleApi) || user.HasRole(auth.RoleSupport) { + if user == nil || user.HasAnyRole([]string{auth.RoleAdmin, auth.RoleApi, auth.RoleSupport}) { return query } From b2aed2f16ba594d01e6072d6ef4d264e0cb366d8 Mon Sep 17 00:00:00 2001 From: Christoph Kluge Date: Fri, 27 Jan 2023 18:36:58 +0100 Subject: [PATCH 02/20] Add 'project' to user table, add 'manager' role, conditional web render - Addresses issues #40 #45 #82 - Reworked Navigation Header for all roles - 'Manager' role added, can be assigned a project-id in config by admins - BREAKING! -> Added 'project' column in SQLite3 table 'user' - Manager-Assigned project will be added to all graphql filters: Only show Jobs and Users of given project - 'My Jobs' Tab for all Roles - Switched from Bool "isAdmin" to integer authLevels - Removed critical data frontend logging - Reworked repo.query.SecurityCheck() --- api/schema.graphqls | 1 + cmd/cc-backend/main.go | 2 +- internal/api/rest.go | 27 +++++- internal/auth/auth.go | 39 +++++++- internal/auth/users.go | 71 ++++++++++++-- internal/graph/schema.resolvers.go | 2 +- internal/graph/stats.go | 42 +++++--- internal/repository/job.go | 8 +- internal/repository/query.go | 30 ++++-- internal/routerConfig/routes.go | 12 ++- web/frontend/src/Config.root.svelte | 2 +- web/frontend/src/Header.svelte | 69 ++++++++++--- web/frontend/src/Jobs.root.svelte | 4 + web/frontend/src/List.root.svelte | 19 +++- web/frontend/src/Node.root.svelte | 2 +- web/frontend/src/StatsTable.svelte | 4 +- web/frontend/src/config/AdminSettings.svelte | 4 + web/frontend/src/config/admin/AddUser.svelte | 33 ++++--- .../src/config/admin/EditProject.svelte | 97 +++++++++++++++++++ web/frontend/src/config/admin/EditRole.svelte | 1 + .../src/config/admin/ShowUsers.svelte | 1 + .../src/config/admin/ShowUsersRow.svelte | 1 + web/frontend/src/filters/Filters.svelte | 4 +- web/frontend/src/filters/Resources.svelte | 4 +- web/frontend/src/joblist/JobList.svelte | 21 +++- web/frontend/src/jobs.entrypoint.js | 4 +- web/frontend/src/list.entrypoint.js | 2 + web/frontend/src/plots/MetricPlot.svelte | 4 +- web/templates/base.tmpl | 3 +- web/templates/home.tmpl | 4 +- web/templates/monitoring/jobs.tmpl | 2 + web/templates/monitoring/list.tmpl | 2 + web/web.go | 4 +- 33 files changed, 433 insertions(+), 92 deletions(-) create mode 100644 web/frontend/src/config/admin/EditProject.svelte 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 " = ". func (r *queryResolver) jobsStatisticsHistogram(ctx context.Context, value string, filters []*model.JobFilter, id, col string) ([]*model.HistoPoint, error) { - query := sq.Select(value, "COUNT(job.id) AS count").From("job") - query = repository.SecurityCheck(ctx, query) + + query, qerr := repository.SecurityCheck(ctx, sq.Select(value, "COUNT(job.id) AS count").From("job")) + + if qerr != nil { + return nil, qerr + } + for _, f := range filters { query = repository.BuildWhereClause(f, query) } diff --git a/internal/repository/job.go b/internal/repository/job.go index b7bba51..3ef1be1 100644 --- a/internal/repository/job.go +++ b/internal/repository/job.go @@ -295,8 +295,12 @@ func (r *JobRepository) CountGroupedJobs(ctx context.Context, aggreg model.Aggre } } - q := sq.Select("job."+string(aggreg), count).From("job").GroupBy("job." + string(aggreg)).OrderBy("count DESC") - q = SecurityCheck(ctx, q) + q, qerr := SecurityCheck(ctx, sq.Select("job."+string(aggreg), count).From("job").GroupBy("job." + string(aggreg)).OrderBy("count DESC")) + + if qerr != nil { + return nil, qerr + } + for _, f := range filters { q = BuildWhereClause(f, q) } diff --git a/internal/repository/query.go b/internal/repository/query.go index a58af17..7a1b8e6 100644 --- a/internal/repository/query.go +++ b/internal/repository/query.go @@ -26,8 +26,11 @@ func (r *JobRepository) QueryJobs( page *model.PageRequest, order *model.OrderByInput) ([]*schema.Job, error) { - query := sq.Select(jobColumns...).From("job") - query = SecurityCheck(ctx, query) + query, qerr := SecurityCheck(ctx, sq.Select(jobColumns...).From("job")) + + if qerr != nil { + return nil, qerr + } if order != nil { field := toSnakeCase(order.Field) @@ -79,8 +82,12 @@ func (r *JobRepository) CountJobs( filters []*model.JobFilter) (int, error) { // count all jobs: - query := sq.Select("count(*)").From("job") - query = SecurityCheck(ctx, query) + query, qerr := SecurityCheck(ctx, sq.Select("count(*)").From("job")) + + if qerr != nil { + return 0, qerr + } + for _, f := range filters { query = BuildWhereClause(f, query) } @@ -92,13 +99,18 @@ func (r *JobRepository) CountJobs( return count, nil } -func SecurityCheck(ctx context.Context, query sq.SelectBuilder) sq.SelectBuilder { +func SecurityCheck(ctx context.Context, query sq.SelectBuilder) (queryOut sq.SelectBuilder, err error) { user := auth.GetUser(ctx) - if user == nil || user.HasAnyRole([]string{auth.RoleAdmin, auth.RoleApi, auth.RoleSupport}) { - return query + if user == nil || user.HasAnyRole([]string{auth.RoleAdmin, auth.RoleSupport, auth.RoleApi}) { + return query, nil + } else if (user.HasRole(auth.RoleManager)) { // Manager (Might be doublefiltered by frontend: should not matter) + return query.Where("job.project = ?", user.Project), nil + } else if (user.HasRole(auth.RoleUser)) { // User + return query.Where("job.user = ?", user.Username), nil + } else { // Unauthorized + var qnil sq.SelectBuilder + return qnil, errors.New(fmt.Sprintf("User '%s' with unknown roles! [%#v]\n", user.Username, user.Roles)) } - - return query.Where("job.user = ?", user.Username) } // Build a sq.SelectBuilder out of a schema.JobFilter. diff --git a/internal/routerConfig/routes.go b/internal/routerConfig/routes.go index 9424df7..5fae877 100644 --- a/internal/routerConfig/routes.go +++ b/internal/routerConfig/routes.go @@ -103,9 +103,11 @@ func setupUserRoute(i InfoType, r *http.Request) InfoType { 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 { i["name"] = user.Name i["email"] = user.Email + // i["project"] = user.Project } return i } @@ -270,17 +272,17 @@ func SetupRoutes(router *mux.Router, version string, hash string, buildTime stri title = strings.Replace(route.Title, "", id.(string), 1) } - username, isAdmin, isSupporter := "", true, true + username, project, authLevel := "", "", 0 if user := auth.GetUser(r.Context()); user != nil { - username = user.Username - isAdmin = user.HasRole(auth.RoleAdmin) - isSupporter = user.HasRole(auth.RoleSupport) + username = user.Username + project = user.Project + authLevel = user.GetAuthLevel() } page := web.Page{ Title: title, - User: web.User{Username: username, IsAdmin: isAdmin, IsSupporter: isSupporter}, + User: web.User{Username: username, Project: project, AuthLevel: authLevel}, Build: web.Build{Version: version, Hash: hash, Buildtime: buildTime}, Config: conf, Infos: infos, diff --git a/web/frontend/src/Config.root.svelte b/web/frontend/src/Config.root.svelte index 6b1eb40..910455f 100644 --- a/web/frontend/src/Config.root.svelte +++ b/web/frontend/src/Config.root.svelte @@ -14,7 +14,7 @@ -{#if user.IsAdmin} +{#if user.AuthLevel == 5} Admin Options diff --git a/web/frontend/src/Header.svelte b/web/frontend/src/Header.svelte index 95675f4..195c3c1 100644 --- a/web/frontend/src/Header.svelte +++ b/web/frontend/src/Header.svelte @@ -4,23 +4,44 @@ Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'sveltestrap' export let username // empty string if auth. is disabled, otherwise the username as string - export let isAdmin // boolean + export let project // empty string if user has no project in db (= not manager), otherwise the managed projectid as string + export let authlevel // integer export let clusters // array of names let isOpen = false - const views = [ - isAdmin - ? { title: 'Jobs', adminOnly: false, href: '/monitoring/jobs/', icon: 'card-list' } - : { title: 'My Jobs', adminOnly: false, href: `/monitoring/user/${username}`, icon: 'bar-chart-line-fill' }, - { title: 'Users', adminOnly: true, href: '/monitoring/users/', icon: 'people-fill' }, - { title: 'Projects', adminOnly: true, href: '/monitoring/projects/', icon: 'folder' }, - { title: 'Tags', adminOnly: false, href: '/monitoring/tags/', icon: 'tags' } + const userviews = [ + { title: 'My Jobs', href: `/monitoring/user/${username}`, icon: 'bar-chart-line-fill' }, + { title: 'Tags', href: '/monitoring/tags/', icon: 'tags' } ] + + const managerviews = [ + { title: 'My Jobs', href: `/monitoring/user/${username}`, icon: 'bar-chart-line-fill' }, + { title: `'${project}' Jobs`, href: '/monitoring/jobs/', icon: 'card-list' }, + { title: `'${project}' Users`, href: '/monitoring/users/', icon: 'people-fill' }, + { title: 'Tags', href: '/monitoring/tags/', icon: 'tags' } + ] + + const supportviews = [ + { title: 'My Jobs', href: `/monitoring/user/${username}`, icon: 'bar-chart-line-fill' }, + { title: 'Jobs', href: '/monitoring/jobs/', icon: 'card-list' }, + { title: 'Users', href: '/monitoring/users/', icon: 'people-fill' }, + { title: 'Projects', href: '/monitoring/projects/', icon: 'folder' }, + { title: 'Tags', href: '/monitoring/tags/', icon: 'tags' } + ] + + const adminviews = [ + { title: 'My Jobs', href: `/monitoring/user/${username}`, icon: 'bar-chart-line-fill' }, + { title: 'Jobs', href: '/monitoring/jobs/', icon: 'card-list' }, + { title: 'Users', href: '/monitoring/users/', icon: 'people-fill' }, + { title: 'Projects', href: '/monitoring/projects/', icon: 'folder' }, + { title: 'Tags', href: '/monitoring/tags/', icon: 'tags' } + ] + const viewsPerCluster = [ - { title: 'Analysis', adminOnly: true, href: '/monitoring/analysis/', icon: 'graph-up' }, - { title: 'Systems', adminOnly: true, href: '/monitoring/systems/', icon: 'cpu' }, - { title: 'Status', adminOnly: true, href: '/monitoring/status/', icon: 'cpu' }, + { title: 'Analysis', authLevel: 4, href: '/monitoring/analysis/', icon: 'graph-up' }, + { title: 'Systems', authLevel: 5, href: '/monitoring/systems/', icon: 'cpu' }, + { title: 'Status', authLevel: 5, href: '/monitoring/status/', icon: 'cpu' }, ] @@ -31,10 +52,26 @@ (isOpen = !isOpen)} /> (isOpen = detail.isOpen)}>