mirror of
				https://github.com/ClusterCockpit/cc-backend
				synced 2025-10-30 23:45:06 +01:00 
			
		
		
		
	Rework roles as enum, change AuthSource to enum
This commit is contained in:
		
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -3,7 +3,6 @@ | ||||
| /var/job-archive | ||||
| /var/*.db | ||||
| /var/machine-state | ||||
| /var-backup | ||||
|  | ||||
| /.env | ||||
| /config.json | ||||
|   | ||||
| @@ -178,7 +178,7 @@ func decode(r io.Reader, val interface{}) 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.RoleApi), http.StatusForbidden, rw) | ||||
| 		handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| @@ -318,7 +318,7 @@ func (api *RestApi) getJobs(rw http.ResponseWriter, r *http.Request) { | ||||
| // @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.RoleApi), http.StatusForbidden, rw) | ||||
| 		handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| @@ -383,7 +383,7 @@ func (api *RestApi) tagJob(rw http.ResponseWriter, r *http.Request) { | ||||
| // @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.RoleApi), http.StatusForbidden, rw) | ||||
| 		handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| @@ -464,7 +464,7 @@ func (api *RestApi) startJob(rw http.ResponseWriter, r *http.Request) { | ||||
| // @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.RoleApi), http.StatusForbidden, rw) | ||||
| 		handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| @@ -517,7 +517,7 @@ func (api *RestApi) stopJobById(rw http.ResponseWriter, r *http.Request) { | ||||
| // @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.RoleApi), http.StatusForbidden, rw) | ||||
| 		handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| @@ -563,7 +563,7 @@ func (api *RestApi) stopJobByRequest(rw http.ResponseWriter, r *http.Request) { | ||||
| // @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.RoleApi), http.StatusForbidden, rw) | ||||
| 		handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| @@ -611,7 +611,7 @@ func (api *RestApi) deleteJobById(rw http.ResponseWriter, r *http.Request) { | ||||
| // @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.RoleApi), http.StatusForbidden, rw) | ||||
| 		handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| @@ -667,7 +667,7 @@ func (api *RestApi) deleteJobByRequest(rw http.ResponseWriter, r *http.Request) | ||||
| // @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.RoleApi), http.StatusForbidden, rw) | ||||
| 		handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| @@ -819,15 +819,15 @@ func (api *RestApi) createUser(rw http.ResponseWriter, r *http.Request) { | ||||
| 	} | ||||
|  | ||||
| 	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 { | ||||
| 	if len(password) == 0 && role != auth.GetRoleString(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 { | ||||
| 	if len(project) != 0 && role != auth.GetRoleString(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 { | ||||
| 	} else if len(project) == 0 && role == auth.GetRoleString(auth.RoleManager) { | ||||
| 		http.Error(rw, "managers require a project to manage (can be changed later)", http.StatusBadRequest) | ||||
| 		return | ||||
| 	} | ||||
|   | ||||
| @@ -12,6 +12,7 @@ import ( | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"os" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/ClusterCockpit/cc-backend/pkg/log" | ||||
| @@ -19,36 +20,83 @@ import ( | ||||
| 	"github.com/jmoiron/sqlx" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	RoleAdmin   string = "admin" | ||||
| 	RoleSupport string = "support" | ||||
| 	RoleManager string = "manager" | ||||
| 	RoleUser    string = "user" | ||||
| 	RoleApi     string = "api" | ||||
| ) | ||||
|  | ||||
| var validRoles = [5]string{RoleUser, RoleManager, RoleSupport, RoleAdmin, RoleApi} | ||||
| type AuthSource int | ||||
|  | ||||
| const ( | ||||
| 	AuthViaLocalPassword int8 = 0 | ||||
| 	AuthViaLDAP          int8 = 1 | ||||
| 	AuthViaToken         int8 = 2 | ||||
| 	AuthViaLocalPassword AuthSource = iota | ||||
| 	AuthViaLDAP | ||||
| 	AuthViaToken | ||||
| ) | ||||
|  | ||||
| type User struct { | ||||
| 	Username   string   `json:"username"` | ||||
| 	Password   string   `json:"-"` | ||||
| 	Name       string   `json:"name"` | ||||
| 	Roles      []string `json:"roles"` | ||||
| 	AuthSource int8     `json:"via"` | ||||
| 	Email      string   `json:"email"` | ||||
| 	Projects   []string `json:"projects"` | ||||
| 	Username   string     `json:"username"` | ||||
| 	Password   string     `json:"-"` | ||||
| 	Name       string     `json:"name"` | ||||
| 	Roles      []string   `json:"roles"` | ||||
| 	AuthSource AuthSource `json:"via"` | ||||
| 	Email      string     `json:"email"` | ||||
| 	Projects   []string   `json:"projects"` | ||||
| 	Expiration time.Time | ||||
| } | ||||
|  | ||||
| func (u *User) HasRole(role string) bool { | ||||
| type Role int | ||||
|  | ||||
| const ( | ||||
| 	RoleAnonymous Role = iota | ||||
| 	RoleApi | ||||
| 	RoleUser | ||||
| 	RoleManager | ||||
| 	RoleSupport | ||||
| 	RoleAdmin | ||||
| 	RoleError | ||||
| ) | ||||
|  | ||||
| func GetRoleString(roleInt Role) string { | ||||
| 	return [6]string{"anonymous", "api", "user", "manager", "support", "admin"}[roleInt] | ||||
| } | ||||
|  | ||||
| func getRoleEnum(roleStr string) Role { | ||||
| 	switch strings.ToLower(roleStr) { | ||||
| 	case "admin": | ||||
| 		return RoleAdmin | ||||
| 	case "support": | ||||
| 		return RoleSupport | ||||
| 	case "manager": | ||||
| 		return RoleManager | ||||
| 	case "user": | ||||
| 		return RoleUser | ||||
| 	case "api": | ||||
| 		return RoleApi | ||||
| 	case "anonymous": | ||||
| 		return RoleAnonymous | ||||
| 	default: | ||||
| 		return RoleError | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func isValidRole(role string) bool { | ||||
| 	if getRoleEnum(role) == RoleError { | ||||
| 		log.Errorf("Unknown Role %s", role) | ||||
| 		return false | ||||
| 	} | ||||
| 	return true | ||||
| } | ||||
|  | ||||
| func (u *User) HasValidRole(role string) (hasRole bool, isValid bool) { | ||||
| 	if isValidRole(role) { | ||||
| 		for _, r := range u.Roles { | ||||
| 			if r == role { | ||||
| 				return true, true | ||||
| 			} | ||||
| 		} | ||||
| 		return false, true | ||||
| 	} | ||||
| 	return false, false | ||||
| } | ||||
|  | ||||
| func (u *User) HasRole(role Role) bool { | ||||
| 	for _, r := range u.Roles { | ||||
| 		if r == role { | ||||
| 		if r == GetRoleString(role) { | ||||
| 			return true | ||||
| 		} | ||||
| 	} | ||||
| @@ -56,10 +104,10 @@ func (u *User) HasRole(role string) bool { | ||||
| } | ||||
|  | ||||
| // Role-Arrays are short: performance not impacted by nested loop | ||||
| func (u *User) HasAnyRole(queryroles []string) bool { | ||||
| func (u *User) HasAnyRole(queryroles []Role) bool { | ||||
| 	for _, ur := range u.Roles { | ||||
| 		for _, qr := range queryroles { | ||||
| 			if ur == qr { | ||||
| 			if ur == GetRoleString(qr) { | ||||
| 				return true | ||||
| 			} | ||||
| 		} | ||||
| @@ -68,12 +116,12 @@ func (u *User) HasAnyRole(queryroles []string) bool { | ||||
| } | ||||
|  | ||||
| // Role-Arrays are short: performance not impacted by nested loop | ||||
| func (u *User) HasAllRoles(queryroles []string) bool { | ||||
| func (u *User) HasAllRoles(queryroles []Role) bool { | ||||
| 	target := len(queryroles) | ||||
| 	matches := 0 | ||||
| 	for _, ur := range u.Roles { | ||||
| 		for _, qr := range queryroles { | ||||
| 			if ur == qr { | ||||
| 			if ur == GetRoleString(qr) { | ||||
| 				matches += 1 | ||||
| 				break | ||||
| 			} | ||||
| @@ -88,11 +136,11 @@ func (u *User) HasAllRoles(queryroles []string) bool { | ||||
| } | ||||
|  | ||||
| // Role-Arrays are short: performance not impacted by nested loop | ||||
| func (u *User) HasNotRoles(queryroles []string) bool { | ||||
| func (u *User) HasNotRoles(queryroles []Role) bool { | ||||
| 	matches := 0 | ||||
| 	for _, ur := range u.Roles { | ||||
| 		for _, qr := range queryroles { | ||||
| 			if ur == qr { | ||||
| 			if ur == GetRoleString(qr) { | ||||
| 				matches += 1 | ||||
| 				break | ||||
| 			} | ||||
| @@ -106,20 +154,47 @@ func (u *User) HasNotRoles(queryroles []string) bool { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Find highest role, returns integer | ||||
| func (u *User) GetAuthLevel() int { | ||||
| // Called by API endpoint '/roles/' from frontend: Only required for admin config -> Check Admin Role | ||||
| func GetValidRoles(user *User) ([]string, error) { | ||||
| 	var vals []string | ||||
| 	if user.HasRole(RoleAdmin) { | ||||
| 		for i := RoleApi; i < RoleError; i++ { | ||||
| 			vals = append(vals, GetRoleString(i)) | ||||
| 		} | ||||
| 		return vals, nil | ||||
| 	} | ||||
|  | ||||
| 	return vals, fmt.Errorf("%s: only admins are allowed to fetch a list of roles", user.Username) | ||||
| } | ||||
|  | ||||
| // Called by routerConfig web.page setup in backend: Only requires known user and/or not API user | ||||
| func GetValidRolesMap(user *User) (map[string]Role, error) { | ||||
| 	named := make(map[string]Role) | ||||
| 	if user.HasNotRoles([]Role{RoleApi, RoleAnonymous}) { | ||||
| 		for i := RoleApi; i < RoleError; i++ { | ||||
| 			named[GetRoleString(i)] = i | ||||
| 		} | ||||
| 		return named, nil | ||||
| 	} | ||||
| 	return named, fmt.Errorf("Only known users are allowed to fetch a list of roles") | ||||
| } | ||||
|  | ||||
| // Find highest role | ||||
| func (u *User) GetAuthLevel() Role { | ||||
| 	if u.HasRole(RoleAdmin) { | ||||
| 		return 5 | ||||
| 		return RoleAdmin | ||||
| 	} else if u.HasRole(RoleSupport) { | ||||
| 		return 4 | ||||
| 		return RoleSupport | ||||
| 	} else if u.HasRole(RoleManager) { | ||||
| 		return 3 | ||||
| 		return RoleManager | ||||
| 	} else if u.HasRole(RoleUser) { | ||||
| 		return 2 | ||||
| 		return RoleUser | ||||
| 	} else if u.HasRole(RoleApi) { | ||||
| 		return 1 | ||||
| 		return RoleApi | ||||
| 	} else if u.HasRole(RoleAnonymous) { | ||||
| 		return RoleAnonymous | ||||
| 	} else { | ||||
| 		return 0 | ||||
| 		return RoleError | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @@ -132,24 +207,6 @@ func (u *User) HasProject(project string) bool { | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| func IsValidRole(role string) bool { | ||||
| 	for _, r := range validRoles { | ||||
| 		if r == role { | ||||
| 			return true | ||||
| 		} | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| func GetValidRoles(user *User) ([5]string, error) { | ||||
| 	var vals [5]string | ||||
| 	if !user.HasRole(RoleAdmin) { | ||||
| 		return vals, fmt.Errorf("%s: only admins are allowed to fetch a list of roles", user.Username) | ||||
| 	} else { | ||||
| 		return validRoles, nil | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func GetUser(ctx context.Context) *User { | ||||
| 	x := ctx.Value(ContextUserKey) | ||||
| 	if x == nil { | ||||
|   | ||||
| @@ -146,12 +146,16 @@ func (ja *JWTAuthenticator) Login( | ||||
| 	if rawroles, ok := claims["roles"].([]interface{}); ok { | ||||
| 		for _, rr := range rawroles { | ||||
| 			if r, ok := rr.(string); ok { | ||||
| 				roles = append(roles, r) | ||||
| 				if isValidRole(r) { | ||||
| 					roles = append(roles, r) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	if rawrole, ok := claims["roles"].(string); ok { | ||||
| 		roles = append(roles, rawrole) | ||||
| 		if isValidRole(rawrole) { | ||||
| 			roles = append(roles, rawrole) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if user == nil { | ||||
|   | ||||
| @@ -164,7 +164,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, "[\""+RoleUser+"\"]"); err != nil { | ||||
| 				username, 1, name, "[\""+GetRoleString(RoleUser)+"\"]"); err != nil { | ||||
| 				log.Errorf("User '%s' new in LDAP: Insert into DB failed", username) | ||||
| 				return err | ||||
| 			} | ||||
|   | ||||
| @@ -10,6 +10,7 @@ import ( | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/ClusterCockpit/cc-backend/internal/graph/model" | ||||
| 	"github.com/ClusterCockpit/cc-backend/pkg/log" | ||||
| @@ -133,23 +134,25 @@ func (auth *Authentication) ListUsers(specialsOnly bool) ([]*User, error) { | ||||
| func (auth *Authentication) AddRole( | ||||
| 	ctx context.Context, | ||||
| 	username string, | ||||
| 	role string) error { | ||||
| 	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 | ||||
| 	} | ||||
|  | ||||
| 	if !IsValidRole(role) { | ||||
| 		return fmt.Errorf("Invalid user role: %v", role) | ||||
| 	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) | ||||
| 	} | ||||
|  | ||||
| 	if user.HasRole(role) { | ||||
| 		return fmt.Errorf("user %#v already has role %#v", username, role) | ||||
| 	} | ||||
|  | ||||
| 	roles, _ := json.Marshal(append(user.Roles, role)) | ||||
| 	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 | ||||
| @@ -157,41 +160,40 @@ func (auth *Authentication) AddRole( | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (auth *Authentication) RemoveRole(ctx context.Context, username string, role string) error { | ||||
| 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 | ||||
| 	} | ||||
|  | ||||
| 	if !IsValidRole(role) { | ||||
| 		return fmt.Errorf("Invalid user role: %#v", role) | ||||
| 	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 role == RoleManager && len(user.Projects) != 0 { | ||||
| 	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 exists bool | ||||
| 	var newroles []string | ||||
| 	for _, r := range user.Roles { | ||||
| 		if r != role { | ||||
| 		if r != oldRole { | ||||
| 			newroles = append(newroles, r) // Append all roles not matching requested to be deleted role | ||||
| 		} else { | ||||
| 			exists = true | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if exists == true { | ||||
| 		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 | ||||
| 	} else { | ||||
| 		return fmt.Errorf("User '%v' already does not have role: %v", username, 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( | ||||
| @@ -262,7 +264,7 @@ func (auth *Authentication) RemoveProject(ctx context.Context, username string, | ||||
|  | ||||
| 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, RoleManager}) { | ||||
| 	if me != nil && me.Username != username && me.HasNotRoles([]Role{RoleAdmin, RoleSupport, RoleManager}) { | ||||
| 		return nil, errors.New("forbidden") | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -170,7 +170,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, auth.RoleManager}) { | ||||
| 	if user := auth.GetUser(ctx); user != nil && job.User != user.Username && user.HasNotRoles([]auth.Role{auth.RoleAdmin, auth.RoleSupport, auth.RoleManager}) { | ||||
| 		return nil, errors.New("you are not allowed to see this job") | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -534,7 +534,7 @@ func (r *JobRepository) FindColumnValue(user *auth.User, searchterm string, tabl | ||||
| 		compareStr = " LIKE ?" | ||||
| 		query = "%" + searchterm + "%" | ||||
| 	} | ||||
| 	if user.HasAnyRole([]string{auth.RoleAdmin, auth.RoleSupport, auth.RoleManager}) { | ||||
| 	if user.HasAnyRole([]auth.Role{auth.RoleAdmin, auth.RoleSupport, auth.RoleManager}) { | ||||
| 		err := sq.Select(table+"."+selectColumn).Distinct().From(table). | ||||
| 			Where(table+"."+whereColumn+compareStr, query). | ||||
| 			RunWith(r.stmtCache).QueryRow().Scan(&result) | ||||
| @@ -552,7 +552,7 @@ 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) { | ||||
| 	emptyResult := make([]string, 0) | ||||
| 	if user.HasAnyRole([]string{auth.RoleAdmin, auth.RoleSupport, auth.RoleManager}) { | ||||
| 	if user.HasAnyRole([]auth.Role{auth.RoleAdmin, auth.RoleSupport, auth.RoleManager}) { | ||||
| 		rows, err := sq.Select(table+"."+selectColumn).Distinct().From(table). | ||||
| 			Where(table+"."+whereColumn+" LIKE ?", fmt.Sprint("%", query, "%")). | ||||
| 			RunWith(r.stmtCache).Query() | ||||
|   | ||||
| @@ -104,7 +104,7 @@ func (r *JobRepository) CountJobs( | ||||
|  | ||||
| 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.RoleSupport, auth.RoleApi}) { // Admin & Co. : All jobs | ||||
| 	if user == nil || user.HasAnyRole([]auth.Role{auth.RoleAdmin, auth.RoleSupport, auth.RoleApi}) { // Admin & Co. : All jobs | ||||
| 		return query, nil | ||||
| 	} else if user.HasRole(auth.RoleManager) { // Manager : Add filter for managed projects' jobs only + personal jobs | ||||
| 		if len(user.Projects) != 0 { | ||||
|   | ||||
| @@ -276,16 +276,15 @@ func SetupRoutes(router *mux.Router, version string, hash string, buildTime stri | ||||
| 				title = strings.Replace(route.Title, "<ID>", id.(string), 1) | ||||
| 			} | ||||
|  | ||||
| 			username, authLevel := "", 0 | ||||
|  | ||||
| 			if user := auth.GetUser(r.Context()); user != nil { | ||||
| 				username = user.Username | ||||
| 				authLevel = user.GetAuthLevel() | ||||
| 			} | ||||
| 			// Get User -> What if NIL? | ||||
| 			user := auth.GetUser(r.Context()) | ||||
| 			// Get Roles | ||||
| 			availableRoles, _ := auth.GetValidRolesMap(user) | ||||
|  | ||||
| 			page := web.Page{ | ||||
| 				Title:  title, | ||||
| 				User:   web.User{Username: username, AuthLevel: authLevel}, | ||||
| 				User:   *user, | ||||
| 				Roles:  availableRoles, | ||||
| 				Build:  web.Build{Version: version, Hash: hash, Buildtime: buildTime}, | ||||
| 				Config: conf, | ||||
| 				Infos:  infos, | ||||
| @@ -314,7 +313,7 @@ func HandleSearchBar(rw http.ResponseWriter, r *http.Request, api *api.RestApi) | ||||
| 			case "projectId": | ||||
| 				http.Redirect(rw, r, "/monitoring/jobs/?projectMatch=eq&project="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusTemporaryRedirect) // All Users: Redirect to Tablequery | ||||
| 			case "username": | ||||
| 				if user.HasAnyRole([]string{auth.RoleAdmin, auth.RoleSupport, auth.RoleManager}) { | ||||
| 				if user.HasAnyRole([]auth.Role{auth.RoleAdmin, auth.RoleSupport, auth.RoleManager}) { | ||||
| 					http.Redirect(rw, r, "/monitoring/users/?user="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusTemporaryRedirect) | ||||
| 				} else { | ||||
| 					http.Redirect(rw, r, "/monitoring/jobs/?", http.StatusTemporaryRedirect) // Users: Redirect to Tablequery | ||||
| @@ -325,7 +324,7 @@ func HandleSearchBar(rw http.ResponseWriter, r *http.Request, api *api.RestApi) | ||||
| 					joinedNames := strings.Join(usernames, "&user=") | ||||
| 					http.Redirect(rw, r, "/monitoring/users/?user="+joinedNames, http.StatusTemporaryRedirect) | ||||
| 				} else { | ||||
| 					if user.HasAnyRole([]string{auth.RoleAdmin, auth.RoleSupport, auth.RoleManager}) { | ||||
| 					if user.HasAnyRole([]auth.Role{auth.RoleAdmin, auth.RoleSupport, auth.RoleManager}) { | ||||
| 						http.Redirect(rw, r, "/monitoring/users/?user=NoUserNameFound", http.StatusTemporaryRedirect) | ||||
| 					} else { | ||||
| 						http.Redirect(rw, r, "/monitoring/jobs/?", http.StatusTemporaryRedirect) // Users: Redirect to Tablequery | ||||
|   | ||||
| @@ -10,11 +10,11 @@ | ||||
|  | ||||
|     const ccconfig = getContext('cc-config') | ||||
|  | ||||
|     export let user | ||||
|     export let isAdmin | ||||
|  | ||||
| </script> | ||||
|  | ||||
| {#if user.AuthLevel == 5} | ||||
| {#if isAdmin == true} | ||||
| <Card style="margin-bottom: 1.5em;"> | ||||
|     <CardHeader> | ||||
|         <CardTitle class="mb-1">Admin Options</CardTitle> | ||||
|   | ||||
| @@ -4,8 +4,9 @@ | ||||
|              Dropdown, DropdownToggle, DropdownMenu, DropdownItem, InputGroupText } from 'sveltestrap' | ||||
|  | ||||
|     export let username // empty string if auth. is disabled, otherwise the username as string | ||||
|     export let authlevel // integer | ||||
|     export let authlevel // Integer | ||||
|     export let clusters // array of names | ||||
|     export let roles // Role Enum-Like | ||||
|  | ||||
|     let isOpen = false | ||||
|  | ||||
| @@ -39,9 +40,9 @@ | ||||
|     ] | ||||
|  | ||||
|     const viewsPerCluster = [ | ||||
|         { 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' }, | ||||
|         { title: 'Analysis', requiredRole: roles.support,  href: '/monitoring/analysis/', icon: 'graph-up' }, | ||||
|         { title: 'Systems',  requiredRole: roles.admin,  href: '/monitoring/systems/',  icon: 'cpu' }, | ||||
|         { title: 'Status',   requiredRole: roles.admin,  href: '/monitoring/status/',   icon: 'cpu' }, | ||||
|     ] | ||||
| </script> | ||||
|  | ||||
| @@ -52,26 +53,26 @@ | ||||
|     <NavbarToggler on:click={() => (isOpen = !isOpen)} /> | ||||
|     <Collapse {isOpen} navbar expand="lg" on:update={({ detail }) => (isOpen = detail.isOpen)}> | ||||
|         <Nav pills> | ||||
|             {#if authlevel == 5} <!-- admin --> | ||||
|             {#if authlevel == roles.admin} | ||||
|                 {#each adminviews as item} | ||||
|                     <NavLink href={item.href} active={window.location.pathname == item.href}><Icon name={item.icon}/> {item.title}</NavLink> | ||||
|                 {/each} | ||||
|             {:else if authlevel == 4} <!-- support --> | ||||
|             {:else if authlevel == roles.support} | ||||
|                 {#each supportviews as item} | ||||
|                     <NavLink href={item.href} active={window.location.pathname == item.href}><Icon name={item.icon}/> {item.title}</NavLink> | ||||
|                 {/each} | ||||
|             {:else if authlevel == 3} <!-- manager --> | ||||
|             {:else if authlevel == roles.manager} | ||||
|                 {#each managerviews as item} | ||||
|                     <NavLink href={item.href} active={window.location.pathname == item.href}><Icon name={item.icon}/> {item.title}</NavLink> | ||||
|                 {/each} | ||||
|             {:else if authlevel == 2} <!-- user --> | ||||
|             {:else if authlevel == roles.user} | ||||
|                 {#each userviews as item} | ||||
|                     <NavLink href={item.href} active={window.location.pathname == item.href}><Icon name={item.icon}/> {item.title}</NavLink> | ||||
|                 {/each} | ||||
|             {:else} | ||||
|                 <p>API User or Unauthorized!</p> | ||||
|             {/if} | ||||
|             {#each viewsPerCluster.filter(item => item.authLevel <= authlevel) as item} | ||||
|             {#each viewsPerCluster.filter(item => item.requiredRole <= authlevel) as item} | ||||
|                 <NavItem> | ||||
|                     <Dropdown nav inNavbar> | ||||
|                         <DropdownToggle nav caret> | ||||
| @@ -94,7 +95,7 @@ | ||||
|             <InputGroup> | ||||
|                 <Input type="text" placeholder="Search 'type:<query>' ..." name="searchId"/> | ||||
|                 <Button outline type="submit"><Icon name="search"/></Button> | ||||
|                 <InputGroupText style="cursor:help;" title={(authlevel >= 4) ? "Example: 'projectId:a100cd', Types are: jobId | jobName | projectId | username | name" : "Example: 'jobName:myjob', Types are jobId | jobName | projectId"}><Icon name="info-circle"/></InputGroupText> | ||||
|                 <InputGroupText style="cursor:help;" title={(authlevel >= roles.support) ? "Example: 'projectId:a100cd', Types are: jobId | jobName | projectId | username | name" : "Example: 'jobName:myjob', Types are jobId | jobName | projectId"}><Icon name="info-circle"/></InputGroupText> | ||||
|             </InputGroup> | ||||
|         </form> | ||||
|         {#if username} | ||||
|   | ||||
| @@ -14,7 +14,8 @@ | ||||
|     const ccconfig = getContext('cc-config') | ||||
|  | ||||
|     export let filterPresets = {} | ||||
|     export let authLevel | ||||
|     export let authlevel | ||||
|     export let roles | ||||
|  | ||||
|     let filters, jobList, matchedJobs = null | ||||
|     let sorting = { field: 'startTime', order: 'DESC' }, isSortingOpen = false, isMetricsSelectionOpen = false | ||||
| @@ -61,7 +62,7 @@ | ||||
|     </Col> | ||||
|  | ||||
|     <Col xs="3" style="margin-left: auto;"> | ||||
|         <UserOrProject bind:authLevel={authLevel} on:update={({ detail }) => filters.update(detail)}/> | ||||
|         <UserOrProject bind:authlevel={authlevel} bind:roles={roles} on:update={({ detail }) => filters.update(detail)}/> | ||||
|     </Col> | ||||
|     <Col xs="2"> | ||||
|         <Refresher on:reload={() => jobList.update()} /> | ||||
|   | ||||
| @@ -4,7 +4,7 @@ import Config from './Config.root.svelte' | ||||
| new Config({ | ||||
|     target: document.getElementById('svelte-app'), | ||||
|     props: { | ||||
|         user: user | ||||
|         isAdmin: isAdmin | ||||
|     }, | ||||
|     context: new Map([ | ||||
|             ['cc-config', clusterCockpitConfig] | ||||
|   | ||||
| @@ -6,7 +6,8 @@ | ||||
|  | ||||
|     export let user = '' | ||||
|     export let project = '' | ||||
|     export let authLevel | ||||
|     export let authlevel | ||||
|     export let roles | ||||
|     let mode = 'user', term = '' | ||||
|     const throttle = 500 | ||||
|  | ||||
| @@ -23,7 +24,7 @@ | ||||
|  | ||||
|     let timeoutId = null | ||||
|     function termChanged(sleep = throttle) { | ||||
|         if (authLevel == 2) { | ||||
|         if (authlevel == roles.user) { | ||||
|             project = term | ||||
|  | ||||
|             if (timeoutId != null) | ||||
| @@ -34,7 +35,7 @@ | ||||
|                     project | ||||
|                 }) | ||||
|             }, sleep) | ||||
|         } else if (authLevel >= 3) { | ||||
|         } else if (authlevel >= roles.manager) { | ||||
|             if (mode == 'user') | ||||
|                 user = term | ||||
|             else | ||||
| @@ -53,13 +54,13 @@ | ||||
|     } | ||||
| </script> | ||||
|  | ||||
| {#if authLevel == 2} | ||||
| {#if authlevel == roles.user} | ||||
|     <InputGroup> | ||||
|         <Input | ||||
|             type="text" bind:value={term} on:change={() => termChanged()} on:keyup={(event) => termChanged(event.key == 'Enter' ? 0 : throttle)} placeholder='filter project...' | ||||
|         /> | ||||
|     </InputGroup> | ||||
| {:else if authLevel >= 3} | ||||
| {:else if authlevel >= roles.manager} | ||||
|     <InputGroup> | ||||
|         <select style="max-width: 175px;" class="form-select" | ||||
|             bind:value={mode} on:change={modeChanged}> | ||||
|   | ||||
| @@ -5,7 +5,8 @@ new Jobs({ | ||||
|     target: document.getElementById('svelte-app'), | ||||
|     props: { | ||||
|         filterPresets: filterPresets, | ||||
|         authLevel: authLevel | ||||
|         authlevel: authlevel, | ||||
|         roles: roles | ||||
|     }, | ||||
|     context: new Map([ | ||||
|             ['cc-config', clusterCockpitConfig] | ||||
|   | ||||
| @@ -15,9 +15,10 @@ | ||||
|         {{end}} | ||||
|         <script> | ||||
|             const header = { | ||||
|                 "username": "{{ .User.Username }}", | ||||
|                 "authlevel": {{ .User.AuthLevel }}, | ||||
|                 "clusters":  {{ .Clusters }}, | ||||
|                 "username":  "{{ .User.Username }}", | ||||
|                 "authlevel":  {{ .User.GetAuthLevel }}, | ||||
|                 "clusters":   {{ .Clusters }}, | ||||
|                 "roles":      {{ .Roles }} | ||||
|             }; | ||||
|         </script> | ||||
|     </head> | ||||
|   | ||||
| @@ -7,7 +7,7 @@ | ||||
| {{end}} | ||||
| {{define "javascript"}} | ||||
|     <script> | ||||
|         const user = {{ .User }}; | ||||
|         const isAdmin = {{ .User.HasRole .Roles.admin }}; | ||||
|         const filterPresets = {{ .FilterPresets }}; | ||||
|         const clusterCockpitConfig = {{ .Config }}; | ||||
|     </script> | ||||
|   | ||||
| @@ -9,14 +9,14 @@ | ||||
|                     <th>Running Jobs (short ones not listed)</th> | ||||
|                     <th>Total Jobs</th> | ||||
|                     <th>Short Jobs in past 24h</th> | ||||
|                     {{if ge .User.AuthLevel 4}} | ||||
|                     {{if .User.HasRole .Roles.admin}} | ||||
|                         <th>System View</th> | ||||
|                         <th>Analysis View</th> | ||||
|                     {{end}} | ||||
|                 </tr> | ||||
|             </thead> | ||||
|             <tbody> | ||||
|                 {{if ge .User.AuthLevel 4}} | ||||
|                 {{if .User.HasRole .Roles.admin}} | ||||
|                     {{range .Infos.clusters}} | ||||
|                         <tr> | ||||
|                             <td>{{.Name}}</td> | ||||
|   | ||||
| @@ -10,7 +10,8 @@ | ||||
|     <script> | ||||
|         const filterPresets = {{ .FilterPresets }}; | ||||
|         const clusterCockpitConfig = {{ .Config }}; | ||||
|         const authLevel = {{ .User.AuthLevel }}; | ||||
|         const authlevel = {{ .User.GetAuthLevel }}; | ||||
|         const roles = {{ .Roles }}; | ||||
|     </script> | ||||
|     <script src='/build/jobs.js'></script> | ||||
| {{end}} | ||||
|   | ||||
| @@ -11,6 +11,7 @@ import ( | ||||
| 	"net/http" | ||||
| 	"strings" | ||||
|  | ||||
| 	"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" | ||||
| @@ -55,11 +56,6 @@ func init() { | ||||
| 	_ = base | ||||
| } | ||||
|  | ||||
| type User struct { | ||||
| 	Username  string // Username of the currently logged in user | ||||
| 	AuthLevel int    // Level of authorization | ||||
| } | ||||
|  | ||||
| type Build struct { | ||||
| 	Version   string | ||||
| 	Hash      string | ||||
| @@ -70,7 +66,8 @@ type Page struct { | ||||
| 	Title         string                 // Page title | ||||
| 	Error         string                 // For generic use (e.g. the exact error message on /login) | ||||
| 	Info          string                 // For generic use (e.g. "Logout successfull" on /login) | ||||
| 	User          User                   // Information about the currently logged in user | ||||
| 	User          auth.User              // Information about the currently logged in user (Full User Info) | ||||
| 	Roles         map[string]auth.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. | ||||
|   | ||||
		Reference in New Issue
	
	Block a user