Refactor auth module

Separate parts
Add user repository
Add user schema
This commit is contained in:
Jan Eitzinger 2023-08-17 10:29:00 +02:00
parent 80aed87415
commit 87ce4f63d4
22 changed files with 637 additions and 600 deletions

View File

@ -228,14 +228,16 @@ func main() {
log.Fatal("invalid argument format for user creation") log.Fatal("invalid argument format for user creation")
} }
if err := authentication.AddUser(&auth.User{ ur := repository.GetUserRepository()
if err := ur.AddUser(&schema.User{
Username: parts[0], Projects: make([]string, 0), Password: parts[2], Roles: strings.Split(parts[1], ","), Username: parts[0], Projects: make([]string, 0), Password: parts[2], Roles: strings.Split(parts[1], ","),
}); err != nil { }); err != nil {
log.Fatalf("adding '%s' user authentication failed: %v", parts[0], err) log.Fatalf("adding '%s' user authentication failed: %v", parts[0], err)
} }
} }
if flagDelUser != "" { if flagDelUser != "" {
if err := authentication.DelUser(flagDelUser); err != nil { ur := repository.GetUserRepository()
if err := ur.DelUser(flagDelUser); err != nil {
log.Fatalf("deleting user failed: %v", err) log.Fatalf("deleting user failed: %v", err)
} }
} }
@ -252,12 +254,13 @@ func main() {
} }
if flagGenJWT != "" { if flagGenJWT != "" {
user, err := authentication.GetUser(flagGenJWT) ur := repository.GetUserRepository()
user, err := ur.GetUser(flagGenJWT)
if err != nil { if err != nil {
log.Fatalf("could not get user from JWT: %v", err) log.Fatalf("could not get user from JWT: %v", err)
} }
if !user.HasRole(auth.RoleApi) { if !user.HasRole(schema.RoleApi) {
log.Warnf("user '%s' does not have the API role", user.Username) log.Warnf("user '%s' does not have the API role", user.Username)
} }
@ -327,15 +330,15 @@ func main() {
r.HandleFunc("/login", func(rw http.ResponseWriter, r *http.Request) { r.HandleFunc("/login", func(rw http.ResponseWriter, r *http.Request) {
rw.Header().Add("Content-Type", "text/html; charset=utf-8") rw.Header().Add("Content-Type", "text/html; charset=utf-8")
web.RenderTemplate(rw, r, "login.tmpl", &web.Page{Title: "Login", Build: buildInfo}) web.RenderTemplate(rw, "login.tmpl", &web.Page{Title: "Login", Build: buildInfo})
}).Methods(http.MethodGet) }).Methods(http.MethodGet)
r.HandleFunc("/imprint", func(rw http.ResponseWriter, r *http.Request) { r.HandleFunc("/imprint", func(rw http.ResponseWriter, r *http.Request) {
rw.Header().Add("Content-Type", "text/html; charset=utf-8") rw.Header().Add("Content-Type", "text/html; charset=utf-8")
web.RenderTemplate(rw, r, "imprint.tmpl", &web.Page{Title: "Imprint", Build: buildInfo}) web.RenderTemplate(rw, "imprint.tmpl", &web.Page{Title: "Imprint", Build: buildInfo})
}) })
r.HandleFunc("/privacy", func(rw http.ResponseWriter, r *http.Request) { r.HandleFunc("/privacy", func(rw http.ResponseWriter, r *http.Request) {
rw.Header().Add("Content-Type", "text/html; charset=utf-8") rw.Header().Add("Content-Type", "text/html; charset=utf-8")
web.RenderTemplate(rw, r, "privacy.tmpl", &web.Page{Title: "Privacy", Build: buildInfo}) web.RenderTemplate(rw, "privacy.tmpl", &web.Page{Title: "Privacy", Build: buildInfo})
}) })
// Some routes, such as /login or /query, should only be accessible to a user that is logged in. // Some routes, such as /login or /query, should only be accessible to a user that is logged in.
@ -351,7 +354,7 @@ func main() {
func(rw http.ResponseWriter, r *http.Request, err error) { func(rw http.ResponseWriter, r *http.Request, err error) {
rw.Header().Add("Content-Type", "text/html; charset=utf-8") rw.Header().Add("Content-Type", "text/html; charset=utf-8")
rw.WriteHeader(http.StatusUnauthorized) rw.WriteHeader(http.StatusUnauthorized)
web.RenderTemplate(rw, r, "login.tmpl", &web.Page{ web.RenderTemplate(rw, "login.tmpl", &web.Page{
Title: "Login failed - ClusterCockpit", Title: "Login failed - ClusterCockpit",
MsgType: "alert-warning", MsgType: "alert-warning",
Message: err.Error(), Message: err.Error(),
@ -362,7 +365,7 @@ func main() {
r.Handle("/logout", authentication.Logout(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { r.Handle("/logout", authentication.Logout(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.Header().Add("Content-Type", "text/html; charset=utf-8") rw.Header().Add("Content-Type", "text/html; charset=utf-8")
rw.WriteHeader(http.StatusOK) rw.WriteHeader(http.StatusOK)
web.RenderTemplate(rw, r, "login.tmpl", &web.Page{ web.RenderTemplate(rw, "login.tmpl", &web.Page{
Title: "Bye - ClusterCockpit", Title: "Bye - ClusterCockpit",
MsgType: "alert-info", MsgType: "alert-info",
Message: "Logout successful", Message: "Logout successful",
@ -378,7 +381,7 @@ func main() {
// On failure: // On failure:
func(rw http.ResponseWriter, r *http.Request, err error) { func(rw http.ResponseWriter, r *http.Request, err error) {
rw.WriteHeader(http.StatusUnauthorized) rw.WriteHeader(http.StatusUnauthorized)
web.RenderTemplate(rw, r, "login.tmpl", &web.Page{ web.RenderTemplate(rw, "login.tmpl", &web.Page{
Title: "Authentication failed - ClusterCockpit", Title: "Authentication failed - ClusterCockpit",
MsgType: "alert-danger", MsgType: "alert-danger",
Message: err.Error(), Message: err.Error(),

View File

@ -182,12 +182,12 @@ func decode(r io.Reader, val interface{}) error {
} }
func securedCheck(r *http.Request) error { func securedCheck(r *http.Request) error {
user := auth.GetUser(r.Context()) user := repository.GetUserFromContext(r.Context())
if user == nil { if user == nil {
return fmt.Errorf("no user in context") return fmt.Errorf("no user in context")
} }
if user.AuthType == auth.AuthToken { if user.AuthType == schema.AuthToken {
// If nothing declared in config: deny all request to this endpoint // If nothing declared in config: deny all request to this endpoint
if config.Keys.ApiAllowedIPs == nil || len(config.Keys.ApiAllowedIPs) == 0 { if config.Keys.ApiAllowedIPs == nil || len(config.Keys.ApiAllowedIPs) == 0 {
return fmt.Errorf("missing configuration key ApiAllowedIPs") return fmt.Errorf("missing configuration key ApiAllowedIPs")
@ -232,8 +232,10 @@ func securedCheck(r *http.Request) error {
// @router /jobs/ [get] // @router /jobs/ [get]
func (api *RestApi) getJobs(rw http.ResponseWriter, r *http.Request) { func (api *RestApi) getJobs(rw http.ResponseWriter, r *http.Request) {
if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) { if user := repository.GetUserFromContext(r.Context()); user != nil &&
handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw) !user.HasRole(schema.RoleApi) {
handleError(fmt.Errorf("missing role: %v", schema.GetRoleString(schema.RoleApi)), http.StatusForbidden, rw)
return return
} }
@ -374,9 +376,11 @@ func (api *RestApi) getJobs(rw http.ResponseWriter, r *http.Request) {
// @security ApiKeyAuth // @security ApiKeyAuth
// @router /jobs/{id} [post] // @router /jobs/{id} [post]
func (api *RestApi) getJobById(rw http.ResponseWriter, r *http.Request) { func (api *RestApi) getJobById(rw http.ResponseWriter, r *http.Request) {
if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) { if user := repository.GetUserFromContext(r.Context()); user != nil &&
!user.HasRole(schema.RoleApi) {
handleError(fmt.Errorf("missing role: %v", handleError(fmt.Errorf("missing role: %v",
auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw) schema.GetRoleString(schema.RoleApi)), http.StatusForbidden, rw)
return return
} }
@ -465,8 +469,10 @@ func (api *RestApi) getJobById(rw http.ResponseWriter, r *http.Request) {
// @security ApiKeyAuth // @security ApiKeyAuth
// @router /jobs/tag_job/{id} [post] // @router /jobs/tag_job/{id} [post]
func (api *RestApi) tagJob(rw http.ResponseWriter, r *http.Request) { func (api *RestApi) tagJob(rw http.ResponseWriter, r *http.Request) {
if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) { if user := repository.GetUserFromContext(r.Context()); user != nil &&
handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw) !user.HasRole(schema.RoleApi) {
handleError(fmt.Errorf("missing role: %v", schema.GetRoleString(schema.RoleApi)), http.StatusForbidden, rw)
return return
} }
@ -530,8 +536,10 @@ func (api *RestApi) tagJob(rw http.ResponseWriter, r *http.Request) {
// @security ApiKeyAuth // @security ApiKeyAuth
// @router /jobs/start_job/ [post] // @router /jobs/start_job/ [post]
func (api *RestApi) startJob(rw http.ResponseWriter, r *http.Request) { func (api *RestApi) startJob(rw http.ResponseWriter, r *http.Request) {
if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) { if user := repository.GetUserFromContext(r.Context()); user != nil &&
handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw) !user.HasRole(schema.RoleApi) {
handleError(fmt.Errorf("missing role: %v", schema.GetRoleString(schema.RoleApi)), http.StatusForbidden, rw)
return return
} }
@ -611,8 +619,10 @@ func (api *RestApi) startJob(rw http.ResponseWriter, r *http.Request) {
// @security ApiKeyAuth // @security ApiKeyAuth
// @router /jobs/stop_job/{id} [post] // @router /jobs/stop_job/{id} [post]
func (api *RestApi) stopJobById(rw http.ResponseWriter, r *http.Request) { func (api *RestApi) stopJobById(rw http.ResponseWriter, r *http.Request) {
if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) { if user := repository.GetUserFromContext(r.Context()); user != nil &&
handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw) !user.HasRole(schema.RoleApi) {
handleError(fmt.Errorf("missing role: %v", schema.GetRoleString(schema.RoleApi)), http.StatusForbidden, rw)
return return
} }
@ -664,8 +674,10 @@ func (api *RestApi) stopJobById(rw http.ResponseWriter, r *http.Request) {
// @security ApiKeyAuth // @security ApiKeyAuth
// @router /jobs/stop_job/ [post] // @router /jobs/stop_job/ [post]
func (api *RestApi) stopJobByRequest(rw http.ResponseWriter, r *http.Request) { func (api *RestApi) stopJobByRequest(rw http.ResponseWriter, r *http.Request) {
if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) { if user := repository.GetUserFromContext(r.Context()); user != nil &&
handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw) !user.HasRole(schema.RoleApi) {
handleError(fmt.Errorf("missing role: %v", schema.GetRoleString(schema.RoleApi)), http.StatusForbidden, rw)
return return
} }
@ -710,8 +722,8 @@ func (api *RestApi) stopJobByRequest(rw http.ResponseWriter, r *http.Request) {
// @security ApiKeyAuth // @security ApiKeyAuth
// @router /jobs/delete_job/{id} [delete] // @router /jobs/delete_job/{id} [delete]
func (api *RestApi) deleteJobById(rw http.ResponseWriter, r *http.Request) { func (api *RestApi) deleteJobById(rw http.ResponseWriter, r *http.Request) {
if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) { if user := repository.GetUserFromContext(r.Context()); user != nil && !user.HasRole(schema.RoleApi) {
handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw) handleError(fmt.Errorf("missing role: %v", schema.GetRoleString(schema.RoleApi)), http.StatusForbidden, rw)
return return
} }
@ -758,8 +770,9 @@ func (api *RestApi) deleteJobById(rw http.ResponseWriter, r *http.Request) {
// @security ApiKeyAuth // @security ApiKeyAuth
// @router /jobs/delete_job/ [delete] // @router /jobs/delete_job/ [delete]
func (api *RestApi) deleteJobByRequest(rw http.ResponseWriter, r *http.Request) { func (api *RestApi) deleteJobByRequest(rw http.ResponseWriter, r *http.Request) {
if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) { if user := repository.GetUserFromContext(r.Context()); user != nil &&
handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw) !user.HasRole(schema.RoleApi) {
handleError(fmt.Errorf("missing role: %v", schema.GetRoleString(schema.RoleApi)), http.StatusForbidden, rw)
return return
} }
@ -814,8 +827,8 @@ func (api *RestApi) deleteJobByRequest(rw http.ResponseWriter, r *http.Request)
// @security ApiKeyAuth // @security ApiKeyAuth
// @router /jobs/delete_job_before/{ts} [delete] // @router /jobs/delete_job_before/{ts} [delete]
func (api *RestApi) deleteJobBefore(rw http.ResponseWriter, r *http.Request) { func (api *RestApi) deleteJobBefore(rw http.ResponseWriter, r *http.Request) {
if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) { if user := repository.GetUserFromContext(r.Context()); user != nil && !user.HasRole(schema.RoleApi) {
handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw) handleError(fmt.Errorf("missing role: %v", schema.GetRoleString(schema.RoleApi)), http.StatusForbidden, rw)
return return
} }
@ -938,8 +951,8 @@ func (api *RestApi) getJWT(rw http.ResponseWriter, r *http.Request) {
rw.Header().Set("Content-Type", "text/plain") rw.Header().Set("Content-Type", "text/plain")
username := r.FormValue("username") username := r.FormValue("username")
me := auth.GetUser(r.Context()) me := repository.GetUserFromContext(r.Context())
if !me.HasRole(auth.RoleAdmin) { if !me.HasRole(schema.RoleAdmin) {
if username != me.Username { if username != me.Username {
http.Error(rw, "Only admins are allowed to sign JWTs not for themselves", http.Error(rw, "Only admins are allowed to sign JWTs not for themselves",
http.StatusForbidden) http.StatusForbidden)
@ -947,7 +960,7 @@ func (api *RestApi) getJWT(rw http.ResponseWriter, r *http.Request) {
} }
} }
user, err := api.Authentication.GetUser(username) user, err := repository.GetUserRepository().GetUser(username)
if err != nil { if err != nil {
http.Error(rw, err.Error(), http.StatusUnprocessableEntity) http.Error(rw, err.Error(), http.StatusUnprocessableEntity)
return return
@ -970,8 +983,8 @@ func (api *RestApi) createUser(rw http.ResponseWriter, r *http.Request) {
} }
rw.Header().Set("Content-Type", "text/plain") rw.Header().Set("Content-Type", "text/plain")
me := auth.GetUser(r.Context()) me := repository.GetUserFromContext(r.Context())
if !me.HasRole(auth.RoleAdmin) { if !me.HasRole(schema.RoleAdmin) {
http.Error(rw, "Only admins are allowed to create new users", http.StatusForbidden) http.Error(rw, "Only admins are allowed to create new users", http.StatusForbidden)
return return
} }
@ -980,22 +993,22 @@ func (api *RestApi) createUser(rw http.ResponseWriter, r *http.Request) {
r.FormValue("password"), r.FormValue("role"), r.FormValue("name"), r.FormValue("password"), r.FormValue("role"), r.FormValue("name"),
r.FormValue("email"), r.FormValue("project") r.FormValue("email"), r.FormValue("project")
if len(password) == 0 && role != auth.GetRoleString(auth.RoleApi) { if len(password) == 0 && role != schema.GetRoleString(schema.RoleApi) {
http.Error(rw, "Only API users are allowed to have a blank password (login will be impossible)", http.StatusBadRequest) http.Error(rw, "Only API users are allowed to have a blank password (login will be impossible)", http.StatusBadRequest)
return return
} }
if len(project) != 0 && role != auth.GetRoleString(auth.RoleManager) { if len(project) != 0 && role != schema.GetRoleString(schema.RoleManager) {
http.Error(rw, "only managers require a project (can be changed later)", http.Error(rw, "only managers require a project (can be changed later)",
http.StatusBadRequest) http.StatusBadRequest)
return return
} else if len(project) == 0 && role == auth.GetRoleString(auth.RoleManager) { } else if len(project) == 0 && role == schema.GetRoleString(schema.RoleManager) {
http.Error(rw, "managers require a project to manage (can be changed later)", http.Error(rw, "managers require a project to manage (can be changed later)",
http.StatusBadRequest) http.StatusBadRequest)
return return
} }
if err := api.Authentication.AddUser(&auth.User{ if err := repository.GetUserRepository().AddUser(&schema.User{
Username: username, Username: username,
Name: name, Name: name,
Password: password, Password: password,
@ -1015,13 +1028,13 @@ func (api *RestApi) deleteUser(rw http.ResponseWriter, r *http.Request) {
http.Error(rw, err.Error(), http.StatusForbidden) http.Error(rw, err.Error(), http.StatusForbidden)
} }
if user := auth.GetUser(r.Context()); !user.HasRole(auth.RoleAdmin) { if user := repository.GetUserFromContext(r.Context()); !user.HasRole(schema.RoleAdmin) {
http.Error(rw, "Only admins are allowed to delete a user", http.StatusForbidden) http.Error(rw, "Only admins are allowed to delete a user", http.StatusForbidden)
return return
} }
username := r.FormValue("username") username := r.FormValue("username")
if err := api.Authentication.DelUser(username); err != nil { if err := repository.GetUserRepository().DelUser(username); err != nil {
http.Error(rw, err.Error(), http.StatusUnprocessableEntity) http.Error(rw, err.Error(), http.StatusUnprocessableEntity)
return return
} }
@ -1035,12 +1048,12 @@ func (api *RestApi) getUsers(rw http.ResponseWriter, r *http.Request) {
http.Error(rw, err.Error(), http.StatusForbidden) http.Error(rw, err.Error(), http.StatusForbidden)
} }
if user := auth.GetUser(r.Context()); !user.HasRole(auth.RoleAdmin) { if user := repository.GetUserFromContext(r.Context()); !user.HasRole(schema.RoleAdmin) {
http.Error(rw, "Only admins are allowed to fetch a list of users", http.StatusForbidden) http.Error(rw, "Only admins are allowed to fetch a list of users", http.StatusForbidden)
return return
} }
users, err := api.Authentication.ListUsers(r.URL.Query().Get("not-just-user") == "true") users, err := repository.GetUserRepository().ListUsers(r.URL.Query().Get("not-just-user") == "true")
if err != nil { if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError) http.Error(rw, err.Error(), http.StatusInternalServerError)
return return
@ -1055,13 +1068,13 @@ func (api *RestApi) getRoles(rw http.ResponseWriter, r *http.Request) {
http.Error(rw, err.Error(), http.StatusForbidden) http.Error(rw, err.Error(), http.StatusForbidden)
} }
user := auth.GetUser(r.Context()) user := repository.GetUserFromContext(r.Context())
if !user.HasRole(auth.RoleAdmin) { if !user.HasRole(schema.RoleAdmin) {
http.Error(rw, "only admins are allowed to fetch a list of roles", http.StatusForbidden) http.Error(rw, "only admins are allowed to fetch a list of roles", http.StatusForbidden)
return return
} }
roles, err := auth.GetValidRoles(user) roles, err := schema.GetValidRoles(user)
if err != nil { if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError) http.Error(rw, err.Error(), http.StatusInternalServerError)
return return
@ -1076,7 +1089,7 @@ func (api *RestApi) updateUser(rw http.ResponseWriter, r *http.Request) {
http.Error(rw, err.Error(), http.StatusForbidden) http.Error(rw, err.Error(), http.StatusForbidden)
} }
if user := auth.GetUser(r.Context()); !user.HasRole(auth.RoleAdmin) { if user := repository.GetUserFromContext(r.Context()); !user.HasRole(schema.RoleAdmin) {
http.Error(rw, "Only admins are allowed to update a user", http.StatusForbidden) http.Error(rw, "Only admins are allowed to update a user", http.StatusForbidden)
return return
} }
@ -1089,25 +1102,25 @@ func (api *RestApi) updateUser(rw http.ResponseWriter, r *http.Request) {
// TODO: Handle anything but roles... // TODO: Handle anything but roles...
if newrole != "" { if newrole != "" {
if err := api.Authentication.AddRole(r.Context(), mux.Vars(r)["id"], newrole); err != nil { if err := repository.GetUserRepository().AddRole(r.Context(), mux.Vars(r)["id"], newrole); err != nil {
http.Error(rw, err.Error(), http.StatusUnprocessableEntity) http.Error(rw, err.Error(), http.StatusUnprocessableEntity)
return return
} }
rw.Write([]byte("Add Role Success")) rw.Write([]byte("Add Role Success"))
} else if delrole != "" { } else if delrole != "" {
if err := api.Authentication.RemoveRole(r.Context(), mux.Vars(r)["id"], delrole); err != nil { if err := repository.GetUserRepository().RemoveRole(r.Context(), mux.Vars(r)["id"], delrole); err != nil {
http.Error(rw, err.Error(), http.StatusUnprocessableEntity) http.Error(rw, err.Error(), http.StatusUnprocessableEntity)
return return
} }
rw.Write([]byte("Remove Role Success")) rw.Write([]byte("Remove Role Success"))
} else if newproj != "" { } else if newproj != "" {
if err := api.Authentication.AddProject(r.Context(), mux.Vars(r)["id"], newproj); err != nil { if err := repository.GetUserRepository().AddProject(r.Context(), mux.Vars(r)["id"], newproj); err != nil {
http.Error(rw, err.Error(), http.StatusUnprocessableEntity) http.Error(rw, err.Error(), http.StatusUnprocessableEntity)
return return
} }
rw.Write([]byte("Add Project Success")) rw.Write([]byte("Add Project Success"))
} else if delproj != "" { } else if delproj != "" {
if err := api.Authentication.RemoveProject(r.Context(), mux.Vars(r)["id"], delproj); err != nil { if err := repository.GetUserRepository().RemoveProject(r.Context(), mux.Vars(r)["id"], delproj); err != nil {
http.Error(rw, err.Error(), http.StatusUnprocessableEntity) http.Error(rw, err.Error(), http.StatusUnprocessableEntity)
return return
} }
@ -1188,7 +1201,7 @@ func (api *RestApi) updateConfiguration(rw http.ResponseWriter, r *http.Request)
fmt.Printf("REST > KEY: %#v\nVALUE: %#v\n", key, value) fmt.Printf("REST > KEY: %#v\nVALUE: %#v\n", key, value)
if err := repository.GetUserCfgRepo().UpdateConfig(key, value, auth.GetUser(r.Context())); err != nil { if err := repository.GetUserCfgRepo().UpdateConfig(key, value, repository.GetUserFromContext(r.Context())); err != nil {
http.Error(rw, err.Error(), http.StatusUnprocessableEntity) http.Error(rw, err.Error(), http.StatusUnprocessableEntity)
return return
} }

View File

@ -14,65 +14,19 @@ import (
"os" "os"
"time" "time"
"github.com/ClusterCockpit/cc-backend/internal/repository"
"github.com/ClusterCockpit/cc-backend/pkg/log" "github.com/ClusterCockpit/cc-backend/pkg/log"
"github.com/ClusterCockpit/cc-backend/pkg/schema"
"github.com/gorilla/sessions" "github.com/gorilla/sessions"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
) )
type AuthSource int
const (
AuthViaLocalPassword AuthSource = iota
AuthViaLDAP
AuthViaToken
)
type AuthType int
const (
AuthToken AuthType = iota
AuthSession
)
type User struct {
Username string `json:"username"`
Password string `json:"-"`
Name string `json:"name"`
Roles []string `json:"roles"`
AuthType AuthType `json:"authType"`
AuthSource AuthSource `json:"authSource"`
Email string `json:"email"`
Projects []string `json:"projects"`
}
func (u *User) HasProject(project string) bool {
for _, p := range u.Projects {
if p == project {
return true
}
}
return false
}
func GetUser(ctx context.Context) *User {
x := ctx.Value(ContextUserKey)
if x == nil {
return nil
}
return x.(*User)
}
type Authenticator interface { type Authenticator interface {
Init(auth *Authentication, config interface{}) error Init(auth *Authentication, config interface{}) error
CanLogin(user *User, username string, rw http.ResponseWriter, r *http.Request) bool CanLogin(user *schema.User, username string, rw http.ResponseWriter, r *http.Request) bool
Login(user *User, rw http.ResponseWriter, r *http.Request) (*User, error) Login(user *schema.User, rw http.ResponseWriter, r *http.Request) (*schema.User, error)
} }
type ContextKey string
const ContextUserKey ContextKey = "user"
type Authentication struct { type Authentication struct {
db *sqlx.DB db *sqlx.DB
sessionStore *sessions.CookieStore sessionStore *sessions.CookieStore
@ -86,7 +40,7 @@ type Authentication struct {
func (auth *Authentication) AuthViaSession( func (auth *Authentication) AuthViaSession(
rw http.ResponseWriter, rw http.ResponseWriter,
r *http.Request) (*User, error) { r *http.Request) (*schema.User, error) {
session, err := auth.sessionStore.Get(r, "session") session, err := auth.sessionStore.Get(r, "session")
if err != nil { if err != nil {
log.Error("Error while getting session store") log.Error("Error while getting session store")
@ -119,11 +73,11 @@ func (auth *Authentication) AuthViaSession(
username, _ := session.Values["username"].(string) username, _ := session.Values["username"].(string)
projects, _ := session.Values["projects"].([]string) projects, _ := session.Values["projects"].([]string)
roles, _ := session.Values["roles"].([]string) roles, _ := session.Values["roles"].([]string)
return &User{ return &schema.User{
Username: username, Username: username,
Projects: projects, Projects: projects,
Roles: roles, Roles: roles,
AuthType: AuthSession, AuthType: schema.AuthSession,
AuthSource: -1, AuthSource: -1,
}, nil }, nil
} }
@ -196,12 +150,13 @@ func (auth *Authentication) Login(
onfailure func(rw http.ResponseWriter, r *http.Request, loginErr error)) http.Handler { onfailure func(rw http.ResponseWriter, r *http.Request, loginErr error)) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
ur := repository.GetUserRepository()
err := errors.New("no authenticator applied") err := errors.New("no authenticator applied")
username := r.FormValue("username") username := r.FormValue("username")
dbUser := (*User)(nil) dbUser := (*schema.User)(nil)
if username != "" { if username != "" {
dbUser, err = auth.GetUser(username) dbUser, err = ur.GetUser(username)
if err != nil && err != sql.ErrNoRows { if err != nil && err != sql.ErrNoRows {
log.Errorf("Error while loading user '%v'", username) log.Errorf("Error while loading user '%v'", username)
} }
@ -211,7 +166,7 @@ func (auth *Authentication) Login(
if !authenticator.CanLogin(dbUser, username, rw, r) { if !authenticator.CanLogin(dbUser, username, rw, r) {
continue continue
} }
dbUser, err = auth.GetUser(username) dbUser, err = ur.GetUser(username)
if err != nil && err != sql.ErrNoRows { if err != nil && err != sql.ErrNoRows {
log.Errorf("Error while loading user '%v'", username) log.Errorf("Error while loading user '%v'", username)
} }
@ -243,7 +198,7 @@ func (auth *Authentication) Login(
} }
if dbUser == nil { if dbUser == nil {
if err := auth.AddUser(user); err != nil { if err := ur.AddUser(user); err != nil {
// TODO Add AuthSource // TODO Add AuthSource
log.Errorf("Error while adding user '%v' to auth from XX", log.Errorf("Error while adding user '%v' to auth from XX",
user.Username) user.Username)
@ -251,7 +206,7 @@ func (auth *Authentication) Login(
} }
log.Infof("login successfull: user: %#v (roles: %v, projects: %v)", user.Username, user.Roles, user.Projects) log.Infof("login successfull: user: %#v (roles: %v, projects: %v)", user.Username, user.Roles, user.Projects)
ctx := context.WithValue(r.Context(), ContextUserKey, user) ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
onsuccess.ServeHTTP(rw, r.WithContext(ctx)) onsuccess.ServeHTTP(rw, r.WithContext(ctx))
return return
} }
@ -284,7 +239,7 @@ func (auth *Authentication) Auth(
} }
if user != nil { if user != nil {
ctx := context.WithValue(r.Context(), ContextUserKey, user) ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
onsuccess.ServeHTTP(rw, r.WithContext(ctx)) onsuccess.ServeHTTP(rw, r.WithContext(ctx))
return return
} }

View File

@ -13,22 +13,19 @@ import (
"strings" "strings"
"time" "time"
"github.com/ClusterCockpit/cc-backend/internal/repository"
"github.com/ClusterCockpit/cc-backend/pkg/log" "github.com/ClusterCockpit/cc-backend/pkg/log"
"github.com/ClusterCockpit/cc-backend/pkg/schema" "github.com/ClusterCockpit/cc-backend/pkg/schema"
"github.com/golang-jwt/jwt/v4" "github.com/golang-jwt/jwt/v4"
) )
type JWTAuthenticator struct { type JWTAuthenticator struct {
auth *Authentication
publicKey ed25519.PublicKey publicKey ed25519.PublicKey
privateKey ed25519.PrivateKey privateKey ed25519.PrivateKey
config *schema.JWTAuthConfig config *schema.JWTAuthConfig
} }
func (ja *JWTAuthenticator) Init(auth *Authentication, conf interface{}) error { func (ja *JWTAuthenticator) Init(auth *Authentication, conf interface{}) error {
ja.auth = auth
ja.config = conf.(*schema.JWTAuthConfig) ja.config = conf.(*schema.JWTAuthConfig)
pubKey, privKey := os.Getenv("JWT_PUBLIC_KEY"), os.Getenv("JWT_PRIVATE_KEY") pubKey, privKey := os.Getenv("JWT_PUBLIC_KEY"), os.Getenv("JWT_PRIVATE_KEY")
@ -54,7 +51,7 @@ func (ja *JWTAuthenticator) Init(auth *Authentication, conf interface{}) error {
func (ja *JWTAuthenticator) AuthViaJWT( func (ja *JWTAuthenticator) AuthViaJWT(
rw http.ResponseWriter, rw http.ResponseWriter,
r *http.Request) (*User, error) { r *http.Request) (*schema.User, error) {
rawtoken := r.Header.Get("X-Auth-Token") rawtoken := r.Header.Get("X-Auth-Token")
if rawtoken == "" { if rawtoken == "" {
@ -90,8 +87,9 @@ func (ja *JWTAuthenticator) AuthViaJWT(
var roles []string var roles []string
// Validate user + roles from JWT against database? // Validate user + roles from JWT against database?
if ja.config != nil && ja.config.ForceJWTValidationViaDatabase { if ja.config != nil && ja.config.ValidateUser {
user, err := ja.auth.GetUser(sub) ur := repository.GetUserRepository()
user, err := ur.GetUser(sub)
// Deny any logins for unknown usernames // Deny any logins for unknown usernames
if err != nil { if err != nil {
@ -111,16 +109,16 @@ func (ja *JWTAuthenticator) AuthViaJWT(
} }
} }
return &User{ return &schema.User{
Username: sub, Username: sub,
Roles: roles, Roles: roles,
AuthType: AuthToken, AuthType: schema.AuthToken,
AuthSource: -1, AuthSource: -1,
}, nil }, nil
} }
// Generate a new JWT that can be used for authentication // Generate a new JWT that can be used for authentication
func (ja *JWTAuthenticator) ProvideJWT(user *User) (string, error) { func (ja *JWTAuthenticator) ProvideJWT(user *schema.User) (string, error) {
if ja.privateKey == nil { if ja.privateKey == nil {
return "", errors.New("environment variable 'JWT_PRIVATE_KEY' not set") return "", errors.New("environment variable 'JWT_PRIVATE_KEY' not set")

View File

@ -73,10 +73,10 @@ func (ja *JWTCookieSessionAuthenticator) Init(auth *Authentication, conf interfa
log.Warn("cookieName for JWTs not configured (cross login via JWT cookie will fail)") log.Warn("cookieName for JWTs not configured (cross login via JWT cookie will fail)")
return errors.New("cookieName for JWTs not configured (cross login via JWT cookie will fail)") return errors.New("cookieName for JWTs not configured (cross login via JWT cookie will fail)")
} }
if !ja.config.ForceJWTValidationViaDatabase { if !ja.config.ValidateUser {
log.Warn("forceJWTValidationViaDatabase not set to true: CC will accept users and roles defined in JWTs regardless of its own database!") log.Warn("forceJWTValidationViaDatabase not set to true: CC will accept users and roles defined in JWTs regardless of its own database!")
} }
if ja.config.TrustedExternalIssuer == "" { if ja.config.TrustedIssuer == "" {
log.Warn("trustedExternalIssuer for JWTs not configured (cross login via JWT cookie will fail)") log.Warn("trustedExternalIssuer for JWTs not configured (cross login via JWT cookie will fail)")
return errors.New("trustedExternalIssuer for JWTs not configured (cross login via JWT cookie will fail)") return errors.New("trustedExternalIssuer for JWTs not configured (cross login via JWT cookie will fail)")
} }
@ -89,7 +89,7 @@ func (ja *JWTCookieSessionAuthenticator) Init(auth *Authentication, conf interfa
} }
func (ja *JWTCookieSessionAuthenticator) CanLogin( func (ja *JWTCookieSessionAuthenticator) CanLogin(
user *User, user *schema.User,
username string, username string,
rw http.ResponseWriter, rw http.ResponseWriter,
r *http.Request) bool { r *http.Request) bool {
@ -112,9 +112,9 @@ func (ja *JWTCookieSessionAuthenticator) CanLogin(
} }
func (ja *JWTCookieSessionAuthenticator) Login( func (ja *JWTCookieSessionAuthenticator) Login(
user *User, user *schema.User,
rw http.ResponseWriter, rw http.ResponseWriter,
r *http.Request) (*User, error) { r *http.Request) (*schema.User, error) {
jwtCookie, err := r.Cookie(ja.config.CookieName) jwtCookie, err := r.Cookie(ja.config.CookieName)
var rawtoken string var rawtoken string
@ -129,7 +129,7 @@ func (ja *JWTCookieSessionAuthenticator) Login(
} }
unvalidatedIssuer, success := t.Claims.(jwt.MapClaims)["iss"].(string) unvalidatedIssuer, success := t.Claims.(jwt.MapClaims)["iss"].(string)
if success && unvalidatedIssuer == ja.config.TrustedExternalIssuer { if success && unvalidatedIssuer == ja.config.TrustedIssuer {
// The (unvalidated) issuer seems to be the expected one, // The (unvalidated) issuer seems to be the expected one,
// use public cross login key from config // use public cross login key from config
return ja.publicKeyCrossLogin, nil return ja.publicKeyCrossLogin, nil
@ -160,7 +160,7 @@ func (ja *JWTCookieSessionAuthenticator) Login(
var roles []string var roles []string
if ja.config.ForceJWTValidationViaDatabase { if ja.config.ValidateUser {
// Deny any logins for unknown usernames // Deny any logins for unknown usernames
if user == nil { if user == nil {
log.Warn("Could not find user from JWT in internal database.") log.Warn("Could not find user from JWT in internal database.")
@ -191,12 +191,12 @@ func (ja *JWTCookieSessionAuthenticator) Login(
http.SetCookie(rw, deletedCookie) http.SetCookie(rw, deletedCookie)
if user == nil { if user == nil {
user = &User{ user = &schema.User{
Username: sub, Username: sub,
Name: name, Name: name,
Roles: roles, Roles: roles,
AuthType: AuthSession, AuthType: schema.AuthSession,
AuthSource: AuthViaToken, AuthSource: schema.AuthViaToken,
} }
} }

View File

@ -12,21 +12,17 @@ import (
"strings" "strings"
"github.com/ClusterCockpit/cc-backend/pkg/log" "github.com/ClusterCockpit/cc-backend/pkg/log"
"github.com/ClusterCockpit/cc-backend/pkg/schema"
"github.com/golang-jwt/jwt/v4" "github.com/golang-jwt/jwt/v4"
) )
type JWTSessionAuthenticator struct { type JWTSessionAuthenticator struct {
auth *Authentication
loginTokenKey []byte // HS256 key loginTokenKey []byte // HS256 key
} }
var _ Authenticator = (*JWTSessionAuthenticator)(nil) var _ Authenticator = (*JWTSessionAuthenticator)(nil)
func (ja *JWTSessionAuthenticator) Init(auth *Authentication, conf interface{}) error { func (ja *JWTSessionAuthenticator) Init(auth *Authentication, conf interface{}) error {
ja.auth = auth
if pubKey := os.Getenv("CROSS_LOGIN_JWT_HS512_KEY"); pubKey != "" { if pubKey := os.Getenv("CROSS_LOGIN_JWT_HS512_KEY"); pubKey != "" {
bytes, err := base64.StdEncoding.DecodeString(pubKey) bytes, err := base64.StdEncoding.DecodeString(pubKey)
if err != nil { if err != nil {
@ -40,7 +36,7 @@ func (ja *JWTSessionAuthenticator) Init(auth *Authentication, conf interface{})
} }
func (ja *JWTSessionAuthenticator) CanLogin( func (ja *JWTSessionAuthenticator) CanLogin(
user *User, user *schema.User,
username string, username string,
rw http.ResponseWriter, rw http.ResponseWriter,
r *http.Request) bool { r *http.Request) bool {
@ -49,9 +45,9 @@ func (ja *JWTSessionAuthenticator) CanLogin(
} }
func (ja *JWTSessionAuthenticator) Login( func (ja *JWTSessionAuthenticator) Login(
user *User, user *schema.User,
rw http.ResponseWriter, rw http.ResponseWriter,
r *http.Request) (*User, error) { r *http.Request) (*schema.User, error) {
rawtoken := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") rawtoken := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
if rawtoken == "" { if rawtoken == "" {
@ -92,14 +88,14 @@ func (ja *JWTSessionAuthenticator) Login(
if rawroles, ok := claims["roles"].([]interface{}); ok { if rawroles, ok := claims["roles"].([]interface{}); ok {
for _, rr := range rawroles { for _, rr := range rawroles {
if r, ok := rr.(string); ok { if r, ok := rr.(string); ok {
if isValidRole(r) { if schema.IsValidRole(r) {
roles = append(roles, r) roles = append(roles, r)
} }
} }
} }
} else if rawroles, ok := claims["roles"]; ok { } else if rawroles, ok := claims["roles"]; ok {
for _, r := range rawroles.([]string) { for _, r := range rawroles.([]string) {
if isValidRole(r) { if schema.IsValidRole(r) {
roles = append(roles, r) roles = append(roles, r)
} }
} }
@ -120,13 +116,13 @@ func (ja *JWTSessionAuthenticator) Login(
// } // }
if user == nil { if user == nil {
user = &User{ user = &schema.User{
Username: sub, Username: sub,
Name: name, Name: name,
Roles: roles, Roles: roles,
Projects: projects, Projects: projects,
AuthType: AuthSession, AuthType: schema.AuthSession,
AuthSource: AuthViaToken, AuthSource: schema.AuthViaToken,
} }
} }

View File

@ -67,12 +67,12 @@ func (la *LdapAuthenticator) Init(
} }
func (la *LdapAuthenticator) CanLogin( func (la *LdapAuthenticator) CanLogin(
user *User, user *schema.User,
username string, username string,
rw http.ResponseWriter, rw http.ResponseWriter,
r *http.Request) bool { r *http.Request) bool {
if user != nil && user.AuthSource == AuthViaLDAP { if user != nil && user.AuthSource == schema.AuthViaLDAP {
return true return true
} else { } else {
if la.config != nil && la.config.SyncUserOnLogin { if la.config != nil && la.config.SyncUserOnLogin {
@ -103,7 +103,7 @@ func (la *LdapAuthenticator) CanLogin(
name := entry.GetAttributeValue("gecos") name := entry.GetAttributeValue("gecos")
if _, err := la.auth.db.Exec(`INSERT INTO user (username, ldap, name, roles) VALUES (?, ?, ?, ?)`, if _, err := la.auth.db.Exec(`INSERT INTO user (username, ldap, name, roles) VALUES (?, ?, ?, ?)`,
username, 1, name, "[\""+GetRoleString(RoleUser)+"\"]"); err != nil { username, 1, name, "[\""+schema.GetRoleString(schema.RoleUser)+"\"]"); err != nil {
log.Errorf("User '%s' new in LDAP: Insert into DB failed", username) log.Errorf("User '%s' new in LDAP: Insert into DB failed", username)
return false return false
} }
@ -116,9 +116,9 @@ func (la *LdapAuthenticator) CanLogin(
} }
func (la *LdapAuthenticator) Login( func (la *LdapAuthenticator) Login(
user *User, user *schema.User,
rw http.ResponseWriter, rw http.ResponseWriter,
r *http.Request) (*User, error) { r *http.Request) (*schema.User, error) {
l, err := la.getLdapConnection(false) l, err := la.getLdapConnection(false)
if err != nil { if err != nil {
@ -203,7 +203,7 @@ func (la *LdapAuthenticator) Sync() error {
name := newnames[username] name := newnames[username]
log.Debugf("sync: add %v (name: %v, roles: [user], ldap: true)", username, name) log.Debugf("sync: add %v (name: %v, roles: [user], ldap: true)", username, name)
if _, err := la.auth.db.Exec(`INSERT INTO user (username, ldap, name, roles) VALUES (?, ?, ?, ?)`, if _, err := la.auth.db.Exec(`INSERT INTO user (username, ldap, name, roles) VALUES (?, ?, ?, ?)`,
username, 1, name, "[\""+GetRoleString(RoleUser)+"\"]"); err != nil { username, 1, name, "[\""+schema.GetRoleString(schema.RoleUser)+"\"]"); err != nil {
log.Errorf("User '%s' new in LDAP: Insert into DB failed", username) log.Errorf("User '%s' new in LDAP: Insert into DB failed", username)
return err return err
} }

View File

@ -9,6 +9,7 @@ import (
"net/http" "net/http"
"github.com/ClusterCockpit/cc-backend/pkg/log" "github.com/ClusterCockpit/cc-backend/pkg/log"
"github.com/ClusterCockpit/cc-backend/pkg/schema"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
@ -27,18 +28,18 @@ func (la *LocalAuthenticator) Init(
} }
func (la *LocalAuthenticator) CanLogin( func (la *LocalAuthenticator) CanLogin(
user *User, user *schema.User,
username string, username string,
rw http.ResponseWriter, rw http.ResponseWriter,
r *http.Request) bool { r *http.Request) bool {
return user != nil && user.AuthSource == AuthViaLocalPassword return user != nil && user.AuthSource == schema.AuthViaLocalPassword
} }
func (la *LocalAuthenticator) Login( func (la *LocalAuthenticator) Login(
user *User, user *schema.User,
rw http.ResponseWriter, rw http.ResponseWriter,
r *http.Request) (*User, error) { r *http.Request) (*schema.User, error) {
if e := bcrypt.CompareHashAndPassword([]byte(user.Password), if e := bcrypt.CompareHashAndPassword([]byte(user.Password),
[]byte(r.FormValue("password"))); e != nil { []byte(r.FormValue("password"))); e != nil {

View File

@ -1,289 +0,0 @@
// 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 (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"strings"
"github.com/ClusterCockpit/cc-backend/internal/graph/model"
"github.com/ClusterCockpit/cc-backend/pkg/log"
sq "github.com/Masterminds/squirrel"
"github.com/jmoiron/sqlx"
"golang.org/x/crypto/bcrypt"
)
func (auth *Authentication) GetUser(username string) (*User, error) {
user := &User{Username: username}
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, &rawProjects); err != nil {
log.Warnf("Error while querying user '%v' from database", username)
return nil, err
}
user.Password = hashedPassword.String
user.Name = name.String
user.Email = email.String
if rawRoles.Valid {
if err := json.Unmarshal([]byte(rawRoles.String), &user.Roles); err != nil {
log.Warn("Error while unmarshaling raw roles from DB")
return nil, err
}
}
if rawProjects.Valid {
if err := json.Unmarshal([]byte(rawProjects.String), &user.Projects); err != nil {
return nil, err
}
}
return user, nil
}
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)}
if user.Name != "" {
cols = append(cols, "name")
vals = append(vals, user.Name)
}
if user.Email != "" {
cols = append(cols, "email")
vals = append(vals, user.Email)
}
if user.Password != "" {
password, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
if err != nil {
log.Error("Error while encrypting new user password")
return err
}
cols = append(cols, "password")
vals = append(vals, string(password))
}
if _, err := sq.Insert("user").Columns(cols...).Values(vals...).RunWith(auth.db).Exec(); err != nil {
log.Errorf("Error while inserting new user '%v' into DB", user.Username)
return err
}
log.Infof("new user %#v created (roles: %s, auth-source: %d, projects: %s)", user.Username, rolesJson, user.AuthSource, projectsJson)
return nil
}
func (auth *Authentication) DelUser(username string) error {
_, err := auth.db.Exec(`DELETE FROM user WHERE user.username = ?`, username)
log.Errorf("Error while deleting user '%s' from DB", username)
return err
}
func (auth *Authentication) ListUsers(specialsOnly bool) ([]*User, error) {
q := sq.Select("username", "name", "email", "roles", "projects").From("user")
if specialsOnly {
q = q.Where("(roles != '[\"user\"]' AND roles != '[]')")
}
rows, err := q.RunWith(auth.db).Query()
if err != nil {
log.Warn("Error while querying user list")
return nil, err
}
users := make([]*User, 0)
defer rows.Close()
for rows.Next() {
rawroles := ""
rawprojects := ""
user := &User{}
var name, email sql.NullString
if err := rows.Scan(&user.Username, &name, &email, &rawroles, &rawprojects); err != nil {
log.Warn("Error while scanning user list")
return nil, err
}
if err := json.Unmarshal([]byte(rawroles), &user.Roles); err != nil {
log.Warn("Error while unmarshaling raw role list")
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)
}
return users, nil
}
func (auth *Authentication) AddRole(
ctx context.Context,
username string,
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
}
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)
}
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
}
return nil
}
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
}
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 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 result interface{}
if len(newprojects) == 0 {
result = "[]"
} else {
result, _ = json.Marshal(newprojects)
}
if _, err := sq.Update("user").Set("projects", result).Where("user.username = ?", username).RunWith(auth.db).Exec(); err != nil {
return err
}
return nil
} else {
return fmt.Errorf("user %s already does not manage project %s", username, project)
}
}
func FetchUser(ctx context.Context, db *sqlx.DB, username string) (*model.User, error) {
me := GetUser(ctx)
if me != nil && me.Username != username && me.HasNotRoles([]Role{RoleAdmin, RoleSupport, RoleManager}) {
return nil, errors.New("forbidden")
}
user := &model.User{Username: username}
var name, email sql.NullString
if err := sq.Select("name", "email").From("user").Where("user.username = ?", username).
RunWith(db).QueryRow().Scan(&name, &email); err != nil {
if err == sql.ErrNoRows {
/* This warning will be logged *often* for non-local users, i.e. users mentioned only in job-table or archive, */
/* since FetchUser will be called to retrieve full name and mail for every job in query/list */
// log.Warnf("User '%s' Not found in DB", username)
return nil, nil
}
log.Warnf("Error while fetching user '%s'", username)
return nil, err
}
user.Name = name.String
user.Email = email.String
return user, nil
}

View File

@ -11,7 +11,6 @@ import (
"strconv" "strconv"
"time" "time"
"github.com/ClusterCockpit/cc-backend/internal/auth"
"github.com/ClusterCockpit/cc-backend/internal/graph/generated" "github.com/ClusterCockpit/cc-backend/internal/graph/generated"
"github.com/ClusterCockpit/cc-backend/internal/graph/model" "github.com/ClusterCockpit/cc-backend/internal/graph/model"
"github.com/ClusterCockpit/cc-backend/internal/metricdata" "github.com/ClusterCockpit/cc-backend/internal/metricdata"
@ -51,7 +50,7 @@ func (r *jobResolver) MetaData(ctx context.Context, obj *schema.Job) (interface{
// UserData is the resolver for the userData field. // UserData is the resolver for the userData field.
func (r *jobResolver) UserData(ctx context.Context, obj *schema.Job) (*model.User, error) { func (r *jobResolver) UserData(ctx context.Context, obj *schema.Job) (*model.User, error) {
return auth.FetchUser(ctx, r.DB, obj.User) return repository.GetUserRepository().FetchUserInCtx(ctx, obj.User)
} }
// CreateTag is the resolver for the createTag field. // CreateTag is the resolver for the createTag field.
@ -122,7 +121,7 @@ func (r *mutationResolver) RemoveTagsFromJob(ctx context.Context, job string, ta
// UpdateConfiguration is the resolver for the updateConfiguration field. // UpdateConfiguration is the resolver for the updateConfiguration field.
func (r *mutationResolver) UpdateConfiguration(ctx context.Context, name string, value string) (*string, error) { func (r *mutationResolver) UpdateConfiguration(ctx context.Context, name string, value string) (*string, error) {
if err := repository.GetUserCfgRepo().UpdateConfig(name, value, auth.GetUser(ctx)); err != nil { if err := repository.GetUserCfgRepo().UpdateConfig(name, value, repository.GetUserFromContext(ctx)); err != nil {
log.Warn("Error while updating user config") log.Warn("Error while updating user config")
return nil, err return nil, err
} }
@ -142,7 +141,7 @@ func (r *queryResolver) Tags(ctx context.Context) ([]*schema.Tag, error) {
// User is the resolver for the user field. // User is the resolver for the user field.
func (r *queryResolver) User(ctx context.Context, username string) (*model.User, error) { func (r *queryResolver) User(ctx context.Context, username string) (*model.User, error) {
return auth.FetchUser(ctx, r.DB, username) return repository.GetUserRepository().FetchUserInCtx(ctx, username)
} }
// AllocatedNodes is the resolver for the allocatedNodes field. // AllocatedNodes is the resolver for the allocatedNodes field.
@ -178,7 +177,9 @@ func (r *queryResolver) Job(ctx context.Context, id string) (*schema.Job, error)
return nil, err return nil, err
} }
if user := auth.GetUser(ctx); user != nil && job.User != user.Username && user.HasNotRoles([]auth.Role{auth.RoleAdmin, auth.RoleSupport, auth.RoleManager}) { if user := repository.GetUserFromContext(ctx); user != nil &&
job.User != user.Username &&
user.HasNotRoles([]schema.Role{schema.RoleAdmin, schema.RoleSupport, schema.RoleManager}) {
return nil, errors.New("you are not allowed to see this job") return nil, errors.New("you are not allowed to see this job")
} }
@ -318,8 +319,8 @@ func (r *queryResolver) RooflineHeatmap(ctx context.Context, filter []*model.Job
// NodeMetrics is the resolver for the nodeMetrics field. // NodeMetrics is the resolver for the nodeMetrics field.
func (r *queryResolver) NodeMetrics(ctx context.Context, cluster string, nodes []string, scopes []schema.MetricScope, metrics []string, from time.Time, to time.Time) ([]*model.NodeMetrics, error) { func (r *queryResolver) NodeMetrics(ctx context.Context, cluster string, nodes []string, scopes []schema.MetricScope, metrics []string, from time.Time, to time.Time) ([]*model.NodeMetrics, error) {
user := auth.GetUser(ctx) user := repository.GetUserFromContext(ctx)
if user != nil && !user.HasRole(auth.RoleAdmin) { if user != nil && !user.HasRole(schema.RoleAdmin) {
return nil, errors.New("you need to be an administrator for this query") return nil, errors.New("you need to be an administrator for this query")
} }

View File

@ -14,7 +14,6 @@ import (
"sync" "sync"
"time" "time"
"github.com/ClusterCockpit/cc-backend/internal/auth"
"github.com/ClusterCockpit/cc-backend/internal/graph/model" "github.com/ClusterCockpit/cc-backend/internal/graph/model"
"github.com/ClusterCockpit/cc-backend/internal/metricdata" "github.com/ClusterCockpit/cc-backend/internal/metricdata"
"github.com/ClusterCockpit/cc-backend/pkg/log" "github.com/ClusterCockpit/cc-backend/pkg/log"
@ -615,7 +614,7 @@ func (r *JobRepository) WaitForArchiving() {
r.archivePending.Wait() r.archivePending.Wait()
} }
func (r *JobRepository) FindUserOrProjectOrJobname(user *auth.User, searchterm string) (jobid string, username string, project string, jobname string) { func (r *JobRepository) FindUserOrProjectOrJobname(user *schema.User, searchterm string) (jobid string, username string, project string, jobname string) {
if _, err := strconv.Atoi(searchterm); err == nil { // Return empty on successful conversion: parent method will redirect for integer jobId if _, err := strconv.Atoi(searchterm); err == nil { // Return empty on successful conversion: parent method will redirect for integer jobId
return searchterm, "", "", "" return searchterm, "", "", ""
} else { // Has to have letters and logged-in user for other guesses } else { // Has to have letters and logged-in user for other guesses
@ -644,14 +643,14 @@ func (r *JobRepository) FindUserOrProjectOrJobname(user *auth.User, searchterm s
var ErrNotFound = errors.New("no such jobname, project or user") var ErrNotFound = errors.New("no such jobname, project or user")
var ErrForbidden = errors.New("not authorized") var ErrForbidden = errors.New("not authorized")
func (r *JobRepository) FindColumnValue(user *auth.User, searchterm string, table string, selectColumn string, whereColumn string, isLike bool) (result string, err error) { func (r *JobRepository) FindColumnValue(user *schema.User, searchterm string, table string, selectColumn string, whereColumn string, isLike bool) (result string, err error) {
compareStr := " = ?" compareStr := " = ?"
query := searchterm query := searchterm
if isLike { if isLike {
compareStr = " LIKE ?" compareStr = " LIKE ?"
query = "%" + searchterm + "%" query = "%" + searchterm + "%"
} }
if user.HasAnyRole([]auth.Role{auth.RoleAdmin, auth.RoleSupport, auth.RoleManager}) { if user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport, schema.RoleManager}) {
theQuery := sq.Select(table+"."+selectColumn).Distinct().From(table). theQuery := sq.Select(table+"."+selectColumn).Distinct().From(table).
Where(table+"."+whereColumn+compareStr, query) Where(table+"."+whereColumn+compareStr, query)
@ -676,9 +675,9 @@ func (r *JobRepository) FindColumnValue(user *auth.User, searchterm string, tabl
} }
} }
func (r *JobRepository) FindColumnValues(user *auth.User, query string, table string, selectColumn string, whereColumn string) (results []string, err error) { func (r *JobRepository) FindColumnValues(user *schema.User, query string, table string, selectColumn string, whereColumn string) (results []string, err error) {
emptyResult := make([]string, 0) emptyResult := make([]string, 0)
if user.HasAnyRole([]auth.Role{auth.RoleAdmin, auth.RoleSupport, auth.RoleManager}) { if user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport, schema.RoleManager}) {
rows, err := sq.Select(table+"."+selectColumn).Distinct().From(table). rows, err := sq.Select(table+"."+selectColumn).Distinct().From(table).
Where(table+"."+whereColumn+" LIKE ?", fmt.Sprint("%", query, "%")). Where(table+"."+whereColumn+" LIKE ?", fmt.Sprint("%", query, "%")).
RunWith(r.stmtCache).Query() RunWith(r.stmtCache).Query()

View File

@ -12,7 +12,6 @@ import (
"strings" "strings"
"time" "time"
"github.com/ClusterCockpit/cc-backend/internal/auth"
"github.com/ClusterCockpit/cc-backend/internal/graph/model" "github.com/ClusterCockpit/cc-backend/internal/graph/model"
"github.com/ClusterCockpit/cc-backend/pkg/log" "github.com/ClusterCockpit/cc-backend/pkg/log"
"github.com/ClusterCockpit/cc-backend/pkg/schema" "github.com/ClusterCockpit/cc-backend/pkg/schema"
@ -130,20 +129,20 @@ func (r *JobRepository) CountJobs(
} }
func SecurityCheck(ctx context.Context, query sq.SelectBuilder) (sq.SelectBuilder, error) { func SecurityCheck(ctx context.Context, query sq.SelectBuilder) (sq.SelectBuilder, error) {
user := auth.GetUser(ctx) user := GetUserFromContext(ctx)
if user == nil { if user == nil {
var qnil sq.SelectBuilder var qnil sq.SelectBuilder
return qnil, fmt.Errorf("user context is nil!") return qnil, fmt.Errorf("user context is nil!")
} else if user.HasAnyRole([]auth.Role{auth.RoleAdmin, auth.RoleSupport, auth.RoleApi}) { // Admin & Co. : All jobs } else if user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport, schema.RoleApi}) { // Admin & Co. : All jobs
return query, nil return query, nil
} else if user.HasRole(auth.RoleManager) { // Manager : Add filter for managed projects' jobs only + personal jobs } else if user.HasRole(schema.RoleManager) { // Manager : Add filter for managed projects' jobs only + personal jobs
if len(user.Projects) != 0 { if len(user.Projects) != 0 {
return query.Where(sq.Or{sq.Eq{"job.project": user.Projects}, sq.Eq{"job.user": user.Username}}), nil return query.Where(sq.Or{sq.Eq{"job.project": user.Projects}, sq.Eq{"job.user": user.Username}}), nil
} else { } else {
log.Debugf("Manager-User '%s' has no defined projects to lookup! Query only personal jobs ...", user.Username) log.Debugf("Manager-User '%s' has no defined projects to lookup! Query only personal jobs ...", user.Username)
return query.Where("job.user = ?", user.Username), nil return query.Where("job.user = ?", user.Username), nil
} }
} else if user.HasRole(auth.RoleUser) { // User : Only personal jobs } else if user.HasRole(schema.RoleUser) { // User : Only personal jobs
return query.Where("job.user = ?", user.Username), nil return query.Where("job.user = ?", user.Username), nil
} else { } else {
// Shortterm compatibility: Return User-Query if no roles: // Shortterm compatibility: Return User-Query if no roles:

View File

@ -10,7 +10,6 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/ClusterCockpit/cc-backend/internal/auth"
"github.com/ClusterCockpit/cc-backend/internal/config" "github.com/ClusterCockpit/cc-backend/internal/config"
"github.com/ClusterCockpit/cc-backend/internal/graph/model" "github.com/ClusterCockpit/cc-backend/internal/graph/model"
"github.com/ClusterCockpit/cc-backend/pkg/log" "github.com/ClusterCockpit/cc-backend/pkg/log"
@ -86,7 +85,7 @@ func (r *JobRepository) buildStatsQuery(
} }
func (r *JobRepository) getUserName(ctx context.Context, id string) string { func (r *JobRepository) getUserName(ctx context.Context, id string) string {
user := auth.GetUser(ctx) user := GetUserFromContext(ctx)
name, _ := r.FindColumnValue(user, id, "user", "name", "username", false) name, _ := r.FindColumnValue(user, id, "user", "name", "username", false)
if name != "" { if name != "" {
return name return name

View File

@ -7,7 +7,6 @@ package repository
import ( import (
"strings" "strings"
"github.com/ClusterCockpit/cc-backend/internal/auth"
"github.com/ClusterCockpit/cc-backend/pkg/archive" "github.com/ClusterCockpit/cc-backend/pkg/archive"
"github.com/ClusterCockpit/cc-backend/pkg/log" "github.com/ClusterCockpit/cc-backend/pkg/log"
"github.com/ClusterCockpit/cc-backend/pkg/schema" "github.com/ClusterCockpit/cc-backend/pkg/schema"
@ -68,7 +67,7 @@ func (r *JobRepository) CreateTag(tagType string, tagName string) (tagId int64,
return res.LastInsertId() return res.LastInsertId()
} }
func (r *JobRepository) CountTags(user *auth.User) (tags []schema.Tag, counts map[string]int, err error) { func (r *JobRepository) CountTags(user *schema.User) (tags []schema.Tag, counts map[string]int, err error) {
tags = make([]schema.Tag, 0, 100) tags = make([]schema.Tag, 0, 100)
xrows, err := r.DB.Queryx("SELECT id, tag_type, tag_name FROM tag") xrows, err := r.DB.Queryx("SELECT id, tag_type, tag_name FROM tag")
if err != nil { if err != nil {
@ -88,10 +87,10 @@ func (r *JobRepository) CountTags(user *auth.User) (tags []schema.Tag, counts ma
LeftJoin("jobtag jt ON t.id = jt.tag_id"). LeftJoin("jobtag jt ON t.id = jt.tag_id").
GroupBy("t.tag_name") GroupBy("t.tag_name")
if user != nil && user.HasAnyRole([]auth.Role{auth.RoleAdmin, auth.RoleSupport}) { // ADMIN || SUPPORT: Count all jobs if user != nil && user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport}) { // ADMIN || SUPPORT: Count all jobs
log.Debug("CountTags: User Admin or Support -> Count all Jobs for Tags") log.Debug("CountTags: User Admin or Support -> Count all Jobs for Tags")
// Unchanged: Needs to be own case still, due to UserRole/NoRole compatibility handling in else case // Unchanged: Needs to be own case still, due to UserRole/NoRole compatibility handling in else case
} else if user != nil && user.HasRole(auth.RoleManager) { // MANAGER: Count own jobs plus project's jobs } else if user != nil && user.HasRole(schema.RoleManager) { // MANAGER: Count own jobs plus project's jobs
// Build ("project1", "project2", ...) list of variable length directly in SQL string // 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) 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 if user != nil { // USER OR NO ROLE (Compatibility): Only count own jobs } else if user != nil { // USER OR NO ROLE (Compatibility): Only count own jobs

View File

@ -1,137 +1,325 @@
// Copyright (C) 2022 NHR@FAU, University Erlangen-Nuremberg. // Copyright (C) 2023 NHR@FAU, University Erlangen-Nuremberg.
// All rights reserved. // All rights reserved.
// Use of this source code is governed by a MIT-style // Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
package repository package repository
import ( import (
"context"
"database/sql"
"encoding/json" "encoding/json"
"errors"
"fmt"
"strings"
"sync" "sync"
"time"
"github.com/ClusterCockpit/cc-backend/internal/auth" "github.com/ClusterCockpit/cc-backend/internal/graph/model"
"github.com/ClusterCockpit/cc-backend/internal/config"
"github.com/ClusterCockpit/cc-backend/pkg/log" "github.com/ClusterCockpit/cc-backend/pkg/log"
"github.com/ClusterCockpit/cc-backend/pkg/lrucache" "github.com/ClusterCockpit/cc-backend/pkg/schema"
sq "github.com/Masterminds/squirrel"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"golang.org/x/crypto/bcrypt"
) )
var ( var (
userCfgRepoOnce sync.Once userRepoOnce sync.Once
userCfgRepoInstance *UserCfgRepo userRepoInstance *UserRepository
) )
type UserCfgRepo struct { type UserRepository struct {
DB *sqlx.DB DB *sqlx.DB
Lookup *sqlx.Stmt driver string
lock sync.RWMutex
uiDefaults map[string]interface{}
cache *lrucache.Cache
} }
func GetUserCfgRepo() *UserCfgRepo { func GetUserRepository() *UserRepository {
userCfgRepoOnce.Do(func() { userRepoOnce.Do(func() {
db := GetConnection() db := GetConnection()
lookupConfigStmt, err := db.DB.Preparex(`SELECT confkey, value FROM configuration WHERE configuration.username = ?`) userRepoInstance = &UserRepository{
if err != nil { DB: db.DB,
log.Fatalf("db.DB.Preparex() error: %v", err) driver: db.Driver,
}
userCfgRepoInstance = &UserCfgRepo{
DB: db.DB,
Lookup: lookupConfigStmt,
uiDefaults: config.Keys.UiDefaults,
cache: lrucache.New(1024),
} }
}) })
return userRepoInstance
return userCfgRepoInstance
} }
// Return the personalised UI config for the currently authenticated func (r *UserRepository) GetUser(username string) (*schema.User, error) {
// user or return the plain default config. user := &schema.User{Username: username}
func (uCfg *UserCfgRepo) GetUIConfig(user *auth.User) (map[string]interface{}, error) { var hashedPassword, name, rawRoles, email, rawProjects sql.NullString
if user == nil { if err := sq.Select("password", "ldap", "name", "roles", "email", "projects").From("user").
uCfg.lock.RLock() Where("user.username = ?", username).RunWith(r.DB).
copy := make(map[string]interface{}, len(uCfg.uiDefaults)) QueryRow().Scan(&hashedPassword, &user.AuthSource, &name, &rawRoles, &email, &rawProjects); err != nil {
for k, v := range uCfg.uiDefaults { log.Warnf("Error while querying user '%v' from database", username)
copy[k] = v
}
uCfg.lock.RUnlock()
return copy, nil
}
data := uCfg.cache.Get(user.Username, func() (interface{}, time.Duration, int) {
uiconfig := make(map[string]interface{}, len(uCfg.uiDefaults))
for k, v := range uCfg.uiDefaults {
uiconfig[k] = v
}
rows, err := uCfg.Lookup.Query(user.Username)
if err != nil {
log.Warnf("Error while looking up user uiconfig for user '%v'", user.Username)
return err, 0, 0
}
size := 0
defer rows.Close()
for rows.Next() {
var key, rawval string
if err := rows.Scan(&key, &rawval); err != nil {
log.Warn("Error while scanning user uiconfig values")
return err, 0, 0
}
var val interface{}
if err := json.Unmarshal([]byte(rawval), &val); err != nil {
log.Warn("Error while unmarshaling raw user uiconfig json")
return err, 0, 0
}
size += len(key)
size += len(rawval)
uiconfig[key] = val
}
// Add global ShortRunningJobsDuration setting as plot_list_hideShortRunningJobs
uiconfig["plot_list_hideShortRunningJobs"] = config.Keys.ShortRunningJobsDuration
return uiconfig, 24 * time.Hour, size
})
if err, ok := data.(error); ok {
log.Error("Error in returned dataset")
return nil, err return nil, err
} }
return data.(map[string]interface{}), nil user.Password = hashedPassword.String
} user.Name = name.String
user.Email = email.String
// If the context does not have a user, update the global ui configuration if rawRoles.Valid {
// without persisting it! If there is a (authenticated) user, update only his if err := json.Unmarshal([]byte(rawRoles.String), &user.Roles); err != nil {
// configuration. log.Warn("Error while unmarshaling raw roles from DB")
func (uCfg *UserCfgRepo) UpdateConfig( return nil, err
key, value string, }
user *auth.User) error { }
if rawProjects.Valid {
if user == nil { if err := json.Unmarshal([]byte(rawProjects.String), &user.Projects); err != nil {
var val interface{} return nil, err
if err := json.Unmarshal([]byte(value), &val); err != nil {
log.Warn("Error while unmarshaling raw user config json")
return err
} }
uCfg.lock.Lock()
defer uCfg.lock.Unlock()
uCfg.uiDefaults[key] = val
return nil
} }
if _, err := uCfg.DB.Exec(`REPLACE INTO configuration (username, confkey, value) VALUES (?, ?, ?)`, user.Username, key, value); err != nil { return user, nil
log.Warnf("Error while replacing user config in DB for user '%v'", user.Username) }
func (r *UserRepository) AddUser(user *schema.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)}
if user.Name != "" {
cols = append(cols, "name")
vals = append(vals, user.Name)
}
if user.Email != "" {
cols = append(cols, "email")
vals = append(vals, user.Email)
}
if user.Password != "" {
password, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
if err != nil {
log.Error("Error while encrypting new user password")
return err
}
cols = append(cols, "password")
vals = append(vals, string(password))
}
if _, err := sq.Insert("user").Columns(cols...).Values(vals...).RunWith(r.DB).Exec(); err != nil {
log.Errorf("Error while inserting new user '%v' into DB", user.Username)
return err return err
} }
uCfg.cache.Del(user.Username) log.Infof("new user %#v created (roles: %s, auth-source: %d, projects: %s)", user.Username, rolesJson, user.AuthSource, projectsJson)
return nil return nil
} }
func (r *UserRepository) DelUser(username string) error {
_, err := r.DB.Exec(`DELETE FROM user WHERE user.username = ?`, username)
log.Errorf("Error while deleting user '%s' from DB", username)
return err
}
func (r *UserRepository) ListUsers(specialsOnly bool) ([]*schema.User, error) {
q := sq.Select("username", "name", "email", "roles", "projects").From("user")
if specialsOnly {
q = q.Where("(roles != '[\"user\"]' AND roles != '[]')")
}
rows, err := q.RunWith(r.DB).Query()
if err != nil {
log.Warn("Error while querying user list")
return nil, err
}
users := make([]*schema.User, 0)
defer rows.Close()
for rows.Next() {
rawroles := ""
rawprojects := ""
user := &schema.User{}
var name, email sql.NullString
if err := rows.Scan(&user.Username, &name, &email, &rawroles, &rawprojects); err != nil {
log.Warn("Error while scanning user list")
return nil, err
}
if err := json.Unmarshal([]byte(rawroles), &user.Roles); err != nil {
log.Warn("Error while unmarshaling raw role list")
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)
}
return users, nil
}
func (r *UserRepository) AddRole(
ctx context.Context,
username string,
queryrole string) error {
newRole := strings.ToLower(queryrole)
user, err := r.GetUser(username)
if err != nil {
log.Warnf("Could not load user '%s'", username)
return err
}
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)
}
roles, _ := json.Marshal(append(user.Roles, newRole))
if _, err := sq.Update("user").Set("roles", roles).Where("user.username = ?", username).RunWith(r.DB).Exec(); err != nil {
log.Errorf("Error while adding new role for user '%s'", user.Username)
return err
}
return nil
}
func (r *UserRepository) RemoveRole(ctx context.Context, username string, queryrole string) error {
oldRole := strings.ToLower(queryrole)
user, err := r.GetUser(username)
if err != nil {
log.Warnf("Could not load user '%s'", username)
return err
}
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 == schema.GetRoleString(schema.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(r.DB).Exec(); err != nil {
log.Errorf("Error while removing role for user '%s'", user.Username)
return err
}
return nil
}
func (r *UserRepository) AddProject(
ctx context.Context,
username string,
project string) error {
user, err := r.GetUser(username)
if err != nil {
return err
}
if !user.HasRole(schema.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(r.DB).Exec(); err != nil {
return err
}
return nil
}
func (r *UserRepository) RemoveProject(ctx context.Context, username string, project string) error {
user, err := r.GetUser(username)
if err != nil {
return err
}
if !user.HasRole(schema.RoleManager) {
return fmt.Errorf("user '%#v' is not a manager!", username)
}
if !user.HasProject(project) {
return fmt.Errorf("user '%#v': Cannot remove project '%#v' - Does not match!", username, project)
}
var exists bool
var newprojects []string
for _, p := range user.Projects {
if p != project {
newprojects = append(newprojects, p) // Append all projects not matching requested to be deleted project
} else {
exists = true
}
}
if exists == true {
var result interface{}
if len(newprojects) == 0 {
result = "[]"
} else {
result, _ = json.Marshal(newprojects)
}
if _, err := sq.Update("user").Set("projects", result).Where("user.username = ?", username).RunWith(r.DB).Exec(); err != nil {
return err
}
return nil
} else {
return fmt.Errorf("user %s already does not manage project %s", username, project)
}
}
type ContextKey string
const ContextUserKey ContextKey = "user"
func GetUserFromContext(ctx context.Context) *schema.User {
x := ctx.Value(ContextUserKey)
if x == nil {
return nil
}
return x.(*schema.User)
}
func (r *UserRepository) FetchUserInCtx(ctx context.Context, username string) (*model.User, error) {
me := GetUserFromContext(ctx)
if me != nil && me.Username != username &&
me.HasNotRoles([]schema.Role{schema.RoleAdmin, schema.RoleSupport, schema.RoleManager}) {
return nil, errors.New("forbidden")
}
user := &model.User{Username: username}
var name, email sql.NullString
if err := sq.Select("name", "email").From("user").Where("user.username = ?", username).
RunWith(r.DB).QueryRow().Scan(&name, &email); err != nil {
if err == sql.ErrNoRows {
/* This warning will be logged *often* for non-local users, i.e. users mentioned only in job-table or archive, */
/* since FetchUser will be called to retrieve full name and mail for every job in query/list */
// log.Warnf("User '%s' Not found in DB", username)
return nil, nil
}
log.Warnf("Error while fetching user '%s'", username)
return nil, err
}
user.Name = name.String
user.Email = email.String
return user, nil
}

View File

@ -0,0 +1,137 @@
// 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 repository
import (
"encoding/json"
"sync"
"time"
"github.com/ClusterCockpit/cc-backend/internal/config"
"github.com/ClusterCockpit/cc-backend/pkg/log"
"github.com/ClusterCockpit/cc-backend/pkg/lrucache"
"github.com/ClusterCockpit/cc-backend/pkg/schema"
"github.com/jmoiron/sqlx"
)
var (
userCfgRepoOnce sync.Once
userCfgRepoInstance *UserCfgRepo
)
type UserCfgRepo struct {
DB *sqlx.DB
Lookup *sqlx.Stmt
lock sync.RWMutex
uiDefaults map[string]interface{}
cache *lrucache.Cache
}
func GetUserCfgRepo() *UserCfgRepo {
userCfgRepoOnce.Do(func() {
db := GetConnection()
lookupConfigStmt, err := db.DB.Preparex(`SELECT confkey, value FROM configuration WHERE configuration.username = ?`)
if err != nil {
log.Fatalf("db.DB.Preparex() error: %v", err)
}
userCfgRepoInstance = &UserCfgRepo{
DB: db.DB,
Lookup: lookupConfigStmt,
uiDefaults: config.Keys.UiDefaults,
cache: lrucache.New(1024),
}
})
return userCfgRepoInstance
}
// Return the personalised UI config for the currently authenticated
// user or return the plain default config.
func (uCfg *UserCfgRepo) GetUIConfig(user *schema.User) (map[string]interface{}, error) {
if user == nil {
uCfg.lock.RLock()
copy := make(map[string]interface{}, len(uCfg.uiDefaults))
for k, v := range uCfg.uiDefaults {
copy[k] = v
}
uCfg.lock.RUnlock()
return copy, nil
}
data := uCfg.cache.Get(user.Username, func() (interface{}, time.Duration, int) {
uiconfig := make(map[string]interface{}, len(uCfg.uiDefaults))
for k, v := range uCfg.uiDefaults {
uiconfig[k] = v
}
rows, err := uCfg.Lookup.Query(user.Username)
if err != nil {
log.Warnf("Error while looking up user uiconfig for user '%v'", user.Username)
return err, 0, 0
}
size := 0
defer rows.Close()
for rows.Next() {
var key, rawval string
if err := rows.Scan(&key, &rawval); err != nil {
log.Warn("Error while scanning user uiconfig values")
return err, 0, 0
}
var val interface{}
if err := json.Unmarshal([]byte(rawval), &val); err != nil {
log.Warn("Error while unmarshaling raw user uiconfig json")
return err, 0, 0
}
size += len(key)
size += len(rawval)
uiconfig[key] = val
}
// Add global ShortRunningJobsDuration setting as plot_list_hideShortRunningJobs
uiconfig["plot_list_hideShortRunningJobs"] = config.Keys.ShortRunningJobsDuration
return uiconfig, 24 * time.Hour, size
})
if err, ok := data.(error); ok {
log.Error("Error in returned dataset")
return nil, err
}
return data.(map[string]interface{}), nil
}
// If the context does not have a user, update the global ui configuration
// without persisting it! If there is a (authenticated) user, update only his
// configuration.
func (uCfg *UserCfgRepo) UpdateConfig(
key, value string,
user *schema.User) error {
if user == nil {
var val interface{}
if err := json.Unmarshal([]byte(value), &val); err != nil {
log.Warn("Error while unmarshaling raw user config json")
return err
}
uCfg.lock.Lock()
defer uCfg.lock.Unlock()
uCfg.uiDefaults[key] = val
return nil
}
if _, err := uCfg.DB.Exec(`REPLACE INTO configuration (username, confkey, value) VALUES (?, ?, ?)`, user.Username, key, value); err != nil {
log.Warnf("Error while replacing user config in DB for user '%v'", user.Username)
return err
}
uCfg.cache.Del(user.Username)
return nil
}

View File

@ -9,9 +9,9 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/ClusterCockpit/cc-backend/internal/auth"
"github.com/ClusterCockpit/cc-backend/internal/config" "github.com/ClusterCockpit/cc-backend/internal/config"
"github.com/ClusterCockpit/cc-backend/pkg/log" "github.com/ClusterCockpit/cc-backend/pkg/log"
"github.com/ClusterCockpit/cc-backend/pkg/schema"
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
) )
@ -53,7 +53,7 @@ func setupUserTest(t *testing.T) *UserCfgRepo {
func TestGetUIConfig(t *testing.T) { func TestGetUIConfig(t *testing.T) {
r := setupUserTest(t) r := setupUserTest(t)
u := auth.User{Username: "demo"} u := schema.User{Username: "demo"}
cfg, err := r.GetUIConfig(&u) cfg, err := r.GetUIConfig(&u)
if err != nil { if err != nil {

View File

@ -13,11 +13,11 @@ import (
"strings" "strings"
"time" "time"
"github.com/ClusterCockpit/cc-backend/internal/auth"
"github.com/ClusterCockpit/cc-backend/internal/graph/model" "github.com/ClusterCockpit/cc-backend/internal/graph/model"
"github.com/ClusterCockpit/cc-backend/internal/repository" "github.com/ClusterCockpit/cc-backend/internal/repository"
"github.com/ClusterCockpit/cc-backend/internal/util" "github.com/ClusterCockpit/cc-backend/internal/util"
"github.com/ClusterCockpit/cc-backend/pkg/log" "github.com/ClusterCockpit/cc-backend/pkg/log"
"github.com/ClusterCockpit/cc-backend/pkg/schema"
"github.com/ClusterCockpit/cc-backend/web" "github.com/ClusterCockpit/cc-backend/web"
"github.com/gorilla/mux" "github.com/gorilla/mux"
) )
@ -81,12 +81,11 @@ func setupJobRoute(i InfoType, r *http.Request) InfoType {
} }
func setupUserRoute(i InfoType, r *http.Request) InfoType { func setupUserRoute(i InfoType, r *http.Request) InfoType {
jobRepo := repository.GetJobRepository()
username := mux.Vars(r)["id"] username := mux.Vars(r)["id"]
i["id"] = username i["id"] = username
i["username"] = username i["username"] = username
// TODO: If forbidden (== err exists), redirect to error page // TODO: If forbidden (== err exists), redirect to error page
if user, _ := auth.FetchUser(r.Context(), jobRepo.DB, username); user != nil { if user, _ := repository.GetUserRepository().FetchUserInCtx(r.Context(), username); user != nil {
i["name"] = user.Name i["name"] = user.Name
i["email"] = user.Email i["email"] = user.Email
} }
@ -125,7 +124,7 @@ func setupAnalysisRoute(i InfoType, r *http.Request) InfoType {
func setupTaglistRoute(i InfoType, r *http.Request) InfoType { func setupTaglistRoute(i InfoType, r *http.Request) InfoType {
jobRepo := repository.GetJobRepository() jobRepo := repository.GetJobRepository()
user := auth.GetUser(r.Context()) user := repository.GetUserFromContext(r.Context())
tags, counts, err := jobRepo.CountTags(user) tags, counts, err := jobRepo.CountTags(user)
tagMap := make(map[string][]map[string]interface{}) tagMap := make(map[string][]map[string]interface{})
@ -255,7 +254,7 @@ func SetupRoutes(router *mux.Router, buildInfo web.Build) {
for _, route := range routes { for _, route := range routes {
route := route route := route
router.HandleFunc(route.Route, func(rw http.ResponseWriter, r *http.Request) { router.HandleFunc(route.Route, func(rw http.ResponseWriter, r *http.Request) {
conf, err := userCfgRepo.GetUIConfig(auth.GetUser(r.Context())) conf, err := userCfgRepo.GetUIConfig(repository.GetUserFromContext(r.Context()))
if err != nil { if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError) http.Error(rw, err.Error(), http.StatusInternalServerError)
return return
@ -268,9 +267,9 @@ func SetupRoutes(router *mux.Router, buildInfo web.Build) {
} }
// Get User -> What if NIL? // Get User -> What if NIL?
user := auth.GetUser(r.Context()) user := repository.GetUserFromContext(r.Context())
// Get Roles // Get Roles
availableRoles, _ := auth.GetValidRolesMap(user) availableRoles, _ := schema.GetValidRolesMap(user)
page := web.Page{ page := web.Page{
Title: title, Title: title,
@ -285,14 +284,14 @@ func SetupRoutes(router *mux.Router, buildInfo web.Build) {
page.FilterPresets = buildFilterPresets(r.URL.Query()) page.FilterPresets = buildFilterPresets(r.URL.Query())
} }
web.RenderTemplate(rw, r, route.Template, &page) web.RenderTemplate(rw, route.Template, &page)
}) })
} }
} }
func HandleSearchBar(rw http.ResponseWriter, r *http.Request, buildInfo web.Build) { func HandleSearchBar(rw http.ResponseWriter, r *http.Request, buildInfo web.Build) {
user := auth.GetUser(r.Context()) user := repository.GetUserFromContext(r.Context())
availableRoles, _ := auth.GetValidRolesMap(user) availableRoles, _ := schema.GetValidRolesMap(user)
if search := r.URL.Query().Get("searchId"); search != "" { if search := r.URL.Query().Get("searchId"); search != "" {
repo := repository.GetJobRepository() repo := repository.GetJobRepository()
@ -309,10 +308,10 @@ func HandleSearchBar(rw http.ResponseWriter, r *http.Request, buildInfo web.Buil
case "arrayJobId": case "arrayJobId":
http.Redirect(rw, r, "/monitoring/jobs/?arrayJobId="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusFound) // All Users: Redirect to Tablequery http.Redirect(rw, r, "/monitoring/jobs/?arrayJobId="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusFound) // All Users: Redirect to Tablequery
case "username": case "username":
if user.HasAnyRole([]auth.Role{auth.RoleAdmin, auth.RoleSupport, auth.RoleManager}) { if user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport, schema.RoleManager}) {
http.Redirect(rw, r, "/monitoring/users/?user="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusFound) http.Redirect(rw, r, "/monitoring/users/?user="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusFound)
} else { } else {
web.RenderTemplate(rw, r, "message.tmpl", &web.Page{Title: "Error", MsgType: "alert-danger", Message: "Missing Access Rights", User: *user, Roles: availableRoles, Build: buildInfo}) web.RenderTemplate(rw, "message.tmpl", &web.Page{Title: "Error", MsgType: "alert-danger", Message: "Missing Access Rights", User: *user, Roles: availableRoles, Build: buildInfo})
} }
case "name": case "name":
usernames, _ := repo.FindColumnValues(user, strings.Trim(splitSearch[1], " "), "user", "username", "name") usernames, _ := repo.FindColumnValues(user, strings.Trim(splitSearch[1], " "), "user", "username", "name")
@ -320,14 +319,14 @@ func HandleSearchBar(rw http.ResponseWriter, r *http.Request, buildInfo web.Buil
joinedNames := strings.Join(usernames, "&user=") joinedNames := strings.Join(usernames, "&user=")
http.Redirect(rw, r, "/monitoring/users/?user="+joinedNames, http.StatusFound) http.Redirect(rw, r, "/monitoring/users/?user="+joinedNames, http.StatusFound)
} else { } else {
if user.HasAnyRole([]auth.Role{auth.RoleAdmin, auth.RoleSupport, auth.RoleManager}) { if user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport, schema.RoleManager}) {
http.Redirect(rw, r, "/monitoring/users/?user=NoUserNameFound", http.StatusPermanentRedirect) http.Redirect(rw, r, "/monitoring/users/?user=NoUserNameFound", http.StatusPermanentRedirect)
} else { } else {
web.RenderTemplate(rw, r, "message.tmpl", &web.Page{Title: "Error", MsgType: "alert-danger", Message: "Missing Access Rights", User: *user, Roles: availableRoles, Build: buildInfo}) web.RenderTemplate(rw, "message.tmpl", &web.Page{Title: "Error", MsgType: "alert-danger", Message: "Missing Access Rights", User: *user, Roles: availableRoles, Build: buildInfo})
} }
} }
default: default:
web.RenderTemplate(rw, r, "message.tmpl", &web.Page{Title: "Warning", MsgType: "alert-warning", Message: fmt.Sprintf("Unknown search type: %s", strings.Trim(splitSearch[0], " ")), User: *user, Roles: availableRoles, Build: buildInfo}) web.RenderTemplate(rw, "message.tmpl", &web.Page{Title: "Warning", MsgType: "alert-warning", Message: fmt.Sprintf("Unknown search type: %s", strings.Trim(splitSearch[0], " ")), User: *user, Roles: availableRoles, Build: buildInfo})
} }
} else if len(splitSearch) == 1 { } else if len(splitSearch) == 1 {
@ -342,13 +341,13 @@ func HandleSearchBar(rw http.ResponseWriter, r *http.Request, buildInfo web.Buil
} else if jobname != "" { } else if jobname != "" {
http.Redirect(rw, r, "/monitoring/jobs/?jobName="+url.QueryEscape(jobname), http.StatusFound) // JobName (contains) http.Redirect(rw, r, "/monitoring/jobs/?jobName="+url.QueryEscape(jobname), http.StatusFound) // JobName (contains)
} else { } else {
web.RenderTemplate(rw, r, "message.tmpl", &web.Page{Title: "Info", MsgType: "alert-info", Message: "Search without result", User: *user, Roles: availableRoles, Build: buildInfo}) web.RenderTemplate(rw, "message.tmpl", &web.Page{Title: "Info", MsgType: "alert-info", Message: "Search without result", User: *user, Roles: availableRoles, Build: buildInfo})
} }
} else { } else {
web.RenderTemplate(rw, r, "message.tmpl", &web.Page{Title: "Error", MsgType: "alert-danger", Message: "Searchbar query parameters malformed", User: *user, Roles: availableRoles, Build: buildInfo}) web.RenderTemplate(rw, "message.tmpl", &web.Page{Title: "Error", MsgType: "alert-danger", Message: "Searchbar query parameters malformed", User: *user, Roles: availableRoles, Build: buildInfo})
} }
} else { } else {
web.RenderTemplate(rw, r, "message.tmpl", &web.Page{Title: "Warning", MsgType: "alert-warning", Message: "Empty search", User: *user, Roles: availableRoles, Build: buildInfo}) web.RenderTemplate(rw, "message.tmpl", &web.Page{Title: "Warning", MsgType: "alert-warning", Message: "Empty search", User: *user, Roles: availableRoles, Build: buildInfo})
} }
} }

View File

@ -17,7 +17,9 @@ type LdapConfig struct {
UserFilter string `json:"user_filter"` UserFilter string `json:"user_filter"`
SyncInterval string `json:"sync_interval"` // Parsed using time.ParseDuration. SyncInterval string `json:"sync_interval"` // Parsed using time.ParseDuration.
SyncDelOldUsers bool `json:"sync_del_old_users"` SyncDelOldUsers bool `json:"sync_del_old_users"`
SyncUserOnLogin bool `json:"syncUserOnLogin"`
// Should an non-existent user be added to the DB if user exists in ldap directory
SyncUserOnLogin bool `json:"syncUserOnLogin"`
} }
type JWTAuthConfig struct { type JWTAuthConfig struct {
@ -30,10 +32,13 @@ type JWTAuthConfig struct {
// Deny login for users not in database (but defined in JWT). // Deny login for users not in database (but defined in JWT).
// Ignore user roles defined in JWTs ('roles' claim), get them from db. // Ignore user roles defined in JWTs ('roles' claim), get them from db.
ForceJWTValidationViaDatabase bool `json:"forceJWTValidationViaDatabase"` ValidateUser bool `json:"validateUser"`
// Specifies which issuer should be accepted when validating external JWTs ('iss' claim) // Specifies which issuer should be accepted when validating external JWTs ('iss' claim)
TrustedExternalIssuer string `json:"trustedExternalIssuer"` TrustedIssuer string `json:"trustedIssuer"`
// Should an non-existent user be added to the DB based on the information in the token
SyncUserOnLogin bool `json:"syncUserOnLogin"`
} }
type IntRange struct { type IntRange struct {

View File

@ -2,7 +2,7 @@
// All rights reserved. // All rights reserved.
// Use of this source code is governed by a MIT-style // Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
package auth package schema
import ( import (
"fmt" "fmt"
@ -21,6 +21,41 @@ const (
RoleError RoleError
) )
type AuthSource int
const (
AuthViaLocalPassword AuthSource = iota
AuthViaLDAP
AuthViaToken
)
type AuthType int
const (
AuthToken AuthType = iota
AuthSession
)
type User struct {
Username string `json:"username"`
Password string `json:"-"`
Name string `json:"name"`
Roles []string `json:"roles"`
AuthType AuthType `json:"authType"`
AuthSource AuthSource `json:"authSource"`
Email string `json:"email"`
Projects []string `json:"projects"`
}
func (u *User) HasProject(project string) bool {
for _, p := range u.Projects {
if p == project {
return true
}
}
return false
}
func GetRoleString(roleInt Role) string { func GetRoleString(roleInt Role) string {
return [6]string{"anonymous", "api", "user", "manager", "support", "admin"}[roleInt] return [6]string{"anonymous", "api", "user", "manager", "support", "admin"}[roleInt]
} }
@ -44,12 +79,12 @@ func getRoleEnum(roleStr string) Role {
} }
} }
func isValidRole(role string) bool { func IsValidRole(role string) bool {
return getRoleEnum(role) != RoleError return getRoleEnum(role) != RoleError
} }
func (u *User) HasValidRole(role string) (hasRole bool, isValid bool) { func (u *User) HasValidRole(role string) (hasRole bool, isValid bool) {
if isValidRole(role) { if IsValidRole(role) {
for _, r := range u.Roles { for _, r := range u.Roles {
if r == role { if r == role {
return true, true return true, true

View File

@ -2,7 +2,7 @@
// All rights reserved. // All rights reserved.
// Use of this source code is governed by a MIT-style // Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
package auth package schema
import ( import (
"testing" "testing"

View File

@ -11,7 +11,6 @@ import (
"net/http" "net/http"
"strings" "strings"
"github.com/ClusterCockpit/cc-backend/internal/auth"
"github.com/ClusterCockpit/cc-backend/internal/config" "github.com/ClusterCockpit/cc-backend/internal/config"
"github.com/ClusterCockpit/cc-backend/internal/util" "github.com/ClusterCockpit/cc-backend/internal/util"
"github.com/ClusterCockpit/cc-backend/pkg/log" "github.com/ClusterCockpit/cc-backend/pkg/log"
@ -92,8 +91,8 @@ type Page struct {
Title string // Page title Title string // Page title
MsgType string // For generic use in message boxes MsgType string // For generic use in message boxes
Message string // For generic use in message boxes Message string // For generic use in message boxes
User auth.User // Information about the currently logged in user (Full User Info) User schema.User // Information about the currently logged in user (Full User Info)
Roles map[string]auth.Role // Available roles for frontend render checks Roles map[string]schema.Role // Available roles for frontend render checks
Build Build // Latest information about the application Build Build // Latest information about the application
Clusters []schema.ClusterConfig // List of all clusters for use in the Header Clusters []schema.ClusterConfig // List of all clusters for use in the Header
FilterPresets map[string]interface{} // For pages with the Filter component, this can be used to set initial filters. FilterPresets map[string]interface{} // For pages with the Filter component, this can be used to set initial filters.
@ -101,7 +100,7 @@ type Page struct {
Config map[string]interface{} // UI settings for the currently logged in user (e.g. line width, ...) Config map[string]interface{} // UI settings for the currently logged in user (e.g. line width, ...)
} }
func RenderTemplate(rw http.ResponseWriter, r *http.Request, file string, page *Page) { func RenderTemplate(rw http.ResponseWriter, file string, page *Page) {
t, ok := templates[file] t, ok := templates[file]
if !ok { if !ok {
log.Errorf("WEB/WEB > template '%s' not found", file) log.Errorf("WEB/WEB > template '%s' not found", file)