mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2024-12-25 04:49:05 +01:00
Refactor auth module
Separate parts Add user repository Add user schema
This commit is contained in:
parent
80aed87415
commit
87ce4f63d4
@ -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(),
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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")
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
|
||||||
}
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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 {
|
|
||||||
log.Fatalf("db.DB.Preparex() error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
userCfgRepoInstance = &UserCfgRepo{
|
|
||||||
DB: db.DB,
|
DB: db.DB,
|
||||||
Lookup: lookupConfigStmt,
|
driver: db.Driver,
|
||||||
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 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
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the context does not have a user, update the global ui configuration
|
func (r *UserRepository) AddUser(user *schema.User) error {
|
||||||
// without persisting it! If there is a (authenticated) user, update only his
|
rolesJson, _ := json.Marshal(user.Roles)
|
||||||
// configuration.
|
projectsJson, _ := json.Marshal(user.Projects)
|
||||||
func (uCfg *UserCfgRepo) UpdateConfig(
|
|
||||||
key, value string,
|
|
||||||
user *auth.User) error {
|
|
||||||
|
|
||||||
if user == nil {
|
cols := []string{"username", "roles", "projects"}
|
||||||
var val interface{}
|
vals := []interface{}{user.Username, string(rolesJson), string(projectsJson)}
|
||||||
if err := json.Unmarshal([]byte(value), &val); err != nil {
|
|
||||||
log.Warn("Error while unmarshaling raw user config json")
|
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.lock.Lock()
|
log.Infof("new user %#v created (roles: %s, auth-source: %d, projects: %s)", user.Username, rolesJson, user.AuthSource, projectsJson)
|
||||||
defer uCfg.lock.Unlock()
|
|
||||||
uCfg.uiDefaults[key] = val
|
|
||||||
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 != '[]')")
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := uCfg.DB.Exec(`REPLACE INTO configuration (username, confkey, value) VALUES (?, ?, ?)`, user.Username, key, value); err != nil {
|
rows, err := q.RunWith(r.DB).Query()
|
||||||
log.Warnf("Error while replacing user config in DB for user '%v'", user.Username)
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
uCfg.cache.Del(user.Username)
|
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
|
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
|
||||||
|
}
|
||||||
|
137
internal/repository/userConfig.go
Normal file
137
internal/repository/userConfig.go
Normal 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
|
||||||
|
}
|
@ -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 {
|
@ -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})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,8 @@ 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"`
|
||||||
|
|
||||||
|
// Should an non-existent user be added to the DB if user exists in ldap directory
|
||||||
SyncUserOnLogin bool `json:"syncUserOnLogin"`
|
SyncUserOnLogin bool `json:"syncUserOnLogin"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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 {
|
||||||
|
@ -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
|
@ -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"
|
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user