mirror of
				https://github.com/ClusterCockpit/cc-backend
				synced 2025-11-04 01:25:06 +01:00 
			
		
		
		
	Refactor auth module
Separate parts Add user repository Add user schema
This commit is contained in:
		cmd/cc-backend
internal
api
auth
graph
repository
routerConfig
pkg/schema
web
@@ -228,14 +228,16 @@ func main() {
 | 
			
		||||
				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], ","),
 | 
			
		||||
			}); err != nil {
 | 
			
		||||
				log.Fatalf("adding '%s' user authentication failed: %v", parts[0], err)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		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)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
@@ -252,12 +254,13 @@ func main() {
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if flagGenJWT != "" {
 | 
			
		||||
			user, err := authentication.GetUser(flagGenJWT)
 | 
			
		||||
			ur := repository.GetUserRepository()
 | 
			
		||||
			user, err := ur.GetUser(flagGenJWT)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				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)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
@@ -327,15 +330,15 @@ func main() {
 | 
			
		||||
 | 
			
		||||
	r.HandleFunc("/login", func(rw http.ResponseWriter, r *http.Request) {
 | 
			
		||||
		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)
 | 
			
		||||
	r.HandleFunc("/imprint", func(rw http.ResponseWriter, r *http.Request) {
 | 
			
		||||
		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) {
 | 
			
		||||
		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.
 | 
			
		||||
@@ -351,7 +354,7 @@ func main() {
 | 
			
		||||
			func(rw http.ResponseWriter, r *http.Request, err error) {
 | 
			
		||||
				rw.Header().Add("Content-Type", "text/html; charset=utf-8")
 | 
			
		||||
				rw.WriteHeader(http.StatusUnauthorized)
 | 
			
		||||
				web.RenderTemplate(rw, r, "login.tmpl", &web.Page{
 | 
			
		||||
				web.RenderTemplate(rw, "login.tmpl", &web.Page{
 | 
			
		||||
					Title:   "Login failed - ClusterCockpit",
 | 
			
		||||
					MsgType: "alert-warning",
 | 
			
		||||
					Message: err.Error(),
 | 
			
		||||
@@ -362,7 +365,7 @@ func main() {
 | 
			
		||||
		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.WriteHeader(http.StatusOK)
 | 
			
		||||
			web.RenderTemplate(rw, r, "login.tmpl", &web.Page{
 | 
			
		||||
			web.RenderTemplate(rw, "login.tmpl", &web.Page{
 | 
			
		||||
				Title:   "Bye - ClusterCockpit",
 | 
			
		||||
				MsgType: "alert-info",
 | 
			
		||||
				Message: "Logout successful",
 | 
			
		||||
@@ -378,7 +381,7 @@ func main() {
 | 
			
		||||
				// On failure:
 | 
			
		||||
				func(rw http.ResponseWriter, r *http.Request, err error) {
 | 
			
		||||
					rw.WriteHeader(http.StatusUnauthorized)
 | 
			
		||||
					web.RenderTemplate(rw, r, "login.tmpl", &web.Page{
 | 
			
		||||
					web.RenderTemplate(rw, "login.tmpl", &web.Page{
 | 
			
		||||
						Title:   "Authentication failed - ClusterCockpit",
 | 
			
		||||
						MsgType: "alert-danger",
 | 
			
		||||
						Message: err.Error(),
 | 
			
		||||
 
 | 
			
		||||
@@ -182,12 +182,12 @@ func decode(r io.Reader, val interface{}) error {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func securedCheck(r *http.Request) error {
 | 
			
		||||
	user := auth.GetUser(r.Context())
 | 
			
		||||
	user := repository.GetUserFromContext(r.Context())
 | 
			
		||||
	if user == nil {
 | 
			
		||||
		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 config.Keys.ApiAllowedIPs == nil || len(config.Keys.ApiAllowedIPs) == 0 {
 | 
			
		||||
			return fmt.Errorf("missing configuration key ApiAllowedIPs")
 | 
			
		||||
@@ -232,8 +232,10 @@ func securedCheck(r *http.Request) error {
 | 
			
		||||
// @router      /jobs/ [get]
 | 
			
		||||
func (api *RestApi) getJobs(rw http.ResponseWriter, r *http.Request) {
 | 
			
		||||
 | 
			
		||||
	if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) {
 | 
			
		||||
		handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw)
 | 
			
		||||
	if user := repository.GetUserFromContext(r.Context()); user != nil &&
 | 
			
		||||
		!user.HasRole(schema.RoleApi) {
 | 
			
		||||
 | 
			
		||||
		handleError(fmt.Errorf("missing role: %v", schema.GetRoleString(schema.RoleApi)), http.StatusForbidden, rw)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -374,9 +376,11 @@ func (api *RestApi) getJobs(rw http.ResponseWriter, r *http.Request) {
 | 
			
		||||
// @security    ApiKeyAuth
 | 
			
		||||
// @router      /jobs/{id} [post]
 | 
			
		||||
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",
 | 
			
		||||
			auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw)
 | 
			
		||||
			schema.GetRoleString(schema.RoleApi)), http.StatusForbidden, rw)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -465,8 +469,10 @@ func (api *RestApi) getJobById(rw http.ResponseWriter, r *http.Request) {
 | 
			
		||||
// @security    ApiKeyAuth
 | 
			
		||||
// @router      /jobs/tag_job/{id} [post]
 | 
			
		||||
func (api *RestApi) tagJob(rw http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) {
 | 
			
		||||
		handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw)
 | 
			
		||||
	if user := repository.GetUserFromContext(r.Context()); user != nil &&
 | 
			
		||||
		!user.HasRole(schema.RoleApi) {
 | 
			
		||||
 | 
			
		||||
		handleError(fmt.Errorf("missing role: %v", schema.GetRoleString(schema.RoleApi)), http.StatusForbidden, rw)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -530,8 +536,10 @@ func (api *RestApi) tagJob(rw http.ResponseWriter, r *http.Request) {
 | 
			
		||||
// @security    ApiKeyAuth
 | 
			
		||||
// @router      /jobs/start_job/ [post]
 | 
			
		||||
func (api *RestApi) startJob(rw http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) {
 | 
			
		||||
		handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw)
 | 
			
		||||
	if user := repository.GetUserFromContext(r.Context()); user != nil &&
 | 
			
		||||
		!user.HasRole(schema.RoleApi) {
 | 
			
		||||
 | 
			
		||||
		handleError(fmt.Errorf("missing role: %v", schema.GetRoleString(schema.RoleApi)), http.StatusForbidden, rw)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -611,8 +619,10 @@ func (api *RestApi) startJob(rw http.ResponseWriter, r *http.Request) {
 | 
			
		||||
// @security    ApiKeyAuth
 | 
			
		||||
// @router      /jobs/stop_job/{id} [post]
 | 
			
		||||
func (api *RestApi) stopJobById(rw http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) {
 | 
			
		||||
		handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw)
 | 
			
		||||
	if user := repository.GetUserFromContext(r.Context()); user != nil &&
 | 
			
		||||
		!user.HasRole(schema.RoleApi) {
 | 
			
		||||
 | 
			
		||||
		handleError(fmt.Errorf("missing role: %v", schema.GetRoleString(schema.RoleApi)), http.StatusForbidden, rw)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -664,8 +674,10 @@ func (api *RestApi) stopJobById(rw http.ResponseWriter, r *http.Request) {
 | 
			
		||||
// @security    ApiKeyAuth
 | 
			
		||||
// @router      /jobs/stop_job/ [post]
 | 
			
		||||
func (api *RestApi) stopJobByRequest(rw http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) {
 | 
			
		||||
		handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw)
 | 
			
		||||
	if user := repository.GetUserFromContext(r.Context()); user != nil &&
 | 
			
		||||
		!user.HasRole(schema.RoleApi) {
 | 
			
		||||
 | 
			
		||||
		handleError(fmt.Errorf("missing role: %v", schema.GetRoleString(schema.RoleApi)), http.StatusForbidden, rw)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -710,8 +722,8 @@ func (api *RestApi) stopJobByRequest(rw http.ResponseWriter, r *http.Request) {
 | 
			
		||||
// @security    ApiKeyAuth
 | 
			
		||||
// @router      /jobs/delete_job/{id} [delete]
 | 
			
		||||
func (api *RestApi) deleteJobById(rw http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) {
 | 
			
		||||
		handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw)
 | 
			
		||||
	if user := repository.GetUserFromContext(r.Context()); user != nil && !user.HasRole(schema.RoleApi) {
 | 
			
		||||
		handleError(fmt.Errorf("missing role: %v", schema.GetRoleString(schema.RoleApi)), http.StatusForbidden, rw)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -758,8 +770,9 @@ func (api *RestApi) deleteJobById(rw http.ResponseWriter, r *http.Request) {
 | 
			
		||||
// @security    ApiKeyAuth
 | 
			
		||||
// @router      /jobs/delete_job/ [delete]
 | 
			
		||||
func (api *RestApi) deleteJobByRequest(rw http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) {
 | 
			
		||||
		handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw)
 | 
			
		||||
	if user := repository.GetUserFromContext(r.Context()); user != nil &&
 | 
			
		||||
		!user.HasRole(schema.RoleApi) {
 | 
			
		||||
		handleError(fmt.Errorf("missing role: %v", schema.GetRoleString(schema.RoleApi)), http.StatusForbidden, rw)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -814,8 +827,8 @@ func (api *RestApi) deleteJobByRequest(rw http.ResponseWriter, r *http.Request)
 | 
			
		||||
// @security    ApiKeyAuth
 | 
			
		||||
// @router      /jobs/delete_job_before/{ts} [delete]
 | 
			
		||||
func (api *RestApi) deleteJobBefore(rw http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) {
 | 
			
		||||
		handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw)
 | 
			
		||||
	if user := repository.GetUserFromContext(r.Context()); user != nil && !user.HasRole(schema.RoleApi) {
 | 
			
		||||
		handleError(fmt.Errorf("missing role: %v", schema.GetRoleString(schema.RoleApi)), http.StatusForbidden, rw)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -938,8 +951,8 @@ func (api *RestApi) getJWT(rw http.ResponseWriter, r *http.Request) {
 | 
			
		||||
 | 
			
		||||
	rw.Header().Set("Content-Type", "text/plain")
 | 
			
		||||
	username := r.FormValue("username")
 | 
			
		||||
	me := auth.GetUser(r.Context())
 | 
			
		||||
	if !me.HasRole(auth.RoleAdmin) {
 | 
			
		||||
	me := repository.GetUserFromContext(r.Context())
 | 
			
		||||
	if !me.HasRole(schema.RoleAdmin) {
 | 
			
		||||
		if username != me.Username {
 | 
			
		||||
			http.Error(rw, "Only admins are allowed to sign JWTs not for themselves",
 | 
			
		||||
				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 {
 | 
			
		||||
		http.Error(rw, err.Error(), http.StatusUnprocessableEntity)
 | 
			
		||||
		return
 | 
			
		||||
@@ -970,8 +983,8 @@ func (api *RestApi) createUser(rw http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rw.Header().Set("Content-Type", "text/plain")
 | 
			
		||||
	me := auth.GetUser(r.Context())
 | 
			
		||||
	if !me.HasRole(auth.RoleAdmin) {
 | 
			
		||||
	me := repository.GetUserFromContext(r.Context())
 | 
			
		||||
	if !me.HasRole(schema.RoleAdmin) {
 | 
			
		||||
		http.Error(rw, "Only admins are allowed to create new users", http.StatusForbidden)
 | 
			
		||||
		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("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)
 | 
			
		||||
		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.StatusBadRequest)
 | 
			
		||||
		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.StatusBadRequest)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := api.Authentication.AddUser(&auth.User{
 | 
			
		||||
	if err := repository.GetUserRepository().AddUser(&schema.User{
 | 
			
		||||
		Username: username,
 | 
			
		||||
		Name:     name,
 | 
			
		||||
		Password: password,
 | 
			
		||||
@@ -1015,13 +1028,13 @@ func (api *RestApi) deleteUser(rw http.ResponseWriter, r *http.Request) {
 | 
			
		||||
		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)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
@@ -1035,12 +1048,12 @@ func (api *RestApi) getUsers(rw http.ResponseWriter, r *http.Request) {
 | 
			
		||||
		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)
 | 
			
		||||
		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 {
 | 
			
		||||
		http.Error(rw, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
		return
 | 
			
		||||
@@ -1055,13 +1068,13 @@ func (api *RestApi) getRoles(rw http.ResponseWriter, r *http.Request) {
 | 
			
		||||
		http.Error(rw, err.Error(), http.StatusForbidden)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	user := auth.GetUser(r.Context())
 | 
			
		||||
	if !user.HasRole(auth.RoleAdmin) {
 | 
			
		||||
	user := repository.GetUserFromContext(r.Context())
 | 
			
		||||
	if !user.HasRole(schema.RoleAdmin) {
 | 
			
		||||
		http.Error(rw, "only admins are allowed to fetch a list of roles", http.StatusForbidden)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	roles, err := auth.GetValidRoles(user)
 | 
			
		||||
	roles, err := schema.GetValidRoles(user)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		http.Error(rw, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
		return
 | 
			
		||||
@@ -1076,7 +1089,7 @@ func (api *RestApi) updateUser(rw http.ResponseWriter, r *http.Request) {
 | 
			
		||||
		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)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
@@ -1089,25 +1102,25 @@ func (api *RestApi) updateUser(rw http.ResponseWriter, r *http.Request) {
 | 
			
		||||
 | 
			
		||||
	// TODO: Handle anything but roles...
 | 
			
		||||
	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)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		rw.Write([]byte("Add Role Success"))
 | 
			
		||||
	} 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)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		rw.Write([]byte("Remove Role Success"))
 | 
			
		||||
	} 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)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		rw.Write([]byte("Add Project Success"))
 | 
			
		||||
	} 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)
 | 
			
		||||
			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)
 | 
			
		||||
 | 
			
		||||
	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)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -14,65 +14,19 @@ import (
 | 
			
		||||
	"os"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/ClusterCockpit/cc-backend/internal/repository"
 | 
			
		||||
	"github.com/ClusterCockpit/cc-backend/pkg/log"
 | 
			
		||||
	"github.com/ClusterCockpit/cc-backend/pkg/schema"
 | 
			
		||||
	"github.com/gorilla/sessions"
 | 
			
		||||
	"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 {
 | 
			
		||||
	Init(auth *Authentication, config interface{}) error
 | 
			
		||||
	CanLogin(user *User, username string, rw http.ResponseWriter, r *http.Request) bool
 | 
			
		||||
	Login(user *User, rw http.ResponseWriter, r *http.Request) (*User, error)
 | 
			
		||||
	CanLogin(user *schema.User, username string, rw http.ResponseWriter, r *http.Request) bool
 | 
			
		||||
	Login(user *schema.User, rw http.ResponseWriter, r *http.Request) (*schema.User, error)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ContextKey string
 | 
			
		||||
 | 
			
		||||
const ContextUserKey ContextKey = "user"
 | 
			
		||||
 | 
			
		||||
type Authentication struct {
 | 
			
		||||
	db            *sqlx.DB
 | 
			
		||||
	sessionStore  *sessions.CookieStore
 | 
			
		||||
@@ -86,7 +40,7 @@ type Authentication struct {
 | 
			
		||||
 | 
			
		||||
func (auth *Authentication) AuthViaSession(
 | 
			
		||||
	rw http.ResponseWriter,
 | 
			
		||||
	r *http.Request) (*User, error) {
 | 
			
		||||
	r *http.Request) (*schema.User, error) {
 | 
			
		||||
	session, err := auth.sessionStore.Get(r, "session")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error("Error while getting session store")
 | 
			
		||||
@@ -119,11 +73,11 @@ func (auth *Authentication) AuthViaSession(
 | 
			
		||||
	username, _ := session.Values["username"].(string)
 | 
			
		||||
	projects, _ := session.Values["projects"].([]string)
 | 
			
		||||
	roles, _ := session.Values["roles"].([]string)
 | 
			
		||||
	return &User{
 | 
			
		||||
	return &schema.User{
 | 
			
		||||
		Username:   username,
 | 
			
		||||
		Projects:   projects,
 | 
			
		||||
		Roles:      roles,
 | 
			
		||||
		AuthType:   AuthSession,
 | 
			
		||||
		AuthType:   schema.AuthSession,
 | 
			
		||||
		AuthSource: -1,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
@@ -196,12 +150,13 @@ func (auth *Authentication) Login(
 | 
			
		||||
	onfailure func(rw http.ResponseWriter, r *http.Request, loginErr error)) http.Handler {
 | 
			
		||||
 | 
			
		||||
	return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
 | 
			
		||||
		ur := repository.GetUserRepository()
 | 
			
		||||
		err := errors.New("no authenticator applied")
 | 
			
		||||
		username := r.FormValue("username")
 | 
			
		||||
		dbUser := (*User)(nil)
 | 
			
		||||
		dbUser := (*schema.User)(nil)
 | 
			
		||||
 | 
			
		||||
		if username != "" {
 | 
			
		||||
			dbUser, err = auth.GetUser(username)
 | 
			
		||||
			dbUser, err = ur.GetUser(username)
 | 
			
		||||
			if err != nil && err != sql.ErrNoRows {
 | 
			
		||||
				log.Errorf("Error while loading user '%v'", username)
 | 
			
		||||
			}
 | 
			
		||||
@@ -211,7 +166,7 @@ func (auth *Authentication) Login(
 | 
			
		||||
			if !authenticator.CanLogin(dbUser, username, rw, r) {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			dbUser, err = auth.GetUser(username)
 | 
			
		||||
			dbUser, err = ur.GetUser(username)
 | 
			
		||||
			if err != nil && err != sql.ErrNoRows {
 | 
			
		||||
				log.Errorf("Error while loading user '%v'", username)
 | 
			
		||||
			}
 | 
			
		||||
@@ -243,7 +198,7 @@ func (auth *Authentication) Login(
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if dbUser == nil {
 | 
			
		||||
				if err := auth.AddUser(user); err != nil {
 | 
			
		||||
				if err := ur.AddUser(user); err != nil {
 | 
			
		||||
					// TODO Add AuthSource
 | 
			
		||||
					log.Errorf("Error while adding user '%v' to auth from XX",
 | 
			
		||||
						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)
 | 
			
		||||
			ctx := context.WithValue(r.Context(), ContextUserKey, user)
 | 
			
		||||
			ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
 | 
			
		||||
			onsuccess.ServeHTTP(rw, r.WithContext(ctx))
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
@@ -284,7 +239,7 @@ func (auth *Authentication) Auth(
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if user != nil {
 | 
			
		||||
			ctx := context.WithValue(r.Context(), ContextUserKey, user)
 | 
			
		||||
			ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
 | 
			
		||||
			onsuccess.ServeHTTP(rw, r.WithContext(ctx))
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
@@ -13,22 +13,19 @@ import (
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/ClusterCockpit/cc-backend/internal/repository"
 | 
			
		||||
	"github.com/ClusterCockpit/cc-backend/pkg/log"
 | 
			
		||||
	"github.com/ClusterCockpit/cc-backend/pkg/schema"
 | 
			
		||||
	"github.com/golang-jwt/jwt/v4"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type JWTAuthenticator struct {
 | 
			
		||||
	auth *Authentication
 | 
			
		||||
 | 
			
		||||
	publicKey  ed25519.PublicKey
 | 
			
		||||
	privateKey ed25519.PrivateKey
 | 
			
		||||
	config     *schema.JWTAuthConfig
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ja *JWTAuthenticator) Init(auth *Authentication, conf interface{}) error {
 | 
			
		||||
 | 
			
		||||
	ja.auth = auth
 | 
			
		||||
	ja.config = conf.(*schema.JWTAuthConfig)
 | 
			
		||||
 | 
			
		||||
	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(
 | 
			
		||||
	rw http.ResponseWriter,
 | 
			
		||||
	r *http.Request) (*User, error) {
 | 
			
		||||
	r *http.Request) (*schema.User, error) {
 | 
			
		||||
 | 
			
		||||
	rawtoken := r.Header.Get("X-Auth-Token")
 | 
			
		||||
	if rawtoken == "" {
 | 
			
		||||
@@ -90,8 +87,9 @@ func (ja *JWTAuthenticator) AuthViaJWT(
 | 
			
		||||
	var roles []string
 | 
			
		||||
 | 
			
		||||
	// Validate user + roles from JWT against database?
 | 
			
		||||
	if ja.config != nil && ja.config.ForceJWTValidationViaDatabase {
 | 
			
		||||
		user, err := ja.auth.GetUser(sub)
 | 
			
		||||
	if ja.config != nil && ja.config.ValidateUser {
 | 
			
		||||
		ur := repository.GetUserRepository()
 | 
			
		||||
		user, err := ur.GetUser(sub)
 | 
			
		||||
 | 
			
		||||
		// Deny any logins for unknown usernames
 | 
			
		||||
		if err != nil {
 | 
			
		||||
@@ -111,16 +109,16 @@ func (ja *JWTAuthenticator) AuthViaJWT(
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &User{
 | 
			
		||||
	return &schema.User{
 | 
			
		||||
		Username:   sub,
 | 
			
		||||
		Roles:      roles,
 | 
			
		||||
		AuthType:   AuthToken,
 | 
			
		||||
		AuthType:   schema.AuthToken,
 | 
			
		||||
		AuthSource: -1,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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 {
 | 
			
		||||
		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)")
 | 
			
		||||
			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!")
 | 
			
		||||
		}
 | 
			
		||||
		if ja.config.TrustedExternalIssuer == "" {
 | 
			
		||||
		if ja.config.TrustedIssuer == "" {
 | 
			
		||||
			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)")
 | 
			
		||||
		}
 | 
			
		||||
@@ -89,7 +89,7 @@ func (ja *JWTCookieSessionAuthenticator) Init(auth *Authentication, conf interfa
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ja *JWTCookieSessionAuthenticator) CanLogin(
 | 
			
		||||
	user *User,
 | 
			
		||||
	user *schema.User,
 | 
			
		||||
	username string,
 | 
			
		||||
	rw http.ResponseWriter,
 | 
			
		||||
	r *http.Request) bool {
 | 
			
		||||
@@ -112,9 +112,9 @@ func (ja *JWTCookieSessionAuthenticator) CanLogin(
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ja *JWTCookieSessionAuthenticator) Login(
 | 
			
		||||
	user *User,
 | 
			
		||||
	user *schema.User,
 | 
			
		||||
	rw http.ResponseWriter,
 | 
			
		||||
	r *http.Request) (*User, error) {
 | 
			
		||||
	r *http.Request) (*schema.User, error) {
 | 
			
		||||
 | 
			
		||||
	jwtCookie, err := r.Cookie(ja.config.CookieName)
 | 
			
		||||
	var rawtoken string
 | 
			
		||||
@@ -129,7 +129,7 @@ func (ja *JWTCookieSessionAuthenticator) Login(
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		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,
 | 
			
		||||
			// use public cross login key from config
 | 
			
		||||
			return ja.publicKeyCrossLogin, nil
 | 
			
		||||
@@ -160,7 +160,7 @@ func (ja *JWTCookieSessionAuthenticator) Login(
 | 
			
		||||
 | 
			
		||||
	var roles []string
 | 
			
		||||
 | 
			
		||||
	if ja.config.ForceJWTValidationViaDatabase {
 | 
			
		||||
	if ja.config.ValidateUser {
 | 
			
		||||
		// Deny any logins for unknown usernames
 | 
			
		||||
		if user == nil {
 | 
			
		||||
			log.Warn("Could not find user from JWT in internal database.")
 | 
			
		||||
@@ -191,12 +191,12 @@ func (ja *JWTCookieSessionAuthenticator) Login(
 | 
			
		||||
	http.SetCookie(rw, deletedCookie)
 | 
			
		||||
 | 
			
		||||
	if user == nil {
 | 
			
		||||
		user = &User{
 | 
			
		||||
		user = &schema.User{
 | 
			
		||||
			Username:   sub,
 | 
			
		||||
			Name:       name,
 | 
			
		||||
			Roles:      roles,
 | 
			
		||||
			AuthType:   AuthSession,
 | 
			
		||||
			AuthSource: AuthViaToken,
 | 
			
		||||
			AuthType:   schema.AuthSession,
 | 
			
		||||
			AuthSource: schema.AuthViaToken,
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -12,21 +12,17 @@ import (
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/ClusterCockpit/cc-backend/pkg/log"
 | 
			
		||||
	"github.com/ClusterCockpit/cc-backend/pkg/schema"
 | 
			
		||||
	"github.com/golang-jwt/jwt/v4"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type JWTSessionAuthenticator struct {
 | 
			
		||||
	auth *Authentication
 | 
			
		||||
 | 
			
		||||
	loginTokenKey []byte // HS256 key
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var _ Authenticator = (*JWTSessionAuthenticator)(nil)
 | 
			
		||||
 | 
			
		||||
func (ja *JWTSessionAuthenticator) Init(auth *Authentication, conf interface{}) error {
 | 
			
		||||
 | 
			
		||||
	ja.auth = auth
 | 
			
		||||
 | 
			
		||||
	if pubKey := os.Getenv("CROSS_LOGIN_JWT_HS512_KEY"); pubKey != "" {
 | 
			
		||||
		bytes, err := base64.StdEncoding.DecodeString(pubKey)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
@@ -40,7 +36,7 @@ func (ja *JWTSessionAuthenticator) Init(auth *Authentication, conf interface{})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ja *JWTSessionAuthenticator) CanLogin(
 | 
			
		||||
	user *User,
 | 
			
		||||
	user *schema.User,
 | 
			
		||||
	username string,
 | 
			
		||||
	rw http.ResponseWriter,
 | 
			
		||||
	r *http.Request) bool {
 | 
			
		||||
@@ -49,9 +45,9 @@ func (ja *JWTSessionAuthenticator) CanLogin(
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ja *JWTSessionAuthenticator) Login(
 | 
			
		||||
	user *User,
 | 
			
		||||
	user *schema.User,
 | 
			
		||||
	rw http.ResponseWriter,
 | 
			
		||||
	r *http.Request) (*User, error) {
 | 
			
		||||
	r *http.Request) (*schema.User, error) {
 | 
			
		||||
 | 
			
		||||
	rawtoken := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
 | 
			
		||||
	if rawtoken == "" {
 | 
			
		||||
@@ -92,14 +88,14 @@ func (ja *JWTSessionAuthenticator) Login(
 | 
			
		||||
	if rawroles, ok := claims["roles"].([]interface{}); ok {
 | 
			
		||||
		for _, rr := range rawroles {
 | 
			
		||||
			if r, ok := rr.(string); ok {
 | 
			
		||||
				if isValidRole(r) {
 | 
			
		||||
				if schema.IsValidRole(r) {
 | 
			
		||||
					roles = append(roles, r)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	} else if rawroles, ok := claims["roles"]; ok {
 | 
			
		||||
		for _, r := range rawroles.([]string) {
 | 
			
		||||
			if isValidRole(r) {
 | 
			
		||||
			if schema.IsValidRole(r) {
 | 
			
		||||
				roles = append(roles, r)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
@@ -120,13 +116,13 @@ func (ja *JWTSessionAuthenticator) Login(
 | 
			
		||||
	// }
 | 
			
		||||
 | 
			
		||||
	if user == nil {
 | 
			
		||||
		user = &User{
 | 
			
		||||
		user = &schema.User{
 | 
			
		||||
			Username:   sub,
 | 
			
		||||
			Name:       name,
 | 
			
		||||
			Roles:      roles,
 | 
			
		||||
			Projects:   projects,
 | 
			
		||||
			AuthType:   AuthSession,
 | 
			
		||||
			AuthSource: AuthViaToken,
 | 
			
		||||
			AuthType:   schema.AuthSession,
 | 
			
		||||
			AuthSource: schema.AuthViaToken,
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -67,12 +67,12 @@ func (la *LdapAuthenticator) Init(
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (la *LdapAuthenticator) CanLogin(
 | 
			
		||||
	user *User,
 | 
			
		||||
	user *schema.User,
 | 
			
		||||
	username string,
 | 
			
		||||
	rw http.ResponseWriter,
 | 
			
		||||
	r *http.Request) bool {
 | 
			
		||||
 | 
			
		||||
	if user != nil && user.AuthSource == AuthViaLDAP {
 | 
			
		||||
	if user != nil && user.AuthSource == schema.AuthViaLDAP {
 | 
			
		||||
		return true
 | 
			
		||||
	} else {
 | 
			
		||||
		if la.config != nil && la.config.SyncUserOnLogin {
 | 
			
		||||
@@ -103,7 +103,7 @@ func (la *LdapAuthenticator) CanLogin(
 | 
			
		||||
			name := entry.GetAttributeValue("gecos")
 | 
			
		||||
 | 
			
		||||
			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)
 | 
			
		||||
				return false
 | 
			
		||||
			}
 | 
			
		||||
@@ -116,9 +116,9 @@ func (la *LdapAuthenticator) CanLogin(
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (la *LdapAuthenticator) Login(
 | 
			
		||||
	user *User,
 | 
			
		||||
	user *schema.User,
 | 
			
		||||
	rw http.ResponseWriter,
 | 
			
		||||
	r *http.Request) (*User, error) {
 | 
			
		||||
	r *http.Request) (*schema.User, error) {
 | 
			
		||||
 | 
			
		||||
	l, err := la.getLdapConnection(false)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@@ -203,7 +203,7 @@ func (la *LdapAuthenticator) Sync() error {
 | 
			
		||||
			name := newnames[username]
 | 
			
		||||
			log.Debugf("sync: add %v (name: %v, roles: [user], ldap: true)", username, name)
 | 
			
		||||
			if _, err := la.auth.db.Exec(`INSERT INTO user (username, ldap, name, roles) VALUES (?, ?, ?, ?)`,
 | 
			
		||||
				username, 1, name, "[\""+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)
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,7 @@ import (
 | 
			
		||||
	"net/http"
 | 
			
		||||
 | 
			
		||||
	"github.com/ClusterCockpit/cc-backend/pkg/log"
 | 
			
		||||
	"github.com/ClusterCockpit/cc-backend/pkg/schema"
 | 
			
		||||
	"golang.org/x/crypto/bcrypt"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -27,18 +28,18 @@ func (la *LocalAuthenticator) Init(
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (la *LocalAuthenticator) CanLogin(
 | 
			
		||||
	user *User,
 | 
			
		||||
	user *schema.User,
 | 
			
		||||
	username string,
 | 
			
		||||
	rw http.ResponseWriter,
 | 
			
		||||
	r *http.Request) bool {
 | 
			
		||||
 | 
			
		||||
	return user != nil && user.AuthSource == AuthViaLocalPassword
 | 
			
		||||
	return user != nil && user.AuthSource == schema.AuthViaLocalPassword
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (la *LocalAuthenticator) Login(
 | 
			
		||||
	user *User,
 | 
			
		||||
	user *schema.User,
 | 
			
		||||
	rw http.ResponseWriter,
 | 
			
		||||
	r *http.Request) (*User, error) {
 | 
			
		||||
	r *http.Request) (*schema.User, error) {
 | 
			
		||||
 | 
			
		||||
	if e := bcrypt.CompareHashAndPassword([]byte(user.Password),
 | 
			
		||||
		[]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"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/ClusterCockpit/cc-backend/internal/auth"
 | 
			
		||||
	"github.com/ClusterCockpit/cc-backend/internal/graph/generated"
 | 
			
		||||
	"github.com/ClusterCockpit/cc-backend/internal/graph/model"
 | 
			
		||||
	"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.
 | 
			
		||||
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.
 | 
			
		||||
@@ -122,7 +121,7 @@ func (r *mutationResolver) RemoveTagsFromJob(ctx context.Context, job string, ta
 | 
			
		||||
 | 
			
		||||
// UpdateConfiguration is the resolver for the updateConfiguration field.
 | 
			
		||||
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")
 | 
			
		||||
		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.
 | 
			
		||||
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.
 | 
			
		||||
@@ -178,7 +177,9 @@ func (r *queryResolver) Job(ctx context.Context, id string) (*schema.Job, error)
 | 
			
		||||
		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")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -318,8 +319,8 @@ func (r *queryResolver) RooflineHeatmap(ctx context.Context, filter []*model.Job
 | 
			
		||||
 | 
			
		||||
// 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) {
 | 
			
		||||
	user := auth.GetUser(ctx)
 | 
			
		||||
	if user != nil && !user.HasRole(auth.RoleAdmin) {
 | 
			
		||||
	user := repository.GetUserFromContext(ctx)
 | 
			
		||||
	if user != nil && !user.HasRole(schema.RoleAdmin) {
 | 
			
		||||
		return nil, errors.New("you need to be an administrator for this query")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,6 @@ import (
 | 
			
		||||
	"sync"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/ClusterCockpit/cc-backend/internal/auth"
 | 
			
		||||
	"github.com/ClusterCockpit/cc-backend/internal/graph/model"
 | 
			
		||||
	"github.com/ClusterCockpit/cc-backend/internal/metricdata"
 | 
			
		||||
	"github.com/ClusterCockpit/cc-backend/pkg/log"
 | 
			
		||||
@@ -615,7 +614,7 @@ func (r *JobRepository) WaitForArchiving() {
 | 
			
		||||
	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
 | 
			
		||||
		return searchterm, "", "", ""
 | 
			
		||||
	} 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 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 := " = ?"
 | 
			
		||||
	query := searchterm
 | 
			
		||||
	if isLike {
 | 
			
		||||
		compareStr = " LIKE ?"
 | 
			
		||||
		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).
 | 
			
		||||
			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)
 | 
			
		||||
	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).
 | 
			
		||||
			Where(table+"."+whereColumn+" LIKE ?", fmt.Sprint("%", query, "%")).
 | 
			
		||||
			RunWith(r.stmtCache).Query()
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,6 @@ import (
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/ClusterCockpit/cc-backend/internal/auth"
 | 
			
		||||
	"github.com/ClusterCockpit/cc-backend/internal/graph/model"
 | 
			
		||||
	"github.com/ClusterCockpit/cc-backend/pkg/log"
 | 
			
		||||
	"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) {
 | 
			
		||||
	user := auth.GetUser(ctx)
 | 
			
		||||
	user := GetUserFromContext(ctx)
 | 
			
		||||
	if user == nil {
 | 
			
		||||
		var qnil sq.SelectBuilder
 | 
			
		||||
		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
 | 
			
		||||
	} 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 {
 | 
			
		||||
			return query.Where(sq.Or{sq.Eq{"job.project": user.Projects}, sq.Eq{"job.user": user.Username}}), nil
 | 
			
		||||
		} else {
 | 
			
		||||
			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
 | 
			
		||||
		}
 | 
			
		||||
	} 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
 | 
			
		||||
	} else {
 | 
			
		||||
		// Shortterm compatibility: Return User-Query if no roles:
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,6 @@ import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/ClusterCockpit/cc-backend/internal/auth"
 | 
			
		||||
	"github.com/ClusterCockpit/cc-backend/internal/config"
 | 
			
		||||
	"github.com/ClusterCockpit/cc-backend/internal/graph/model"
 | 
			
		||||
	"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 {
 | 
			
		||||
	user := auth.GetUser(ctx)
 | 
			
		||||
	user := GetUserFromContext(ctx)
 | 
			
		||||
	name, _ := r.FindColumnValue(user, id, "user", "name", "username", false)
 | 
			
		||||
	if name != "" {
 | 
			
		||||
		return name
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,6 @@ package repository
 | 
			
		||||
import (
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/ClusterCockpit/cc-backend/internal/auth"
 | 
			
		||||
	"github.com/ClusterCockpit/cc-backend/pkg/archive"
 | 
			
		||||
	"github.com/ClusterCockpit/cc-backend/pkg/log"
 | 
			
		||||
	"github.com/ClusterCockpit/cc-backend/pkg/schema"
 | 
			
		||||
@@ -68,7 +67,7 @@ func (r *JobRepository) CreateTag(tagType string, tagName string) (tagId int64,
 | 
			
		||||
	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)
 | 
			
		||||
	xrows, err := r.DB.Queryx("SELECT id, tag_type, tag_name FROM tag")
 | 
			
		||||
	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").
 | 
			
		||||
		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")
 | 
			
		||||
		// 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
 | 
			
		||||
		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
 | 
			
		||||
 
 | 
			
		||||
@@ -1,137 +1,325 @@
 | 
			
		||||
// Copyright (C) 2022 NHR@FAU, University Erlangen-Nuremberg.
 | 
			
		||||
// Copyright (C) 2023 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 (
 | 
			
		||||
	"context"
 | 
			
		||||
	"database/sql"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/ClusterCockpit/cc-backend/internal/auth"
 | 
			
		||||
	"github.com/ClusterCockpit/cc-backend/internal/config"
 | 
			
		||||
	"github.com/ClusterCockpit/cc-backend/internal/graph/model"
 | 
			
		||||
	"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"
 | 
			
		||||
	"golang.org/x/crypto/bcrypt"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	userCfgRepoOnce     sync.Once
 | 
			
		||||
	userCfgRepoInstance *UserCfgRepo
 | 
			
		||||
	userRepoOnce     sync.Once
 | 
			
		||||
	userRepoInstance *UserRepository
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type UserCfgRepo struct {
 | 
			
		||||
	DB         *sqlx.DB
 | 
			
		||||
	Lookup     *sqlx.Stmt
 | 
			
		||||
	lock       sync.RWMutex
 | 
			
		||||
	uiDefaults map[string]interface{}
 | 
			
		||||
	cache      *lrucache.Cache
 | 
			
		||||
type UserRepository struct {
 | 
			
		||||
	DB     *sqlx.DB
 | 
			
		||||
	driver string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetUserCfgRepo() *UserCfgRepo {
 | 
			
		||||
	userCfgRepoOnce.Do(func() {
 | 
			
		||||
func GetUserRepository() *UserRepository {
 | 
			
		||||
	userRepoOnce.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),
 | 
			
		||||
		userRepoInstance = &UserRepository{
 | 
			
		||||
			DB:     db.DB,
 | 
			
		||||
			driver: db.Driver,
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	return userCfgRepoInstance
 | 
			
		||||
	return userRepoInstance
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Return the personalised UI config for the currently authenticated
 | 
			
		||||
// user or return the plain default config.
 | 
			
		||||
func (uCfg *UserCfgRepo) GetUIConfig(user *auth.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")
 | 
			
		||||
func (r *UserRepository) GetUser(username string) (*schema.User, error) {
 | 
			
		||||
	user := &schema.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(r.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
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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 *auth.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
 | 
			
		||||
	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
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		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 user, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *UserRepository) AddUser(user *schema.User) error {
 | 
			
		||||
	rolesJson, _ := json.Marshal(user.Roles)
 | 
			
		||||
	projectsJson, _ := json.Marshal(user.Projects)
 | 
			
		||||
 | 
			
		||||
	cols := []string{"username", "roles", "projects"}
 | 
			
		||||
	vals := []interface{}{user.Username, string(rolesJson), string(projectsJson)}
 | 
			
		||||
 | 
			
		||||
	if user.Name != "" {
 | 
			
		||||
		cols = append(cols, "name")
 | 
			
		||||
		vals = append(vals, user.Name)
 | 
			
		||||
	}
 | 
			
		||||
	if user.Email != "" {
 | 
			
		||||
		cols = append(cols, "email")
 | 
			
		||||
		vals = append(vals, user.Email)
 | 
			
		||||
	}
 | 
			
		||||
	if user.Password != "" {
 | 
			
		||||
		password, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Error("Error while encrypting new user password")
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		cols = append(cols, "password")
 | 
			
		||||
		vals = append(vals, string(password))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if _, err := sq.Insert("user").Columns(cols...).Values(vals...).RunWith(r.DB).Exec(); err != nil {
 | 
			
		||||
		log.Errorf("Error while inserting new user '%v' into DB", user.Username)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	uCfg.cache.Del(user.Username)
 | 
			
		||||
	log.Infof("new user %#v created (roles: %s, auth-source: %d, projects: %s)", user.Username, rolesJson, user.AuthSource, projectsJson)
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *UserRepository) DelUser(username string) error {
 | 
			
		||||
 | 
			
		||||
	_, err := r.DB.Exec(`DELETE FROM user WHERE user.username = ?`, username)
 | 
			
		||||
	log.Errorf("Error while deleting user '%s' from DB", username)
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *UserRepository) ListUsers(specialsOnly bool) ([]*schema.User, error) {
 | 
			
		||||
 | 
			
		||||
	q := sq.Select("username", "name", "email", "roles", "projects").From("user")
 | 
			
		||||
	if specialsOnly {
 | 
			
		||||
		q = q.Where("(roles != '[\"user\"]' AND roles != '[]')")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rows, err := q.RunWith(r.DB).Query()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Warn("Error while querying user list")
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	users := make([]*schema.User, 0)
 | 
			
		||||
	defer rows.Close()
 | 
			
		||||
	for rows.Next() {
 | 
			
		||||
		rawroles := ""
 | 
			
		||||
		rawprojects := ""
 | 
			
		||||
		user := &schema.User{}
 | 
			
		||||
		var name, email sql.NullString
 | 
			
		||||
		if err := rows.Scan(&user.Username, &name, &email, &rawroles, &rawprojects); err != nil {
 | 
			
		||||
			log.Warn("Error while scanning user list")
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err := json.Unmarshal([]byte(rawroles), &user.Roles); err != nil {
 | 
			
		||||
			log.Warn("Error while unmarshaling raw role list")
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err := json.Unmarshal([]byte(rawprojects), &user.Projects); err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		user.Name = name.String
 | 
			
		||||
		user.Email = email.String
 | 
			
		||||
		users = append(users, user)
 | 
			
		||||
	}
 | 
			
		||||
	return users, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *UserRepository) AddRole(
 | 
			
		||||
	ctx context.Context,
 | 
			
		||||
	username string,
 | 
			
		||||
	queryrole string) error {
 | 
			
		||||
 | 
			
		||||
	newRole := strings.ToLower(queryrole)
 | 
			
		||||
	user, err := r.GetUser(username)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Warnf("Could not load user '%s'", username)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	exists, valid := user.HasValidRole(newRole)
 | 
			
		||||
 | 
			
		||||
	if !valid {
 | 
			
		||||
		return fmt.Errorf("Supplied role is no valid option : %v", newRole)
 | 
			
		||||
	}
 | 
			
		||||
	if exists {
 | 
			
		||||
		return fmt.Errorf("User %v already has role %v", username, newRole)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	roles, _ := json.Marshal(append(user.Roles, newRole))
 | 
			
		||||
	if _, err := sq.Update("user").Set("roles", roles).Where("user.username = ?", username).RunWith(r.DB).Exec(); err != nil {
 | 
			
		||||
		log.Errorf("Error while adding new role for user '%s'", user.Username)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *UserRepository) RemoveRole(ctx context.Context, username string, queryrole string) error {
 | 
			
		||||
	oldRole := strings.ToLower(queryrole)
 | 
			
		||||
	user, err := r.GetUser(username)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Warnf("Could not load user '%s'", username)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	exists, valid := user.HasValidRole(oldRole)
 | 
			
		||||
 | 
			
		||||
	if !valid {
 | 
			
		||||
		return fmt.Errorf("Supplied role is no valid option : %v", oldRole)
 | 
			
		||||
	}
 | 
			
		||||
	if !exists {
 | 
			
		||||
		return fmt.Errorf("Role already deleted for user '%v': %v", username, oldRole)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if oldRole == schema.GetRoleString(schema.RoleManager) && len(user.Projects) != 0 {
 | 
			
		||||
		return fmt.Errorf("Cannot remove role 'manager' while user %s still has assigned project(s) : %v", username, user.Projects)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var newroles []string
 | 
			
		||||
	for _, r := range user.Roles {
 | 
			
		||||
		if r != oldRole {
 | 
			
		||||
			newroles = append(newroles, r) // Append all roles not matching requested to be deleted role
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var mroles, _ = json.Marshal(newroles)
 | 
			
		||||
	if _, err := sq.Update("user").Set("roles", mroles).Where("user.username = ?", username).RunWith(r.DB).Exec(); err != nil {
 | 
			
		||||
		log.Errorf("Error while removing role for user '%s'", user.Username)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *UserRepository) AddProject(
 | 
			
		||||
	ctx context.Context,
 | 
			
		||||
	username string,
 | 
			
		||||
	project string) error {
 | 
			
		||||
 | 
			
		||||
	user, err := r.GetUser(username)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !user.HasRole(schema.RoleManager) {
 | 
			
		||||
		return fmt.Errorf("user '%s' is not a manager!", username)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if user.HasProject(project) {
 | 
			
		||||
		return fmt.Errorf("user '%s' already manages project '%s'", username, project)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	projects, _ := json.Marshal(append(user.Projects, project))
 | 
			
		||||
	if _, err := sq.Update("user").Set("projects", projects).Where("user.username = ?", username).RunWith(r.DB).Exec(); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *UserRepository) RemoveProject(ctx context.Context, username string, project string) error {
 | 
			
		||||
	user, err := r.GetUser(username)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !user.HasRole(schema.RoleManager) {
 | 
			
		||||
		return fmt.Errorf("user '%#v' is not a manager!", username)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !user.HasProject(project) {
 | 
			
		||||
		return fmt.Errorf("user '%#v': Cannot remove project '%#v' - Does not match!", username, project)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var exists bool
 | 
			
		||||
	var newprojects []string
 | 
			
		||||
	for _, p := range user.Projects {
 | 
			
		||||
		if p != project {
 | 
			
		||||
			newprojects = append(newprojects, p) // Append all projects not matching requested to be deleted project
 | 
			
		||||
		} else {
 | 
			
		||||
			exists = true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if exists == true {
 | 
			
		||||
		var result interface{}
 | 
			
		||||
		if len(newprojects) == 0 {
 | 
			
		||||
			result = "[]"
 | 
			
		||||
		} else {
 | 
			
		||||
			result, _ = json.Marshal(newprojects)
 | 
			
		||||
		}
 | 
			
		||||
		if _, err := sq.Update("user").Set("projects", result).Where("user.username = ?", username).RunWith(r.DB).Exec(); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		return nil
 | 
			
		||||
	} else {
 | 
			
		||||
		return fmt.Errorf("user %s already does not manage project %s", username, project)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ContextKey string
 | 
			
		||||
 | 
			
		||||
const ContextUserKey ContextKey = "user"
 | 
			
		||||
 | 
			
		||||
func GetUserFromContext(ctx context.Context) *schema.User {
 | 
			
		||||
	x := ctx.Value(ContextUserKey)
 | 
			
		||||
	if x == nil {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return x.(*schema.User)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *UserRepository) FetchUserInCtx(ctx context.Context, username string) (*model.User, error) {
 | 
			
		||||
	me := GetUserFromContext(ctx)
 | 
			
		||||
	if me != nil && me.Username != username &&
 | 
			
		||||
		me.HasNotRoles([]schema.Role{schema.RoleAdmin, schema.RoleSupport, schema.RoleManager}) {
 | 
			
		||||
		return nil, errors.New("forbidden")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	user := &model.User{Username: username}
 | 
			
		||||
	var name, email sql.NullString
 | 
			
		||||
	if err := sq.Select("name", "email").From("user").Where("user.username = ?", username).
 | 
			
		||||
		RunWith(r.DB).QueryRow().Scan(&name, &email); err != nil {
 | 
			
		||||
		if err == sql.ErrNoRows {
 | 
			
		||||
			/* This warning will be logged *often* for non-local users, i.e. users mentioned only in job-table or archive, */
 | 
			
		||||
			/* since FetchUser will be called to retrieve full name and mail for every job in query/list									 */
 | 
			
		||||
			// log.Warnf("User '%s' Not found in DB", username)
 | 
			
		||||
			return nil, nil
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		log.Warnf("Error while fetching user '%s'", username)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	user.Name = name.String
 | 
			
		||||
	user.Email = email.String
 | 
			
		||||
	return user, nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/ClusterCockpit/cc-backend/internal/auth"
 | 
			
		||||
	"github.com/ClusterCockpit/cc-backend/internal/config"
 | 
			
		||||
	"github.com/ClusterCockpit/cc-backend/pkg/log"
 | 
			
		||||
	"github.com/ClusterCockpit/cc-backend/pkg/schema"
 | 
			
		||||
	_ "github.com/mattn/go-sqlite3"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -53,7 +53,7 @@ func setupUserTest(t *testing.T) *UserCfgRepo {
 | 
			
		||||
 | 
			
		||||
func TestGetUIConfig(t *testing.T) {
 | 
			
		||||
	r := setupUserTest(t)
 | 
			
		||||
	u := auth.User{Username: "demo"}
 | 
			
		||||
	u := schema.User{Username: "demo"}
 | 
			
		||||
 | 
			
		||||
	cfg, err := r.GetUIConfig(&u)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@@ -13,11 +13,11 @@ import (
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/ClusterCockpit/cc-backend/internal/auth"
 | 
			
		||||
	"github.com/ClusterCockpit/cc-backend/internal/graph/model"
 | 
			
		||||
	"github.com/ClusterCockpit/cc-backend/internal/repository"
 | 
			
		||||
	"github.com/ClusterCockpit/cc-backend/internal/util"
 | 
			
		||||
	"github.com/ClusterCockpit/cc-backend/pkg/log"
 | 
			
		||||
	"github.com/ClusterCockpit/cc-backend/pkg/schema"
 | 
			
		||||
	"github.com/ClusterCockpit/cc-backend/web"
 | 
			
		||||
	"github.com/gorilla/mux"
 | 
			
		||||
)
 | 
			
		||||
@@ -81,12 +81,11 @@ func setupJobRoute(i InfoType, r *http.Request) InfoType {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func setupUserRoute(i InfoType, r *http.Request) InfoType {
 | 
			
		||||
	jobRepo := repository.GetJobRepository()
 | 
			
		||||
	username := mux.Vars(r)["id"]
 | 
			
		||||
	i["id"] = username
 | 
			
		||||
	i["username"] = username
 | 
			
		||||
	// TODO: If forbidden (== err exists), redirect to error page
 | 
			
		||||
	if user, _ := auth.FetchUser(r.Context(), jobRepo.DB, username); user != nil {
 | 
			
		||||
	if user, _ := repository.GetUserRepository().FetchUserInCtx(r.Context(), username); user != nil {
 | 
			
		||||
		i["name"] = user.Name
 | 
			
		||||
		i["email"] = user.Email
 | 
			
		||||
	}
 | 
			
		||||
@@ -125,7 +124,7 @@ func setupAnalysisRoute(i InfoType, r *http.Request) InfoType {
 | 
			
		||||
 | 
			
		||||
func setupTaglistRoute(i InfoType, r *http.Request) InfoType {
 | 
			
		||||
	jobRepo := repository.GetJobRepository()
 | 
			
		||||
	user := auth.GetUser(r.Context())
 | 
			
		||||
	user := repository.GetUserFromContext(r.Context())
 | 
			
		||||
 | 
			
		||||
	tags, counts, err := jobRepo.CountTags(user)
 | 
			
		||||
	tagMap := make(map[string][]map[string]interface{})
 | 
			
		||||
@@ -255,7 +254,7 @@ func SetupRoutes(router *mux.Router, buildInfo web.Build) {
 | 
			
		||||
	for _, route := range routes {
 | 
			
		||||
		route := route
 | 
			
		||||
		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 {
 | 
			
		||||
				http.Error(rw, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
				return
 | 
			
		||||
@@ -268,9 +267,9 @@ func SetupRoutes(router *mux.Router, buildInfo web.Build) {
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Get User -> What if NIL?
 | 
			
		||||
			user := auth.GetUser(r.Context())
 | 
			
		||||
			user := repository.GetUserFromContext(r.Context())
 | 
			
		||||
			// Get Roles
 | 
			
		||||
			availableRoles, _ := auth.GetValidRolesMap(user)
 | 
			
		||||
			availableRoles, _ := schema.GetValidRolesMap(user)
 | 
			
		||||
 | 
			
		||||
			page := web.Page{
 | 
			
		||||
				Title:  title,
 | 
			
		||||
@@ -285,14 +284,14 @@ func SetupRoutes(router *mux.Router, buildInfo web.Build) {
 | 
			
		||||
				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) {
 | 
			
		||||
	user := auth.GetUser(r.Context())
 | 
			
		||||
	availableRoles, _ := auth.GetValidRolesMap(user)
 | 
			
		||||
	user := repository.GetUserFromContext(r.Context())
 | 
			
		||||
	availableRoles, _ := schema.GetValidRolesMap(user)
 | 
			
		||||
 | 
			
		||||
	if search := r.URL.Query().Get("searchId"); search != "" {
 | 
			
		||||
		repo := repository.GetJobRepository()
 | 
			
		||||
@@ -309,10 +308,10 @@ func HandleSearchBar(rw http.ResponseWriter, r *http.Request, buildInfo web.Buil
 | 
			
		||||
			case "arrayJobId":
 | 
			
		||||
				http.Redirect(rw, r, "/monitoring/jobs/?arrayJobId="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusFound) // All Users: Redirect to Tablequery
 | 
			
		||||
			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)
 | 
			
		||||
				} 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":
 | 
			
		||||
				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=")
 | 
			
		||||
					http.Redirect(rw, r, "/monitoring/users/?user="+joinedNames, http.StatusFound)
 | 
			
		||||
				} 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)
 | 
			
		||||
					} 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:
 | 
			
		||||
				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 {
 | 
			
		||||
 | 
			
		||||
@@ -342,13 +341,13 @@ func HandleSearchBar(rw http.ResponseWriter, r *http.Request, buildInfo web.Buil
 | 
			
		||||
			} else if jobname != "" {
 | 
			
		||||
				http.Redirect(rw, r, "/monitoring/jobs/?jobName="+url.QueryEscape(jobname), http.StatusFound) // JobName (contains)
 | 
			
		||||
			} 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 {
 | 
			
		||||
			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 {
 | 
			
		||||
		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,7 +17,9 @@ type LdapConfig struct {
 | 
			
		||||
	UserFilter      string `json:"user_filter"`
 | 
			
		||||
	SyncInterval    string `json:"sync_interval"` // Parsed using time.ParseDuration.
 | 
			
		||||
	SyncDelOldUsers bool   `json:"sync_del_old_users"`
 | 
			
		||||
	SyncUserOnLogin bool   `json:"syncUserOnLogin"`
 | 
			
		||||
 | 
			
		||||
	// Should an non-existent user be added to the DB if user exists in ldap directory
 | 
			
		||||
	SyncUserOnLogin bool `json:"syncUserOnLogin"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type JWTAuthConfig struct {
 | 
			
		||||
@@ -30,10 +32,13 @@ type JWTAuthConfig struct {
 | 
			
		||||
 | 
			
		||||
	// Deny login for users not in database (but defined in JWT).
 | 
			
		||||
	// 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)
 | 
			
		||||
	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 {
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@
 | 
			
		||||
// All rights reserved.
 | 
			
		||||
// Use of this source code is governed by a MIT-style
 | 
			
		||||
// license that can be found in the LICENSE file.
 | 
			
		||||
package auth
 | 
			
		||||
package schema
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
@@ -21,6 +21,41 @@ const (
 | 
			
		||||
	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 {
 | 
			
		||||
	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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (u *User) HasValidRole(role string) (hasRole bool, isValid bool) {
 | 
			
		||||
	if isValidRole(role) {
 | 
			
		||||
	if IsValidRole(role) {
 | 
			
		||||
		for _, r := range u.Roles {
 | 
			
		||||
			if r == role {
 | 
			
		||||
				return true, true
 | 
			
		||||
@@ -2,7 +2,7 @@
 | 
			
		||||
// All rights reserved.
 | 
			
		||||
// Use of this source code is governed by a MIT-style
 | 
			
		||||
// license that can be found in the LICENSE file.
 | 
			
		||||
package auth
 | 
			
		||||
package schema
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
@@ -11,7 +11,6 @@ import (
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/ClusterCockpit/cc-backend/internal/auth"
 | 
			
		||||
	"github.com/ClusterCockpit/cc-backend/internal/config"
 | 
			
		||||
	"github.com/ClusterCockpit/cc-backend/internal/util"
 | 
			
		||||
	"github.com/ClusterCockpit/cc-backend/pkg/log"
 | 
			
		||||
@@ -92,8 +91,8 @@ type Page struct {
 | 
			
		||||
	Title         string                 // Page title
 | 
			
		||||
	MsgType       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)
 | 
			
		||||
	Roles         map[string]auth.Role   // Available roles for frontend render checks
 | 
			
		||||
	User          schema.User            // Information about the currently logged in user (Full User Info)
 | 
			
		||||
	Roles         map[string]schema.Role // Available roles for frontend render checks
 | 
			
		||||
	Build         Build                  // Latest information about the application
 | 
			
		||||
	Clusters      []schema.ClusterConfig // List of all clusters for use in the Header
 | 
			
		||||
	FilterPresets map[string]interface{} // For pages with the Filter component, this can be used to set initial filters.
 | 
			
		||||
@@ -101,7 +100,7 @@ type Page struct {
 | 
			
		||||
	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]
 | 
			
		||||
	if !ok {
 | 
			
		||||
		log.Errorf("WEB/WEB > template '%s' not found", file)
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user