mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2024-12-24 12:29:05 +01:00
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:
commit
13c31155fc
@ -72,7 +72,7 @@ func main() {
|
||||
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.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(&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>,...`")
|
||||
@ -142,7 +142,7 @@ func main() {
|
||||
}
|
||||
|
||||
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 {
|
||||
log.Fatalf("adding '%s' user authentication failed: %v", parts[0], err)
|
||||
}
|
||||
|
@ -1,42 +1,35 @@
|
||||
## Docs for ClusterCockpit Searchbar
|
||||
# Docs for ClusterCockpit Searchbar
|
||||
|
||||
### Usage
|
||||
## Usage
|
||||
|
||||
* Searchtags are implemented as `type:<query>` search-string
|
||||
* Types `jobId, jobName, projectId, username, name` for roles `admin` and `support`
|
||||
* `jobName` is jobName as persisted in `job.meta_data` 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
|
||||
* Types `jobId, jobName` for role `user`
|
||||
* Examples:
|
||||
* `jobName:myJob12`
|
||||
* `jobId:123456`
|
||||
* `username:abcd100`
|
||||
* `name:Paul`
|
||||
* Types `jobId, jobName, projectId, username, name` for roles `admin` and `support`
|
||||
* `jobName` is jobName as persisted in `job.meta_data` 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
|
||||
* Types `jobId, jobName, projectId` for role `user`
|
||||
* Examples:
|
||||
* `jobName:myJob12`
|
||||
* `jobId:123456`
|
||||
* `username:abcd100`
|
||||
* `name:Paul`
|
||||
* If no searchTag used: Best guess search with the following hierarchy
|
||||
* `jobId -> username -> name -> projectId -> jobName`
|
||||
* `jobId -> username -> name -> projectId -> jobName`
|
||||
* Destinations:
|
||||
* JobId: Always 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)
|
||||
* ProjectId: Always Job-Table
|
||||
* Username
|
||||
* If *one* match found: Opens detailed user-view (`/monitoring/user/$USER`)
|
||||
* If *multiple* matches 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.
|
||||
* 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)
|
||||
* Simple HTML Error if ...
|
||||
* Best guess search fails -> 'Not Found'
|
||||
* Query `type` is unknown
|
||||
* More than two colons in string -> 'malformed'
|
||||
* JobId: Job-Table (Allows multiple identical matches, e.g. JobIds from different clusters)
|
||||
* JobName: Job-Table (Allows multiple identical matches, e.g. JobNames from different clusters)
|
||||
* ProjectId: Job-Table
|
||||
* Username: Users-Table
|
||||
* **Please Note**: Only users with jobs will be shown in table! I.e., Users without jobs will be missing in table.
|
||||
* Name: Users-Table
|
||||
* **Please Note**: Only users with jobs will be shown in table! I.e., Users without jobs will be missing in table.
|
||||
* Best guess search always redirects to Job-Table or `/monitoring/user/$USER` (first username match)
|
||||
* Unprocessable queries will redirect to `/monitoring/jobs/?`
|
||||
* Spaces trimmed (both for searchTag and queryString)
|
||||
* ` job12` == `job12`
|
||||
* `projectID : abcd ` == `projectId:abcd`
|
||||
* ` job12` == `job12`
|
||||
* `projectID : abcd ` == `projectId:abcd`
|
||||
* `jobName`- and `name-`queries work with a part of the target-string
|
||||
* `jobName:myjob` for jobName "myjob_cluster1"
|
||||
* `name:Paul` for name "Paul Atreides"
|
||||
* `jobName:myjob` for jobName "myjob_cluster1"
|
||||
* `name:Paul` for name "Paul Atreides"
|
||||
|
||||
* JobName GQL Query is resolved as matching the query as a part of the whole metaData-JSON in the SQL DB.
|
||||
|
@ -76,6 +76,7 @@ func (api *RestApi) MountRoutes(r *mux.Router) {
|
||||
|
||||
if api.Authentication != nil {
|
||||
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.getUsers).Methods(http.MethodGet)
|
||||
r.HandleFunc("/users/", api.deleteUser).Methods(http.MethodDelete)
|
||||
@ -177,7 +178,7 @@ func decode(r io.Reader, val interface{}) error {
|
||||
// @router /jobs/ [get]
|
||||
func (api *RestApi) getJobs(rw http.ResponseWriter, r *http.Request) {
|
||||
if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) {
|
||||
handleError(fmt.Errorf("missing role: %v", auth.RoleApi), http.StatusForbidden, rw)
|
||||
handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw)
|
||||
return
|
||||
}
|
||||
|
||||
@ -317,7 +318,7 @@ func (api *RestApi) getJobs(rw http.ResponseWriter, r *http.Request) {
|
||||
// @router /jobs/tag_job/{id} [post]
|
||||
func (api *RestApi) tagJob(rw http.ResponseWriter, r *http.Request) {
|
||||
if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) {
|
||||
handleError(fmt.Errorf("missing role: %v", auth.RoleApi), http.StatusForbidden, rw)
|
||||
handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw)
|
||||
return
|
||||
}
|
||||
|
||||
@ -382,7 +383,7 @@ func (api *RestApi) tagJob(rw http.ResponseWriter, r *http.Request) {
|
||||
// @router /jobs/start_job/ [post]
|
||||
func (api *RestApi) startJob(rw http.ResponseWriter, r *http.Request) {
|
||||
if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) {
|
||||
handleError(fmt.Errorf("missing role: %v", auth.RoleApi), http.StatusForbidden, rw)
|
||||
handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw)
|
||||
return
|
||||
}
|
||||
|
||||
@ -463,7 +464,7 @@ func (api *RestApi) startJob(rw http.ResponseWriter, r *http.Request) {
|
||||
// @router /jobs/stop_job/{id} [post]
|
||||
func (api *RestApi) stopJobById(rw http.ResponseWriter, r *http.Request) {
|
||||
if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) {
|
||||
handleError(fmt.Errorf("missing role: %v", auth.RoleApi), http.StatusForbidden, rw)
|
||||
handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw)
|
||||
return
|
||||
}
|
||||
|
||||
@ -516,7 +517,7 @@ func (api *RestApi) stopJobById(rw http.ResponseWriter, r *http.Request) {
|
||||
// @router /jobs/stop_job/ [post]
|
||||
func (api *RestApi) stopJobByRequest(rw http.ResponseWriter, r *http.Request) {
|
||||
if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) {
|
||||
handleError(fmt.Errorf("missing role: %v", auth.RoleApi), http.StatusForbidden, rw)
|
||||
handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw)
|
||||
return
|
||||
}
|
||||
|
||||
@ -562,7 +563,7 @@ func (api *RestApi) stopJobByRequest(rw http.ResponseWriter, r *http.Request) {
|
||||
// @router /jobs/delete_job/{id} [delete]
|
||||
func (api *RestApi) deleteJobById(rw http.ResponseWriter, r *http.Request) {
|
||||
if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) {
|
||||
handleError(fmt.Errorf("missing role: %v", auth.RoleApi), http.StatusForbidden, rw)
|
||||
handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw)
|
||||
return
|
||||
}
|
||||
|
||||
@ -610,7 +611,7 @@ func (api *RestApi) deleteJobById(rw http.ResponseWriter, r *http.Request) {
|
||||
// @router /jobs/delete_job/ [delete]
|
||||
func (api *RestApi) deleteJobByRequest(rw http.ResponseWriter, r *http.Request) {
|
||||
if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) {
|
||||
handleError(fmt.Errorf("missing role: %v", auth.RoleApi), http.StatusForbidden, rw)
|
||||
handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw)
|
||||
return
|
||||
}
|
||||
|
||||
@ -666,7 +667,7 @@ func (api *RestApi) deleteJobByRequest(rw http.ResponseWriter, r *http.Request)
|
||||
// @router /jobs/delete_job_before/{ts} [delete]
|
||||
func (api *RestApi) deleteJobBefore(rw http.ResponseWriter, r *http.Request) {
|
||||
if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) {
|
||||
handleError(fmt.Errorf("missing role: %v", auth.RoleApi), http.StatusForbidden, rw)
|
||||
handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw)
|
||||
return
|
||||
}
|
||||
|
||||
@ -817,17 +818,26 @@ func (api *RestApi) createUser(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
username, password, role, name, email := r.FormValue("username"), r.FormValue("password"), r.FormValue("role"), r.FormValue("name"), r.FormValue("email")
|
||||
if len(password) == 0 && role != auth.RoleApi {
|
||||
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.GetRoleString(auth.RoleApi) {
|
||||
http.Error(rw, "Only API users are allowed to have a blank password (login will be impossible)", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if len(project) != 0 && role != auth.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{
|
||||
Username: username,
|
||||
Name: name,
|
||||
Password: password,
|
||||
Email: email,
|
||||
Projects: []string{project},
|
||||
Roles: []string{role}}); err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusUnprocessableEntity)
|
||||
return
|
||||
@ -866,6 +876,22 @@ func (api *RestApi) getUsers(rw http.ResponseWriter, r *http.Request) {
|
||||
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) {
|
||||
if user := auth.GetUser(r.Context()); !user.HasRole(auth.RoleAdmin) {
|
||||
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
|
||||
newrole := r.FormValue("add-role")
|
||||
delrole := r.FormValue("remove-role")
|
||||
newproj := r.FormValue("add-project")
|
||||
delproj := r.FormValue("remove-project")
|
||||
|
||||
// TODO: Handle anything but roles...
|
||||
if newrole != "" {
|
||||
@ -889,8 +917,20 @@ func (api *RestApi) updateUser(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
rw.Write([]byte("Remove Role Success"))
|
||||
} else if newproj != "" {
|
||||
if err := api.Authentication.AddProject(r.Context(), mux.Vars(r)["id"], newproj); err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
rw.Write([]byte("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 {
|
||||
http.Error(rw, "Not Add or Del?", http.StatusInternalServerError)
|
||||
http.Error(rw, "Not Add or Del [role|project]?", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9,8 +9,10 @@ import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ClusterCockpit/cc-backend/pkg/log"
|
||||
@ -18,32 +20,186 @@ import (
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
const (
|
||||
RoleAdmin string = "admin"
|
||||
RoleSupport string = "support"
|
||||
RoleApi string = "api"
|
||||
RoleUser string = "user"
|
||||
)
|
||||
type AuthSource int
|
||||
|
||||
const (
|
||||
AuthViaLocalPassword int8 = 0
|
||||
AuthViaLDAP int8 = 1
|
||||
AuthViaToken int8 = 2
|
||||
AuthViaLocalPassword AuthSource = iota
|
||||
AuthViaLDAP
|
||||
AuthViaToken
|
||||
)
|
||||
|
||||
type User struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"-"`
|
||||
Name string `json:"name"`
|
||||
Roles []string `json:"roles"`
|
||||
AuthSource int8 `json:"via"`
|
||||
Email string `json:"email"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"-"`
|
||||
Name string `json:"name"`
|
||||
Roles []string `json:"roles"`
|
||||
AuthSource AuthSource `json:"via"`
|
||||
Email string `json:"email"`
|
||||
Projects []string `json:"projects"`
|
||||
Expiration time.Time
|
||||
}
|
||||
|
||||
func (u *User) HasRole(role string) bool {
|
||||
type Role int
|
||||
|
||||
const (
|
||||
RoleAnonymous Role = iota
|
||||
RoleApi
|
||||
RoleUser
|
||||
RoleManager
|
||||
RoleSupport
|
||||
RoleAdmin
|
||||
RoleError
|
||||
)
|
||||
|
||||
func GetRoleString(roleInt Role) string {
|
||||
return [6]string{"anonymous", "api", "user", "manager", "support", "admin"}[roleInt]
|
||||
}
|
||||
|
||||
func getRoleEnum(roleStr string) Role {
|
||||
switch strings.ToLower(roleStr) {
|
||||
case "admin":
|
||||
return RoleAdmin
|
||||
case "support":
|
||||
return RoleSupport
|
||||
case "manager":
|
||||
return RoleManager
|
||||
case "user":
|
||||
return RoleUser
|
||||
case "api":
|
||||
return RoleApi
|
||||
case "anonymous":
|
||||
return RoleAnonymous
|
||||
default:
|
||||
return RoleError
|
||||
}
|
||||
}
|
||||
|
||||
func isValidRole(role string) bool {
|
||||
if getRoleEnum(role) == RoleError {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (u *User) HasValidRole(role string) (hasRole bool, isValid bool) {
|
||||
if isValidRole(role) {
|
||||
for _, r := range u.Roles {
|
||||
if r == role {
|
||||
return true, true
|
||||
}
|
||||
}
|
||||
return false, true
|
||||
}
|
||||
return false, false
|
||||
}
|
||||
|
||||
func (u *User) HasRole(role Role) bool {
|
||||
for _, r := range u.Roles {
|
||||
if r == role {
|
||||
if r == GetRoleString(role) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -145,9 +301,11 @@ func (auth *Authentication) AuthViaSession(
|
||||
}
|
||||
|
||||
username, _ := session.Values["username"].(string)
|
||||
projects, _ := session.Values["projects"].([]string)
|
||||
roles, _ := session.Values["roles"].([]string)
|
||||
return &User{
|
||||
Username: username,
|
||||
Projects: projects,
|
||||
Roles: roles,
|
||||
AuthSource: -1,
|
||||
}, nil
|
||||
@ -192,6 +350,7 @@ func (auth *Authentication) Login(
|
||||
session.Options.MaxAge = int(auth.SessionMaxAge.Seconds())
|
||||
}
|
||||
session.Values["username"] = user.Username
|
||||
session.Values["projects"] = user.Projects
|
||||
session.Values["roles"] = user.Roles
|
||||
if err := auth.sessionStore.Save(r, rw, session); err != nil {
|
||||
log.Warnf("session save failed: %s", err.Error())
|
||||
@ -199,7 +358,7 @@ func (auth *Authentication) Login(
|
||||
return
|
||||
}
|
||||
|
||||
log.Infof("login successfull: user: %v (roles: %v)", user.Username, user.Roles)
|
||||
log.Infof("login successfull: user: %#v (roles: %v, projects: %v)", user.Username, user.Roles, user.Projects)
|
||||
ctx := context.WithValue(r.Context(), ContextUserKey, user)
|
||||
onsuccess.ServeHTTP(rw, r.WithContext(ctx))
|
||||
return
|
||||
|
@ -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
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -146,12 +146,16 @@ func (ja *JWTAuthenticator) Login(
|
||||
if rawroles, ok := claims["roles"].([]interface{}); ok {
|
||||
for _, rr := range rawroles {
|
||||
if r, ok := rr.(string); ok {
|
||||
roles = append(roles, r)
|
||||
if isValidRole(r) {
|
||||
roles = append(roles, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if rawrole, ok := claims["roles"].(string); ok {
|
||||
roles = append(roles, rawrole)
|
||||
if isValidRole(rawrole) {
|
||||
roles = append(roles, rawrole)
|
||||
}
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
|
@ -164,7 +164,7 @@ func (la *LdapAuthenticator) Sync() error {
|
||||
name := newnames[username]
|
||||
log.Debugf("sync: add %v (name: %v, roles: [user], ldap: true)", username, name)
|
||||
if _, err := la.auth.db.Exec(`INSERT INTO user (username, ldap, name, roles) VALUES (?, ?, ?, ?)`,
|
||||
username, 1, name, "[\""+RoleUser+"\"]"); err != nil {
|
||||
username, 1, name, "[\""+GetRoleString(RoleUser)+"\"]"); err != nil {
|
||||
log.Errorf("User '%s' new in LDAP: Insert into DB failed", username)
|
||||
return err
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/ClusterCockpit/cc-backend/internal/graph/model"
|
||||
"github.com/ClusterCockpit/cc-backend/pkg/log"
|
||||
@ -21,10 +22,10 @@ import (
|
||||
func (auth *Authentication) GetUser(username string) (*User, error) {
|
||||
|
||||
user := &User{Username: username}
|
||||
var hashedPassword, name, rawRoles, email sql.NullString
|
||||
if err := sq.Select("password", "ldap", "name", "roles", "email").From("user").
|
||||
var hashedPassword, name, rawRoles, email, rawProjects sql.NullString
|
||||
if err := sq.Select("password", "ldap", "name", "roles", "email", "projects").From("user").
|
||||
Where("user.username = ?", username).RunWith(auth.db).
|
||||
QueryRow().Scan(&hashedPassword, &user.AuthSource, &name, &rawRoles, &email); err != nil {
|
||||
QueryRow().Scan(&hashedPassword, &user.AuthSource, &name, &rawRoles, &email, &rawProjects); err != nil {
|
||||
log.Warnf("Error while querying user '%v' from database", username)
|
||||
return nil, err
|
||||
}
|
||||
@ -38,6 +39,11 @@ func (auth *Authentication) GetUser(username string) (*User, error) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if rawProjects.Valid {
|
||||
if err := json.Unmarshal([]byte(rawProjects.String), &user.Projects); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
@ -45,9 +51,11 @@ func (auth *Authentication) GetUser(username string) (*User, error) {
|
||||
func (auth *Authentication) AddUser(user *User) error {
|
||||
|
||||
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 != "" {
|
||||
cols = append(cols, "name")
|
||||
vals = append(vals, user.Name)
|
||||
@ -71,7 +79,7 @@ func (auth *Authentication) AddUser(user *User) error {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Infof("new user %v created (roles: %s, auth-source: %d)", user.Username, rolesJson, user.AuthSource)
|
||||
log.Infof("new user %#v created (roles: %s, auth-source: %d, projects: %s)", user.Username, rolesJson, user.AuthSource, projectsJson)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -84,7 +92,7 @@ func (auth *Authentication) DelUser(username string) error {
|
||||
|
||||
func (auth *Authentication) ListUsers(specialsOnly bool) ([]*User, error) {
|
||||
|
||||
q := sq.Select("username", "name", "email", "roles").From("user")
|
||||
q := sq.Select("username", "name", "email", "roles", "projects").From("user")
|
||||
if specialsOnly {
|
||||
q = q.Where("(roles != '[\"user\"]' AND roles != '[]')")
|
||||
}
|
||||
@ -99,9 +107,10 @@ func (auth *Authentication) ListUsers(specialsOnly bool) ([]*User, error) {
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
rawroles := ""
|
||||
rawprojects := ""
|
||||
user := &User{}
|
||||
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")
|
||||
return nil, err
|
||||
}
|
||||
@ -111,6 +120,10 @@ func (auth *Authentication) ListUsers(specialsOnly bool) ([]*User, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(rawprojects), &user.Projects); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user.Name = name.String
|
||||
user.Email = email.String
|
||||
users = append(users, user)
|
||||
@ -121,25 +134,25 @@ func (auth *Authentication) ListUsers(specialsOnly bool) ([]*User, error) {
|
||||
func (auth *Authentication) AddRole(
|
||||
ctx context.Context,
|
||||
username string,
|
||||
role string) error {
|
||||
queryrole string) error {
|
||||
|
||||
newRole := strings.ToLower(queryrole)
|
||||
user, err := auth.GetUser(username)
|
||||
if err != nil {
|
||||
log.Warnf("Could not load user '%s'", username)
|
||||
return err
|
||||
}
|
||||
|
||||
if role != RoleAdmin && role != RoleApi && role != RoleUser && role != RoleSupport {
|
||||
return fmt.Errorf("Invalid user role: %v", role)
|
||||
exists, valid := user.HasValidRole(newRole)
|
||||
|
||||
if !valid {
|
||||
return fmt.Errorf("Supplied role is no valid option : %v", newRole)
|
||||
}
|
||||
if exists {
|
||||
return fmt.Errorf("User %v already has role %v", username, newRole)
|
||||
}
|
||||
|
||||
for _, r := range user.Roles {
|
||||
if r == role {
|
||||
return fmt.Errorf("User %v already has role %v", username, role)
|
||||
}
|
||||
}
|
||||
|
||||
roles, _ := json.Marshal(append(user.Roles, role))
|
||||
roles, _ := json.Marshal(append(user.Roles, newRole))
|
||||
if _, err := sq.Update("user").Set("roles", roles).Where("user.username = ?", username).RunWith(auth.db).Exec(); err != nil {
|
||||
log.Errorf("Error while adding new role for user '%s'", user.Username)
|
||||
return err
|
||||
@ -147,42 +160,111 @@ func (auth *Authentication) AddRole(
|
||||
return nil
|
||||
}
|
||||
|
||||
func (auth *Authentication) RemoveRole(ctx context.Context, username string, role string) error {
|
||||
func (auth *Authentication) RemoveRole(ctx context.Context, username string, queryrole string) error {
|
||||
oldRole := strings.ToLower(queryrole)
|
||||
user, err := auth.GetUser(username)
|
||||
if err != nil {
|
||||
log.Warnf("Could not load user '%s'", username)
|
||||
return err
|
||||
}
|
||||
|
||||
if role != RoleAdmin && role != RoleApi && role != RoleUser && role != RoleSupport {
|
||||
return fmt.Errorf("Invalid user role: %v", role)
|
||||
exists, valid := user.HasValidRole(oldRole)
|
||||
|
||||
if !valid {
|
||||
return fmt.Errorf("Supplied role is no valid option : %v", oldRole)
|
||||
}
|
||||
if !exists {
|
||||
return fmt.Errorf("Role already deleted for user '%v': %v", username, oldRole)
|
||||
}
|
||||
|
||||
if 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 newroles []string
|
||||
for _, r := range user.Roles {
|
||||
if r != oldRole {
|
||||
newroles = append(newroles, r) // Append all roles not matching requested to be deleted role
|
||||
}
|
||||
}
|
||||
|
||||
var mroles, _ = json.Marshal(newroles)
|
||||
if _, err := sq.Update("user").Set("roles", mroles).Where("user.username = ?", username).RunWith(auth.db).Exec(); err != nil {
|
||||
log.Errorf("Error while removing role for user '%s'", user.Username)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (auth *Authentication) AddProject(
|
||||
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 newroles []string
|
||||
for _, r := range user.Roles {
|
||||
if r != role {
|
||||
newroles = append(newroles, r) // Append all roles not matching requested delete role
|
||||
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 {
|
||||
exists = true
|
||||
}
|
||||
}
|
||||
|
||||
if (exists == true) {
|
||||
var mroles, _ = json.Marshal(newroles)
|
||||
if _, err := sq.Update("user").Set("roles", mroles).Where("user.username = ?", username).RunWith(auth.db).Exec(); err != nil {
|
||||
log.Errorf("Error while removing role for user '%s'", user.Username)
|
||||
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 '%v' already does not have role: %v", username, role)
|
||||
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) {
|
||||
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")
|
||||
}
|
||||
|
||||
|
@ -170,7 +170,7 @@ func (r *queryResolver) Job(ctx context.Context, id string) (*schema.Job, error)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user := auth.GetUser(ctx); user != nil && !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")
|
||||
}
|
||||
|
||||
|
@ -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 = SecurityCheck(ctx, q)
|
||||
q, qerr := SecurityCheck(ctx, sq.Select("job."+string(aggreg), count).From("job").GroupBy("job."+string(aggreg)).OrderBy("count DESC"))
|
||||
|
||||
if qerr != nil {
|
||||
return nil, qerr
|
||||
}
|
||||
|
||||
for _, f := range filters {
|
||||
q = BuildWhereClause(f, q)
|
||||
}
|
||||
@ -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 nothing matches the search, `ErrNotFound` is returned.
|
||||
|
||||
func (r *JobRepository) FindJobnameOrUserOrProject(ctx context.Context, searchterm string) (metasnip string, username string, project string, err error) {
|
||||
user := auth.GetUser(ctx)
|
||||
func (r *JobRepository) FindUserOrProjectOrJobname(ctx context.Context, searchterm string) (username string, project string, metasnip string, err error) {
|
||||
if _, err := strconv.Atoi(searchterm); err == nil { // Return empty on successful conversion: parent method will redirect for integer jobId
|
||||
return "", "", "", nil
|
||||
} else { // has to have letters
|
||||
|
||||
if user == nil || user.HasRole(auth.RoleAdmin) || user.HasRole(auth.RoleSupport) {
|
||||
err := sq.Select("job.user").Distinct().From("job").
|
||||
Where("job.user = ?", searchterm).
|
||||
RunWith(r.stmtCache).QueryRow().Scan(&username)
|
||||
} else { // Has to have letters and logged-in user for other guesses
|
||||
user := auth.GetUser(ctx)
|
||||
if user != nil {
|
||||
// Find username in jobs (match)
|
||||
uresult, _ := r.FindColumnValue(user, searchterm, "job", "user", "user", false)
|
||||
if uresult != "" {
|
||||
return uresult, "", "", nil
|
||||
}
|
||||
// Find username by name (like)
|
||||
nresult, _ := r.FindColumnValue(user, searchterm, "user", "username", "name", true)
|
||||
if nresult != "" {
|
||||
return nresult, "", "", 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
|
||||
err := sq.Select("job.cluster").Distinct().From("job").
|
||||
Where("job.meta_data LIKE ?", "%"+searchterm+"%").
|
||||
RunWith(r.stmtCache).QueryRow().Scan(&metasnip)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return "", "", "", err
|
||||
} else if err == nil {
|
||||
return "", username, "", nil
|
||||
}
|
||||
|
||||
if username == "" { // Try with Name2Username query
|
||||
errtwo := sq.Select("user.username").Distinct().From("user").
|
||||
Where("user.name LIKE ?", fmt.Sprint("%"+searchterm+"%")).
|
||||
RunWith(r.stmtCache).QueryRow().Scan(&username)
|
||||
if errtwo != nil && errtwo != sql.ErrNoRows {
|
||||
return "", "", "", errtwo
|
||||
} else if errtwo == nil {
|
||||
return "", username, "", nil
|
||||
}
|
||||
return "", "", metasnip[0:1], nil
|
||||
}
|
||||
}
|
||||
|
||||
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").
|
||||
Where("job.meta_data LIKE ?", "%"+searchterm+"%").
|
||||
RunWith(r.stmtCache).QueryRow().Scan(&metasnip)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return "", "", "", err
|
||||
} else if err == nil {
|
||||
return metasnip[0:1], "", "", nil
|
||||
}
|
||||
|
||||
return "", "", "", ErrNotFound
|
||||
}
|
||||
}
|
||||
|
||||
func (r *JobRepository) FindUser(ctx context.Context, searchterm string) (username string, err error) {
|
||||
user := auth.GetUser(ctx)
|
||||
if user == nil || user.HasRole(auth.RoleAdmin) || user.HasRole(auth.RoleSupport) {
|
||||
err := sq.Select("job.user").Distinct().From("job").
|
||||
Where("job.user = ?", searchterm).
|
||||
RunWith(r.stmtCache).QueryRow().Scan(&username)
|
||||
func (r *JobRepository) FindColumnValue(user *auth.User, searchterm string, table string, selectColumn string, whereColumn string, isLike bool) (result string, err error) {
|
||||
compareStr := " = ?"
|
||||
query := searchterm
|
||||
if isLike == true {
|
||||
compareStr = " LIKE ?"
|
||||
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 {
|
||||
return "", err
|
||||
} else if err == nil {
|
||||
return username, nil
|
||||
return result, nil
|
||||
}
|
||||
return "", ErrNotFound
|
||||
|
||||
} 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
|
||||
}
|
||||
}
|
||||
|
||||
func (r *JobRepository) FindUserByName(ctx context.Context, searchterm string) (username 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)
|
||||
func (r *JobRepository) FindColumnValues(user *auth.User, query string, table string, selectColumn string, whereColumn string) (results []string, err error) {
|
||||
emptyResult := make([]string, 0)
|
||||
if user == nil || user.HasRole(auth.RoleAdmin) || user.HasRole(auth.RoleSupport) {
|
||||
rows, err := sq.Select("job.user").Distinct().From("job").
|
||||
Where("job.user LIKE ?", fmt.Sprint("%", searchterm, "%")).
|
||||
if user.HasAnyRole([]auth.Role{auth.RoleAdmin, auth.RoleSupport, auth.RoleManager}) {
|
||||
rows, err := sq.Select(table+"."+selectColumn).Distinct().From(table).
|
||||
Where(table+"."+whereColumn+" LIKE ?", fmt.Sprint("%", query, "%")).
|
||||
RunWith(r.stmtCache).Query()
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return emptyResult, err
|
||||
} else if err == nil {
|
||||
for rows.Next() {
|
||||
var name string
|
||||
err := rows.Scan(&name)
|
||||
var result string
|
||||
err := rows.Scan(&result)
|
||||
if err != nil {
|
||||
rows.Close()
|
||||
log.Warnf("Error while scanning rows: %v", err)
|
||||
return emptyResult, err
|
||||
}
|
||||
usernames = append(usernames, name)
|
||||
results = append(results, result)
|
||||
}
|
||||
return usernames, nil
|
||||
return results, nil
|
||||
}
|
||||
return emptyResult, ErrNotFound
|
||||
|
||||
} 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
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
var err error
|
||||
start := time.Now()
|
||||
@ -796,9 +699,9 @@ func (r *JobRepository) JobsStatistics(ctx context.Context,
|
||||
for _, cluster := range archive.Clusters {
|
||||
for _, subcluster := range cluster.SubClusters {
|
||||
corehoursCol := fmt.Sprintf("CAST(ROUND(SUM(job.duration * job.num_nodes * %d * %d) / 3600) as %s)", subcluster.SocketsPerNode, subcluster.CoresPerSocket, castType)
|
||||
var query sq.SelectBuilder
|
||||
var rawQuery sq.SelectBuilder
|
||||
if groupBy == nil {
|
||||
query = sq.Select(
|
||||
rawQuery = sq.Select(
|
||||
"''",
|
||||
"COUNT(job.id)",
|
||||
fmt.Sprintf("CAST(ROUND(SUM(job.duration) / 3600) as %s)", castType),
|
||||
@ -806,7 +709,7 @@ func (r *JobRepository) JobsStatistics(ctx context.Context,
|
||||
).From("job")
|
||||
} else {
|
||||
col := groupBy2column[*groupBy]
|
||||
query = sq.Select(
|
||||
rawQuery = sq.Select(
|
||||
col,
|
||||
"COUNT(job.id)",
|
||||
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)
|
||||
}
|
||||
|
||||
query = query.
|
||||
rawQuery = rawQuery.
|
||||
Where("job.cluster = ?", cluster.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 {
|
||||
query = BuildWhereClause(f, query)
|
||||
}
|
||||
@ -857,8 +765,14 @@ func (r *JobRepository) JobsStatistics(ctx context.Context,
|
||||
}
|
||||
|
||||
if groupBy == nil {
|
||||
|
||||
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 {
|
||||
query = BuildWhereClause(f, query)
|
||||
}
|
||||
@ -868,8 +782,15 @@ func (r *JobRepository) JobsStatistics(ctx context.Context,
|
||||
}
|
||||
} else {
|
||||
col := groupBy2column[*groupBy]
|
||||
|
||||
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 {
|
||||
query = BuildWhereClause(f, query)
|
||||
}
|
||||
@ -895,7 +816,8 @@ func (r *JobRepository) JobsStatistics(ctx context.Context,
|
||||
if col == "job.user" {
|
||||
for id := range stats {
|
||||
emptyDash := "-"
|
||||
name, _ := r.FindNameByUser(ctx, id)
|
||||
user := auth.GetUser(ctx)
|
||||
name, _ := r.FindColumnValue(user, id, "user", "name", "username", false)
|
||||
if name != "" {
|
||||
stats[id].Name = &name
|
||||
} else {
|
||||
@ -952,7 +874,12 @@ func (r *JobRepository) jobsStatisticsHistogram(ctx context.Context,
|
||||
|
||||
start := time.Now()
|
||||
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 {
|
||||
query = BuildWhereClause(f, query)
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ import (
|
||||
"github.com/golang-migrate/migrate/v4/source/iofs"
|
||||
)
|
||||
|
||||
const supportedVersion uint = 2
|
||||
const supportedVersion uint = 3
|
||||
|
||||
//go:embed migrations/*
|
||||
var migrationFiles embed.FS
|
||||
|
@ -2,4 +2,4 @@ DROP TABLE IF EXISTS job;
|
||||
DROP TABLE IF EXISTS tags;
|
||||
DROP TABLE IF EXISTS jobtag;
|
||||
DROP TABLE IF EXISTS configuration;
|
||||
DROP TABLE IF EXISTS user;
|
||||
DROP TABLE IF EXISTS user;
|
||||
|
@ -2,4 +2,4 @@ DROP INDEX IF EXISTS job_stats;
|
||||
DROP INDEX IF EXISTS job_by_user;
|
||||
DROP INDEX IF EXISTS job_by_starttime;
|
||||
DROP INDEX IF EXISTS job_by_job_id;
|
||||
DROP INDEX IF EXISTS job_by_state;
|
||||
DROP INDEX IF EXISTS job_by_state;
|
||||
|
@ -2,4 +2,4 @@ CREATE INDEX IF NOT EXISTS job_stats ON job (cluster,subcluster,user);
|
||||
CREATE INDEX IF NOT EXISTS job_by_user ON job (user);
|
||||
CREATE INDEX IF NOT EXISTS job_by_starttime ON job (start_time);
|
||||
CREATE INDEX IF NOT EXISTS job_by_job_id ON job (job_id);
|
||||
CREATE INDEX IF NOT EXISTS job_by_state ON job (job_state);
|
||||
CREATE INDEX IF NOT EXISTS job_by_state ON job (job_state);
|
||||
|
@ -0,0 +1 @@
|
||||
ALTER TABLE user DROP COLUMN projects;
|
@ -0,0 +1 @@
|
||||
ALTER TABLE user ADD COLUMN projects varchar(255) NOT NULL DEFAULT "[]";
|
@ -2,4 +2,4 @@ DROP TABLE IF EXISTS job;
|
||||
DROP TABLE IF EXISTS tags;
|
||||
DROP TABLE IF EXISTS jobtag;
|
||||
DROP TABLE IF EXISTS configuration;
|
||||
DROP TABLE IF EXISTS user;
|
||||
DROP TABLE IF EXISTS user;
|
||||
|
@ -2,4 +2,4 @@ DROP INDEX IF EXISTS job_stats;
|
||||
DROP INDEX IF EXISTS job_by_user;
|
||||
DROP INDEX IF EXISTS job_by_starttime;
|
||||
DROP INDEX IF EXISTS job_by_job_id;
|
||||
DROP INDEX IF EXISTS job_by_state;
|
||||
DROP INDEX IF EXISTS job_by_state;
|
||||
|
@ -2,4 +2,4 @@ CREATE INDEX IF NOT EXISTS job_stats ON job (cluster,subcluster,user);
|
||||
CREATE INDEX IF NOT EXISTS job_by_user ON job (user);
|
||||
CREATE INDEX IF NOT EXISTS job_by_starttime ON job (start_time);
|
||||
CREATE INDEX IF NOT EXISTS job_by_job_id ON job (job_id);
|
||||
CREATE INDEX IF NOT EXISTS job_by_state ON job (job_state);
|
||||
CREATE INDEX IF NOT EXISTS job_by_state ON job (job_state);
|
||||
|
@ -0,0 +1 @@
|
||||
ALTER TABLE user DROP COLUMN projects;
|
@ -0,0 +1 @@
|
||||
ALTER TABLE user ADD COLUMN projects varchar(255) NOT NULL DEFAULT "[]";
|
@ -26,8 +26,11 @@ func (r *JobRepository) QueryJobs(
|
||||
page *model.PageRequest,
|
||||
order *model.OrderByInput) ([]*schema.Job, error) {
|
||||
|
||||
query := sq.Select(jobColumns...).From("job")
|
||||
query = SecurityCheck(ctx, query)
|
||||
query, qerr := SecurityCheck(ctx, sq.Select(jobColumns...).From("job"))
|
||||
|
||||
if qerr != nil {
|
||||
return nil, qerr
|
||||
}
|
||||
|
||||
if order != nil {
|
||||
field := toSnakeCase(order.Field)
|
||||
@ -82,8 +85,12 @@ func (r *JobRepository) CountJobs(
|
||||
filters []*model.JobFilter) (int, error) {
|
||||
|
||||
// count all jobs:
|
||||
query := sq.Select("count(*)").From("job")
|
||||
query = SecurityCheck(ctx, query)
|
||||
query, qerr := SecurityCheck(ctx, sq.Select("count(*)").From("job"))
|
||||
|
||||
if qerr != nil {
|
||||
return 0, qerr
|
||||
}
|
||||
|
||||
for _, f := range filters {
|
||||
query = BuildWhereClause(f, query)
|
||||
}
|
||||
@ -95,13 +102,23 @@ func (r *JobRepository) CountJobs(
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func SecurityCheck(ctx context.Context, query sq.SelectBuilder) sq.SelectBuilder {
|
||||
func SecurityCheck(ctx context.Context, query sq.SelectBuilder) (queryOut sq.SelectBuilder, err error) {
|
||||
user := auth.GetUser(ctx)
|
||||
if user == nil || user.HasRole(auth.RoleAdmin) || user.HasRole(auth.RoleApi) || user.HasRole(auth.RoleSupport) {
|
||||
return query
|
||||
if user == nil || user.HasAnyRole([]auth.Role{auth.RoleAdmin, auth.RoleSupport, auth.RoleApi}) { // Admin & Co. : All jobs
|
||||
return query, nil
|
||||
} else if user.HasRole(auth.RoleManager) { // Manager : Add filter for managed projects' jobs only + personal jobs
|
||||
if len(user.Projects) != 0 {
|
||||
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.
|
||||
|
@ -5,9 +5,12 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/ClusterCockpit/cc-backend/internal/auth"
|
||||
"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/schema"
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
)
|
||||
|
||||
@ -65,7 +68,7 @@ func (r *JobRepository) CreateTag(tagType string, tagName string) (tagId int64,
|
||||
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)
|
||||
xrows, err := r.DB.Queryx("SELECT * FROM tag")
|
||||
if err != nil {
|
||||
@ -84,9 +87,13 @@ func (r *JobRepository) CountTags(user *string) (tags []schema.Tag, counts map[s
|
||||
From("tag t").
|
||||
LeftJoin("jobtag jt ON t.id = jt.tag_id").
|
||||
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()
|
||||
if err != nil {
|
||||
|
@ -104,6 +104,7 @@ func setupUserRoute(i InfoType, r *http.Request) InfoType {
|
||||
username := mux.Vars(r)["id"]
|
||||
i["id"] = username
|
||||
i["username"] = username
|
||||
// TODO: If forbidden (== err exists), redirect to error page
|
||||
if user, _ := auth.FetchUser(r.Context(), jobRepo.DB, username); user != nil {
|
||||
i["name"] = user.Name
|
||||
i["email"] = user.Email
|
||||
@ -142,13 +143,10 @@ func setupAnalysisRoute(i InfoType, r *http.Request) InfoType {
|
||||
}
|
||||
|
||||
func setupTaglistRoute(i InfoType, r *http.Request) InfoType {
|
||||
var username *string = nil
|
||||
jobRepo := repository.GetJobRepository()
|
||||
if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleAdmin) {
|
||||
username = &user.Username
|
||||
}
|
||||
user := auth.GetUser(r.Context())
|
||||
|
||||
tags, counts, err := jobRepo.CountTags(username)
|
||||
tags, counts, err := jobRepo.CountTags(user)
|
||||
tagMap := make(map[string][]map[string]interface{})
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
username, isAdmin, isSupporter := "", true, true
|
||||
|
||||
if user := auth.GetUser(r.Context()); user != nil {
|
||||
username = user.Username
|
||||
isAdmin = user.HasRole(auth.RoleAdmin)
|
||||
isSupporter = user.HasRole(auth.RoleSupport)
|
||||
}
|
||||
// Get User -> What if NIL?
|
||||
user := auth.GetUser(r.Context())
|
||||
// Get Roles
|
||||
availableRoles, _ := auth.GetValidRolesMap(user)
|
||||
|
||||
page := web.Page{
|
||||
Title: title,
|
||||
User: web.User{Username: username, IsAdmin: isAdmin, IsSupporter: isSupporter},
|
||||
User: *user,
|
||||
Roles: availableRoles,
|
||||
Build: web.Build{Version: version, Hash: hash, Buildtime: buildTime},
|
||||
Config: conf,
|
||||
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) {
|
||||
if search := r.URL.Query().Get("searchId"); search != "" {
|
||||
user := auth.GetUser(r.Context())
|
||||
splitSearch := strings.Split(search, ":")
|
||||
|
||||
if len(splitSearch) == 2 {
|
||||
switch strings.Trim(splitSearch[0], " ") {
|
||||
case "jobId":
|
||||
http.Redirect(rw, r, "/monitoring/jobs/?jobId="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusTemporaryRedirect) // All Users: Redirect to Tablequery
|
||||
return
|
||||
case "jobName":
|
||||
http.Redirect(rw, r, "/monitoring/jobs/?jobName="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusTemporaryRedirect) // All Users: Redirect to Tablequery
|
||||
return
|
||||
case "projectId":
|
||||
project, _ := api.JobRepository.FindProject(r.Context(), strings.Trim(splitSearch[1], " ")) // Restricted: projectId
|
||||
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
|
||||
}
|
||||
http.Redirect(rw, r, "/monitoring/jobs/?projectMatch=eq&project="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusTemporaryRedirect) // All Users: Redirect to Tablequery
|
||||
case "username":
|
||||
usernames, _ := api.JobRepository.FindUsers(r.Context(), strings.Trim(splitSearch[1], " ")) // Restricted: usernames
|
||||
if len(usernames) == 1 {
|
||||
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
|
||||
if user.HasAnyRole([]auth.Role{auth.RoleAdmin, auth.RoleSupport, auth.RoleManager}) {
|
||||
http.Redirect(rw, r, "/monitoring/users/?user="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusTemporaryRedirect)
|
||||
} else {
|
||||
http.Redirect(rw, r, "/monitoring/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":
|
||||
usernames, _ := api.JobRepository.FindUsersByName(r.Context(), strings.Trim(splitSearch[1], " ")) // Restricted: usernames queried by name
|
||||
if len(usernames) == 1 {
|
||||
http.Redirect(rw, r, "/monitoring/user/"+usernames[0], http.StatusTemporaryRedirect)
|
||||
return
|
||||
} else if len(usernames) > 1 {
|
||||
usernames, _ := api.JobRepository.FindColumnValues(user, strings.Trim(splitSearch[1], " "), "user", "username", "name")
|
||||
if len(usernames) != 0 {
|
||||
joinedNames := strings.Join(usernames, "&user=")
|
||||
http.Redirect(rw, r, "/monitoring/users/?user="+joinedNames, http.StatusTemporaryRedirect) // > 1 Matches: Redirect to user table
|
||||
return
|
||||
http.Redirect(rw, r, "/monitoring/users/?user="+joinedNames, http.StatusTemporaryRedirect)
|
||||
} 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:
|
||||
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 {
|
||||
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 {
|
||||
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
log.Errorf("Error while searchbar best guess: %v", err.Error())
|
||||
http.Redirect(rw, r, "/monitoring/jobs/?", http.StatusTemporaryRedirect) // Unknown: Redirect to Tablequery
|
||||
}
|
||||
|
||||
if username != "" {
|
||||
http.Redirect(rw, r, "/monitoring/user/"+username, http.StatusTemporaryRedirect) // User: Redirect to user page
|
||||
return
|
||||
} else if project != "" {
|
||||
http.Redirect(rw, r, "/monitoring/jobs/?projectMatch=eq&project="+url.QueryEscape(strings.Trim(search, " ")), http.StatusTemporaryRedirect) // projectId (equal)
|
||||
return
|
||||
} else if jobname != "" {
|
||||
http.Redirect(rw, r, "/monitoring/jobs/?jobName="+url.QueryEscape(strings.Trim(search, " ")), http.StatusTemporaryRedirect) // JobName (contains)
|
||||
return
|
||||
} else {
|
||||
http.Redirect(rw, r, "/monitoring/jobs/?jobId="+url.QueryEscape(strings.Trim(search, " ")), http.StatusTemporaryRedirect) // No Result: Probably jobId
|
||||
return
|
||||
}
|
||||
|
||||
} 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 {
|
||||
|
BIN
test/test.db
BIN
test/test.db
Binary file not shown.
@ -10,11 +10,11 @@
|
||||
|
||||
const ccconfig = getContext('cc-config')
|
||||
|
||||
export let user
|
||||
export let isAdmin
|
||||
|
||||
</script>
|
||||
|
||||
{#if user.IsAdmin}
|
||||
{#if isAdmin == true}
|
||||
<Card style="margin-bottom: 1.5em;">
|
||||
<CardHeader>
|
||||
<CardTitle class="mb-1">Admin Options</CardTitle>
|
||||
|
@ -4,23 +4,45 @@
|
||||
Dropdown, DropdownToggle, DropdownMenu, DropdownItem, InputGroupText } from 'sveltestrap'
|
||||
|
||||
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 roles // Role Enum-Like
|
||||
|
||||
let isOpen = false
|
||||
|
||||
const views = [
|
||||
isAdmin
|
||||
? { title: 'Jobs', adminOnly: false, href: '/monitoring/jobs/', icon: 'card-list' }
|
||||
: { title: 'My Jobs', adminOnly: false, href: `/monitoring/user/${username}`, icon: 'bar-chart-line-fill' },
|
||||
{ title: 'Users', adminOnly: true, href: '/monitoring/users/', icon: 'people-fill' },
|
||||
{ title: 'Projects', adminOnly: true, href: '/monitoring/projects/', icon: 'folder' },
|
||||
{ title: 'Tags', adminOnly: false, href: '/monitoring/tags/', icon: 'tags' }
|
||||
const userviews = [
|
||||
{ title: 'My Jobs', href: `/monitoring/user/${username}`, icon: 'bar-chart-line-fill' },
|
||||
{ title: `Job Search`, href: '/monitoring/jobs/', icon: 'card-list' },
|
||||
{ title: 'Tags', 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 = [
|
||||
{ title: 'Analysis', adminOnly: true, href: '/monitoring/analysis/', icon: 'graph-up' },
|
||||
{ title: 'Systems', adminOnly: true, href: '/monitoring/systems/', icon: 'cpu' },
|
||||
{ title: 'Status', adminOnly: true, href: '/monitoring/status/', icon: 'cpu' },
|
||||
{ title: 'Analysis', requiredRole: roles.support, href: '/monitoring/analysis/', icon: 'graph-up' },
|
||||
{ title: 'Systems', requiredRole: roles.admin, href: '/monitoring/systems/', icon: 'cpu' },
|
||||
{ title: 'Status', requiredRole: roles.admin, href: '/monitoring/status/', icon: 'cpu' },
|
||||
]
|
||||
</script>
|
||||
|
||||
@ -31,10 +53,26 @@
|
||||
<NavbarToggler on:click={() => (isOpen = !isOpen)} />
|
||||
<Collapse {isOpen} navbar expand="lg" on:update={({ detail }) => (isOpen = detail.isOpen)}>
|
||||
<Nav pills>
|
||||
{#each views.filter(item => isAdmin || !item.adminOnly) as item}
|
||||
<NavLink href={item.href} active={window.location.pathname == item.href}><Icon name={item.icon}/> {item.title}</NavLink>
|
||||
{/each}
|
||||
{#each viewsPerCluster.filter(item => !item.adminOnly || isAdmin) 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>
|
||||
{/each}
|
||||
{: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>
|
||||
<Dropdown nav inNavbar>
|
||||
<DropdownToggle nav caret>
|
||||
@ -57,7 +95,7 @@
|
||||
<InputGroup>
|
||||
<Input type="text" placeholder="Search 'type:<query>' ..." name="searchId"/>
|
||||
<Button outline type="submit"><Icon name="search"/></Button>
|
||||
<InputGroupText style="cursor:help;" title={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>
|
||||
</form>
|
||||
{#if username}
|
||||
|
@ -14,6 +14,8 @@
|
||||
const ccconfig = getContext('cc-config')
|
||||
|
||||
export let filterPresets = {}
|
||||
export let authlevel
|
||||
export let roles
|
||||
|
||||
let filters, jobList, matchedJobs = null
|
||||
let sorting = { field: 'startTime', order: 'DESC' }, isSortingOpen = false, isMetricsSelectionOpen = false
|
||||
@ -60,7 +62,7 @@
|
||||
</Col>
|
||||
|
||||
<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 xs="2">
|
||||
<Refresher on:reload={() => jobList.update()} />
|
||||
|
@ -1,4 +1,4 @@
|
||||
<!--
|
||||
<!--
|
||||
@component List of users or projects
|
||||
-->
|
||||
<script>
|
||||
@ -55,7 +55,7 @@
|
||||
: (sorting.direction == 'up'
|
||||
? (a, b) => a[sorting.field] - b[sorting.field]
|
||||
: (a, b) => b[sorting.field] - a[sorting.field])
|
||||
|
||||
|
||||
return stats.filter(u => u.id.includes(nameFilter)).sort(cmp)
|
||||
}
|
||||
|
||||
@ -161,4 +161,4 @@
|
||||
{/each}
|
||||
{/if}
|
||||
</tbody>
|
||||
</Table>
|
||||
</Table>
|
||||
|
@ -48,7 +48,7 @@
|
||||
|
||||
query(nodesQuery)
|
||||
|
||||
$: console.log($nodesQuery?.data?.nodeMetrics[0].metrics)
|
||||
// $: console.log($nodesQuery?.data?.nodeMetrics[0].metrics)
|
||||
</script>
|
||||
|
||||
<Row>
|
||||
|
@ -48,7 +48,7 @@
|
||||
if (s1 == null || s2 == null)
|
||||
return -1
|
||||
|
||||
return s.dir != 'up' ? s1[stat] - s2[stat] : s2[stat] - s1[stat]
|
||||
return s.dir != 'up' ? s1[stat] - s2[stat] : s2[stat] - s1[stat]
|
||||
})
|
||||
}
|
||||
|
||||
@ -61,7 +61,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<Button outline on:click={() => (isMetricSelectionOpen = true, console.log(isMetricSelectionOpen))}>
|
||||
<Button outline on:click={() => (isMetricSelectionOpen = true)}> <!-- log to click ', console.log(isMetricSelectionOpen)' -->
|
||||
Metrics
|
||||
</Button>
|
||||
</th>
|
||||
|
@ -4,7 +4,7 @@ import Config from './Config.root.svelte'
|
||||
new Config({
|
||||
target: document.getElementById('svelte-app'),
|
||||
props: {
|
||||
user: user
|
||||
isAdmin: isAdmin
|
||||
},
|
||||
context: new Map([
|
||||
['cc-config', clusterCockpitConfig]
|
||||
|
@ -2,11 +2,13 @@
|
||||
import { Row, Col } from 'sveltestrap'
|
||||
import { onMount } from 'svelte'
|
||||
import EditRole from './admin/EditRole.svelte'
|
||||
import EditProject from './admin/EditProject.svelte'
|
||||
import AddUser from './admin/AddUser.svelte'
|
||||
import ShowUsers from './admin/ShowUsers.svelte'
|
||||
import Options from './admin/Options.svelte'
|
||||
|
||||
let users = []
|
||||
let roles = []
|
||||
|
||||
function getUserList() {
|
||||
fetch('/api/users/?via-ldap=false¬-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>
|
||||
|
||||
<Row cols={2} class="p-2 g-2" >
|
||||
<Col class="mb-1">
|
||||
<AddUser on:reload={getUserList}/>
|
||||
<AddUser roles={roles} on:reload={getUserList}/>
|
||||
</Col>
|
||||
<Col class="mb-1">
|
||||
<ShowUsers on:reload={getUserList} bind:users={users}/>
|
||||
</Col>
|
||||
<Col>
|
||||
<EditRole on:reload={getUserList}/>
|
||||
<EditRole roles={roles} on:reload={getUserList}/>
|
||||
</Col>
|
||||
<Col>
|
||||
<EditProject on:reload={getUserList}/>
|
||||
</Col>
|
||||
<Col>
|
||||
<Options/>
|
||||
|
@ -8,6 +8,8 @@
|
||||
let message = {msg: '', color: '#d63384'}
|
||||
let displayMessage = false
|
||||
|
||||
export let roles = []
|
||||
|
||||
async function handleUserSubmit() {
|
||||
let form = document.querySelector('#create-user-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}>
|
||||
<CardTitle class="mb-3">Create User</CardTitle>
|
||||
<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">
|
||||
<label for="username" class="form-label">Username</label>
|
||||
<label for="username" class="form-label">Username (ID)</label>
|
||||
<input type="text" class="form-control" id="username" name="username" aria-describedby="usernameHelp"/>
|
||||
<div id="usernameHelp" class="form-text">Must be unique.</div>
|
||||
</div>
|
||||
@ -64,24 +56,38 @@
|
||||
<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>
|
||||
<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">
|
||||
<p>Role:</p>
|
||||
<div>
|
||||
<input type="radio" id="user" name="role" value="user" checked/>
|
||||
<label for="user">User (regular user, same as if created via LDAP sync.)</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="radio" id="api" name="role" value="api"/>
|
||||
<label for="api">API</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>
|
||||
{#each roles as role, i}
|
||||
{#if i == 0}
|
||||
<div>
|
||||
<input type="radio" id={role} name="role" value={role} checked/>
|
||||
<label for={role}>{role.charAt(0).toUpperCase() + role.slice(1)} (regular user, same as if created via LDAP sync.)</label>
|
||||
</div>
|
||||
{:else}
|
||||
<div>
|
||||
<input type="radio" id={role} name="role" value={role}/>
|
||||
<label for={role}>{role.charAt(0).toUpperCase() + role.slice(1)}</label>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
<p style="display: flex; align-items: center;">
|
||||
<Button type="submit" color="primary">Submit</Button>
|
||||
|
97
web/frontend/src/config/admin/EditProject.svelte
Normal file
97
web/frontend/src/config/admin/EditProject.svelte
Normal 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>
|
@ -8,6 +8,8 @@
|
||||
let message = {msg: '', color: '#d63384'}
|
||||
let displayMessage = false
|
||||
|
||||
export let roles = []
|
||||
|
||||
async function handleAddRole() {
|
||||
const username = document.querySelector('#role-username').value
|
||||
const role = document.querySelector('#role-select').value
|
||||
@ -86,10 +88,9 @@
|
||||
<input type="text" class="form-control" placeholder="username" id="role-username"/>
|
||||
<select class="form-select" id="role-select">
|
||||
<option selected value="">Role...</option>
|
||||
<option value="user">User</option>
|
||||
<option value="support">Support</option>
|
||||
<option value="admin">Admin</option>
|
||||
<option value="api">API</option>
|
||||
{#each roles as role}
|
||||
<option value={role}>{role.charAt(0).toUpperCase() + role.slice(1)}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<!-- 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 -->
|
||||
|
@ -41,6 +41,7 @@
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Name</th>
|
||||
<th>Project(s)</th>
|
||||
<th>Email</th>
|
||||
<th>Roles</th>
|
||||
<th>JWT</th>
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
<td>{user.username}</td>
|
||||
<td>{user.name}</td>
|
||||
<td>{user.projects}</td>
|
||||
<td>{user.email}</td>
|
||||
<td><code>{user.roles.join(', ')}</code></td>
|
||||
<td>
|
||||
|
@ -224,7 +224,7 @@
|
||||
on:change={({ detail: { from, to } }) => {
|
||||
filters.startTime.from = from?.toISOString()
|
||||
filters.startTime.to = to?.toISOString()
|
||||
console.log(filters.startTime)
|
||||
// console.log(filters.startTime)
|
||||
update()
|
||||
}}
|
||||
/>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<script>
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import { Button, Modal, ModalBody, ModalHeader, ModalFooter } from 'sveltestrap'
|
||||
import Header from '../Header.svelte';
|
||||
import Header from '../Header.svelte';
|
||||
import DoubleRangeSlider from './DoubleRangeSlider.svelte'
|
||||
|
||||
const clusters = getContext('clusters'),
|
||||
@ -23,7 +23,7 @@ import Header from '../Header.svelte';
|
||||
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)
|
||||
|
||||
console.log(header)
|
||||
// console.log(header)
|
||||
let minNumNodes = 1, maxNumNodes = 0, minNumHWThreads = 1, maxNumHWThreads = 0, minNumAccelerators = 0, maxNumAccelerators = 0
|
||||
$: {
|
||||
if ($initialized) {
|
||||
|
@ -6,6 +6,8 @@
|
||||
|
||||
export let user = ''
|
||||
export let project = ''
|
||||
export let authlevel
|
||||
export let roles
|
||||
let mode = 'user', term = ''
|
||||
const throttle = 500
|
||||
|
||||
@ -22,30 +24,53 @@
|
||||
|
||||
let timeoutId = null
|
||||
function termChanged(sleep = throttle) {
|
||||
if (mode == 'user')
|
||||
user = term
|
||||
else
|
||||
if (authlevel == roles.user) {
|
||||
project = term
|
||||
|
||||
if (timeoutId != null)
|
||||
clearTimeout(timeoutId)
|
||||
if (timeoutId != null)
|
||||
clearTimeout(timeoutId)
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
dispatch('update', {
|
||||
user,
|
||||
project
|
||||
})
|
||||
}, sleep)
|
||||
timeoutId = setTimeout(() => {
|
||||
dispatch('update', {
|
||||
project
|
||||
})
|
||||
}, sleep)
|
||||
} else if (authlevel >= roles.manager) {
|
||||
if (mode == 'user')
|
||||
user = term
|
||||
else
|
||||
project = term
|
||||
|
||||
if (timeoutId != null)
|
||||
clearTimeout(timeoutId)
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
dispatch('update', {
|
||||
user,
|
||||
project
|
||||
})
|
||||
}, sleep)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<InputGroup>
|
||||
<select style="max-width: 175px;" class="form-select"
|
||||
bind:value={mode} on:change={modeChanged}>
|
||||
<option value={'user'}>Search User</option>
|
||||
<option value={'project'}>Search Project</option>
|
||||
</select>
|
||||
<Input
|
||||
type="text" bind:value={term} on:change={() => termChanged()} on:keyup={(event) => termChanged(event.key == 'Enter' ? 0 : throttle)}
|
||||
placeholder={mode == 'user' ? 'filter username...' : 'filter project...'} />
|
||||
</InputGroup>
|
||||
{#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>
|
||||
<select style="max-width: 175px;" class="form-select"
|
||||
bind:value={mode} on:change={modeChanged}>
|
||||
<option value={'user'}>Search User</option>
|
||||
<option value={'project'}>Search Project</option>
|
||||
</select>
|
||||
<Input
|
||||
type="text" bind:value={term} on:change={() => termChanged()} on:keyup={(event) => termChanged(event.key == 'Enter' ? 0 : throttle)}
|
||||
placeholder={mode == 'user' ? 'filter username...' : 'filter project...'} />
|
||||
</InputGroup>
|
||||
{:else}
|
||||
Unauthorized
|
||||
{/if}
|
||||
|
@ -27,6 +27,7 @@
|
||||
let itemsPerPage = ccconfig.plot_list_jobsPerPage
|
||||
let page = 1
|
||||
let paging = { itemsPerPage, page }
|
||||
let filter = []
|
||||
|
||||
const jobs = operationStore(`
|
||||
query($filter: [JobFilter!]!, $sorting: OrderByInput!, $paging: PageRequest! ){
|
||||
@ -45,7 +46,7 @@
|
||||
}`, {
|
||||
paging,
|
||||
sorting,
|
||||
filter: []
|
||||
filter,
|
||||
}, {
|
||||
pause: true
|
||||
})
|
||||
@ -68,7 +69,7 @@
|
||||
}
|
||||
|
||||
$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
|
||||
|
@ -4,7 +4,9 @@ import Jobs from './Jobs.root.svelte'
|
||||
new Jobs({
|
||||
target: document.getElementById('svelte-app'),
|
||||
props: {
|
||||
filterPresets: filterPresets
|
||||
filterPresets: filterPresets,
|
||||
authlevel: authlevel,
|
||||
roles: roles
|
||||
},
|
||||
context: new Map([
|
||||
['cc-config', clusterCockpitConfig]
|
||||
|
@ -1,4 +1,4 @@
|
||||
<!--
|
||||
<!--
|
||||
@component
|
||||
|
||||
Only width/height should change reactively.
|
||||
@ -285,7 +285,7 @@
|
||||
else if (scope == 'hwthread')
|
||||
divisor = subCluster.topology.node.length
|
||||
else {
|
||||
console.log('TODO: how to calc thresholds for ', scope)
|
||||
// console.log('TODO: how to calc thresholds for ', scope)
|
||||
return null
|
||||
}
|
||||
|
||||
|
@ -15,9 +15,10 @@
|
||||
{{end}}
|
||||
<script>
|
||||
const header = {
|
||||
"username": "{{ .User.Username }}",
|
||||
"isAdmin": {{ .User.IsAdmin }},
|
||||
"clusters": {{ .Clusters }},
|
||||
"username": "{{ .User.Username }}",
|
||||
"authlevel": {{ .User.GetAuthLevel }},
|
||||
"clusters": {{ .Clusters }},
|
||||
"roles": {{ .Roles }}
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
|
@ -7,7 +7,7 @@
|
||||
{{end}}
|
||||
{{define "javascript"}}
|
||||
<script>
|
||||
const user = {{ .User }};
|
||||
const isAdmin = {{ .User.HasRole .Roles.admin }};
|
||||
const filterPresets = {{ .FilterPresets }};
|
||||
const clusterCockpitConfig = {{ .Config }};
|
||||
</script>
|
||||
|
@ -9,14 +9,14 @@
|
||||
<th>Running Jobs (short ones not listed)</th>
|
||||
<th>Total Jobs</th>
|
||||
<th>Short Jobs in past 24h</th>
|
||||
{{if .User.IsAdmin}}
|
||||
{{if .User.HasRole .Roles.admin}}
|
||||
<th>System View</th>
|
||||
<th>Analysis View</th>
|
||||
{{end}}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{if .User.IsAdmin}}
|
||||
{{if .User.HasRole .Roles.admin}}
|
||||
{{range .Infos.clusters}}
|
||||
<tr>
|
||||
<td>{{.Name}}</td>
|
||||
|
@ -10,6 +10,8 @@
|
||||
<script>
|
||||
const filterPresets = {{ .FilterPresets }};
|
||||
const clusterCockpitConfig = {{ .Config }};
|
||||
const authlevel = {{ .User.GetAuthLevel }};
|
||||
const roles = {{ .Roles }};
|
||||
</script>
|
||||
<script src='/build/jobs.js'></script>
|
||||
{{end}}
|
||||
|
16
web/web.go
16
web/web.go
@ -11,6 +11,7 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/ClusterCockpit/cc-backend/internal/auth"
|
||||
"github.com/ClusterCockpit/cc-backend/internal/config"
|
||||
"github.com/ClusterCockpit/cc-backend/pkg/log"
|
||||
"github.com/ClusterCockpit/cc-backend/pkg/schema"
|
||||
@ -55,15 +56,9 @@ func init() {
|
||||
_ = base
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Username string // Username of the currently logged in user
|
||||
IsAdmin bool
|
||||
IsSupporter bool
|
||||
}
|
||||
|
||||
type Build struct {
|
||||
Version string
|
||||
Hash string
|
||||
Version string
|
||||
Hash string
|
||||
Buildtime string
|
||||
}
|
||||
|
||||
@ -71,8 +66,9 @@ type Page struct {
|
||||
Title string // Page title
|
||||
Error string // For generic use (e.g. the exact error message on /login)
|
||||
Info string // For generic use (e.g. "Logout successfull" on /login)
|
||||
User User // Information about the currently logged in user
|
||||
Build Build // Latest information about the application
|
||||
User auth.User // Information about the currently logged in user (Full User Info)
|
||||
Roles map[string]auth.Role // Available roles for frontend render checks
|
||||
Build Build // Latest information about the application
|
||||
Clusters []schema.ClusterConfig // List of all clusters for use in the Header
|
||||
FilterPresets map[string]interface{} // For pages with the Filter component, this can be used to set initial filters.
|
||||
Infos map[string]interface{} // For generic use (e.g. username for /monitoring/user/<id>, job id for /monitoring/job/<id>)
|
||||
|
Loading…
Reference in New Issue
Block a user