Rework roles as enum, change AuthSource to enum

This commit is contained in:
Christoph Kluge 2023-03-06 11:44:38 +01:00
parent df44bd9621
commit f37e7c26f6
21 changed files with 205 additions and 141 deletions

1
.gitignore vendored
View File

@ -3,7 +3,6 @@
/var/job-archive /var/job-archive
/var/*.db /var/*.db
/var/machine-state /var/machine-state
/var-backup
/.env /.env
/config.json /config.json

View File

@ -178,7 +178,7 @@ func decode(r io.Reader, val interface{}) error {
// @router /jobs/ [get] // @router /jobs/ [get]
func (api *RestApi) getJobs(rw http.ResponseWriter, r *http.Request) { func (api *RestApi) getJobs(rw http.ResponseWriter, r *http.Request) {
if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) { 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 return
} }
@ -318,7 +318,7 @@ func (api *RestApi) getJobs(rw http.ResponseWriter, r *http.Request) {
// @router /jobs/tag_job/{id} [post] // @router /jobs/tag_job/{id} [post]
func (api *RestApi) tagJob(rw http.ResponseWriter, r *http.Request) { func (api *RestApi) tagJob(rw http.ResponseWriter, r *http.Request) {
if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) { 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 return
} }
@ -383,7 +383,7 @@ func (api *RestApi) tagJob(rw http.ResponseWriter, r *http.Request) {
// @router /jobs/start_job/ [post] // @router /jobs/start_job/ [post]
func (api *RestApi) startJob(rw http.ResponseWriter, r *http.Request) { func (api *RestApi) startJob(rw http.ResponseWriter, r *http.Request) {
if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) { 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 return
} }
@ -464,7 +464,7 @@ func (api *RestApi) startJob(rw http.ResponseWriter, r *http.Request) {
// @router /jobs/stop_job/{id} [post] // @router /jobs/stop_job/{id} [post]
func (api *RestApi) stopJobById(rw http.ResponseWriter, r *http.Request) { func (api *RestApi) stopJobById(rw http.ResponseWriter, r *http.Request) {
if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) { 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 return
} }
@ -517,7 +517,7 @@ func (api *RestApi) stopJobById(rw http.ResponseWriter, r *http.Request) {
// @router /jobs/stop_job/ [post] // @router /jobs/stop_job/ [post]
func (api *RestApi) stopJobByRequest(rw http.ResponseWriter, r *http.Request) { func (api *RestApi) stopJobByRequest(rw http.ResponseWriter, r *http.Request) {
if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) { 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 return
} }
@ -563,7 +563,7 @@ func (api *RestApi) stopJobByRequest(rw http.ResponseWriter, r *http.Request) {
// @router /jobs/delete_job/{id} [delete] // @router /jobs/delete_job/{id} [delete]
func (api *RestApi) deleteJobById(rw http.ResponseWriter, r *http.Request) { func (api *RestApi) deleteJobById(rw http.ResponseWriter, r *http.Request) {
if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) { 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 return
} }
@ -611,7 +611,7 @@ func (api *RestApi) deleteJobById(rw http.ResponseWriter, r *http.Request) {
// @router /jobs/delete_job/ [delete] // @router /jobs/delete_job/ [delete]
func (api *RestApi) deleteJobByRequest(rw http.ResponseWriter, r *http.Request) { func (api *RestApi) deleteJobByRequest(rw http.ResponseWriter, r *http.Request) {
if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) { 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 return
} }
@ -667,7 +667,7 @@ func (api *RestApi) deleteJobByRequest(rw http.ResponseWriter, r *http.Request)
// @router /jobs/delete_job_before/{ts} [delete] // @router /jobs/delete_job_before/{ts} [delete]
func (api *RestApi) deleteJobBefore(rw http.ResponseWriter, r *http.Request) { func (api *RestApi) deleteJobBefore(rw http.ResponseWriter, r *http.Request) {
if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) { 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 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") 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) http.Error(rw, "Only API users are allowed to have a blank password (login will be impossible)", http.StatusBadRequest)
return 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) http.Error(rw, "only managers require a project (can be changed later)", http.StatusBadRequest)
return 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) http.Error(rw, "managers require a project to manage (can be changed later)", http.StatusBadRequest)
return return
} }

View File

@ -12,6 +12,7 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
"strings"
"time" "time"
"github.com/ClusterCockpit/cc-backend/pkg/log" "github.com/ClusterCockpit/cc-backend/pkg/log"
@ -19,20 +20,12 @@ import (
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
) )
const ( type AuthSource int
RoleAdmin string = "admin"
RoleSupport string = "support"
RoleManager string = "manager"
RoleUser string = "user"
RoleApi string = "api"
)
var validRoles = [5]string{RoleUser, RoleManager, RoleSupport, RoleAdmin, RoleApi}
const ( const (
AuthViaLocalPassword int8 = 0 AuthViaLocalPassword AuthSource = iota
AuthViaLDAP int8 = 1 AuthViaLDAP
AuthViaToken int8 = 2 AuthViaToken
) )
type User struct { type User struct {
@ -40,15 +33,70 @@ type User struct {
Password string `json:"-"` Password string `json:"-"`
Name string `json:"name"` Name string `json:"name"`
Roles []string `json:"roles"` Roles []string `json:"roles"`
AuthSource int8 `json:"via"` AuthSource AuthSource `json:"via"`
Email string `json:"email"` Email string `json:"email"`
Projects []string `json:"projects"` Projects []string `json:"projects"`
Expiration time.Time 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 { for _, r := range u.Roles {
if r == role { 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 == GetRoleString(role) {
return true return true
} }
} }
@ -56,10 +104,10 @@ func (u *User) HasRole(role string) bool {
} }
// Role-Arrays are short: performance not impacted by nested loop // 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 _, ur := range u.Roles {
for _, qr := range queryroles { for _, qr := range queryroles {
if ur == qr { if ur == GetRoleString(qr) {
return true return true
} }
} }
@ -68,12 +116,12 @@ func (u *User) HasAnyRole(queryroles []string) bool {
} }
// Role-Arrays are short: performance not impacted by nested loop // 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) target := len(queryroles)
matches := 0 matches := 0
for _, ur := range u.Roles { for _, ur := range u.Roles {
for _, qr := range queryroles { for _, qr := range queryroles {
if ur == qr { if ur == GetRoleString(qr) {
matches += 1 matches += 1
break break
} }
@ -88,11 +136,11 @@ func (u *User) HasAllRoles(queryroles []string) bool {
} }
// Role-Arrays are short: performance not impacted by nested loop // 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 matches := 0
for _, ur := range u.Roles { for _, ur := range u.Roles {
for _, qr := range queryroles { for _, qr := range queryroles {
if ur == qr { if ur == GetRoleString(qr) {
matches += 1 matches += 1
break break
} }
@ -106,20 +154,47 @@ func (u *User) HasNotRoles(queryroles []string) bool {
} }
} }
// Find highest role, returns integer // Called by API endpoint '/roles/' from frontend: Only required for admin config -> Check Admin Role
func (u *User) GetAuthLevel() int { 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) { if u.HasRole(RoleAdmin) {
return 5 return RoleAdmin
} else if u.HasRole(RoleSupport) { } else if u.HasRole(RoleSupport) {
return 4 return RoleSupport
} else if u.HasRole(RoleManager) { } else if u.HasRole(RoleManager) {
return 3 return RoleManager
} else if u.HasRole(RoleUser) { } else if u.HasRole(RoleUser) {
return 2 return RoleUser
} else if u.HasRole(RoleApi) { } else if u.HasRole(RoleApi) {
return 1 return RoleApi
} else if u.HasRole(RoleAnonymous) {
return RoleAnonymous
} else { } else {
return 0 return RoleError
} }
} }
@ -132,24 +207,6 @@ func (u *User) HasProject(project string) bool {
return false 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 { func GetUser(ctx context.Context) *User {
x := ctx.Value(ContextUserKey) x := ctx.Value(ContextUserKey)
if x == nil { if x == nil {

View File

@ -146,13 +146,17 @@ func (ja *JWTAuthenticator) Login(
if rawroles, ok := claims["roles"].([]interface{}); ok { if rawroles, ok := claims["roles"].([]interface{}); ok {
for _, rr := range rawroles { for _, rr := range rawroles {
if r, ok := rr.(string); ok { if r, ok := rr.(string); ok {
if isValidRole(r) {
roles = append(roles, r) roles = append(roles, r)
} }
} }
} }
}
if rawrole, ok := claims["roles"].(string); ok { if rawrole, ok := claims["roles"].(string); ok {
if isValidRole(rawrole) {
roles = append(roles, rawrole) roles = append(roles, rawrole)
} }
}
if user == nil { if user == nil {
user, err = ja.auth.GetUser(sub) user, err = ja.auth.GetUser(sub)

View File

@ -164,7 +164,7 @@ func (la *LdapAuthenticator) Sync() error {
name := newnames[username] name := newnames[username]
log.Debugf("sync: add %v (name: %v, roles: [user], ldap: true)", username, name) 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 (?, ?, ?, ?)`, 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) log.Errorf("User '%s' new in LDAP: Insert into DB failed", username)
return err return err
} }

View File

@ -10,6 +10,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"strings"
"github.com/ClusterCockpit/cc-backend/internal/graph/model" "github.com/ClusterCockpit/cc-backend/internal/graph/model"
"github.com/ClusterCockpit/cc-backend/pkg/log" "github.com/ClusterCockpit/cc-backend/pkg/log"
@ -133,23 +134,25 @@ func (auth *Authentication) ListUsers(specialsOnly bool) ([]*User, error) {
func (auth *Authentication) AddRole( func (auth *Authentication) AddRole(
ctx context.Context, ctx context.Context,
username string, username string,
role string) error { queryrole string) error {
newRole := strings.ToLower(queryrole)
user, err := auth.GetUser(username) user, err := auth.GetUser(username)
if err != nil { if err != nil {
log.Warnf("Could not load user '%s'", username) log.Warnf("Could not load user '%s'", username)
return err return err
} }
if !IsValidRole(role) { exists, valid := user.HasValidRole(newRole)
return fmt.Errorf("Invalid user role: %v", role)
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) { roles, _ := json.Marshal(append(user.Roles, newRole))
return fmt.Errorf("user %#v already has role %#v", username, role)
}
roles, _ := json.Marshal(append(user.Roles, role))
if _, err := sq.Update("user").Set("roles", roles).Where("user.username = ?", username).RunWith(auth.db).Exec(); err != nil { 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) log.Errorf("Error while adding new role for user '%s'", user.Username)
return err return err
@ -157,41 +160,40 @@ func (auth *Authentication) AddRole(
return nil 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) user, err := auth.GetUser(username)
if err != nil { if err != nil {
log.Warnf("Could not load user '%s'", username) log.Warnf("Could not load user '%s'", username)
return err return err
} }
if !IsValidRole(role) { exists, valid := user.HasValidRole(oldRole)
return fmt.Errorf("Invalid user role: %#v", role)
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) 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 var newroles []string
for _, r := range user.Roles { 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 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) var mroles, _ = json.Marshal(newroles)
if _, err := sq.Update("user").Set("roles", mroles).Where("user.username = ?", username).RunWith(auth.db).Exec(); err != nil { 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) log.Errorf("Error while removing role for user '%s'", user.Username)
return err return err
} }
return nil return nil
} else {
return fmt.Errorf("User '%v' already does not have role: %v", username, role)
}
} }
func (auth *Authentication) AddProject( 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) { func FetchUser(ctx context.Context, db *sqlx.DB, username string) (*model.User, error) {
me := GetUser(ctx) 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") return nil, errors.New("forbidden")
} }

View File

@ -170,7 +170,7 @@ func (r *queryResolver) Job(ctx context.Context, id string) (*schema.Job, error)
return nil, err 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") return nil, errors.New("you are not allowed to see this job")
} }

View File

@ -534,7 +534,7 @@ func (r *JobRepository) FindColumnValue(user *auth.User, searchterm string, tabl
compareStr = " LIKE ?" compareStr = " LIKE ?"
query = "%" + searchterm + "%" 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). err := sq.Select(table+"."+selectColumn).Distinct().From(table).
Where(table+"."+whereColumn+compareStr, query). Where(table+"."+whereColumn+compareStr, query).
RunWith(r.stmtCache).QueryRow().Scan(&result) 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) { func (r *JobRepository) FindColumnValues(user *auth.User, query string, table string, selectColumn string, whereColumn string) (results []string, err error) {
emptyResult := make([]string, 0) 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). rows, err := sq.Select(table+"."+selectColumn).Distinct().From(table).
Where(table+"."+whereColumn+" LIKE ?", fmt.Sprint("%", query, "%")). Where(table+"."+whereColumn+" LIKE ?", fmt.Sprint("%", query, "%")).
RunWith(r.stmtCache).Query() RunWith(r.stmtCache).Query()

View File

@ -104,7 +104,7 @@ func (r *JobRepository) CountJobs(
func SecurityCheck(ctx context.Context, query sq.SelectBuilder) (queryOut sq.SelectBuilder, err error) { func SecurityCheck(ctx context.Context, query sq.SelectBuilder) (queryOut sq.SelectBuilder, err error) {
user := auth.GetUser(ctx) 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 return query, nil
} else if user.HasRole(auth.RoleManager) { // Manager : Add filter for managed projects' jobs only + personal jobs } else if user.HasRole(auth.RoleManager) { // Manager : Add filter for managed projects' jobs only + personal jobs
if len(user.Projects) != 0 { if len(user.Projects) != 0 {

View File

@ -276,16 +276,15 @@ func SetupRoutes(router *mux.Router, version string, hash string, buildTime stri
title = strings.Replace(route.Title, "<ID>", id.(string), 1) title = strings.Replace(route.Title, "<ID>", id.(string), 1)
} }
username, authLevel := "", 0 // Get User -> What if NIL?
user := auth.GetUser(r.Context())
if user := auth.GetUser(r.Context()); user != nil { // Get Roles
username = user.Username availableRoles, _ := auth.GetValidRolesMap(user)
authLevel = user.GetAuthLevel()
}
page := web.Page{ page := web.Page{
Title: title, Title: title,
User: web.User{Username: username, AuthLevel: authLevel}, User: *user,
Roles: availableRoles,
Build: web.Build{Version: version, Hash: hash, Buildtime: buildTime}, Build: web.Build{Version: version, Hash: hash, Buildtime: buildTime},
Config: conf, Config: conf,
Infos: infos, Infos: infos,
@ -314,7 +313,7 @@ func HandleSearchBar(rw http.ResponseWriter, r *http.Request, api *api.RestApi)
case "projectId": case "projectId":
http.Redirect(rw, r, "/monitoring/jobs/?projectMatch=eq&project="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusTemporaryRedirect) // All Users: Redirect to Tablequery http.Redirect(rw, r, "/monitoring/jobs/?projectMatch=eq&project="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusTemporaryRedirect) // All Users: Redirect to Tablequery
case "username": 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) http.Redirect(rw, r, "/monitoring/users/?user="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusTemporaryRedirect)
} else { } else {
http.Redirect(rw, r, "/monitoring/jobs/?", http.StatusTemporaryRedirect) // Users: Redirect to Tablequery 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=") joinedNames := strings.Join(usernames, "&user=")
http.Redirect(rw, r, "/monitoring/users/?user="+joinedNames, http.StatusTemporaryRedirect) http.Redirect(rw, r, "/monitoring/users/?user="+joinedNames, http.StatusTemporaryRedirect)
} else { } 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) http.Redirect(rw, r, "/monitoring/users/?user=NoUserNameFound", http.StatusTemporaryRedirect)
} else { } else {
http.Redirect(rw, r, "/monitoring/jobs/?", http.StatusTemporaryRedirect) // Users: Redirect to Tablequery http.Redirect(rw, r, "/monitoring/jobs/?", http.StatusTemporaryRedirect) // Users: Redirect to Tablequery

View File

@ -10,11 +10,11 @@
const ccconfig = getContext('cc-config') const ccconfig = getContext('cc-config')
export let user export let isAdmin
</script> </script>
{#if user.AuthLevel == 5} {#if isAdmin == true}
<Card style="margin-bottom: 1.5em;"> <Card style="margin-bottom: 1.5em;">
<CardHeader> <CardHeader>
<CardTitle class="mb-1">Admin Options</CardTitle> <CardTitle class="mb-1">Admin Options</CardTitle>

View File

@ -4,8 +4,9 @@
Dropdown, DropdownToggle, DropdownMenu, DropdownItem, InputGroupText } from 'sveltestrap' Dropdown, DropdownToggle, DropdownMenu, DropdownItem, InputGroupText } from 'sveltestrap'
export let username // empty string if auth. is disabled, otherwise the username as string 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 clusters // array of names
export let roles // Role Enum-Like
let isOpen = false let isOpen = false
@ -39,9 +40,9 @@
] ]
const viewsPerCluster = [ const viewsPerCluster = [
{ title: 'Analysis', authLevel: 4, href: '/monitoring/analysis/', icon: 'graph-up' }, { title: 'Analysis', requiredRole: roles.support, href: '/monitoring/analysis/', icon: 'graph-up' },
{ title: 'Systems', authLevel: 5, href: '/monitoring/systems/', icon: 'cpu' }, { title: 'Systems', requiredRole: roles.admin, href: '/monitoring/systems/', icon: 'cpu' },
{ title: 'Status', authLevel: 5, href: '/monitoring/status/', icon: 'cpu' }, { title: 'Status', requiredRole: roles.admin, href: '/monitoring/status/', icon: 'cpu' },
] ]
</script> </script>
@ -52,26 +53,26 @@
<NavbarToggler on:click={() => (isOpen = !isOpen)} /> <NavbarToggler on:click={() => (isOpen = !isOpen)} />
<Collapse {isOpen} navbar expand="lg" on:update={({ detail }) => (isOpen = detail.isOpen)}> <Collapse {isOpen} navbar expand="lg" on:update={({ detail }) => (isOpen = detail.isOpen)}>
<Nav pills> <Nav pills>
{#if authlevel == 5} <!-- admin --> {#if authlevel == roles.admin}
{#each adminviews as item} {#each adminviews as item}
<NavLink href={item.href} active={window.location.pathname == item.href}><Icon name={item.icon}/> {item.title}</NavLink> <NavLink href={item.href} active={window.location.pathname == item.href}><Icon name={item.icon}/> {item.title}</NavLink>
{/each} {/each}
{:else if authlevel == 4} <!-- support --> {:else if authlevel == roles.support}
{#each supportviews as item} {#each supportviews as item}
<NavLink href={item.href} active={window.location.pathname == item.href}><Icon name={item.icon}/> {item.title}</NavLink> <NavLink href={item.href} active={window.location.pathname == item.href}><Icon name={item.icon}/> {item.title}</NavLink>
{/each} {/each}
{:else if authlevel == 3} <!-- manager --> {:else if authlevel == roles.manager}
{#each managerviews as item} {#each managerviews as item}
<NavLink href={item.href} active={window.location.pathname == item.href}><Icon name={item.icon}/> {item.title}</NavLink> <NavLink href={item.href} active={window.location.pathname == item.href}><Icon name={item.icon}/> {item.title}</NavLink>
{/each} {/each}
{:else if authlevel == 2} <!-- user --> {:else if authlevel == roles.user}
{#each userviews as item} {#each userviews as item}
<NavLink href={item.href} active={window.location.pathname == item.href}><Icon name={item.icon}/> {item.title}</NavLink> <NavLink href={item.href} active={window.location.pathname == item.href}><Icon name={item.icon}/> {item.title}</NavLink>
{/each} {/each}
{:else} {:else}
<p>API User or Unauthorized!</p> <p>API User or Unauthorized!</p>
{/if} {/if}
{#each viewsPerCluster.filter(item => item.authLevel <= authlevel) as item} {#each viewsPerCluster.filter(item => item.requiredRole <= authlevel) as item}
<NavItem> <NavItem>
<Dropdown nav inNavbar> <Dropdown nav inNavbar>
<DropdownToggle nav caret> <DropdownToggle nav caret>
@ -94,7 +95,7 @@
<InputGroup> <InputGroup>
<Input type="text" placeholder="Search 'type:<query>' ..." name="searchId"/> <Input type="text" placeholder="Search 'type:<query>' ..." name="searchId"/>
<Button outline type="submit"><Icon name="search"/></Button> <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> </InputGroup>
</form> </form>
{#if username} {#if username}

View File

@ -14,7 +14,8 @@
const ccconfig = getContext('cc-config') const ccconfig = getContext('cc-config')
export let filterPresets = {} export let filterPresets = {}
export let authLevel export let authlevel
export let roles
let filters, jobList, matchedJobs = null let filters, jobList, matchedJobs = null
let sorting = { field: 'startTime', order: 'DESC' }, isSortingOpen = false, isMetricsSelectionOpen = false let sorting = { field: 'startTime', order: 'DESC' }, isSortingOpen = false, isMetricsSelectionOpen = false
@ -61,7 +62,7 @@
</Col> </Col>
<Col xs="3" style="margin-left: auto;"> <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>
<Col xs="2"> <Col xs="2">
<Refresher on:reload={() => jobList.update()} /> <Refresher on:reload={() => jobList.update()} />

View File

@ -4,7 +4,7 @@ import Config from './Config.root.svelte'
new Config({ new Config({
target: document.getElementById('svelte-app'), target: document.getElementById('svelte-app'),
props: { props: {
user: user isAdmin: isAdmin
}, },
context: new Map([ context: new Map([
['cc-config', clusterCockpitConfig] ['cc-config', clusterCockpitConfig]

View File

@ -6,7 +6,8 @@
export let user = '' export let user = ''
export let project = '' export let project = ''
export let authLevel export let authlevel
export let roles
let mode = 'user', term = '' let mode = 'user', term = ''
const throttle = 500 const throttle = 500
@ -23,7 +24,7 @@
let timeoutId = null let timeoutId = null
function termChanged(sleep = throttle) { function termChanged(sleep = throttle) {
if (authLevel == 2) { if (authlevel == roles.user) {
project = term project = term
if (timeoutId != null) if (timeoutId != null)
@ -34,7 +35,7 @@
project project
}) })
}, sleep) }, sleep)
} else if (authLevel >= 3) { } else if (authlevel >= roles.manager) {
if (mode == 'user') if (mode == 'user')
user = term user = term
else else
@ -53,13 +54,13 @@
} }
</script> </script>
{#if authLevel == 2} {#if authlevel == roles.user}
<InputGroup> <InputGroup>
<Input <Input
type="text" bind:value={term} on:change={() => termChanged()} on:keyup={(event) => termChanged(event.key == 'Enter' ? 0 : throttle)} placeholder='filter project...' type="text" bind:value={term} on:change={() => termChanged()} on:keyup={(event) => termChanged(event.key == 'Enter' ? 0 : throttle)} placeholder='filter project...'
/> />
</InputGroup> </InputGroup>
{:else if authLevel >= 3} {:else if authlevel >= roles.manager}
<InputGroup> <InputGroup>
<select style="max-width: 175px;" class="form-select" <select style="max-width: 175px;" class="form-select"
bind:value={mode} on:change={modeChanged}> bind:value={mode} on:change={modeChanged}>

View File

@ -5,7 +5,8 @@ new Jobs({
target: document.getElementById('svelte-app'), target: document.getElementById('svelte-app'),
props: { props: {
filterPresets: filterPresets, filterPresets: filterPresets,
authLevel: authLevel authlevel: authlevel,
roles: roles
}, },
context: new Map([ context: new Map([
['cc-config', clusterCockpitConfig] ['cc-config', clusterCockpitConfig]

View File

@ -16,8 +16,9 @@
<script> <script>
const header = { const header = {
"username": "{{ .User.Username }}", "username": "{{ .User.Username }}",
"authlevel": {{ .User.AuthLevel }}, "authlevel": {{ .User.GetAuthLevel }},
"clusters": {{ .Clusters }}, "clusters": {{ .Clusters }},
"roles": {{ .Roles }}
}; };
</script> </script>
</head> </head>

View File

@ -7,7 +7,7 @@
{{end}} {{end}}
{{define "javascript"}} {{define "javascript"}}
<script> <script>
const user = {{ .User }}; const isAdmin = {{ .User.HasRole .Roles.admin }};
const filterPresets = {{ .FilterPresets }}; const filterPresets = {{ .FilterPresets }};
const clusterCockpitConfig = {{ .Config }}; const clusterCockpitConfig = {{ .Config }};
</script> </script>

View File

@ -9,14 +9,14 @@
<th>Running Jobs (short ones not listed)</th> <th>Running Jobs (short ones not listed)</th>
<th>Total Jobs</th> <th>Total Jobs</th>
<th>Short Jobs in past 24h</th> <th>Short Jobs in past 24h</th>
{{if ge .User.AuthLevel 4}} {{if .User.HasRole .Roles.admin}}
<th>System View</th> <th>System View</th>
<th>Analysis View</th> <th>Analysis View</th>
{{end}} {{end}}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{{if ge .User.AuthLevel 4}} {{if .User.HasRole .Roles.admin}}
{{range .Infos.clusters}} {{range .Infos.clusters}}
<tr> <tr>
<td>{{.Name}}</td> <td>{{.Name}}</td>

View File

@ -10,7 +10,8 @@
<script> <script>
const filterPresets = {{ .FilterPresets }}; const filterPresets = {{ .FilterPresets }};
const clusterCockpitConfig = {{ .Config }}; const clusterCockpitConfig = {{ .Config }};
const authLevel = {{ .User.AuthLevel }}; const authlevel = {{ .User.GetAuthLevel }};
const roles = {{ .Roles }};
</script> </script>
<script src='/build/jobs.js'></script> <script src='/build/jobs.js'></script>
{{end}} {{end}}

View File

@ -11,6 +11,7 @@ import (
"net/http" "net/http"
"strings" "strings"
"github.com/ClusterCockpit/cc-backend/internal/auth"
"github.com/ClusterCockpit/cc-backend/internal/config" "github.com/ClusterCockpit/cc-backend/internal/config"
"github.com/ClusterCockpit/cc-backend/pkg/log" "github.com/ClusterCockpit/cc-backend/pkg/log"
"github.com/ClusterCockpit/cc-backend/pkg/schema" "github.com/ClusterCockpit/cc-backend/pkg/schema"
@ -55,11 +56,6 @@ func init() {
_ = base _ = base
} }
type User struct {
Username string // Username of the currently logged in user
AuthLevel int // Level of authorization
}
type Build struct { type Build struct {
Version string Version string
Hash string Hash string
@ -70,7 +66,8 @@ type Page struct {
Title string // Page title Title string // Page title
Error string // For generic use (e.g. the exact error message on /login) Error string // For generic use (e.g. the exact error message on /login)
Info string // For generic use (e.g. "Logout successfull" 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 Build Build // Latest information about the application
Clusters []schema.ClusterConfig // List of all clusters for use in the Header 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. FilterPresets map[string]interface{} // For pages with the Filter component, this can be used to set initial filters.