Merge pull request #83 from ClusterCockpit/40_45_82_update_roles

Update Roles and Role Handling 
 Fixes Issues #40 #45 #82
This commit is contained in:
Jan Eitzinger 2023-04-07 08:23:49 +02:00 committed by GitHub
commit 13c31155fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 965 additions and 431 deletions

View File

@ -72,7 +72,7 @@ func main() {
flag.BoolVar(&flagMigrateDB, "migrate-db", false, "Migrate database to supported version and exit") flag.BoolVar(&flagMigrateDB, "migrate-db", false, "Migrate database to supported version and exit")
flag.BoolVar(&flagLogDateTime, "logdate", false, "Set this flag to add date and time to log messages") flag.BoolVar(&flagLogDateTime, "logdate", false, "Set this flag to add date and time to log messages")
flag.StringVar(&flagConfigFile, "config", "./config.json", "Specify alternative path to `config.json`") flag.StringVar(&flagConfigFile, "config", "./config.json", "Specify alternative path to `config.json`")
flag.StringVar(&flagNewUser, "add-user", "", "Add a new user. Argument format: `<username>:[admin,support,api,user]:<password>`") flag.StringVar(&flagNewUser, "add-user", "", "Add a new user. Argument format: `<username>:[admin,support,manager,api,user]:<password>`")
flag.StringVar(&flagDelUser, "del-user", "", "Remove user by `username`") flag.StringVar(&flagDelUser, "del-user", "", "Remove user by `username`")
flag.StringVar(&flagGenJWT, "jwt", "", "Generate and print a JWT for the user specified by its `username`") flag.StringVar(&flagGenJWT, "jwt", "", "Generate and print a JWT for the user specified by its `username`")
flag.StringVar(&flagImportJob, "import-job", "", "Import a job. Argument format: `<path-to-meta.json>:<path-to-data.json>,...`") flag.StringVar(&flagImportJob, "import-job", "", "Import a job. Argument format: `<path-to-meta.json>:<path-to-data.json>,...`")
@ -142,7 +142,7 @@ func main() {
} }
if err := authentication.AddUser(&auth.User{ if err := authentication.AddUser(&auth.User{
Username: parts[0], Password: parts[2], Roles: strings.Split(parts[1], ","), Username: parts[0], Projects: make([]string, 0), Password: parts[2], Roles: strings.Split(parts[1], ","),
}); err != nil { }); err != nil {
log.Fatalf("adding '%s' user authentication failed: %v", parts[0], err) log.Fatalf("adding '%s' user authentication failed: %v", parts[0], err)
} }

View File

@ -1,13 +1,13 @@
## Docs for ClusterCockpit Searchbar # Docs for ClusterCockpit Searchbar
### Usage ## Usage
* Searchtags are implemented as `type:<query>` search-string * Searchtags are implemented as `type:<query>` search-string
* Types `jobId, jobName, projectId, username, name` for roles `admin` and `support` * Types `jobId, jobName, projectId, username, name` for roles `admin` and `support`
* `jobName` is jobName as persisted in `job.meta_data` table-column * `jobName` is jobName as persisted in `job.meta_data` table-column
* `username` is actual account identifier as persisted in `job.user` table-column * `username` is actual account identifier as persisted in `job.user` table-column
* `name` is account owners name as persisted in `user.name` table-column * `name` is account owners name as persisted in `user.name` table-column
* Types `jobId, jobName` for role `user` * Types `jobId, jobName, projectId` for role `user`
* Examples: * Examples:
* `jobName:myJob12` * `jobName:myJob12`
* `jobId:123456` * `jobId:123456`
@ -16,22 +16,15 @@
* If no searchTag used: Best guess search with the following hierarchy * If no searchTag used: Best guess search with the following hierarchy
* `jobId -> username -> name -> projectId -> jobName` * `jobId -> username -> name -> projectId -> jobName`
* Destinations: * Destinations:
* JobId: Always Job-Table (Allows multiple identical matches, e.g. JobIds from different clusters) * JobId: Job-Table (Allows multiple identical matches, e.g. JobIds from different clusters)
* JobName: Always Job-Table (Allows multiple identical matches, e.g. JobNames from different clusters) * JobName: Job-Table (Allows multiple identical matches, e.g. JobNames from different clusters)
* ProjectId: Always Job-Table * ProjectId: Job-Table
* Username * Username: Users-Table
* If *one* match found: Opens detailed user-view (`/monitoring/user/$USER`) * **Please Note**: Only users with jobs will be shown in table! I.e., Users without jobs will be missing in table.
* If *multiple* matches found: Opens user-table with matches listed (`/monitoring/users/`) * Name: Users-Table
* **Please Note**: Only users with jobs will be shown in table! I.e., "multiple matches" can still be only one entry in table. * **Please Note**: Only users with jobs will be shown in table! I.e., Users without jobs will be missing in table.
* Name
* If *one* matching username found: Opens detailed user-view (`/monitoring/user/$USER`)
* If *multiple* usernames found: Opens user-table with matches listed (`/monitoring/users/`)
* **Please Note**: Only users with jobs will be shown in table! I.e., "multiple matches" can still be only one entry in table.
* Best guess search always redirects to Job-Table or `/monitoring/user/$USER` (first username match) * Best guess search always redirects to Job-Table or `/monitoring/user/$USER` (first username match)
* Simple HTML Error if ... * Unprocessable queries will redirect to `/monitoring/jobs/?`
* Best guess search fails -> 'Not Found'
* Query `type` is unknown
* More than two colons in string -> 'malformed'
* Spaces trimmed (both for searchTag and queryString) * Spaces trimmed (both for searchTag and queryString)
* ` job12` == `job12` * ` job12` == `job12`
* `projectID : abcd ` == `projectId:abcd` * `projectID : abcd ` == `projectId:abcd`

View File

@ -76,6 +76,7 @@ func (api *RestApi) MountRoutes(r *mux.Router) {
if api.Authentication != nil { if api.Authentication != nil {
r.HandleFunc("/jwt/", api.getJWT).Methods(http.MethodGet) r.HandleFunc("/jwt/", api.getJWT).Methods(http.MethodGet)
r.HandleFunc("/roles/", api.getRoles).Methods(http.MethodGet)
r.HandleFunc("/users/", api.createUser).Methods(http.MethodPost, http.MethodPut) r.HandleFunc("/users/", api.createUser).Methods(http.MethodPost, http.MethodPut)
r.HandleFunc("/users/", api.getUsers).Methods(http.MethodGet) r.HandleFunc("/users/", api.getUsers).Methods(http.MethodGet)
r.HandleFunc("/users/", api.deleteUser).Methods(http.MethodDelete) r.HandleFunc("/users/", api.deleteUser).Methods(http.MethodDelete)
@ -177,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
} }
@ -317,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
} }
@ -382,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
} }
@ -463,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
} }
@ -516,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
} }
@ -562,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
} }
@ -610,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
} }
@ -666,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
} }
@ -817,17 +818,26 @@ func (api *RestApi) createUser(rw http.ResponseWriter, r *http.Request) {
return 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 { 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.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.GetRoleString(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{ if err := api.Authentication.AddUser(&auth.User{
Username: username, Username: username,
Name: name, Name: name,
Password: password, Password: password,
Email: email, Email: email,
Projects: []string{project},
Roles: []string{role}}); err != nil { Roles: []string{role}}); err != nil {
http.Error(rw, err.Error(), http.StatusUnprocessableEntity) http.Error(rw, err.Error(), http.StatusUnprocessableEntity)
return return
@ -866,6 +876,22 @@ func (api *RestApi) getUsers(rw http.ResponseWriter, r *http.Request) {
json.NewEncoder(rw).Encode(users) json.NewEncoder(rw).Encode(users)
} }
func (api *RestApi) getRoles(rw http.ResponseWriter, r *http.Request) {
user := auth.GetUser(r.Context())
if !user.HasRole(auth.RoleAdmin) {
http.Error(rw, "only admins are allowed to fetch a list of roles", http.StatusForbidden)
return
}
roles, err := auth.GetValidRoles(user)
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(rw).Encode(roles)
}
func (api *RestApi) updateUser(rw http.ResponseWriter, r *http.Request) { func (api *RestApi) updateUser(rw http.ResponseWriter, r *http.Request) {
if user := auth.GetUser(r.Context()); !user.HasRole(auth.RoleAdmin) { if user := auth.GetUser(r.Context()); !user.HasRole(auth.RoleAdmin) {
http.Error(rw, "Only admins are allowed to update a user", http.StatusForbidden) http.Error(rw, "Only admins are allowed to update a user", http.StatusForbidden)
@ -875,6 +901,8 @@ func (api *RestApi) updateUser(rw http.ResponseWriter, r *http.Request) {
// Get Values // Get Values
newrole := r.FormValue("add-role") newrole := r.FormValue("add-role")
delrole := r.FormValue("remove-role") delrole := r.FormValue("remove-role")
newproj := r.FormValue("add-project")
delproj := r.FormValue("remove-project")
// TODO: Handle anything but roles... // TODO: Handle anything but roles...
if newrole != "" { if newrole != "" {
@ -889,8 +917,20 @@ func (api *RestApi) updateUser(rw http.ResponseWriter, r *http.Request) {
return return
} }
rw.Write([]byte("Remove Role Success")) 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("Add 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("Remove Project Success"))
} else { } else {
http.Error(rw, "Not Add or Del?", http.StatusInternalServerError) http.Error(rw, "Not Add or Del [role|project]?", http.StatusInternalServerError)
} }
} }

View File

@ -9,8 +9,10 @@ import (
"crypto/rand" "crypto/rand"
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt"
"net/http" "net/http"
"os" "os"
"strings"
"time" "time"
"github.com/ClusterCockpit/cc-backend/pkg/log" "github.com/ClusterCockpit/cc-backend/pkg/log"
@ -18,17 +20,12 @@ import (
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
) )
const ( type AuthSource int
RoleAdmin string = "admin"
RoleSupport string = "support"
RoleApi string = "api"
RoleUser string = "user"
)
const ( const (
AuthViaLocalPassword int8 = 0 AuthViaLocalPassword AuthSource = iota
AuthViaLDAP int8 = 1 AuthViaLDAP
AuthViaToken int8 = 2 AuthViaToken
) )
type User struct { type User struct {
@ -36,14 +33,173 @@ 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"`
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 {
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 false
}
// Role-Arrays are short: performance not impacted by nested loop
func (u *User) HasAnyRole(queryroles []Role) bool {
for _, ur := range u.Roles {
for _, qr := range queryroles {
if ur == GetRoleString(qr) {
return true
}
}
}
return false
}
// Role-Arrays are short: performance not impacted by nested loop
func (u *User) HasAllRoles(queryroles []Role) bool {
target := len(queryroles)
matches := 0
for _, ur := range u.Roles {
for _, qr := range queryroles {
if ur == GetRoleString(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 []Role) bool {
matches := 0
for _, ur := range u.Roles {
for _, qr := range queryroles {
if ur == GetRoleString(qr) {
matches += 1
break
}
}
}
if matches == 0 {
return true
} else {
return false
}
}
// 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 RoleAdmin
} else if u.HasRole(RoleSupport) {
return RoleSupport
} else if u.HasRole(RoleManager) {
return RoleManager
} else if u.HasRole(RoleUser) {
return RoleUser
} else if u.HasRole(RoleApi) {
return RoleApi
} else if u.HasRole(RoleAnonymous) {
return RoleAnonymous
} else {
return RoleError
}
}
func (u *User) HasProject(project string) bool {
for _, p := range u.Projects {
if p == project {
return true return true
} }
} }
@ -145,9 +301,11 @@ func (auth *Authentication) AuthViaSession(
} }
username, _ := session.Values["username"].(string) username, _ := session.Values["username"].(string)
projects, _ := session.Values["projects"].([]string)
roles, _ := session.Values["roles"].([]string) roles, _ := session.Values["roles"].([]string)
return &User{ return &User{
Username: username, Username: username,
Projects: projects,
Roles: roles, Roles: roles,
AuthSource: -1, AuthSource: -1,
}, nil }, nil
@ -192,6 +350,7 @@ func (auth *Authentication) Login(
session.Options.MaxAge = int(auth.SessionMaxAge.Seconds()) session.Options.MaxAge = int(auth.SessionMaxAge.Seconds())
} }
session.Values["username"] = user.Username session.Values["username"] = user.Username
session.Values["projects"] = user.Projects
session.Values["roles"] = user.Roles session.Values["roles"] = user.Roles
if err := auth.sessionStore.Save(r, rw, session); err != nil { if err := auth.sessionStore.Save(r, rw, session); err != nil {
log.Warnf("session save failed: %s", err.Error()) log.Warnf("session save failed: %s", err.Error())
@ -199,7 +358,7 @@ func (auth *Authentication) Login(
return return
} }
log.Infof("login successfull: user: %v (roles: %v)", user.Username, user.Roles) log.Infof("login successfull: user: %#v (roles: %v, projects: %v)", user.Username, user.Roles, user.Projects)
ctx := context.WithValue(r.Context(), ContextUserKey, user) ctx := context.WithValue(r.Context(), ContextUserKey, user)
onsuccess.ServeHTTP(rw, r.WithContext(ctx)) onsuccess.ServeHTTP(rw, r.WithContext(ctx))
return return

View File

@ -1 +1,129 @@
// Copyright (C) 2022 NHR@FAU, University Erlangen-Nuremberg.
// All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package auth package auth
import (
"testing"
)
func TestHasValidRole(t *testing.T) {
u := User{Username: "testuser", Roles: []string{"user"}}
exists, _ := u.HasValidRole("user")
if !exists {
t.Fatalf(`User{Roles: ["user"]} -> HasValidRole("user"): EXISTS = %v, expected 'true'.`, exists)
}
}
func TestHasNotValidRole(t *testing.T) {
u := User{Username: "testuser", Roles: []string{"user"}}
exists, _ := u.HasValidRole("manager")
if exists {
t.Fatalf(`User{Roles: ["user"]} -> HasValidRole("manager"): EXISTS = %v, expected 'false'.`, exists)
}
}
func TestHasInvalidRole(t *testing.T) {
u := User{Username: "testuser", Roles: []string{"user"}}
_, valid := u.HasValidRole("invalid")
if valid {
t.Fatalf(`User{Roles: ["user"]} -> HasValidRole("invalid"): VALID = %v, expected 'false'.`, valid)
}
}
func TestHasNotInvalidRole(t *testing.T) {
u := User{Username: "testuser", Roles: []string{"user"}}
_, valid := u.HasValidRole("user")
if !valid {
t.Fatalf(`User{Roles: ["user"]} -> HasValidRole("user"): VALID = %v, expected 'true'.`, valid)
}
}
func TestHasRole(t *testing.T) {
u := User{Username: "testuser", Roles: []string{"user"}}
exists := u.HasRole(RoleUser)
if !exists {
t.Fatalf(`User{Roles: ["user"]} -> HasRole(RoleUser): EXISTS = %v, expected 'true'.`, exists)
}
}
func TestHasNotRole(t *testing.T) {
u := User{Username: "testuser", Roles: []string{"user"}}
exists := u.HasRole(RoleManager)
if exists {
t.Fatalf(`User{Roles: ["user"]} -> HasRole(RoleManager): EXISTS = %v, expected 'false'.`, exists)
}
}
func TestHasAnyRole(t *testing.T) {
u := User{Username: "testuser", Roles: []string{"user", "manager"}}
result := u.HasAnyRole([]Role{RoleManager, RoleSupport, RoleAdmin})
if !result {
t.Fatalf(`User{Roles: ["user", "manager"]} -> HasAnyRole([]Role{RoleManager, RoleSupport, RoleAdmin}): RESULT = %v, expected 'true'.`, result)
}
}
func TestHasNotAnyRole(t *testing.T) {
u := User{Username: "testuser", Roles: []string{"user", "manager"}}
result := u.HasAnyRole([]Role{RoleSupport, RoleAdmin})
if result {
t.Fatalf(`User{Roles: ["user", "manager"]} -> HasAllRoles([]Role{RoleSupport, RoleAdmin}): RESULT = %v, expected 'false'.`, result)
}
}
func TestHasAllRoles(t *testing.T) {
u := User{Username: "testuser", Roles: []string{"user", "manager", "support"}}
result := u.HasAllRoles([]Role{RoleUser, RoleManager, RoleSupport})
if !result {
t.Fatalf(`User{Roles: ["user", "manager", "support"]} -> HasAllRoles([]Role{RoleUser, RoleManager, RoleSupport}): RESULT = %v, expected 'true'.`, result)
}
}
func TestHasNotAllRoles(t *testing.T) {
u := User{Username: "testuser", Roles: []string{"user", "manager"}}
result := u.HasAllRoles([]Role{RoleUser, RoleManager, RoleSupport})
if result {
t.Fatalf(`User{Roles: ["user", "manager"]} -> HasAllRoles([]Role{RoleUser, RoleManager, RoleSupport}): RESULT = %v, expected 'false'.`, result)
}
}
func TestHasNotRoles(t *testing.T) {
u := User{Username: "testuser", Roles: []string{"user", "manager"}}
result := u.HasNotRoles([]Role{RoleSupport, RoleAdmin})
if !result {
t.Fatalf(`User{Roles: ["user", "manager"]} -> HasNotRoles([]Role{RoleSupport, RoleAdmin}): RESULT = %v, expected 'true'.`, result)
}
}
func TestHasAllNotRoles(t *testing.T) {
u := User{Username: "testuser", Roles: []string{"user", "manager"}}
result := u.HasNotRoles([]Role{RoleUser, RoleManager})
if result {
t.Fatalf(`User{Roles: ["user", "manager"]} -> HasNotRoles([]Role{RoleUser, RoleManager}): RESULT = %v, expected 'false'.`, result)
}
}

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"
@ -21,10 +22,10 @@ import (
func (auth *Authentication) GetUser(username string) (*User, error) { func (auth *Authentication) GetUser(username string) (*User, error) {
user := &User{Username: username} user := &User{Username: username}
var hashedPassword, name, rawRoles, email sql.NullString var hashedPassword, name, rawRoles, email, rawProjects sql.NullString
if err := sq.Select("password", "ldap", "name", "roles", "email").From("user"). if err := sq.Select("password", "ldap", "name", "roles", "email", "projects").From("user").
Where("user.username = ?", username).RunWith(auth.db). 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, &rawProjects); err != nil {
log.Warnf("Error while querying user '%v' from database", username) log.Warnf("Error while querying user '%v' from database", username)
return nil, err return nil, err
} }
@ -38,6 +39,11 @@ func (auth *Authentication) GetUser(username string) (*User, error) {
return nil, err return nil, err
} }
} }
if rawProjects.Valid {
if err := json.Unmarshal([]byte(rawProjects.String), &user.Projects); err != nil {
return nil, err
}
}
return user, nil return user, nil
} }
@ -45,9 +51,11 @@ func (auth *Authentication) GetUser(username string) (*User, error) {
func (auth *Authentication) AddUser(user *User) error { func (auth *Authentication) AddUser(user *User) error {
rolesJson, _ := json.Marshal(user.Roles) rolesJson, _ := json.Marshal(user.Roles)
projectsJson, _ := json.Marshal(user.Projects)
cols := []string{"username", "roles", "projects"}
vals := []interface{}{user.Username, string(rolesJson), string(projectsJson)}
cols := []string{"username", "roles"}
vals := []interface{}{user.Username, string(rolesJson)}
if user.Name != "" { if user.Name != "" {
cols = append(cols, "name") cols = append(cols, "name")
vals = append(vals, user.Name) vals = append(vals, user.Name)
@ -71,7 +79,7 @@ func (auth *Authentication) AddUser(user *User) error {
return err 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, projects: %s)", user.Username, rolesJson, user.AuthSource, projectsJson)
return nil return nil
} }
@ -84,7 +92,7 @@ func (auth *Authentication) DelUser(username string) error {
func (auth *Authentication) ListUsers(specialsOnly bool) ([]*User, 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", "projects").From("user")
if specialsOnly { if specialsOnly {
q = q.Where("(roles != '[\"user\"]' AND roles != '[]')") q = q.Where("(roles != '[\"user\"]' AND roles != '[]')")
} }
@ -99,9 +107,10 @@ func (auth *Authentication) ListUsers(specialsOnly bool) ([]*User, error) {
defer rows.Close() defer rows.Close()
for rows.Next() { for rows.Next() {
rawroles := "" rawroles := ""
rawprojects := ""
user := &User{} user := &User{}
var name, email sql.NullString var name, email sql.NullString
if err := rows.Scan(&user.Username, &name, &email, &rawroles); err != nil { if err := rows.Scan(&user.Username, &name, &email, &rawroles, &rawprojects); err != nil {
log.Warn("Error while scanning user list") log.Warn("Error while scanning user list")
return nil, err return nil, err
} }
@ -111,6 +120,10 @@ func (auth *Authentication) ListUsers(specialsOnly bool) ([]*User, error) {
return nil, err return nil, err
} }
if err := json.Unmarshal([]byte(rawprojects), &user.Projects); err != nil {
return nil, err
}
user.Name = name.String user.Name = name.String
user.Email = email.String user.Email = email.String
users = append(users, user) users = append(users, user)
@ -121,25 +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 role != RoleAdmin && role != RoleApi && role != RoleUser && role != RoleSupport { 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)
} }
for _, r := range user.Roles { roles, _ := json.Marshal(append(user.Roles, newRole))
if r == role {
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
@ -147,42 +160,111 @@ 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 role != RoleAdmin && role != RoleApi && role != RoleUser && role != RoleSupport { 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 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 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 delete 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
}
func (auth *Authentication) AddProject(
ctx context.Context,
username string,
project string) error {
user, err := auth.GetUser(username)
if err != nil {
return err
}
if !user.HasRole(RoleManager) {
return fmt.Errorf("user '%s' is not a manager!", username)
}
if user.HasProject(project) {
return fmt.Errorf("user '%s' already manages project '%s'", username, project)
}
projects, _ := json.Marshal(append(user.Projects, project))
if _, err := sq.Update("user").Set("projects", projects).Where("user.username = ?", username).RunWith(auth.db).Exec(); err != nil {
return err
}
return nil
}
func (auth *Authentication) RemoveProject(ctx context.Context, username string, project string) error {
user, err := auth.GetUser(username)
if err != nil {
return err
}
if !user.HasRole(RoleManager) {
return fmt.Errorf("user '%#v' is not a manager!", username)
}
if !user.HasProject(project) {
return fmt.Errorf("user '%#v': Cannot remove project '%#v' - Does not match!", username, project)
}
var exists bool
var newprojects []string
for _, p := range user.Projects {
if p != project {
newprojects = append(newprojects, p) // Append all projects not matching requested to be deleted project
} else { } else {
return fmt.Errorf("User '%v' already does not have role: %v", username, role) exists = true
}
}
if exists == true {
var result interface{}
if len(newprojects) == 0 {
result = "[]"
} else {
result, _ = json.Marshal(newprojects)
}
if _, err := sq.Update("user").Set("projects", result).Where("user.username = ?", username).RunWith(auth.db).Exec(); err != nil {
return err
}
return nil
} else {
return fmt.Errorf("user %s already does not manage project %s", username, project)
} }
} }
func FetchUser(ctx context.Context, db *sqlx.DB, username string) (*model.User, error) { func FetchUser(ctx context.Context, db *sqlx.DB, username string) (*model.User, error) {
me := GetUser(ctx) me := GetUser(ctx)
if me != nil && !me.HasRole(RoleAdmin) && !me.HasRole(RoleSupport) && me.Username != username { 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 && !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([]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

@ -356,8 +356,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, qerr := SecurityCheck(ctx, sq.Select("job."+string(aggreg), count).From("job").GroupBy("job."+string(aggreg)).OrderBy("count DESC"))
q = SecurityCheck(ctx, q)
if qerr != nil {
return nil, qerr
}
for _, f := range filters { for _, f := range filters {
q = BuildWhereClause(f, q) q = BuildWhereClause(f, q)
} }
@ -488,195 +492,94 @@ var ErrForbidden = errors.New("not authorized")
// If query is found to be an integer (= conversion to INT datatype succeeds), skip back to parent call // If query is found to be an integer (= conversion to INT datatype succeeds), skip back to parent call
// If nothing matches the search, `ErrNotFound` is returned. // If nothing matches the search, `ErrNotFound` is returned.
func (r *JobRepository) FindJobnameOrUserOrProject(ctx context.Context, searchterm string) (metasnip string, username string, project string, err error) { func (r *JobRepository) FindUserOrProjectOrJobname(ctx context.Context, searchterm string) (username string, project string, metasnip string, err error) {
user := auth.GetUser(ctx)
if _, err := strconv.Atoi(searchterm); err == nil { // Return empty on successful conversion: parent method will redirect for integer jobId if _, err := strconv.Atoi(searchterm); err == nil { // Return empty on successful conversion: parent method will redirect for integer jobId
return "", "", "", nil return "", "", "", nil
} else { // has to have letters } else { // Has to have letters and logged-in user for other guesses
user := auth.GetUser(ctx)
if user == nil || user.HasRole(auth.RoleAdmin) || user.HasRole(auth.RoleSupport) { if user != nil {
err := sq.Select("job.user").Distinct().From("job"). // Find username in jobs (match)
Where("job.user = ?", searchterm). uresult, _ := r.FindColumnValue(user, searchterm, "job", "user", "user", false)
RunWith(r.stmtCache).QueryRow().Scan(&username) if uresult != "" {
if err != nil && err != sql.ErrNoRows { return uresult, "", "", nil
return "", "", "", err
} else if err == nil {
return "", username, "", nil
} }
// Find username by name (like)
if username == "" { // Try with Name2Username query nresult, _ := r.FindColumnValue(user, searchterm, "user", "username", "name", true)
errtwo := sq.Select("user.username").Distinct().From("user"). if nresult != "" {
Where("user.name LIKE ?", fmt.Sprint("%"+searchterm+"%")). return nresult, "", "", nil
RunWith(r.stmtCache).QueryRow().Scan(&username)
if errtwo != nil && errtwo != sql.ErrNoRows {
return "", "", "", errtwo
} else if errtwo == nil {
return "", username, "", nil
} }
// Find projectId in jobs (match)
presult, _ := r.FindColumnValue(user, searchterm, "job", "project", "project", false)
if presult != "" {
return "", presult, "", nil
} }
} // Still no return (or not authorized for above): Try JobName
// Match Metadata, on hit, parent method redirects to jobName GQL query
if user == nil || user.HasRole(auth.RoleAdmin) || user.HasRole(auth.RoleSupport) {
err := sq.Select("job.project").Distinct().From("job").
Where("job.project = ?", searchterm).
RunWith(r.stmtCache).QueryRow().Scan(&project)
if err != nil && err != sql.ErrNoRows {
return "", "", "", err
} else if err == nil {
return "", "", project, nil
}
}
// All Authorizations: If unlabeled query not username or projectId, try for jobname: Match Metadata, on hit, parent method redirects to jobName GQL query
err := sq.Select("job.cluster").Distinct().From("job"). err := sq.Select("job.cluster").Distinct().From("job").
Where("job.meta_data LIKE ?", "%"+searchterm+"%"). Where("job.meta_data LIKE ?", "%"+searchterm+"%").
RunWith(r.stmtCache).QueryRow().Scan(&metasnip) RunWith(r.stmtCache).QueryRow().Scan(&metasnip)
if err != nil && err != sql.ErrNoRows { if err != nil && err != sql.ErrNoRows {
return "", "", "", err return "", "", "", err
} else if err == nil { } else if err == nil {
return metasnip[0:1], "", "", nil return "", "", metasnip[0:1], nil
}
} }
return "", "", "", ErrNotFound return "", "", "", ErrNotFound
} }
} }
func (r *JobRepository) FindUser(ctx context.Context, searchterm string) (username string, err error) { func (r *JobRepository) FindColumnValue(user *auth.User, searchterm string, table string, selectColumn string, whereColumn string, isLike bool) (result string, err error) {
user := auth.GetUser(ctx) compareStr := " = ?"
if user == nil || user.HasRole(auth.RoleAdmin) || user.HasRole(auth.RoleSupport) { query := searchterm
err := sq.Select("job.user").Distinct().From("job"). if isLike == true {
Where("job.user = ?", searchterm). compareStr = " LIKE ?"
RunWith(r.stmtCache).QueryRow().Scan(&username) query = "%" + searchterm + "%"
}
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)
if err != nil && err != sql.ErrNoRows { if err != nil && err != sql.ErrNoRows {
return "", err return "", err
} else if err == nil { } else if err == nil {
return username, nil return result, nil
} }
return "", ErrNotFound return "", ErrNotFound
} else { } else {
log.Infof("Non-Admin User %s : Requested Query Username -> %s: Forbidden", user.Name, searchterm) log.Infof("Non-Admin User %s : Requested Query '%s' on table '%s' : Forbidden", user.Name, query, table)
return "", ErrForbidden return "", ErrForbidden
} }
} }
func (r *JobRepository) FindUserByName(ctx context.Context, searchterm string) (username string, err error) { func (r *JobRepository) FindColumnValues(user *auth.User, query string, table string, selectColumn string, whereColumn string) (results []string, err error) {
user := auth.GetUser(ctx)
if user == nil || user.HasRole(auth.RoleAdmin) || user.HasRole(auth.RoleSupport) {
err := sq.Select("user.username").Distinct().From("user").
Where("user.name = ?", searchterm).
RunWith(r.stmtCache).QueryRow().Scan(&username)
if err != nil && err != sql.ErrNoRows {
return "", err
} else if err == nil {
return username, nil
}
return "", ErrNotFound
} else {
log.Infof("Non-Admin User %s : Requested Query Name -> %s: Forbidden", user.Name, searchterm)
return "", ErrForbidden
}
}
func (r *JobRepository) FindUsers(ctx context.Context, searchterm string) (usernames []string, err error) {
user := auth.GetUser(ctx)
emptyResult := make([]string, 0) emptyResult := make([]string, 0)
if user == nil || user.HasRole(auth.RoleAdmin) || user.HasRole(auth.RoleSupport) { if user.HasAnyRole([]auth.Role{auth.RoleAdmin, auth.RoleSupport, auth.RoleManager}) {
rows, err := sq.Select("job.user").Distinct().From("job"). rows, err := sq.Select(table+"."+selectColumn).Distinct().From(table).
Where("job.user LIKE ?", fmt.Sprint("%", searchterm, "%")). Where(table+"."+whereColumn+" LIKE ?", fmt.Sprint("%", query, "%")).
RunWith(r.stmtCache).Query() RunWith(r.stmtCache).Query()
if err != nil && err != sql.ErrNoRows { if err != nil && err != sql.ErrNoRows {
return emptyResult, err return emptyResult, err
} else if err == nil { } else if err == nil {
for rows.Next() { for rows.Next() {
var name string var result string
err := rows.Scan(&name) err := rows.Scan(&result)
if err != nil { if err != nil {
rows.Close() rows.Close()
log.Warnf("Error while scanning rows: %v", err) log.Warnf("Error while scanning rows: %v", err)
return emptyResult, err return emptyResult, err
} }
usernames = append(usernames, name) results = append(results, result)
} }
return usernames, nil return results, nil
} }
return emptyResult, ErrNotFound return emptyResult, ErrNotFound
} else { } else {
log.Infof("Non-Admin User %s : Requested Query Usernames -> %s: Forbidden", user.Name, searchterm) log.Infof("Non-Admin User %s : Requested Query '%s' on table '%s' : Forbidden", user.Name, query, table)
return emptyResult, ErrForbidden return emptyResult, ErrForbidden
} }
} }
func (r *JobRepository) FindUsersByName(ctx context.Context, searchterm string) (usernames []string, err error) {
user := auth.GetUser(ctx)
emptyResult := make([]string, 0)
if user == nil || user.HasRole(auth.RoleAdmin) || user.HasRole(auth.RoleSupport) {
rows, err := sq.Select("user.username").Distinct().From("user").
Where("user.name LIKE ?", fmt.Sprint("%", searchterm, "%")).
RunWith(r.stmtCache).Query()
if err != nil && err != sql.ErrNoRows {
return emptyResult, err
} else if err == nil {
for rows.Next() {
var username string
err := rows.Scan(&username)
if err != nil {
rows.Close()
log.Warnf("Error while scanning rows: %v", err)
return emptyResult, err
}
usernames = append(usernames, username)
}
return usernames, nil
}
return emptyResult, ErrNotFound
} else {
log.Infof("Non-Admin User %s : Requested Query name -> %s: Forbidden", user.Name, searchterm)
return emptyResult, ErrForbidden
}
}
func (r *JobRepository) FindNameByUser(ctx context.Context, searchterm string) (name string, err error) {
user := auth.GetUser(ctx)
if user == nil || user.HasRole(auth.RoleAdmin) || user.HasRole(auth.RoleSupport) {
err := sq.Select("user.name").Distinct().From("user").
Where("user.username = ?", searchterm).
RunWith(r.stmtCache).QueryRow().Scan(&name)
if err != nil && err != sql.ErrNoRows {
return "", err
} else if err == nil {
return name, nil
}
return "", ErrNotFound
} else {
log.Infof("Non-Admin User %s : Requested Query Name -> %s: Forbidden", user.Name, searchterm)
return "", ErrForbidden
}
}
func (r *JobRepository) FindProject(ctx context.Context, searchterm string) (project string, err error) {
user := auth.GetUser(ctx)
if user == nil || user.HasRole(auth.RoleAdmin) || user.HasRole(auth.RoleSupport) {
err := sq.Select("job.project").Distinct().From("job").
Where("job.project = ?", searchterm).
RunWith(r.stmtCache).QueryRow().Scan(&project)
if err != nil && err != sql.ErrNoRows {
return "", err
} else if err == nil {
return project, nil
}
return "", ErrNotFound
} else {
log.Infof("Non-Admin User %s : Requested Query Project -> %s: Forbidden", user.Name, project)
return "", ErrForbidden
}
}
func (r *JobRepository) Partitions(cluster string) ([]string, error) { func (r *JobRepository) Partitions(cluster string) ([]string, error) {
var err error var err error
start := time.Now() start := time.Now()
@ -796,9 +699,9 @@ func (r *JobRepository) JobsStatistics(ctx context.Context,
for _, cluster := range archive.Clusters { for _, cluster := range archive.Clusters {
for _, subcluster := range cluster.SubClusters { for _, subcluster := range cluster.SubClusters {
corehoursCol := fmt.Sprintf("CAST(ROUND(SUM(job.duration * job.num_nodes * %d * %d) / 3600) as %s)", subcluster.SocketsPerNode, subcluster.CoresPerSocket, castType) corehoursCol := fmt.Sprintf("CAST(ROUND(SUM(job.duration * job.num_nodes * %d * %d) / 3600) as %s)", subcluster.SocketsPerNode, subcluster.CoresPerSocket, castType)
var query sq.SelectBuilder var rawQuery sq.SelectBuilder
if groupBy == nil { if groupBy == nil {
query = sq.Select( rawQuery = sq.Select(
"''", "''",
"COUNT(job.id)", "COUNT(job.id)",
fmt.Sprintf("CAST(ROUND(SUM(job.duration) / 3600) as %s)", castType), fmt.Sprintf("CAST(ROUND(SUM(job.duration) / 3600) as %s)", castType),
@ -806,7 +709,7 @@ func (r *JobRepository) JobsStatistics(ctx context.Context,
).From("job") ).From("job")
} else { } else {
col := groupBy2column[*groupBy] col := groupBy2column[*groupBy]
query = sq.Select( rawQuery = sq.Select(
col, col,
"COUNT(job.id)", "COUNT(job.id)",
fmt.Sprintf("CAST(ROUND(SUM(job.duration) / 3600) as %s)", castType), fmt.Sprintf("CAST(ROUND(SUM(job.duration) / 3600) as %s)", castType),
@ -814,11 +717,16 @@ func (r *JobRepository) JobsStatistics(ctx context.Context,
).From("job").GroupBy(col) ).From("job").GroupBy(col)
} }
query = query. rawQuery = rawQuery.
Where("job.cluster = ?", cluster.Name). Where("job.cluster = ?", cluster.Name).
Where("job.subcluster = ?", subcluster.Name) Where("job.subcluster = ?", subcluster.Name)
query = SecurityCheck(ctx, query) query, qerr := SecurityCheck(ctx, rawQuery)
if qerr != nil {
return nil, qerr
}
for _, f := range filter { for _, f := range filter {
query = BuildWhereClause(f, query) query = BuildWhereClause(f, query)
} }
@ -857,8 +765,14 @@ func (r *JobRepository) JobsStatistics(ctx context.Context,
} }
if groupBy == nil { if groupBy == nil {
query := sq.Select("COUNT(job.id)").From("job").Where("job.duration < ?", config.Keys.ShortRunningJobsDuration) query := sq.Select("COUNT(job.id)").From("job").Where("job.duration < ?", config.Keys.ShortRunningJobsDuration)
query = SecurityCheck(ctx, query) query, qerr := SecurityCheck(ctx, query)
if qerr != nil {
return nil, qerr
}
for _, f := range filter { for _, f := range filter {
query = BuildWhereClause(f, query) query = BuildWhereClause(f, query)
} }
@ -868,8 +782,15 @@ func (r *JobRepository) JobsStatistics(ctx context.Context,
} }
} else { } else {
col := groupBy2column[*groupBy] col := groupBy2column[*groupBy]
query := sq.Select(col, "COUNT(job.id)").From("job").Where("job.duration < ?", config.Keys.ShortRunningJobsDuration) query := sq.Select(col, "COUNT(job.id)").From("job").Where("job.duration < ?", config.Keys.ShortRunningJobsDuration)
query = SecurityCheck(ctx, query)
query, qerr := SecurityCheck(ctx, query)
if qerr != nil {
return nil, qerr
}
for _, f := range filter { for _, f := range filter {
query = BuildWhereClause(f, query) query = BuildWhereClause(f, query)
} }
@ -895,7 +816,8 @@ func (r *JobRepository) JobsStatistics(ctx context.Context,
if col == "job.user" { if col == "job.user" {
for id := range stats { for id := range stats {
emptyDash := "-" emptyDash := "-"
name, _ := r.FindNameByUser(ctx, id) user := auth.GetUser(ctx)
name, _ := r.FindColumnValue(user, id, "user", "name", "username", false)
if name != "" { if name != "" {
stats[id].Name = &name stats[id].Name = &name
} else { } else {
@ -952,7 +874,12 @@ func (r *JobRepository) jobsStatisticsHistogram(ctx context.Context,
start := time.Now() start := time.Now()
query := sq.Select(value, "COUNT(job.id) AS count").From("job") query := sq.Select(value, "COUNT(job.id) AS count").From("job")
query = SecurityCheck(ctx, query) query, qerr := SecurityCheck(ctx, sq.Select(value, "COUNT(job.id) AS count").From("job"))
if qerr != nil {
return nil, qerr
}
for _, f := range filters { for _, f := range filters {
query = BuildWhereClause(f, query) query = BuildWhereClause(f, query)
} }

View File

@ -17,7 +17,7 @@ import (
"github.com/golang-migrate/migrate/v4/source/iofs" "github.com/golang-migrate/migrate/v4/source/iofs"
) )
const supportedVersion uint = 2 const supportedVersion uint = 3
//go:embed migrations/* //go:embed migrations/*
var migrationFiles embed.FS var migrationFiles embed.FS

View File

@ -0,0 +1 @@
ALTER TABLE user DROP COLUMN projects;

View File

@ -0,0 +1 @@
ALTER TABLE user ADD COLUMN projects varchar(255) NOT NULL DEFAULT "[]";

View File

@ -0,0 +1 @@
ALTER TABLE user DROP COLUMN projects;

View File

@ -0,0 +1 @@
ALTER TABLE user ADD COLUMN projects varchar(255) NOT NULL DEFAULT "[]";

View File

@ -26,8 +26,11 @@ func (r *JobRepository) QueryJobs(
page *model.PageRequest, page *model.PageRequest,
order *model.OrderByInput) ([]*schema.Job, error) { order *model.OrderByInput) ([]*schema.Job, error) {
query := sq.Select(jobColumns...).From("job") query, qerr := SecurityCheck(ctx, sq.Select(jobColumns...).From("job"))
query = SecurityCheck(ctx, query)
if qerr != nil {
return nil, qerr
}
if order != nil { if order != nil {
field := toSnakeCase(order.Field) field := toSnakeCase(order.Field)
@ -82,8 +85,12 @@ func (r *JobRepository) CountJobs(
filters []*model.JobFilter) (int, error) { filters []*model.JobFilter) (int, error) {
// count all jobs: // count all jobs:
query := sq.Select("count(*)").From("job") query, qerr := SecurityCheck(ctx, sq.Select("count(*)").From("job"))
query = SecurityCheck(ctx, query)
if qerr != nil {
return 0, qerr
}
for _, f := range filters { for _, f := range filters {
query = BuildWhereClause(f, query) query = BuildWhereClause(f, query)
} }
@ -95,13 +102,23 @@ func (r *JobRepository) CountJobs(
return count, nil 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) user := auth.GetUser(ctx)
if user == nil || user.HasRole(auth.RoleAdmin) || user.HasRole(auth.RoleApi) || user.HasRole(auth.RoleSupport) { if user == nil || user.HasAnyRole([]auth.Role{auth.RoleAdmin, auth.RoleSupport, auth.RoleApi}) { // Admin & Co. : All jobs
return query return query, nil
} else if user.HasRole(auth.RoleManager) { // Manager : Add filter for managed projects' jobs only + personal jobs
if len(user.Projects) != 0 {
return query.Where(sq.Or{sq.Eq{"job.project": user.Projects}, sq.Eq{"job.user": user.Username}}), nil
} else {
log.Infof("Manager-User '%s' has no defined projects to lookup! Query only personal jobs ...", user.Username)
return query.Where("job.user = ?", user.Username), nil
}
} else if user.HasRole(auth.RoleUser) { // User : Only personal jobs
return query.Where("job.user = ?", user.Username), nil
} else { // Unauthorized : Error
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. // Build a sq.SelectBuilder out of a schema.JobFilter.

View File

@ -5,9 +5,12 @@
package repository package repository
import ( import (
"strings"
"github.com/ClusterCockpit/cc-backend/internal/auth"
"github.com/ClusterCockpit/cc-backend/pkg/archive" "github.com/ClusterCockpit/cc-backend/pkg/archive"
"github.com/ClusterCockpit/cc-backend/pkg/schema"
"github.com/ClusterCockpit/cc-backend/pkg/log" "github.com/ClusterCockpit/cc-backend/pkg/log"
"github.com/ClusterCockpit/cc-backend/pkg/schema"
sq "github.com/Masterminds/squirrel" sq "github.com/Masterminds/squirrel"
) )
@ -65,7 +68,7 @@ func (r *JobRepository) CreateTag(tagType string, tagName string) (tagId int64,
return res.LastInsertId() return res.LastInsertId()
} }
func (r *JobRepository) CountTags(user *string) (tags []schema.Tag, counts map[string]int, err error) { func (r *JobRepository) CountTags(user *auth.User) (tags []schema.Tag, counts map[string]int, err error) {
tags = make([]schema.Tag, 0, 100) tags = make([]schema.Tag, 0, 100)
xrows, err := r.DB.Queryx("SELECT * FROM tag") xrows, err := r.DB.Queryx("SELECT * FROM tag")
if err != nil { if err != nil {
@ -84,9 +87,13 @@ func (r *JobRepository) CountTags(user *string) (tags []schema.Tag, counts map[s
From("tag t"). From("tag t").
LeftJoin("jobtag jt ON t.id = jt.tag_id"). LeftJoin("jobtag jt ON t.id = jt.tag_id").
GroupBy("t.tag_name") GroupBy("t.tag_name")
if user != nil {
q = q.Where("jt.job_id IN (SELECT id FROM job WHERE job.user = ?)", *user) if user != nil && user.HasRole(auth.RoleUser) { // USER: Only count own jobs
} q = q.Where("jt.job_id IN (SELECT id FROM job WHERE job.user = ?)", user.Username)
} else if user != nil && user.HasRole(auth.RoleManager) { // MANAGER: Count own jobs plus project's jobs
// Build ("project1", "project2", ...) list of variable length directly in SQL string
q = q.Where("jt.job_id IN (SELECT id FROM job WHERE job.user = ? OR job.project IN (\""+strings.Join(user.Projects, "\",\"")+"\"))", user.Username)
} // else: ADMIN || SUPPORT: Count all jobs
rows, err := q.RunWith(r.stmtCache).Query() rows, err := q.RunWith(r.stmtCache).Query()
if err != nil { if err != nil {

View File

@ -104,6 +104,7 @@ func setupUserRoute(i InfoType, r *http.Request) InfoType {
username := mux.Vars(r)["id"] username := mux.Vars(r)["id"]
i["id"] = username i["id"] = username
i["username"] = username i["username"] = username
// TODO: If forbidden (== err exists), redirect to error page
if user, _ := auth.FetchUser(r.Context(), jobRepo.DB, username); user != nil { if user, _ := auth.FetchUser(r.Context(), jobRepo.DB, username); user != nil {
i["name"] = user.Name i["name"] = user.Name
i["email"] = user.Email i["email"] = user.Email
@ -142,13 +143,10 @@ func setupAnalysisRoute(i InfoType, r *http.Request) InfoType {
} }
func setupTaglistRoute(i InfoType, r *http.Request) InfoType { func setupTaglistRoute(i InfoType, r *http.Request) InfoType {
var username *string = nil
jobRepo := repository.GetJobRepository() jobRepo := repository.GetJobRepository()
if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleAdmin) { user := auth.GetUser(r.Context())
username = &user.Username
}
tags, counts, err := jobRepo.CountTags(username) tags, counts, err := jobRepo.CountTags(user)
tagMap := make(map[string][]map[string]interface{}) tagMap := make(map[string][]map[string]interface{})
if err != nil { if err != nil {
log.Warnf("GetTags failed: %s", err.Error()) log.Warnf("GetTags failed: %s", err.Error())
@ -279,17 +277,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, isAdmin, isSupporter := "", true, true // 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)
isAdmin = user.HasRole(auth.RoleAdmin)
isSupporter = user.HasRole(auth.RoleSupport)
}
page := web.Page{ page := web.Page{
Title: title, Title: title,
User: web.User{Username: username, IsAdmin: isAdmin, IsSupporter: isSupporter}, 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,
@ -306,75 +302,61 @@ func SetupRoutes(router *mux.Router, version string, hash string, buildTime stri
func HandleSearchBar(rw http.ResponseWriter, r *http.Request, api *api.RestApi) { func HandleSearchBar(rw http.ResponseWriter, r *http.Request, api *api.RestApi) {
if search := r.URL.Query().Get("searchId"); search != "" { if search := r.URL.Query().Get("searchId"); search != "" {
user := auth.GetUser(r.Context())
splitSearch := strings.Split(search, ":") splitSearch := strings.Split(search, ":")
if len(splitSearch) == 2 { if len(splitSearch) == 2 {
switch strings.Trim(splitSearch[0], " ") { switch strings.Trim(splitSearch[0], " ") {
case "jobId": case "jobId":
http.Redirect(rw, r, "/monitoring/jobs/?jobId="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusTemporaryRedirect) // All Users: Redirect to Tablequery http.Redirect(rw, r, "/monitoring/jobs/?jobId="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusTemporaryRedirect) // All Users: Redirect to Tablequery
return
case "jobName": case "jobName":
http.Redirect(rw, r, "/monitoring/jobs/?jobName="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusTemporaryRedirect) // All Users: Redirect to Tablequery http.Redirect(rw, r, "/monitoring/jobs/?jobName="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusTemporaryRedirect) // All Users: Redirect to Tablequery
return
case "projectId": case "projectId":
project, _ := api.JobRepository.FindProject(r.Context(), strings.Trim(splitSearch[1], " ")) // Restricted: projectId http.Redirect(rw, r, "/monitoring/jobs/?projectMatch=eq&project="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusTemporaryRedirect) // All Users: Redirect to Tablequery
if project != "" {
http.Redirect(rw, r, "/monitoring/jobs/?projectMatch=eq&project="+url.QueryEscape(project), http.StatusTemporaryRedirect)
return
} else {
http.Redirect(rw, r, "/monitoring/jobs/?jobId=NotFound", http.StatusTemporaryRedirect) // Workaround to display correctly empty table
}
case "username": case "username":
usernames, _ := api.JobRepository.FindUsers(r.Context(), strings.Trim(splitSearch[1], " ")) // Restricted: usernames if user.HasAnyRole([]auth.Role{auth.RoleAdmin, auth.RoleSupport, auth.RoleManager}) {
if len(usernames) == 1 { http.Redirect(rw, r, "/monitoring/users/?user="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusTemporaryRedirect)
http.Redirect(rw, r, "/monitoring/user/"+usernames[0], http.StatusTemporaryRedirect) // One Match: Redirect to User View
return
} else if len(usernames) > 1 {
http.Redirect(rw, r, "/monitoring/users/?user="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusTemporaryRedirect) // > 1 Matches: Redirect to user table
return
} else { } else {
http.Redirect(rw, r, "/monitoring/users/?user=NotFound", http.StatusTemporaryRedirect) // Workaround to display correctly empty table http.Redirect(rw, r, "/monitoring/jobs/?", http.StatusTemporaryRedirect) // Users: Redirect to Tablequery
} }
case "name": case "name":
usernames, _ := api.JobRepository.FindUsersByName(r.Context(), strings.Trim(splitSearch[1], " ")) // Restricted: usernames queried by name usernames, _ := api.JobRepository.FindColumnValues(user, strings.Trim(splitSearch[1], " "), "user", "username", "name")
if len(usernames) == 1 { if len(usernames) != 0 {
http.Redirect(rw, r, "/monitoring/user/"+usernames[0], http.StatusTemporaryRedirect)
return
} else if len(usernames) > 1 {
joinedNames := strings.Join(usernames, "&user=") joinedNames := strings.Join(usernames, "&user=")
http.Redirect(rw, r, "/monitoring/users/?user="+joinedNames, http.StatusTemporaryRedirect) // > 1 Matches: Redirect to user table http.Redirect(rw, r, "/monitoring/users/?user="+joinedNames, http.StatusTemporaryRedirect)
return
} else { } else {
http.Redirect(rw, r, "/monitoring/users/?user=NotFound", http.StatusTemporaryRedirect) // Workaround to display correctly empty table 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
}
} }
default: default:
http.Error(rw, "'searchId' type parameter unknown", http.StatusBadRequest) log.Warnf("Searchbar type parameter '%s' unknown", strings.Trim(splitSearch[0], " "))
http.Redirect(rw, r, "/monitoring/jobs/?", http.StatusTemporaryRedirect) // Unknown: Redirect to Tablequery
} }
} else if len(splitSearch) == 1 { } else if len(splitSearch) == 1 {
jobname, username, project, err := api.JobRepository.FindJobnameOrUserOrProject(r.Context(), strings.Trim(search, " ")) // Determine Access within username, project, jobname, err := api.JobRepository.FindUserOrProjectOrJobname(r.Context(), strings.Trim(search, " "))
if err != nil { if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError) log.Errorf("Error while searchbar best guess: %v", err.Error())
return http.Redirect(rw, r, "/monitoring/jobs/?", http.StatusTemporaryRedirect) // Unknown: Redirect to Tablequery
} }
if username != "" { if username != "" {
http.Redirect(rw, r, "/monitoring/user/"+username, http.StatusTemporaryRedirect) // User: Redirect to user page http.Redirect(rw, r, "/monitoring/user/"+username, http.StatusTemporaryRedirect) // User: Redirect to user page
return
} else if project != "" { } else if project != "" {
http.Redirect(rw, r, "/monitoring/jobs/?projectMatch=eq&project="+url.QueryEscape(strings.Trim(search, " ")), http.StatusTemporaryRedirect) // projectId (equal) http.Redirect(rw, r, "/monitoring/jobs/?projectMatch=eq&project="+url.QueryEscape(strings.Trim(search, " ")), http.StatusTemporaryRedirect) // projectId (equal)
return
} else if jobname != "" { } else if jobname != "" {
http.Redirect(rw, r, "/monitoring/jobs/?jobName="+url.QueryEscape(strings.Trim(search, " ")), http.StatusTemporaryRedirect) // JobName (contains) http.Redirect(rw, r, "/monitoring/jobs/?jobName="+url.QueryEscape(strings.Trim(search, " ")), http.StatusTemporaryRedirect) // JobName (contains)
return
} else { } else {
http.Redirect(rw, r, "/monitoring/jobs/?jobId="+url.QueryEscape(strings.Trim(search, " ")), http.StatusTemporaryRedirect) // No Result: Probably jobId http.Redirect(rw, r, "/monitoring/jobs/?jobId="+url.QueryEscape(strings.Trim(search, " ")), http.StatusTemporaryRedirect) // No Result: Probably jobId
return
} }
} else { } else {
http.Error(rw, "'searchId' query parameter malformed", http.StatusBadRequest) log.Warnf("Searchbar query parameters malformed: %v", search)
http.Redirect(rw, r, "/monitoring/jobs/?", http.StatusTemporaryRedirect) // Unknown: Redirect to Tablequery
} }
} else { } else {

Binary file not shown.

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.IsAdmin} {#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,23 +4,45 @@
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 isAdmin // boolean 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
const views = [ const userviews = [
isAdmin { title: 'My Jobs', href: `/monitoring/user/${username}`, icon: 'bar-chart-line-fill' },
? { title: 'Jobs', adminOnly: false, href: '/monitoring/jobs/', icon: 'card-list' } { title: `Job Search`, href: '/monitoring/jobs/', icon: 'card-list' },
: { title: 'My Jobs', adminOnly: false, href: `/monitoring/user/${username}`, icon: 'bar-chart-line-fill' }, { title: 'Tags', href: '/monitoring/tags/', icon: 'tags' }
{ 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 managerviews = [
{ title: 'My Jobs', href: `/monitoring/user/${username}`, icon: 'bar-chart-line-fill' },
{ title: `Managed Jobs`, href: '/monitoring/jobs/', icon: 'card-list' },
{ title: `Managed 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 = [ const viewsPerCluster = [
{ title: 'Analysis', adminOnly: true, href: '/monitoring/analysis/', icon: 'graph-up' }, { title: 'Analysis', requiredRole: roles.support, href: '/monitoring/analysis/', icon: 'graph-up' },
{ title: 'Systems', adminOnly: true, href: '/monitoring/systems/', icon: 'cpu' }, { title: 'Systems', requiredRole: roles.admin, href: '/monitoring/systems/', icon: 'cpu' },
{ title: 'Status', adminOnly: true, href: '/monitoring/status/', icon: 'cpu' }, { title: 'Status', requiredRole: roles.admin, href: '/monitoring/status/', icon: 'cpu' },
] ]
</script> </script>
@ -31,10 +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>
{#each views.filter(item => isAdmin || !item.adminOnly) as item} {#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> <NavLink href={item.href} active={window.location.pathname == item.href}><Icon name={item.icon}/> {item.title}</NavLink>
{/each} {/each}
{#each viewsPerCluster.filter(item => !item.adminOnly || isAdmin) as item} {: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 == 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 == 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.requiredRole <= authlevel) as item}
<NavItem> <NavItem>
<Dropdown nav inNavbar> <Dropdown nav inNavbar>
<DropdownToggle nav caret> <DropdownToggle nav caret>
@ -57,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={isAdmin ? "Example: 'projectId:a100cd', Types are: jobId | jobName | projectId | username" | "name" : "Example: 'jobName:myjob', Types are jobId | jobName"}><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,6 +14,8 @@
const ccconfig = getContext('cc-config') const ccconfig = getContext('cc-config')
export let filterPresets = {} export let filterPresets = {}
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
@ -60,7 +62,7 @@
</Col> </Col>
<Col xs="3" style="margin-left: auto;"> <Col xs="3" style="margin-left: auto;">
<UserOrProject 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

@ -48,7 +48,7 @@
query(nodesQuery) query(nodesQuery)
$: console.log($nodesQuery?.data?.nodeMetrics[0].metrics) // $: console.log($nodesQuery?.data?.nodeMetrics[0].metrics)
</script> </script>
<Row> <Row>

View File

@ -61,7 +61,7 @@
<thead> <thead>
<tr> <tr>
<th> <th>
<Button outline on:click={() => (isMetricSelectionOpen = true, console.log(isMetricSelectionOpen))}> <Button outline on:click={() => (isMetricSelectionOpen = true)}> <!-- log to click ', console.log(isMetricSelectionOpen)' -->
Metrics Metrics
</Button> </Button>
</th> </th>

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

@ -2,11 +2,13 @@
import { Row, Col } from 'sveltestrap' import { Row, Col } from 'sveltestrap'
import { onMount } from 'svelte' import { onMount } from 'svelte'
import EditRole from './admin/EditRole.svelte' import EditRole from './admin/EditRole.svelte'
import EditProject from './admin/EditProject.svelte'
import AddUser from './admin/AddUser.svelte' import AddUser from './admin/AddUser.svelte'
import ShowUsers from './admin/ShowUsers.svelte' import ShowUsers from './admin/ShowUsers.svelte'
import Options from './admin/Options.svelte' import Options from './admin/Options.svelte'
let users = [] let users = []
let roles = []
function getUserList() { function getUserList() {
fetch('/api/users/?via-ldap=false&not-just-user=true') fetch('/api/users/?via-ldap=false&not-just-user=true')
@ -16,19 +18,35 @@
}) })
} }
onMount(() => getUserList()) function getValidRoles() {
fetch('/api/roles/')
.then(res => res.json())
.then(rolesRaw => {
roles = rolesRaw
})
}
function initAdmin() {
getUserList()
getValidRoles()
}
onMount(() => initAdmin())
</script> </script>
<Row cols={2} class="p-2 g-2" > <Row cols={2} class="p-2 g-2" >
<Col class="mb-1"> <Col class="mb-1">
<AddUser on:reload={getUserList}/> <AddUser roles={roles} on:reload={getUserList}/>
</Col> </Col>
<Col class="mb-1"> <Col class="mb-1">
<ShowUsers on:reload={getUserList} bind:users={users}/> <ShowUsers on:reload={getUserList} bind:users={users}/>
</Col> </Col>
<Col> <Col>
<EditRole on:reload={getUserList}/> <EditRole roles={roles} on:reload={getUserList}/>
</Col>
<Col>
<EditProject on:reload={getUserList}/>
</Col> </Col>
<Col> <Col>
<Options/> <Options/>

View File

@ -8,6 +8,8 @@
let message = {msg: '', color: '#d63384'} let message = {msg: '', color: '#d63384'}
let displayMessage = false let displayMessage = false
export let roles = []
async function handleUserSubmit() { async function handleUserSubmit() {
let form = document.querySelector('#create-user-form') let form = document.querySelector('#create-user-form')
let formData = new FormData(form) let formData = new FormData(form)
@ -45,17 +47,7 @@
<form id="create-user-form" method="post" action="/api/users/" class="card-body" on:submit|preventDefault={handleUserSubmit}> <form id="create-user-form" method="post" action="/api/users/" class="card-body" on:submit|preventDefault={handleUserSubmit}>
<CardTitle class="mb-3">Create User</CardTitle> <CardTitle class="mb-3">Create User</CardTitle>
<div class="mb-3"> <div class="mb-3">
<label for="name" class="form-label">Name</label> <label for="username" class="form-label">Username (ID)</label>
<input type="text" class="form-control" id="name" name="name" aria-describedby="nameHelp"/>
<div id="nameHelp" class="form-text">Optional, can be blank.</div>
</div>
<div class="mb-3">
<label for="email" class="form-label">Email address</label>
<input type="email" class="form-control" id="email" name="email" aria-describedby="emailHelp"/>
<div id="emailHelp" class="form-text">Optional, can be blank.</div>
</div>
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" name="username" aria-describedby="usernameHelp"/> <input type="text" class="form-control" id="username" name="username" aria-describedby="usernameHelp"/>
<div id="usernameHelp" class="form-text">Must be unique.</div> <div id="usernameHelp" class="form-text">Must be unique.</div>
</div> </div>
@ -64,24 +56,38 @@
<input type="password" class="form-control" id="password" name="password" aria-describedby="passwordHelp"/> <input type="password" class="form-control" id="password" name="password" aria-describedby="passwordHelp"/>
<div id="passwordHelp" class="form-text">Only API users are allowed to have a blank password. Users with a blank password can only authenticate via Tokens.</div> <div id="passwordHelp" class="form-text">Only API users are allowed to have a blank password. Users with a blank password can only authenticate via Tokens.</div>
</div> </div>
<div class="mb-3">
<label for="name" class="form-label">Project</label>
<input type="text" class="form-control" id="project" name="project" aria-describedby="projectHelp"/>
<div id="projectHelp" class="form-text">Only Manager users can have a project. Allows to inspect jobs and users of given project.</div>
</div>
<div class="mb-3">
<label for="name" class="form-label">Name</label>
<input type="text" class="form-control" id="name" name="name" aria-describedby="nameHelp"/>
<div id="nameHelp" class="form-text">Optional, can be blank.</div>
</div>
<div class="mb-3">
<label for="email" class="form-label">Email address</label>
<input type="email" class="form-control" id="email" name="email" aria-describedby="emailHelp"/>
<div id="emailHelp" class="form-text">Optional, can be blank.</div>
</div>
<div class="mb-3"> <div class="mb-3">
<p>Role:</p> <p>Role:</p>
{#each roles as role, i}
{#if i == 0}
<div> <div>
<input type="radio" id="user" name="role" value="user" checked/> <input type="radio" id={role} name="role" value={role} checked/>
<label for="user">User (regular user, same as if created via LDAP sync.)</label> <label for={role}>{role.charAt(0).toUpperCase() + role.slice(1)} (regular user, same as if created via LDAP sync.)</label>
</div> </div>
{:else}
<div> <div>
<input type="radio" id="api" name="role" value="api"/> <input type="radio" id={role} name="role" value={role}/>
<label for="api">API</label> <label for={role}>{role.charAt(0).toUpperCase() + role.slice(1)}</label>
</div>
<div>
<input type="radio" id="support" name="role" value="support"/>
<label for="support">Support</label>
</div>
<div>
<input type="radio" id="admin" name="role" value="admin"/>
<label for="admin">Admin</label>
</div> </div>
{/if}
{/each}
</div> </div>
<p style="display: flex; align-items: center;"> <p style="display: flex; align-items: center;">
<Button type="submit" color="primary">Submit</Button> <Button type="submit" color="primary">Submit</Button>

View File

@ -0,0 +1,97 @@
<script>
import { Card, CardTitle, CardBody } from 'sveltestrap'
import { createEventDispatcher } from 'svelte'
import { fade } from 'svelte/transition'
const dispatch = createEventDispatcher()
let message = {msg: '', color: '#d63384'}
let displayMessage = false
async function handleAddProject() {
const username = document.querySelector('#project-username').value
const project = document.querySelector('#project-id').value
if (username == "" || project == "") {
alert('Please fill in a username and select a project.')
return
}
let formData = new FormData()
formData.append('username', username)
formData.append('add-project', project)
try {
const res = await fetch(`/api/user/${username}`, { method: 'POST', body: formData })
if (res.ok) {
let text = await res.text()
popMessage(text, '#048109')
reloadUserList()
} else {
let text = await res.text()
// console.log(res.statusText)
throw new Error('Response Code ' + res.status + '-> ' + text)
}
} catch (err) {
popMessage(err, '#d63384')
}
}
async function handleRemoveProject() {
const username = document.querySelector('#project-username').value
const project = document.querySelector('#project-id').value
if (username == "" || project == "") {
alert('Please fill in a username and select a project.')
return
}
let formData = new FormData()
formData.append('username', username)
formData.append('remove-project', project)
try {
const res = await fetch(`/api/user/${username}`, { method: 'POST', body: formData })
if (res.ok) {
let text = await res.text()
popMessage(text, '#048109')
reloadUserList()
} else {
let text = await res.text()
// console.log(res.statusText)
throw new Error('Response Code ' + res.status + '-> ' + text)
}
} catch (err) {
popMessage(err, '#d63384')
}
}
function popMessage(response, rescolor) {
message = {msg: response, color: rescolor}
displayMessage = true
setTimeout(function() {
displayMessage = false
}, 3500)
}
function reloadUserList() {
dispatch('reload')
}
</script>
<Card>
<CardBody>
<CardTitle class="mb-3">Edit Project Managed By User (Manager Only)</CardTitle>
<div class="input-group mb-3">
<input type="text" class="form-control" placeholder="username" id="project-username"/>
<input type="text" class="form-control" placeholder="project-id" id="project-id"/>
<!-- PreventDefault on Sveltestrap-Button more complex to achieve than just use good ol' html button -->
<!-- see: https://stackoverflow.com/questions/69630422/svelte-how-to-use-event-modifiers-in-my-own-components -->
<button class="btn btn-primary" type="button" id="add-project-button" on:click|preventDefault={handleAddProject}>Add</button>
<button class="btn btn-danger" type="button" id="remove-project-button" on:click|preventDefault={handleRemoveProject}>Remove</button>
</div>
<p>
{#if displayMessage}<b><code style="color: {message.color};" out:fade>Update: {message.msg}</code></b>{/if}
</p>
</CardBody>
</Card>

View File

@ -8,6 +8,8 @@
let message = {msg: '', color: '#d63384'} let message = {msg: '', color: '#d63384'}
let displayMessage = false let displayMessage = false
export let roles = []
async function handleAddRole() { async function handleAddRole() {
const username = document.querySelector('#role-username').value const username = document.querySelector('#role-username').value
const role = document.querySelector('#role-select').value const role = document.querySelector('#role-select').value
@ -86,10 +88,9 @@
<input type="text" class="form-control" placeholder="username" id="role-username"/> <input type="text" class="form-control" placeholder="username" id="role-username"/>
<select class="form-select" id="role-select"> <select class="form-select" id="role-select">
<option selected value="">Role...</option> <option selected value="">Role...</option>
<option value="user">User</option> {#each roles as role}
<option value="support">Support</option> <option value={role}>{role.charAt(0).toUpperCase() + role.slice(1)}</option>
<option value="admin">Admin</option> {/each}
<option value="api">API</option>
</select> </select>
<!-- PreventDefault on Sveltestrap-Button more complex to achieve than just use good ol' html button --> <!-- PreventDefault on Sveltestrap-Button more complex to achieve than just use good ol' html button -->
<!-- see: https://stackoverflow.com/questions/69630422/svelte-how-to-use-event-modifiers-in-my-own-components --> <!-- see: https://stackoverflow.com/questions/69630422/svelte-how-to-use-event-modifiers-in-my-own-components -->

View File

@ -41,6 +41,7 @@
<tr> <tr>
<th>Username</th> <th>Username</th>
<th>Name</th> <th>Name</th>
<th>Project(s)</th>
<th>Email</th> <th>Email</th>
<th>Roles</th> <th>Roles</th>
<th>JWT</th> <th>JWT</th>

View File

@ -16,6 +16,7 @@
<td>{user.username}</td> <td>{user.username}</td>
<td>{user.name}</td> <td>{user.name}</td>
<td>{user.projects}</td>
<td>{user.email}</td> <td>{user.email}</td>
<td><code>{user.roles.join(', ')}</code></td> <td><code>{user.roles.join(', ')}</code></td>
<td> <td>

View File

@ -224,7 +224,7 @@
on:change={({ detail: { from, to } }) => { on:change={({ detail: { from, to } }) => {
filters.startTime.from = from?.toISOString() filters.startTime.from = from?.toISOString()
filters.startTime.to = to?.toISOString() filters.startTime.to = to?.toISOString()
console.log(filters.startTime) // console.log(filters.startTime)
update() update()
}} }}
/> />

View File

@ -23,7 +23,7 @@ import Header from '../Header.svelte';
const findMaxNumAccels = clusters => clusters.reduce((max, cluster) => Math.max(max, const findMaxNumAccels = clusters => clusters.reduce((max, cluster) => Math.max(max,
cluster.subClusters.reduce((max, sc) => Math.max(max, sc.topology.accelerators?.length || 0), 0)), 0) cluster.subClusters.reduce((max, sc) => Math.max(max, sc.topology.accelerators?.length || 0), 0)), 0)
console.log(header) // console.log(header)
let minNumNodes = 1, maxNumNodes = 0, minNumHWThreads = 1, maxNumHWThreads = 0, minNumAccelerators = 0, maxNumAccelerators = 0 let minNumNodes = 1, maxNumNodes = 0, minNumHWThreads = 1, maxNumHWThreads = 0, minNumAccelerators = 0, maxNumAccelerators = 0
$: { $: {
if ($initialized) { if ($initialized) {

View File

@ -6,6 +6,8 @@
export let user = '' export let user = ''
export let project = '' export let project = ''
export let authlevel
export let roles
let mode = 'user', term = '' let mode = 'user', term = ''
const throttle = 500 const throttle = 500
@ -22,6 +24,18 @@
let timeoutId = null let timeoutId = null
function termChanged(sleep = throttle) { function termChanged(sleep = throttle) {
if (authlevel == roles.user) {
project = term
if (timeoutId != null)
clearTimeout(timeoutId)
timeoutId = setTimeout(() => {
dispatch('update', {
project
})
}, sleep)
} else if (authlevel >= roles.manager) {
if (mode == 'user') if (mode == 'user')
user = term user = term
else else
@ -37,8 +51,16 @@
}) })
}, sleep) }, sleep)
} }
}
</script> </script>
{#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 >= 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}>
@ -49,3 +71,6 @@
type="text" bind:value={term} on:change={() => termChanged()} on:keyup={(event) => termChanged(event.key == 'Enter' ? 0 : throttle)} type="text" bind:value={term} on:change={() => termChanged()} on:keyup={(event) => termChanged(event.key == 'Enter' ? 0 : throttle)}
placeholder={mode == 'user' ? 'filter username...' : 'filter project...'} /> placeholder={mode == 'user' ? 'filter username...' : 'filter project...'} />
</InputGroup> </InputGroup>
{:else}
Unauthorized
{/if}

View File

@ -27,6 +27,7 @@
let itemsPerPage = ccconfig.plot_list_jobsPerPage let itemsPerPage = ccconfig.plot_list_jobsPerPage
let page = 1 let page = 1
let paging = { itemsPerPage, page } let paging = { itemsPerPage, page }
let filter = []
const jobs = operationStore(` const jobs = operationStore(`
query($filter: [JobFilter!]!, $sorting: OrderByInput!, $paging: PageRequest! ){ query($filter: [JobFilter!]!, $sorting: OrderByInput!, $paging: PageRequest! ){
@ -45,7 +46,7 @@
}`, { }`, {
paging, paging,
sorting, sorting,
filter: [] filter,
}, { }, {
pause: true pause: true
}) })
@ -68,7 +69,7 @@
} }
$jobs.variables.filter = filters $jobs.variables.filter = filters
console.log('filters:', ...filters.map(f => Object.entries(f)).flat(2)) // console.log('filters:', ...filters.map(f => Object.entries(f)).flat(2))
} }
page = 1 page = 1

View File

@ -4,7 +4,9 @@ import Jobs from './Jobs.root.svelte'
new Jobs({ new Jobs({
target: document.getElementById('svelte-app'), target: document.getElementById('svelte-app'),
props: { props: {
filterPresets: filterPresets filterPresets: filterPresets,
authlevel: authlevel,
roles: roles
}, },
context: new Map([ context: new Map([
['cc-config', clusterCockpitConfig] ['cc-config', clusterCockpitConfig]

View File

@ -285,7 +285,7 @@
else if (scope == 'hwthread') else if (scope == 'hwthread')
divisor = subCluster.topology.node.length divisor = subCluster.topology.node.length
else { else {
console.log('TODO: how to calc thresholds for ', scope) // console.log('TODO: how to calc thresholds for ', scope)
return null return null
} }

View File

@ -16,8 +16,9 @@
<script> <script>
const header = { const header = {
"username": "{{ .User.Username }}", "username": "{{ .User.Username }}",
"isAdmin": {{ .User.IsAdmin }}, "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 .User.IsAdmin}} {{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 .User.IsAdmin}} {{if .User.HasRole .Roles.admin}}
{{range .Infos.clusters}} {{range .Infos.clusters}}
<tr> <tr>
<td>{{.Name}}</td> <td>{{.Name}}</td>

View File

@ -10,6 +10,8 @@
<script> <script>
const filterPresets = {{ .FilterPresets }}; const filterPresets = {{ .FilterPresets }};
const clusterCockpitConfig = {{ .Config }}; const clusterCockpitConfig = {{ .Config }};
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,12 +56,6 @@ func init() {
_ = base _ = base
} }
type User struct {
Username string // Username of the currently logged in user
IsAdmin bool
IsSupporter bool
}
type Build struct { type Build struct {
Version string Version string
Hash string Hash string
@ -71,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.