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:
		@@ -228,14 +228,16 @@ func main() {
 | 
				
			|||||||
				log.Fatal("invalid argument format for user creation")
 | 
									log.Fatal("invalid argument format for user creation")
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if err := authentication.AddUser(&auth.User{
 | 
								ur := repository.GetUserRepository()
 | 
				
			||||||
 | 
								if err := ur.AddUser(&schema.User{
 | 
				
			||||||
				Username: parts[0], Projects: make([]string, 0), Password: parts[2], Roles: strings.Split(parts[1], ","),
 | 
									Username: parts[0], Projects: make([]string, 0), Password: parts[2], Roles: strings.Split(parts[1], ","),
 | 
				
			||||||
			}); err != nil {
 | 
								}); err != nil {
 | 
				
			||||||
				log.Fatalf("adding '%s' user authentication failed: %v", parts[0], err)
 | 
									log.Fatalf("adding '%s' user authentication failed: %v", parts[0], err)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		if flagDelUser != "" {
 | 
							if flagDelUser != "" {
 | 
				
			||||||
			if err := authentication.DelUser(flagDelUser); err != nil {
 | 
								ur := repository.GetUserRepository()
 | 
				
			||||||
 | 
								if err := ur.DelUser(flagDelUser); err != nil {
 | 
				
			||||||
				log.Fatalf("deleting user failed: %v", err)
 | 
									log.Fatalf("deleting user failed: %v", err)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -252,12 +254,13 @@ func main() {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if flagGenJWT != "" {
 | 
							if flagGenJWT != "" {
 | 
				
			||||||
			user, err := authentication.GetUser(flagGenJWT)
 | 
								ur := repository.GetUserRepository()
 | 
				
			||||||
 | 
								user, err := ur.GetUser(flagGenJWT)
 | 
				
			||||||
			if err != nil {
 | 
								if err != nil {
 | 
				
			||||||
				log.Fatalf("could not get user from JWT: %v", err)
 | 
									log.Fatalf("could not get user from JWT: %v", err)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if !user.HasRole(auth.RoleApi) {
 | 
								if !user.HasRole(schema.RoleApi) {
 | 
				
			||||||
				log.Warnf("user '%s' does not have the API role", user.Username)
 | 
									log.Warnf("user '%s' does not have the API role", user.Username)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -327,15 +330,15 @@ func main() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	r.HandleFunc("/login", func(rw http.ResponseWriter, r *http.Request) {
 | 
						r.HandleFunc("/login", func(rw http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
		rw.Header().Add("Content-Type", "text/html; charset=utf-8")
 | 
							rw.Header().Add("Content-Type", "text/html; charset=utf-8")
 | 
				
			||||||
		web.RenderTemplate(rw, r, "login.tmpl", &web.Page{Title: "Login", Build: buildInfo})
 | 
							web.RenderTemplate(rw, "login.tmpl", &web.Page{Title: "Login", Build: buildInfo})
 | 
				
			||||||
	}).Methods(http.MethodGet)
 | 
						}).Methods(http.MethodGet)
 | 
				
			||||||
	r.HandleFunc("/imprint", func(rw http.ResponseWriter, r *http.Request) {
 | 
						r.HandleFunc("/imprint", func(rw http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
		rw.Header().Add("Content-Type", "text/html; charset=utf-8")
 | 
							rw.Header().Add("Content-Type", "text/html; charset=utf-8")
 | 
				
			||||||
		web.RenderTemplate(rw, r, "imprint.tmpl", &web.Page{Title: "Imprint", Build: buildInfo})
 | 
							web.RenderTemplate(rw, "imprint.tmpl", &web.Page{Title: "Imprint", Build: buildInfo})
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
	r.HandleFunc("/privacy", func(rw http.ResponseWriter, r *http.Request) {
 | 
						r.HandleFunc("/privacy", func(rw http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
		rw.Header().Add("Content-Type", "text/html; charset=utf-8")
 | 
							rw.Header().Add("Content-Type", "text/html; charset=utf-8")
 | 
				
			||||||
		web.RenderTemplate(rw, r, "privacy.tmpl", &web.Page{Title: "Privacy", Build: buildInfo})
 | 
							web.RenderTemplate(rw, "privacy.tmpl", &web.Page{Title: "Privacy", Build: buildInfo})
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Some routes, such as /login or /query, should only be accessible to a user that is logged in.
 | 
						// Some routes, such as /login or /query, should only be accessible to a user that is logged in.
 | 
				
			||||||
@@ -351,7 +354,7 @@ func main() {
 | 
				
			|||||||
			func(rw http.ResponseWriter, r *http.Request, err error) {
 | 
								func(rw http.ResponseWriter, r *http.Request, err error) {
 | 
				
			||||||
				rw.Header().Add("Content-Type", "text/html; charset=utf-8")
 | 
									rw.Header().Add("Content-Type", "text/html; charset=utf-8")
 | 
				
			||||||
				rw.WriteHeader(http.StatusUnauthorized)
 | 
									rw.WriteHeader(http.StatusUnauthorized)
 | 
				
			||||||
				web.RenderTemplate(rw, r, "login.tmpl", &web.Page{
 | 
									web.RenderTemplate(rw, "login.tmpl", &web.Page{
 | 
				
			||||||
					Title:   "Login failed - ClusterCockpit",
 | 
										Title:   "Login failed - ClusterCockpit",
 | 
				
			||||||
					MsgType: "alert-warning",
 | 
										MsgType: "alert-warning",
 | 
				
			||||||
					Message: err.Error(),
 | 
										Message: err.Error(),
 | 
				
			||||||
@@ -362,7 +365,7 @@ func main() {
 | 
				
			|||||||
		r.Handle("/logout", authentication.Logout(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
 | 
							r.Handle("/logout", authentication.Logout(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
			rw.Header().Add("Content-Type", "text/html; charset=utf-8")
 | 
								rw.Header().Add("Content-Type", "text/html; charset=utf-8")
 | 
				
			||||||
			rw.WriteHeader(http.StatusOK)
 | 
								rw.WriteHeader(http.StatusOK)
 | 
				
			||||||
			web.RenderTemplate(rw, r, "login.tmpl", &web.Page{
 | 
								web.RenderTemplate(rw, "login.tmpl", &web.Page{
 | 
				
			||||||
				Title:   "Bye - ClusterCockpit",
 | 
									Title:   "Bye - ClusterCockpit",
 | 
				
			||||||
				MsgType: "alert-info",
 | 
									MsgType: "alert-info",
 | 
				
			||||||
				Message: "Logout successful",
 | 
									Message: "Logout successful",
 | 
				
			||||||
@@ -378,7 +381,7 @@ func main() {
 | 
				
			|||||||
				// On failure:
 | 
									// On failure:
 | 
				
			||||||
				func(rw http.ResponseWriter, r *http.Request, err error) {
 | 
									func(rw http.ResponseWriter, r *http.Request, err error) {
 | 
				
			||||||
					rw.WriteHeader(http.StatusUnauthorized)
 | 
										rw.WriteHeader(http.StatusUnauthorized)
 | 
				
			||||||
					web.RenderTemplate(rw, r, "login.tmpl", &web.Page{
 | 
										web.RenderTemplate(rw, "login.tmpl", &web.Page{
 | 
				
			||||||
						Title:   "Authentication failed - ClusterCockpit",
 | 
											Title:   "Authentication failed - ClusterCockpit",
 | 
				
			||||||
						MsgType: "alert-danger",
 | 
											MsgType: "alert-danger",
 | 
				
			||||||
						Message: err.Error(),
 | 
											Message: err.Error(),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -182,12 +182,12 @@ func decode(r io.Reader, val interface{}) error {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func securedCheck(r *http.Request) error {
 | 
					func securedCheck(r *http.Request) error {
 | 
				
			||||||
	user := auth.GetUser(r.Context())
 | 
						user := repository.GetUserFromContext(r.Context())
 | 
				
			||||||
	if user == nil {
 | 
						if user == nil {
 | 
				
			||||||
		return fmt.Errorf("no user in context")
 | 
							return fmt.Errorf("no user in context")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if user.AuthType == auth.AuthToken {
 | 
						if user.AuthType == schema.AuthToken {
 | 
				
			||||||
		// If nothing declared in config: deny all request to this endpoint
 | 
							// If nothing declared in config: deny all request to this endpoint
 | 
				
			||||||
		if config.Keys.ApiAllowedIPs == nil || len(config.Keys.ApiAllowedIPs) == 0 {
 | 
							if config.Keys.ApiAllowedIPs == nil || len(config.Keys.ApiAllowedIPs) == 0 {
 | 
				
			||||||
			return fmt.Errorf("missing configuration key ApiAllowedIPs")
 | 
								return fmt.Errorf("missing configuration key ApiAllowedIPs")
 | 
				
			||||||
@@ -232,8 +232,10 @@ func securedCheck(r *http.Request) error {
 | 
				
			|||||||
// @router      /jobs/ [get]
 | 
					// @router      /jobs/ [get]
 | 
				
			||||||
func (api *RestApi) getJobs(rw http.ResponseWriter, r *http.Request) {
 | 
					func (api *RestApi) getJobs(rw http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) {
 | 
						if user := repository.GetUserFromContext(r.Context()); user != nil &&
 | 
				
			||||||
		handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw)
 | 
							!user.HasRole(schema.RoleApi) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							handleError(fmt.Errorf("missing role: %v", schema.GetRoleString(schema.RoleApi)), http.StatusForbidden, rw)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -374,9 +376,11 @@ func (api *RestApi) getJobs(rw http.ResponseWriter, r *http.Request) {
 | 
				
			|||||||
// @security    ApiKeyAuth
 | 
					// @security    ApiKeyAuth
 | 
				
			||||||
// @router      /jobs/{id} [post]
 | 
					// @router      /jobs/{id} [post]
 | 
				
			||||||
func (api *RestApi) getJobById(rw http.ResponseWriter, r *http.Request) {
 | 
					func (api *RestApi) getJobById(rw http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
	if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) {
 | 
						if user := repository.GetUserFromContext(r.Context()); user != nil &&
 | 
				
			||||||
 | 
							!user.HasRole(schema.RoleApi) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		handleError(fmt.Errorf("missing role: %v",
 | 
							handleError(fmt.Errorf("missing role: %v",
 | 
				
			||||||
			auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw)
 | 
								schema.GetRoleString(schema.RoleApi)), http.StatusForbidden, rw)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -465,8 +469,10 @@ func (api *RestApi) getJobById(rw http.ResponseWriter, r *http.Request) {
 | 
				
			|||||||
// @security    ApiKeyAuth
 | 
					// @security    ApiKeyAuth
 | 
				
			||||||
// @router      /jobs/tag_job/{id} [post]
 | 
					// @router      /jobs/tag_job/{id} [post]
 | 
				
			||||||
func (api *RestApi) tagJob(rw http.ResponseWriter, r *http.Request) {
 | 
					func (api *RestApi) tagJob(rw http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
	if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) {
 | 
						if user := repository.GetUserFromContext(r.Context()); user != nil &&
 | 
				
			||||||
		handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw)
 | 
							!user.HasRole(schema.RoleApi) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							handleError(fmt.Errorf("missing role: %v", schema.GetRoleString(schema.RoleApi)), http.StatusForbidden, rw)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -530,8 +536,10 @@ func (api *RestApi) tagJob(rw http.ResponseWriter, r *http.Request) {
 | 
				
			|||||||
// @security    ApiKeyAuth
 | 
					// @security    ApiKeyAuth
 | 
				
			||||||
// @router      /jobs/start_job/ [post]
 | 
					// @router      /jobs/start_job/ [post]
 | 
				
			||||||
func (api *RestApi) startJob(rw http.ResponseWriter, r *http.Request) {
 | 
					func (api *RestApi) startJob(rw http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
	if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) {
 | 
						if user := repository.GetUserFromContext(r.Context()); user != nil &&
 | 
				
			||||||
		handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw)
 | 
							!user.HasRole(schema.RoleApi) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							handleError(fmt.Errorf("missing role: %v", schema.GetRoleString(schema.RoleApi)), http.StatusForbidden, rw)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -611,8 +619,10 @@ func (api *RestApi) startJob(rw http.ResponseWriter, r *http.Request) {
 | 
				
			|||||||
// @security    ApiKeyAuth
 | 
					// @security    ApiKeyAuth
 | 
				
			||||||
// @router      /jobs/stop_job/{id} [post]
 | 
					// @router      /jobs/stop_job/{id} [post]
 | 
				
			||||||
func (api *RestApi) stopJobById(rw http.ResponseWriter, r *http.Request) {
 | 
					func (api *RestApi) stopJobById(rw http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
	if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) {
 | 
						if user := repository.GetUserFromContext(r.Context()); user != nil &&
 | 
				
			||||||
		handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw)
 | 
							!user.HasRole(schema.RoleApi) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							handleError(fmt.Errorf("missing role: %v", schema.GetRoleString(schema.RoleApi)), http.StatusForbidden, rw)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -664,8 +674,10 @@ func (api *RestApi) stopJobById(rw http.ResponseWriter, r *http.Request) {
 | 
				
			|||||||
// @security    ApiKeyAuth
 | 
					// @security    ApiKeyAuth
 | 
				
			||||||
// @router      /jobs/stop_job/ [post]
 | 
					// @router      /jobs/stop_job/ [post]
 | 
				
			||||||
func (api *RestApi) stopJobByRequest(rw http.ResponseWriter, r *http.Request) {
 | 
					func (api *RestApi) stopJobByRequest(rw http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
	if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) {
 | 
						if user := repository.GetUserFromContext(r.Context()); user != nil &&
 | 
				
			||||||
		handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw)
 | 
							!user.HasRole(schema.RoleApi) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							handleError(fmt.Errorf("missing role: %v", schema.GetRoleString(schema.RoleApi)), http.StatusForbidden, rw)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -710,8 +722,8 @@ func (api *RestApi) stopJobByRequest(rw http.ResponseWriter, r *http.Request) {
 | 
				
			|||||||
// @security    ApiKeyAuth
 | 
					// @security    ApiKeyAuth
 | 
				
			||||||
// @router      /jobs/delete_job/{id} [delete]
 | 
					// @router      /jobs/delete_job/{id} [delete]
 | 
				
			||||||
func (api *RestApi) deleteJobById(rw http.ResponseWriter, r *http.Request) {
 | 
					func (api *RestApi) deleteJobById(rw http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
	if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) {
 | 
						if user := repository.GetUserFromContext(r.Context()); user != nil && !user.HasRole(schema.RoleApi) {
 | 
				
			||||||
		handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw)
 | 
							handleError(fmt.Errorf("missing role: %v", schema.GetRoleString(schema.RoleApi)), http.StatusForbidden, rw)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -758,8 +770,9 @@ func (api *RestApi) deleteJobById(rw http.ResponseWriter, r *http.Request) {
 | 
				
			|||||||
// @security    ApiKeyAuth
 | 
					// @security    ApiKeyAuth
 | 
				
			||||||
// @router      /jobs/delete_job/ [delete]
 | 
					// @router      /jobs/delete_job/ [delete]
 | 
				
			||||||
func (api *RestApi) deleteJobByRequest(rw http.ResponseWriter, r *http.Request) {
 | 
					func (api *RestApi) deleteJobByRequest(rw http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
	if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) {
 | 
						if user := repository.GetUserFromContext(r.Context()); user != nil &&
 | 
				
			||||||
		handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw)
 | 
							!user.HasRole(schema.RoleApi) {
 | 
				
			||||||
 | 
							handleError(fmt.Errorf("missing role: %v", schema.GetRoleString(schema.RoleApi)), http.StatusForbidden, rw)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -814,8 +827,8 @@ func (api *RestApi) deleteJobByRequest(rw http.ResponseWriter, r *http.Request)
 | 
				
			|||||||
// @security    ApiKeyAuth
 | 
					// @security    ApiKeyAuth
 | 
				
			||||||
// @router      /jobs/delete_job_before/{ts} [delete]
 | 
					// @router      /jobs/delete_job_before/{ts} [delete]
 | 
				
			||||||
func (api *RestApi) deleteJobBefore(rw http.ResponseWriter, r *http.Request) {
 | 
					func (api *RestApi) deleteJobBefore(rw http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
	if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) {
 | 
						if user := repository.GetUserFromContext(r.Context()); user != nil && !user.HasRole(schema.RoleApi) {
 | 
				
			||||||
		handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw)
 | 
							handleError(fmt.Errorf("missing role: %v", schema.GetRoleString(schema.RoleApi)), http.StatusForbidden, rw)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -938,8 +951,8 @@ func (api *RestApi) getJWT(rw http.ResponseWriter, r *http.Request) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	rw.Header().Set("Content-Type", "text/plain")
 | 
						rw.Header().Set("Content-Type", "text/plain")
 | 
				
			||||||
	username := r.FormValue("username")
 | 
						username := r.FormValue("username")
 | 
				
			||||||
	me := auth.GetUser(r.Context())
 | 
						me := repository.GetUserFromContext(r.Context())
 | 
				
			||||||
	if !me.HasRole(auth.RoleAdmin) {
 | 
						if !me.HasRole(schema.RoleAdmin) {
 | 
				
			||||||
		if username != me.Username {
 | 
							if username != me.Username {
 | 
				
			||||||
			http.Error(rw, "Only admins are allowed to sign JWTs not for themselves",
 | 
								http.Error(rw, "Only admins are allowed to sign JWTs not for themselves",
 | 
				
			||||||
				http.StatusForbidden)
 | 
									http.StatusForbidden)
 | 
				
			||||||
@@ -947,7 +960,7 @@ func (api *RestApi) getJWT(rw http.ResponseWriter, r *http.Request) {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	user, err := api.Authentication.GetUser(username)
 | 
						user, err := repository.GetUserRepository().GetUser(username)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		http.Error(rw, err.Error(), http.StatusUnprocessableEntity)
 | 
							http.Error(rw, err.Error(), http.StatusUnprocessableEntity)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
@@ -970,8 +983,8 @@ func (api *RestApi) createUser(rw http.ResponseWriter, r *http.Request) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	rw.Header().Set("Content-Type", "text/plain")
 | 
						rw.Header().Set("Content-Type", "text/plain")
 | 
				
			||||||
	me := auth.GetUser(r.Context())
 | 
						me := repository.GetUserFromContext(r.Context())
 | 
				
			||||||
	if !me.HasRole(auth.RoleAdmin) {
 | 
						if !me.HasRole(schema.RoleAdmin) {
 | 
				
			||||||
		http.Error(rw, "Only admins are allowed to create new users", http.StatusForbidden)
 | 
							http.Error(rw, "Only admins are allowed to create new users", http.StatusForbidden)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -980,22 +993,22 @@ func (api *RestApi) createUser(rw http.ResponseWriter, r *http.Request) {
 | 
				
			|||||||
		r.FormValue("password"), r.FormValue("role"), r.FormValue("name"),
 | 
							r.FormValue("password"), r.FormValue("role"), r.FormValue("name"),
 | 
				
			||||||
		r.FormValue("email"), r.FormValue("project")
 | 
							r.FormValue("email"), r.FormValue("project")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if len(password) == 0 && role != auth.GetRoleString(auth.RoleApi) {
 | 
						if len(password) == 0 && role != schema.GetRoleString(schema.RoleApi) {
 | 
				
			||||||
		http.Error(rw, "Only API users are allowed to have a blank password (login will be impossible)", http.StatusBadRequest)
 | 
							http.Error(rw, "Only API users are allowed to have a blank password (login will be impossible)", http.StatusBadRequest)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if len(project) != 0 && role != auth.GetRoleString(auth.RoleManager) {
 | 
						if len(project) != 0 && role != schema.GetRoleString(schema.RoleManager) {
 | 
				
			||||||
		http.Error(rw, "only managers require a project (can be changed later)",
 | 
							http.Error(rw, "only managers require a project (can be changed later)",
 | 
				
			||||||
			http.StatusBadRequest)
 | 
								http.StatusBadRequest)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	} else if len(project) == 0 && role == auth.GetRoleString(auth.RoleManager) {
 | 
						} else if len(project) == 0 && role == schema.GetRoleString(schema.RoleManager) {
 | 
				
			||||||
		http.Error(rw, "managers require a project to manage (can be changed later)",
 | 
							http.Error(rw, "managers require a project to manage (can be changed later)",
 | 
				
			||||||
			http.StatusBadRequest)
 | 
								http.StatusBadRequest)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := api.Authentication.AddUser(&auth.User{
 | 
						if err := repository.GetUserRepository().AddUser(&schema.User{
 | 
				
			||||||
		Username: username,
 | 
							Username: username,
 | 
				
			||||||
		Name:     name,
 | 
							Name:     name,
 | 
				
			||||||
		Password: password,
 | 
							Password: password,
 | 
				
			||||||
@@ -1015,13 +1028,13 @@ func (api *RestApi) deleteUser(rw http.ResponseWriter, r *http.Request) {
 | 
				
			|||||||
		http.Error(rw, err.Error(), http.StatusForbidden)
 | 
							http.Error(rw, err.Error(), http.StatusForbidden)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if user := auth.GetUser(r.Context()); !user.HasRole(auth.RoleAdmin) {
 | 
						if user := repository.GetUserFromContext(r.Context()); !user.HasRole(schema.RoleAdmin) {
 | 
				
			||||||
		http.Error(rw, "Only admins are allowed to delete a user", http.StatusForbidden)
 | 
							http.Error(rw, "Only admins are allowed to delete a user", http.StatusForbidden)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	username := r.FormValue("username")
 | 
						username := r.FormValue("username")
 | 
				
			||||||
	if err := api.Authentication.DelUser(username); err != nil {
 | 
						if err := repository.GetUserRepository().DelUser(username); err != nil {
 | 
				
			||||||
		http.Error(rw, err.Error(), http.StatusUnprocessableEntity)
 | 
							http.Error(rw, err.Error(), http.StatusUnprocessableEntity)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -1035,12 +1048,12 @@ func (api *RestApi) getUsers(rw http.ResponseWriter, r *http.Request) {
 | 
				
			|||||||
		http.Error(rw, err.Error(), http.StatusForbidden)
 | 
							http.Error(rw, err.Error(), http.StatusForbidden)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if user := auth.GetUser(r.Context()); !user.HasRole(auth.RoleAdmin) {
 | 
						if user := repository.GetUserFromContext(r.Context()); !user.HasRole(schema.RoleAdmin) {
 | 
				
			||||||
		http.Error(rw, "Only admins are allowed to fetch a list of users", http.StatusForbidden)
 | 
							http.Error(rw, "Only admins are allowed to fetch a list of users", http.StatusForbidden)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	users, err := api.Authentication.ListUsers(r.URL.Query().Get("not-just-user") == "true")
 | 
						users, err := repository.GetUserRepository().ListUsers(r.URL.Query().Get("not-just-user") == "true")
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		http.Error(rw, err.Error(), http.StatusInternalServerError)
 | 
							http.Error(rw, err.Error(), http.StatusInternalServerError)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
@@ -1055,13 +1068,13 @@ func (api *RestApi) getRoles(rw http.ResponseWriter, r *http.Request) {
 | 
				
			|||||||
		http.Error(rw, err.Error(), http.StatusForbidden)
 | 
							http.Error(rw, err.Error(), http.StatusForbidden)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	user := auth.GetUser(r.Context())
 | 
						user := repository.GetUserFromContext(r.Context())
 | 
				
			||||||
	if !user.HasRole(auth.RoleAdmin) {
 | 
						if !user.HasRole(schema.RoleAdmin) {
 | 
				
			||||||
		http.Error(rw, "only admins are allowed to fetch a list of roles", http.StatusForbidden)
 | 
							http.Error(rw, "only admins are allowed to fetch a list of roles", http.StatusForbidden)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	roles, err := auth.GetValidRoles(user)
 | 
						roles, err := schema.GetValidRoles(user)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		http.Error(rw, err.Error(), http.StatusInternalServerError)
 | 
							http.Error(rw, err.Error(), http.StatusInternalServerError)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
@@ -1076,7 +1089,7 @@ func (api *RestApi) updateUser(rw http.ResponseWriter, r *http.Request) {
 | 
				
			|||||||
		http.Error(rw, err.Error(), http.StatusForbidden)
 | 
							http.Error(rw, err.Error(), http.StatusForbidden)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if user := auth.GetUser(r.Context()); !user.HasRole(auth.RoleAdmin) {
 | 
						if user := repository.GetUserFromContext(r.Context()); !user.HasRole(schema.RoleAdmin) {
 | 
				
			||||||
		http.Error(rw, "Only admins are allowed to update a user", http.StatusForbidden)
 | 
							http.Error(rw, "Only admins are allowed to update a user", http.StatusForbidden)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -1089,25 +1102,25 @@ func (api *RestApi) updateUser(rw http.ResponseWriter, r *http.Request) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	// TODO: Handle anything but roles...
 | 
						// TODO: Handle anything but roles...
 | 
				
			||||||
	if newrole != "" {
 | 
						if newrole != "" {
 | 
				
			||||||
		if err := api.Authentication.AddRole(r.Context(), mux.Vars(r)["id"], newrole); err != nil {
 | 
							if err := repository.GetUserRepository().AddRole(r.Context(), mux.Vars(r)["id"], newrole); err != nil {
 | 
				
			||||||
			http.Error(rw, err.Error(), http.StatusUnprocessableEntity)
 | 
								http.Error(rw, err.Error(), http.StatusUnprocessableEntity)
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		rw.Write([]byte("Add Role Success"))
 | 
							rw.Write([]byte("Add Role Success"))
 | 
				
			||||||
	} else if delrole != "" {
 | 
						} else if delrole != "" {
 | 
				
			||||||
		if err := api.Authentication.RemoveRole(r.Context(), mux.Vars(r)["id"], delrole); err != nil {
 | 
							if err := repository.GetUserRepository().RemoveRole(r.Context(), mux.Vars(r)["id"], delrole); err != nil {
 | 
				
			||||||
			http.Error(rw, err.Error(), http.StatusUnprocessableEntity)
 | 
								http.Error(rw, err.Error(), http.StatusUnprocessableEntity)
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		rw.Write([]byte("Remove Role Success"))
 | 
							rw.Write([]byte("Remove Role Success"))
 | 
				
			||||||
	} else if newproj != "" {
 | 
						} else if newproj != "" {
 | 
				
			||||||
		if err := api.Authentication.AddProject(r.Context(), mux.Vars(r)["id"], newproj); err != nil {
 | 
							if err := repository.GetUserRepository().AddProject(r.Context(), mux.Vars(r)["id"], newproj); err != nil {
 | 
				
			||||||
			http.Error(rw, err.Error(), http.StatusUnprocessableEntity)
 | 
								http.Error(rw, err.Error(), http.StatusUnprocessableEntity)
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		rw.Write([]byte("Add Project Success"))
 | 
							rw.Write([]byte("Add Project Success"))
 | 
				
			||||||
	} else if delproj != "" {
 | 
						} else if delproj != "" {
 | 
				
			||||||
		if err := api.Authentication.RemoveProject(r.Context(), mux.Vars(r)["id"], delproj); err != nil {
 | 
							if err := repository.GetUserRepository().RemoveProject(r.Context(), mux.Vars(r)["id"], delproj); err != nil {
 | 
				
			||||||
			http.Error(rw, err.Error(), http.StatusUnprocessableEntity)
 | 
								http.Error(rw, err.Error(), http.StatusUnprocessableEntity)
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -1188,7 +1201,7 @@ func (api *RestApi) updateConfiguration(rw http.ResponseWriter, r *http.Request)
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	fmt.Printf("REST > KEY: %#v\nVALUE: %#v\n", key, value)
 | 
						fmt.Printf("REST > KEY: %#v\nVALUE: %#v\n", key, value)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := repository.GetUserCfgRepo().UpdateConfig(key, value, auth.GetUser(r.Context())); err != nil {
 | 
						if err := repository.GetUserCfgRepo().UpdateConfig(key, value, repository.GetUserFromContext(r.Context())); err != nil {
 | 
				
			||||||
		http.Error(rw, err.Error(), http.StatusUnprocessableEntity)
 | 
							http.Error(rw, err.Error(), http.StatusUnprocessableEntity)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,65 +14,19 @@ import (
 | 
				
			|||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/ClusterCockpit/cc-backend/internal/repository"
 | 
				
			||||||
	"github.com/ClusterCockpit/cc-backend/pkg/log"
 | 
						"github.com/ClusterCockpit/cc-backend/pkg/log"
 | 
				
			||||||
 | 
						"github.com/ClusterCockpit/cc-backend/pkg/schema"
 | 
				
			||||||
	"github.com/gorilla/sessions"
 | 
						"github.com/gorilla/sessions"
 | 
				
			||||||
	"github.com/jmoiron/sqlx"
 | 
						"github.com/jmoiron/sqlx"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type AuthSource int
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const (
 | 
					 | 
				
			||||||
	AuthViaLocalPassword AuthSource = iota
 | 
					 | 
				
			||||||
	AuthViaLDAP
 | 
					 | 
				
			||||||
	AuthViaToken
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type AuthType int
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const (
 | 
					 | 
				
			||||||
	AuthToken AuthType = iota
 | 
					 | 
				
			||||||
	AuthSession
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type User struct {
 | 
					 | 
				
			||||||
	Username   string     `json:"username"`
 | 
					 | 
				
			||||||
	Password   string     `json:"-"`
 | 
					 | 
				
			||||||
	Name       string     `json:"name"`
 | 
					 | 
				
			||||||
	Roles      []string   `json:"roles"`
 | 
					 | 
				
			||||||
	AuthType   AuthType   `json:"authType"`
 | 
					 | 
				
			||||||
	AuthSource AuthSource `json:"authSource"`
 | 
					 | 
				
			||||||
	Email      string     `json:"email"`
 | 
					 | 
				
			||||||
	Projects   []string   `json:"projects"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (u *User) HasProject(project string) bool {
 | 
					 | 
				
			||||||
	for _, p := range u.Projects {
 | 
					 | 
				
			||||||
		if p == project {
 | 
					 | 
				
			||||||
			return true
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return false
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func GetUser(ctx context.Context) *User {
 | 
					 | 
				
			||||||
	x := ctx.Value(ContextUserKey)
 | 
					 | 
				
			||||||
	if x == nil {
 | 
					 | 
				
			||||||
		return nil
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return x.(*User)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type Authenticator interface {
 | 
					type Authenticator interface {
 | 
				
			||||||
	Init(auth *Authentication, config interface{}) error
 | 
						Init(auth *Authentication, config interface{}) error
 | 
				
			||||||
	CanLogin(user *User, username string, rw http.ResponseWriter, r *http.Request) bool
 | 
						CanLogin(user *schema.User, username string, rw http.ResponseWriter, r *http.Request) bool
 | 
				
			||||||
	Login(user *User, rw http.ResponseWriter, r *http.Request) (*User, error)
 | 
						Login(user *schema.User, rw http.ResponseWriter, r *http.Request) (*schema.User, error)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type ContextKey string
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const ContextUserKey ContextKey = "user"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type Authentication struct {
 | 
					type Authentication struct {
 | 
				
			||||||
	db            *sqlx.DB
 | 
						db            *sqlx.DB
 | 
				
			||||||
	sessionStore  *sessions.CookieStore
 | 
						sessionStore  *sessions.CookieStore
 | 
				
			||||||
@@ -86,7 +40,7 @@ type Authentication struct {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func (auth *Authentication) AuthViaSession(
 | 
					func (auth *Authentication) AuthViaSession(
 | 
				
			||||||
	rw http.ResponseWriter,
 | 
						rw http.ResponseWriter,
 | 
				
			||||||
	r *http.Request) (*User, error) {
 | 
						r *http.Request) (*schema.User, error) {
 | 
				
			||||||
	session, err := auth.sessionStore.Get(r, "session")
 | 
						session, err := auth.sessionStore.Get(r, "session")
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		log.Error("Error while getting session store")
 | 
							log.Error("Error while getting session store")
 | 
				
			||||||
@@ -119,11 +73,11 @@ func (auth *Authentication) AuthViaSession(
 | 
				
			|||||||
	username, _ := session.Values["username"].(string)
 | 
						username, _ := session.Values["username"].(string)
 | 
				
			||||||
	projects, _ := session.Values["projects"].([]string)
 | 
						projects, _ := session.Values["projects"].([]string)
 | 
				
			||||||
	roles, _ := session.Values["roles"].([]string)
 | 
						roles, _ := session.Values["roles"].([]string)
 | 
				
			||||||
	return &User{
 | 
						return &schema.User{
 | 
				
			||||||
		Username:   username,
 | 
							Username:   username,
 | 
				
			||||||
		Projects:   projects,
 | 
							Projects:   projects,
 | 
				
			||||||
		Roles:      roles,
 | 
							Roles:      roles,
 | 
				
			||||||
		AuthType:   AuthSession,
 | 
							AuthType:   schema.AuthSession,
 | 
				
			||||||
		AuthSource: -1,
 | 
							AuthSource: -1,
 | 
				
			||||||
	}, nil
 | 
						}, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -196,12 +150,13 @@ func (auth *Authentication) Login(
 | 
				
			|||||||
	onfailure func(rw http.ResponseWriter, r *http.Request, loginErr error)) http.Handler {
 | 
						onfailure func(rw http.ResponseWriter, r *http.Request, loginErr error)) http.Handler {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
 | 
						return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
 | 
							ur := repository.GetUserRepository()
 | 
				
			||||||
		err := errors.New("no authenticator applied")
 | 
							err := errors.New("no authenticator applied")
 | 
				
			||||||
		username := r.FormValue("username")
 | 
							username := r.FormValue("username")
 | 
				
			||||||
		dbUser := (*User)(nil)
 | 
							dbUser := (*schema.User)(nil)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if username != "" {
 | 
							if username != "" {
 | 
				
			||||||
			dbUser, err = auth.GetUser(username)
 | 
								dbUser, err = ur.GetUser(username)
 | 
				
			||||||
			if err != nil && err != sql.ErrNoRows {
 | 
								if err != nil && err != sql.ErrNoRows {
 | 
				
			||||||
				log.Errorf("Error while loading user '%v'", username)
 | 
									log.Errorf("Error while loading user '%v'", username)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
@@ -211,7 +166,7 @@ func (auth *Authentication) Login(
 | 
				
			|||||||
			if !authenticator.CanLogin(dbUser, username, rw, r) {
 | 
								if !authenticator.CanLogin(dbUser, username, rw, r) {
 | 
				
			||||||
				continue
 | 
									continue
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			dbUser, err = auth.GetUser(username)
 | 
								dbUser, err = ur.GetUser(username)
 | 
				
			||||||
			if err != nil && err != sql.ErrNoRows {
 | 
								if err != nil && err != sql.ErrNoRows {
 | 
				
			||||||
				log.Errorf("Error while loading user '%v'", username)
 | 
									log.Errorf("Error while loading user '%v'", username)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
@@ -243,7 +198,7 @@ func (auth *Authentication) Login(
 | 
				
			|||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if dbUser == nil {
 | 
								if dbUser == nil {
 | 
				
			||||||
				if err := auth.AddUser(user); err != nil {
 | 
									if err := ur.AddUser(user); err != nil {
 | 
				
			||||||
					// TODO Add AuthSource
 | 
										// TODO Add AuthSource
 | 
				
			||||||
					log.Errorf("Error while adding user '%v' to auth from XX",
 | 
										log.Errorf("Error while adding user '%v' to auth from XX",
 | 
				
			||||||
						user.Username)
 | 
											user.Username)
 | 
				
			||||||
@@ -251,7 +206,7 @@ func (auth *Authentication) Login(
 | 
				
			|||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			log.Infof("login successfull: user: %#v (roles: %v, projects: %v)", user.Username, user.Roles, user.Projects)
 | 
								log.Infof("login successfull: user: %#v (roles: %v, projects: %v)", user.Username, user.Roles, user.Projects)
 | 
				
			||||||
			ctx := context.WithValue(r.Context(), ContextUserKey, user)
 | 
								ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
 | 
				
			||||||
			onsuccess.ServeHTTP(rw, r.WithContext(ctx))
 | 
								onsuccess.ServeHTTP(rw, r.WithContext(ctx))
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -284,7 +239,7 @@ func (auth *Authentication) Auth(
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if user != nil {
 | 
							if user != nil {
 | 
				
			||||||
			ctx := context.WithValue(r.Context(), ContextUserKey, user)
 | 
								ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
 | 
				
			||||||
			onsuccess.ServeHTTP(rw, r.WithContext(ctx))
 | 
								onsuccess.ServeHTTP(rw, r.WithContext(ctx))
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,22 +13,19 @@ import (
 | 
				
			|||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/ClusterCockpit/cc-backend/internal/repository"
 | 
				
			||||||
	"github.com/ClusterCockpit/cc-backend/pkg/log"
 | 
						"github.com/ClusterCockpit/cc-backend/pkg/log"
 | 
				
			||||||
	"github.com/ClusterCockpit/cc-backend/pkg/schema"
 | 
						"github.com/ClusterCockpit/cc-backend/pkg/schema"
 | 
				
			||||||
	"github.com/golang-jwt/jwt/v4"
 | 
						"github.com/golang-jwt/jwt/v4"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type JWTAuthenticator struct {
 | 
					type JWTAuthenticator struct {
 | 
				
			||||||
	auth *Authentication
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	publicKey  ed25519.PublicKey
 | 
						publicKey  ed25519.PublicKey
 | 
				
			||||||
	privateKey ed25519.PrivateKey
 | 
						privateKey ed25519.PrivateKey
 | 
				
			||||||
	config     *schema.JWTAuthConfig
 | 
						config     *schema.JWTAuthConfig
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (ja *JWTAuthenticator) Init(auth *Authentication, conf interface{}) error {
 | 
					func (ja *JWTAuthenticator) Init(auth *Authentication, conf interface{}) error {
 | 
				
			||||||
 | 
					 | 
				
			||||||
	ja.auth = auth
 | 
					 | 
				
			||||||
	ja.config = conf.(*schema.JWTAuthConfig)
 | 
						ja.config = conf.(*schema.JWTAuthConfig)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	pubKey, privKey := os.Getenv("JWT_PUBLIC_KEY"), os.Getenv("JWT_PRIVATE_KEY")
 | 
						pubKey, privKey := os.Getenv("JWT_PUBLIC_KEY"), os.Getenv("JWT_PRIVATE_KEY")
 | 
				
			||||||
@@ -54,7 +51,7 @@ func (ja *JWTAuthenticator) Init(auth *Authentication, conf interface{}) error {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func (ja *JWTAuthenticator) AuthViaJWT(
 | 
					func (ja *JWTAuthenticator) AuthViaJWT(
 | 
				
			||||||
	rw http.ResponseWriter,
 | 
						rw http.ResponseWriter,
 | 
				
			||||||
	r *http.Request) (*User, error) {
 | 
						r *http.Request) (*schema.User, error) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	rawtoken := r.Header.Get("X-Auth-Token")
 | 
						rawtoken := r.Header.Get("X-Auth-Token")
 | 
				
			||||||
	if rawtoken == "" {
 | 
						if rawtoken == "" {
 | 
				
			||||||
@@ -90,8 +87,9 @@ func (ja *JWTAuthenticator) AuthViaJWT(
 | 
				
			|||||||
	var roles []string
 | 
						var roles []string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Validate user + roles from JWT against database?
 | 
						// Validate user + roles from JWT against database?
 | 
				
			||||||
	if ja.config != nil && ja.config.ForceJWTValidationViaDatabase {
 | 
						if ja.config != nil && ja.config.ValidateUser {
 | 
				
			||||||
		user, err := ja.auth.GetUser(sub)
 | 
							ur := repository.GetUserRepository()
 | 
				
			||||||
 | 
							user, err := ur.GetUser(sub)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Deny any logins for unknown usernames
 | 
							// Deny any logins for unknown usernames
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
@@ -111,16 +109,16 @@ func (ja *JWTAuthenticator) AuthViaJWT(
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return &User{
 | 
						return &schema.User{
 | 
				
			||||||
		Username:   sub,
 | 
							Username:   sub,
 | 
				
			||||||
		Roles:      roles,
 | 
							Roles:      roles,
 | 
				
			||||||
		AuthType:   AuthToken,
 | 
							AuthType:   schema.AuthToken,
 | 
				
			||||||
		AuthSource: -1,
 | 
							AuthSource: -1,
 | 
				
			||||||
	}, nil
 | 
						}, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Generate a new JWT that can be used for authentication
 | 
					// Generate a new JWT that can be used for authentication
 | 
				
			||||||
func (ja *JWTAuthenticator) ProvideJWT(user *User) (string, error) {
 | 
					func (ja *JWTAuthenticator) ProvideJWT(user *schema.User) (string, error) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if ja.privateKey == nil {
 | 
						if ja.privateKey == nil {
 | 
				
			||||||
		return "", errors.New("environment variable 'JWT_PRIVATE_KEY' not set")
 | 
							return "", errors.New("environment variable 'JWT_PRIVATE_KEY' not set")
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -73,10 +73,10 @@ func (ja *JWTCookieSessionAuthenticator) Init(auth *Authentication, conf interfa
 | 
				
			|||||||
			log.Warn("cookieName for JWTs not configured (cross login via JWT cookie will fail)")
 | 
								log.Warn("cookieName for JWTs not configured (cross login via JWT cookie will fail)")
 | 
				
			||||||
			return errors.New("cookieName for JWTs not configured (cross login via JWT cookie will fail)")
 | 
								return errors.New("cookieName for JWTs not configured (cross login via JWT cookie will fail)")
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		if !ja.config.ForceJWTValidationViaDatabase {
 | 
							if !ja.config.ValidateUser {
 | 
				
			||||||
			log.Warn("forceJWTValidationViaDatabase not set to true: CC will accept users and roles defined in JWTs regardless of its own database!")
 | 
								log.Warn("forceJWTValidationViaDatabase not set to true: CC will accept users and roles defined in JWTs regardless of its own database!")
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		if ja.config.TrustedExternalIssuer == "" {
 | 
							if ja.config.TrustedIssuer == "" {
 | 
				
			||||||
			log.Warn("trustedExternalIssuer for JWTs not configured (cross login via JWT cookie will fail)")
 | 
								log.Warn("trustedExternalIssuer for JWTs not configured (cross login via JWT cookie will fail)")
 | 
				
			||||||
			return errors.New("trustedExternalIssuer for JWTs not configured (cross login via JWT cookie will fail)")
 | 
								return errors.New("trustedExternalIssuer for JWTs not configured (cross login via JWT cookie will fail)")
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -89,7 +89,7 @@ func (ja *JWTCookieSessionAuthenticator) Init(auth *Authentication, conf interfa
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (ja *JWTCookieSessionAuthenticator) CanLogin(
 | 
					func (ja *JWTCookieSessionAuthenticator) CanLogin(
 | 
				
			||||||
	user *User,
 | 
						user *schema.User,
 | 
				
			||||||
	username string,
 | 
						username string,
 | 
				
			||||||
	rw http.ResponseWriter,
 | 
						rw http.ResponseWriter,
 | 
				
			||||||
	r *http.Request) bool {
 | 
						r *http.Request) bool {
 | 
				
			||||||
@@ -112,9 +112,9 @@ func (ja *JWTCookieSessionAuthenticator) CanLogin(
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (ja *JWTCookieSessionAuthenticator) Login(
 | 
					func (ja *JWTCookieSessionAuthenticator) Login(
 | 
				
			||||||
	user *User,
 | 
						user *schema.User,
 | 
				
			||||||
	rw http.ResponseWriter,
 | 
						rw http.ResponseWriter,
 | 
				
			||||||
	r *http.Request) (*User, error) {
 | 
						r *http.Request) (*schema.User, error) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	jwtCookie, err := r.Cookie(ja.config.CookieName)
 | 
						jwtCookie, err := r.Cookie(ja.config.CookieName)
 | 
				
			||||||
	var rawtoken string
 | 
						var rawtoken string
 | 
				
			||||||
@@ -129,7 +129,7 @@ func (ja *JWTCookieSessionAuthenticator) Login(
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		unvalidatedIssuer, success := t.Claims.(jwt.MapClaims)["iss"].(string)
 | 
							unvalidatedIssuer, success := t.Claims.(jwt.MapClaims)["iss"].(string)
 | 
				
			||||||
		if success && unvalidatedIssuer == ja.config.TrustedExternalIssuer {
 | 
							if success && unvalidatedIssuer == ja.config.TrustedIssuer {
 | 
				
			||||||
			// The (unvalidated) issuer seems to be the expected one,
 | 
								// The (unvalidated) issuer seems to be the expected one,
 | 
				
			||||||
			// use public cross login key from config
 | 
								// use public cross login key from config
 | 
				
			||||||
			return ja.publicKeyCrossLogin, nil
 | 
								return ja.publicKeyCrossLogin, nil
 | 
				
			||||||
@@ -160,7 +160,7 @@ func (ja *JWTCookieSessionAuthenticator) Login(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	var roles []string
 | 
						var roles []string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if ja.config.ForceJWTValidationViaDatabase {
 | 
						if ja.config.ValidateUser {
 | 
				
			||||||
		// Deny any logins for unknown usernames
 | 
							// Deny any logins for unknown usernames
 | 
				
			||||||
		if user == nil {
 | 
							if user == nil {
 | 
				
			||||||
			log.Warn("Could not find user from JWT in internal database.")
 | 
								log.Warn("Could not find user from JWT in internal database.")
 | 
				
			||||||
@@ -191,12 +191,12 @@ func (ja *JWTCookieSessionAuthenticator) Login(
 | 
				
			|||||||
	http.SetCookie(rw, deletedCookie)
 | 
						http.SetCookie(rw, deletedCookie)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if user == nil {
 | 
						if user == nil {
 | 
				
			||||||
		user = &User{
 | 
							user = &schema.User{
 | 
				
			||||||
			Username:   sub,
 | 
								Username:   sub,
 | 
				
			||||||
			Name:       name,
 | 
								Name:       name,
 | 
				
			||||||
			Roles:      roles,
 | 
								Roles:      roles,
 | 
				
			||||||
			AuthType:   AuthSession,
 | 
								AuthType:   schema.AuthSession,
 | 
				
			||||||
			AuthSource: AuthViaToken,
 | 
								AuthSource: schema.AuthViaToken,
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,21 +12,17 @@ import (
 | 
				
			|||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/ClusterCockpit/cc-backend/pkg/log"
 | 
						"github.com/ClusterCockpit/cc-backend/pkg/log"
 | 
				
			||||||
 | 
						"github.com/ClusterCockpit/cc-backend/pkg/schema"
 | 
				
			||||||
	"github.com/golang-jwt/jwt/v4"
 | 
						"github.com/golang-jwt/jwt/v4"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type JWTSessionAuthenticator struct {
 | 
					type JWTSessionAuthenticator struct {
 | 
				
			||||||
	auth *Authentication
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	loginTokenKey []byte // HS256 key
 | 
						loginTokenKey []byte // HS256 key
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var _ Authenticator = (*JWTSessionAuthenticator)(nil)
 | 
					var _ Authenticator = (*JWTSessionAuthenticator)(nil)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (ja *JWTSessionAuthenticator) Init(auth *Authentication, conf interface{}) error {
 | 
					func (ja *JWTSessionAuthenticator) Init(auth *Authentication, conf interface{}) error {
 | 
				
			||||||
 | 
					 | 
				
			||||||
	ja.auth = auth
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if pubKey := os.Getenv("CROSS_LOGIN_JWT_HS512_KEY"); pubKey != "" {
 | 
						if pubKey := os.Getenv("CROSS_LOGIN_JWT_HS512_KEY"); pubKey != "" {
 | 
				
			||||||
		bytes, err := base64.StdEncoding.DecodeString(pubKey)
 | 
							bytes, err := base64.StdEncoding.DecodeString(pubKey)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
@@ -40,7 +36,7 @@ func (ja *JWTSessionAuthenticator) Init(auth *Authentication, conf interface{})
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (ja *JWTSessionAuthenticator) CanLogin(
 | 
					func (ja *JWTSessionAuthenticator) CanLogin(
 | 
				
			||||||
	user *User,
 | 
						user *schema.User,
 | 
				
			||||||
	username string,
 | 
						username string,
 | 
				
			||||||
	rw http.ResponseWriter,
 | 
						rw http.ResponseWriter,
 | 
				
			||||||
	r *http.Request) bool {
 | 
						r *http.Request) bool {
 | 
				
			||||||
@@ -49,9 +45,9 @@ func (ja *JWTSessionAuthenticator) CanLogin(
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (ja *JWTSessionAuthenticator) Login(
 | 
					func (ja *JWTSessionAuthenticator) Login(
 | 
				
			||||||
	user *User,
 | 
						user *schema.User,
 | 
				
			||||||
	rw http.ResponseWriter,
 | 
						rw http.ResponseWriter,
 | 
				
			||||||
	r *http.Request) (*User, error) {
 | 
						r *http.Request) (*schema.User, error) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	rawtoken := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
 | 
						rawtoken := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
 | 
				
			||||||
	if rawtoken == "" {
 | 
						if rawtoken == "" {
 | 
				
			||||||
@@ -92,14 +88,14 @@ func (ja *JWTSessionAuthenticator) Login(
 | 
				
			|||||||
	if rawroles, ok := claims["roles"].([]interface{}); ok {
 | 
						if rawroles, ok := claims["roles"].([]interface{}); ok {
 | 
				
			||||||
		for _, rr := range rawroles {
 | 
							for _, rr := range rawroles {
 | 
				
			||||||
			if r, ok := rr.(string); ok {
 | 
								if r, ok := rr.(string); ok {
 | 
				
			||||||
				if isValidRole(r) {
 | 
									if schema.IsValidRole(r) {
 | 
				
			||||||
					roles = append(roles, r)
 | 
										roles = append(roles, r)
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	} else if rawroles, ok := claims["roles"]; ok {
 | 
						} else if rawroles, ok := claims["roles"]; ok {
 | 
				
			||||||
		for _, r := range rawroles.([]string) {
 | 
							for _, r := range rawroles.([]string) {
 | 
				
			||||||
			if isValidRole(r) {
 | 
								if schema.IsValidRole(r) {
 | 
				
			||||||
				roles = append(roles, r)
 | 
									roles = append(roles, r)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -120,13 +116,13 @@ func (ja *JWTSessionAuthenticator) Login(
 | 
				
			|||||||
	// }
 | 
						// }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if user == nil {
 | 
						if user == nil {
 | 
				
			||||||
		user = &User{
 | 
							user = &schema.User{
 | 
				
			||||||
			Username:   sub,
 | 
								Username:   sub,
 | 
				
			||||||
			Name:       name,
 | 
								Name:       name,
 | 
				
			||||||
			Roles:      roles,
 | 
								Roles:      roles,
 | 
				
			||||||
			Projects:   projects,
 | 
								Projects:   projects,
 | 
				
			||||||
			AuthType:   AuthSession,
 | 
								AuthType:   schema.AuthSession,
 | 
				
			||||||
			AuthSource: AuthViaToken,
 | 
								AuthSource: schema.AuthViaToken,
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -67,12 +67,12 @@ func (la *LdapAuthenticator) Init(
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (la *LdapAuthenticator) CanLogin(
 | 
					func (la *LdapAuthenticator) CanLogin(
 | 
				
			||||||
	user *User,
 | 
						user *schema.User,
 | 
				
			||||||
	username string,
 | 
						username string,
 | 
				
			||||||
	rw http.ResponseWriter,
 | 
						rw http.ResponseWriter,
 | 
				
			||||||
	r *http.Request) bool {
 | 
						r *http.Request) bool {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if user != nil && user.AuthSource == AuthViaLDAP {
 | 
						if user != nil && user.AuthSource == schema.AuthViaLDAP {
 | 
				
			||||||
		return true
 | 
							return true
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		if la.config != nil && la.config.SyncUserOnLogin {
 | 
							if la.config != nil && la.config.SyncUserOnLogin {
 | 
				
			||||||
@@ -103,7 +103,7 @@ func (la *LdapAuthenticator) CanLogin(
 | 
				
			|||||||
			name := entry.GetAttributeValue("gecos")
 | 
								name := entry.GetAttributeValue("gecos")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if _, err := la.auth.db.Exec(`INSERT INTO user (username, ldap, name, roles) VALUES (?, ?, ?, ?)`,
 | 
								if _, err := la.auth.db.Exec(`INSERT INTO user (username, ldap, name, roles) VALUES (?, ?, ?, ?)`,
 | 
				
			||||||
				username, 1, name, "[\""+GetRoleString(RoleUser)+"\"]"); err != nil {
 | 
									username, 1, name, "[\""+schema.GetRoleString(schema.RoleUser)+"\"]"); err != nil {
 | 
				
			||||||
				log.Errorf("User '%s' new in LDAP: Insert into DB failed", username)
 | 
									log.Errorf("User '%s' new in LDAP: Insert into DB failed", username)
 | 
				
			||||||
				return false
 | 
									return false
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
@@ -116,9 +116,9 @@ func (la *LdapAuthenticator) CanLogin(
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (la *LdapAuthenticator) Login(
 | 
					func (la *LdapAuthenticator) Login(
 | 
				
			||||||
	user *User,
 | 
						user *schema.User,
 | 
				
			||||||
	rw http.ResponseWriter,
 | 
						rw http.ResponseWriter,
 | 
				
			||||||
	r *http.Request) (*User, error) {
 | 
						r *http.Request) (*schema.User, error) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	l, err := la.getLdapConnection(false)
 | 
						l, err := la.getLdapConnection(false)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
@@ -203,7 +203,7 @@ func (la *LdapAuthenticator) Sync() error {
 | 
				
			|||||||
			name := newnames[username]
 | 
								name := newnames[username]
 | 
				
			||||||
			log.Debugf("sync: add %v (name: %v, roles: [user], ldap: true)", username, name)
 | 
								log.Debugf("sync: add %v (name: %v, roles: [user], ldap: true)", username, name)
 | 
				
			||||||
			if _, err := la.auth.db.Exec(`INSERT INTO user (username, ldap, name, roles) VALUES (?, ?, ?, ?)`,
 | 
								if _, err := la.auth.db.Exec(`INSERT INTO user (username, ldap, name, roles) VALUES (?, ?, ?, ?)`,
 | 
				
			||||||
				username, 1, name, "[\""+GetRoleString(RoleUser)+"\"]"); err != nil {
 | 
									username, 1, name, "[\""+schema.GetRoleString(schema.RoleUser)+"\"]"); err != nil {
 | 
				
			||||||
				log.Errorf("User '%s' new in LDAP: Insert into DB failed", username)
 | 
									log.Errorf("User '%s' new in LDAP: Insert into DB failed", username)
 | 
				
			||||||
				return err
 | 
									return err
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,6 +9,7 @@ import (
 | 
				
			|||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/ClusterCockpit/cc-backend/pkg/log"
 | 
						"github.com/ClusterCockpit/cc-backend/pkg/log"
 | 
				
			||||||
 | 
						"github.com/ClusterCockpit/cc-backend/pkg/schema"
 | 
				
			||||||
	"golang.org/x/crypto/bcrypt"
 | 
						"golang.org/x/crypto/bcrypt"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -27,18 +28,18 @@ func (la *LocalAuthenticator) Init(
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (la *LocalAuthenticator) CanLogin(
 | 
					func (la *LocalAuthenticator) CanLogin(
 | 
				
			||||||
	user *User,
 | 
						user *schema.User,
 | 
				
			||||||
	username string,
 | 
						username string,
 | 
				
			||||||
	rw http.ResponseWriter,
 | 
						rw http.ResponseWriter,
 | 
				
			||||||
	r *http.Request) bool {
 | 
						r *http.Request) bool {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return user != nil && user.AuthSource == AuthViaLocalPassword
 | 
						return user != nil && user.AuthSource == schema.AuthViaLocalPassword
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (la *LocalAuthenticator) Login(
 | 
					func (la *LocalAuthenticator) Login(
 | 
				
			||||||
	user *User,
 | 
						user *schema.User,
 | 
				
			||||||
	rw http.ResponseWriter,
 | 
						rw http.ResponseWriter,
 | 
				
			||||||
	r *http.Request) (*User, error) {
 | 
						r *http.Request) (*schema.User, error) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if e := bcrypt.CompareHashAndPassword([]byte(user.Password),
 | 
						if e := bcrypt.CompareHashAndPassword([]byte(user.Password),
 | 
				
			||||||
		[]byte(r.FormValue("password"))); e != nil {
 | 
							[]byte(r.FormValue("password"))); e != nil {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,289 +0,0 @@
 | 
				
			|||||||
// Copyright (C) 2022 NHR@FAU, University Erlangen-Nuremberg.
 | 
					 | 
				
			||||||
// All rights reserved.
 | 
					 | 
				
			||||||
// Use of this source code is governed by a MIT-style
 | 
					 | 
				
			||||||
// license that can be found in the LICENSE file.
 | 
					 | 
				
			||||||
package auth
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"context"
 | 
					 | 
				
			||||||
	"database/sql"
 | 
					 | 
				
			||||||
	"encoding/json"
 | 
					 | 
				
			||||||
	"errors"
 | 
					 | 
				
			||||||
	"fmt"
 | 
					 | 
				
			||||||
	"strings"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"github.com/ClusterCockpit/cc-backend/internal/graph/model"
 | 
					 | 
				
			||||||
	"github.com/ClusterCockpit/cc-backend/pkg/log"
 | 
					 | 
				
			||||||
	sq "github.com/Masterminds/squirrel"
 | 
					 | 
				
			||||||
	"github.com/jmoiron/sqlx"
 | 
					 | 
				
			||||||
	"golang.org/x/crypto/bcrypt"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (auth *Authentication) GetUser(username string) (*User, error) {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	user := &User{Username: username}
 | 
					 | 
				
			||||||
	var hashedPassword, name, rawRoles, email, rawProjects sql.NullString
 | 
					 | 
				
			||||||
	if err := sq.Select("password", "ldap", "name", "roles", "email", "projects").From("user").
 | 
					 | 
				
			||||||
		Where("user.username = ?", username).RunWith(auth.db).
 | 
					 | 
				
			||||||
		QueryRow().Scan(&hashedPassword, &user.AuthSource, &name, &rawRoles, &email, &rawProjects); err != nil {
 | 
					 | 
				
			||||||
		log.Warnf("Error while querying user '%v' from database", username)
 | 
					 | 
				
			||||||
		return nil, err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	user.Password = hashedPassword.String
 | 
					 | 
				
			||||||
	user.Name = name.String
 | 
					 | 
				
			||||||
	user.Email = email.String
 | 
					 | 
				
			||||||
	if rawRoles.Valid {
 | 
					 | 
				
			||||||
		if err := json.Unmarshal([]byte(rawRoles.String), &user.Roles); err != nil {
 | 
					 | 
				
			||||||
			log.Warn("Error while unmarshaling raw roles from DB")
 | 
					 | 
				
			||||||
			return nil, err
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if rawProjects.Valid {
 | 
					 | 
				
			||||||
		if err := json.Unmarshal([]byte(rawProjects.String), &user.Projects); err != nil {
 | 
					 | 
				
			||||||
			return nil, err
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return user, nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (auth *Authentication) AddUser(user *User) error {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	rolesJson, _ := json.Marshal(user.Roles)
 | 
					 | 
				
			||||||
	projectsJson, _ := json.Marshal(user.Projects)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	cols := []string{"username", "roles", "projects"}
 | 
					 | 
				
			||||||
	vals := []interface{}{user.Username, string(rolesJson), string(projectsJson)}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if user.Name != "" {
 | 
					 | 
				
			||||||
		cols = append(cols, "name")
 | 
					 | 
				
			||||||
		vals = append(vals, user.Name)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if user.Email != "" {
 | 
					 | 
				
			||||||
		cols = append(cols, "email")
 | 
					 | 
				
			||||||
		vals = append(vals, user.Email)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if user.Password != "" {
 | 
					 | 
				
			||||||
		password, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			log.Error("Error while encrypting new user password")
 | 
					 | 
				
			||||||
			return err
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		cols = append(cols, "password")
 | 
					 | 
				
			||||||
		vals = append(vals, string(password))
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if _, err := sq.Insert("user").Columns(cols...).Values(vals...).RunWith(auth.db).Exec(); err != nil {
 | 
					 | 
				
			||||||
		log.Errorf("Error while inserting new user '%v' into DB", user.Username)
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	log.Infof("new user %#v created (roles: %s, auth-source: %d, projects: %s)", user.Username, rolesJson, user.AuthSource, projectsJson)
 | 
					 | 
				
			||||||
	return nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (auth *Authentication) DelUser(username string) error {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	_, err := auth.db.Exec(`DELETE FROM user WHERE user.username = ?`, username)
 | 
					 | 
				
			||||||
	log.Errorf("Error while deleting user '%s' from DB", username)
 | 
					 | 
				
			||||||
	return err
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (auth *Authentication) ListUsers(specialsOnly bool) ([]*User, error) {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	q := sq.Select("username", "name", "email", "roles", "projects").From("user")
 | 
					 | 
				
			||||||
	if specialsOnly {
 | 
					 | 
				
			||||||
		q = q.Where("(roles != '[\"user\"]' AND roles != '[]')")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	rows, err := q.RunWith(auth.db).Query()
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		log.Warn("Error while querying user list")
 | 
					 | 
				
			||||||
		return nil, err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	users := make([]*User, 0)
 | 
					 | 
				
			||||||
	defer rows.Close()
 | 
					 | 
				
			||||||
	for rows.Next() {
 | 
					 | 
				
			||||||
		rawroles := ""
 | 
					 | 
				
			||||||
		rawprojects := ""
 | 
					 | 
				
			||||||
		user := &User{}
 | 
					 | 
				
			||||||
		var name, email sql.NullString
 | 
					 | 
				
			||||||
		if err := rows.Scan(&user.Username, &name, &email, &rawroles, &rawprojects); err != nil {
 | 
					 | 
				
			||||||
			log.Warn("Error while scanning user list")
 | 
					 | 
				
			||||||
			return nil, err
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if err := json.Unmarshal([]byte(rawroles), &user.Roles); err != nil {
 | 
					 | 
				
			||||||
			log.Warn("Error while unmarshaling raw role list")
 | 
					 | 
				
			||||||
			return nil, err
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if err := json.Unmarshal([]byte(rawprojects), &user.Projects); err != nil {
 | 
					 | 
				
			||||||
			return nil, err
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		user.Name = name.String
 | 
					 | 
				
			||||||
		user.Email = email.String
 | 
					 | 
				
			||||||
		users = append(users, user)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return users, nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (auth *Authentication) AddRole(
 | 
					 | 
				
			||||||
	ctx context.Context,
 | 
					 | 
				
			||||||
	username string,
 | 
					 | 
				
			||||||
	queryrole string) error {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	newRole := strings.ToLower(queryrole)
 | 
					 | 
				
			||||||
	user, err := auth.GetUser(username)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		log.Warnf("Could not load user '%s'", username)
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	exists, valid := user.HasValidRole(newRole)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if !valid {
 | 
					 | 
				
			||||||
		return fmt.Errorf("Supplied role is no valid option : %v", newRole)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if exists {
 | 
					 | 
				
			||||||
		return fmt.Errorf("User %v already has role %v", username, newRole)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	roles, _ := json.Marshal(append(user.Roles, newRole))
 | 
					 | 
				
			||||||
	if _, err := sq.Update("user").Set("roles", roles).Where("user.username = ?", username).RunWith(auth.db).Exec(); err != nil {
 | 
					 | 
				
			||||||
		log.Errorf("Error while adding new role for user '%s'", user.Username)
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (auth *Authentication) RemoveRole(ctx context.Context, username string, queryrole string) error {
 | 
					 | 
				
			||||||
	oldRole := strings.ToLower(queryrole)
 | 
					 | 
				
			||||||
	user, err := auth.GetUser(username)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		log.Warnf("Could not load user '%s'", username)
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	exists, valid := user.HasValidRole(oldRole)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if !valid {
 | 
					 | 
				
			||||||
		return fmt.Errorf("Supplied role is no valid option : %v", oldRole)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if !exists {
 | 
					 | 
				
			||||||
		return fmt.Errorf("Role already deleted for user '%v': %v", username, oldRole)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if oldRole == GetRoleString(RoleManager) && len(user.Projects) != 0 {
 | 
					 | 
				
			||||||
		return fmt.Errorf("Cannot remove role 'manager' while user %s still has assigned project(s) : %v", username, user.Projects)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	var newroles []string
 | 
					 | 
				
			||||||
	for _, r := range user.Roles {
 | 
					 | 
				
			||||||
		if r != oldRole {
 | 
					 | 
				
			||||||
			newroles = append(newroles, r) // Append all roles not matching requested to be deleted role
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	var mroles, _ = json.Marshal(newroles)
 | 
					 | 
				
			||||||
	if _, err := sq.Update("user").Set("roles", mroles).Where("user.username = ?", username).RunWith(auth.db).Exec(); err != nil {
 | 
					 | 
				
			||||||
		log.Errorf("Error while removing role for user '%s'", user.Username)
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (auth *Authentication) AddProject(
 | 
					 | 
				
			||||||
	ctx context.Context,
 | 
					 | 
				
			||||||
	username string,
 | 
					 | 
				
			||||||
	project string) error {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	user, err := auth.GetUser(username)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if !user.HasRole(RoleManager) {
 | 
					 | 
				
			||||||
		return fmt.Errorf("user '%s' is not a manager!", username)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if user.HasProject(project) {
 | 
					 | 
				
			||||||
		return fmt.Errorf("user '%s' already manages project '%s'", username, project)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	projects, _ := json.Marshal(append(user.Projects, project))
 | 
					 | 
				
			||||||
	if _, err := sq.Update("user").Set("projects", projects).Where("user.username = ?", username).RunWith(auth.db).Exec(); err != nil {
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (auth *Authentication) RemoveProject(ctx context.Context, username string, project string) error {
 | 
					 | 
				
			||||||
	user, err := auth.GetUser(username)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if !user.HasRole(RoleManager) {
 | 
					 | 
				
			||||||
		return fmt.Errorf("user '%#v' is not a manager!", username)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if !user.HasProject(project) {
 | 
					 | 
				
			||||||
		return fmt.Errorf("user '%#v': Cannot remove project '%#v' - Does not match!", username, project)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	var exists bool
 | 
					 | 
				
			||||||
	var newprojects []string
 | 
					 | 
				
			||||||
	for _, p := range user.Projects {
 | 
					 | 
				
			||||||
		if p != project {
 | 
					 | 
				
			||||||
			newprojects = append(newprojects, p) // Append all projects not matching requested to be deleted project
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			exists = true
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if exists == true {
 | 
					 | 
				
			||||||
		var result interface{}
 | 
					 | 
				
			||||||
		if len(newprojects) == 0 {
 | 
					 | 
				
			||||||
			result = "[]"
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			result, _ = json.Marshal(newprojects)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		if _, err := sq.Update("user").Set("projects", result).Where("user.username = ?", username).RunWith(auth.db).Exec(); err != nil {
 | 
					 | 
				
			||||||
			return err
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		return nil
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		return fmt.Errorf("user %s already does not manage project %s", username, project)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func FetchUser(ctx context.Context, db *sqlx.DB, username string) (*model.User, error) {
 | 
					 | 
				
			||||||
	me := GetUser(ctx)
 | 
					 | 
				
			||||||
	if me != nil && me.Username != username && me.HasNotRoles([]Role{RoleAdmin, RoleSupport, RoleManager}) {
 | 
					 | 
				
			||||||
		return nil, errors.New("forbidden")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	user := &model.User{Username: username}
 | 
					 | 
				
			||||||
	var name, email sql.NullString
 | 
					 | 
				
			||||||
	if err := sq.Select("name", "email").From("user").Where("user.username = ?", username).
 | 
					 | 
				
			||||||
		RunWith(db).QueryRow().Scan(&name, &email); err != nil {
 | 
					 | 
				
			||||||
		if err == sql.ErrNoRows {
 | 
					 | 
				
			||||||
			/* This warning will be logged *often* for non-local users, i.e. users mentioned only in job-table or archive, */
 | 
					 | 
				
			||||||
			/* since FetchUser will be called to retrieve full name and mail for every job in query/list									 */
 | 
					 | 
				
			||||||
			// log.Warnf("User '%s' Not found in DB", username)
 | 
					 | 
				
			||||||
			return nil, nil
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		log.Warnf("Error while fetching user '%s'", username)
 | 
					 | 
				
			||||||
		return nil, err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	user.Name = name.String
 | 
					 | 
				
			||||||
	user.Email = email.String
 | 
					 | 
				
			||||||
	return user, nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -11,7 +11,6 @@ import (
 | 
				
			|||||||
	"strconv"
 | 
						"strconv"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/ClusterCockpit/cc-backend/internal/auth"
 | 
					 | 
				
			||||||
	"github.com/ClusterCockpit/cc-backend/internal/graph/generated"
 | 
						"github.com/ClusterCockpit/cc-backend/internal/graph/generated"
 | 
				
			||||||
	"github.com/ClusterCockpit/cc-backend/internal/graph/model"
 | 
						"github.com/ClusterCockpit/cc-backend/internal/graph/model"
 | 
				
			||||||
	"github.com/ClusterCockpit/cc-backend/internal/metricdata"
 | 
						"github.com/ClusterCockpit/cc-backend/internal/metricdata"
 | 
				
			||||||
@@ -51,7 +50,7 @@ func (r *jobResolver) MetaData(ctx context.Context, obj *schema.Job) (interface{
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// UserData is the resolver for the userData field.
 | 
					// UserData is the resolver for the userData field.
 | 
				
			||||||
func (r *jobResolver) UserData(ctx context.Context, obj *schema.Job) (*model.User, error) {
 | 
					func (r *jobResolver) UserData(ctx context.Context, obj *schema.Job) (*model.User, error) {
 | 
				
			||||||
	return auth.FetchUser(ctx, r.DB, obj.User)
 | 
						return repository.GetUserRepository().FetchUserInCtx(ctx, obj.User)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// CreateTag is the resolver for the createTag field.
 | 
					// CreateTag is the resolver for the createTag field.
 | 
				
			||||||
@@ -122,7 +121,7 @@ func (r *mutationResolver) RemoveTagsFromJob(ctx context.Context, job string, ta
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// UpdateConfiguration is the resolver for the updateConfiguration field.
 | 
					// UpdateConfiguration is the resolver for the updateConfiguration field.
 | 
				
			||||||
func (r *mutationResolver) UpdateConfiguration(ctx context.Context, name string, value string) (*string, error) {
 | 
					func (r *mutationResolver) UpdateConfiguration(ctx context.Context, name string, value string) (*string, error) {
 | 
				
			||||||
	if err := repository.GetUserCfgRepo().UpdateConfig(name, value, auth.GetUser(ctx)); err != nil {
 | 
						if err := repository.GetUserCfgRepo().UpdateConfig(name, value, repository.GetUserFromContext(ctx)); err != nil {
 | 
				
			||||||
		log.Warn("Error while updating user config")
 | 
							log.Warn("Error while updating user config")
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -142,7 +141,7 @@ func (r *queryResolver) Tags(ctx context.Context) ([]*schema.Tag, error) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// User is the resolver for the user field.
 | 
					// User is the resolver for the user field.
 | 
				
			||||||
func (r *queryResolver) User(ctx context.Context, username string) (*model.User, error) {
 | 
					func (r *queryResolver) User(ctx context.Context, username string) (*model.User, error) {
 | 
				
			||||||
	return auth.FetchUser(ctx, r.DB, username)
 | 
						return repository.GetUserRepository().FetchUserInCtx(ctx, username)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// AllocatedNodes is the resolver for the allocatedNodes field.
 | 
					// AllocatedNodes is the resolver for the allocatedNodes field.
 | 
				
			||||||
@@ -178,7 +177,9 @@ func (r *queryResolver) Job(ctx context.Context, id string) (*schema.Job, error)
 | 
				
			|||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if user := auth.GetUser(ctx); user != nil && job.User != user.Username && user.HasNotRoles([]auth.Role{auth.RoleAdmin, auth.RoleSupport, auth.RoleManager}) {
 | 
						if user := repository.GetUserFromContext(ctx); user != nil &&
 | 
				
			||||||
 | 
							job.User != user.Username &&
 | 
				
			||||||
 | 
							user.HasNotRoles([]schema.Role{schema.RoleAdmin, schema.RoleSupport, schema.RoleManager}) {
 | 
				
			||||||
		return nil, errors.New("you are not allowed to see this job")
 | 
							return nil, errors.New("you are not allowed to see this job")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -318,8 +319,8 @@ func (r *queryResolver) RooflineHeatmap(ctx context.Context, filter []*model.Job
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// NodeMetrics is the resolver for the nodeMetrics field.
 | 
					// NodeMetrics is the resolver for the nodeMetrics field.
 | 
				
			||||||
func (r *queryResolver) NodeMetrics(ctx context.Context, cluster string, nodes []string, scopes []schema.MetricScope, metrics []string, from time.Time, to time.Time) ([]*model.NodeMetrics, error) {
 | 
					func (r *queryResolver) NodeMetrics(ctx context.Context, cluster string, nodes []string, scopes []schema.MetricScope, metrics []string, from time.Time, to time.Time) ([]*model.NodeMetrics, error) {
 | 
				
			||||||
	user := auth.GetUser(ctx)
 | 
						user := repository.GetUserFromContext(ctx)
 | 
				
			||||||
	if user != nil && !user.HasRole(auth.RoleAdmin) {
 | 
						if user != nil && !user.HasRole(schema.RoleAdmin) {
 | 
				
			||||||
		return nil, errors.New("you need to be an administrator for this query")
 | 
							return nil, errors.New("you need to be an administrator for this query")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,7 +14,6 @@ import (
 | 
				
			|||||||
	"sync"
 | 
						"sync"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/ClusterCockpit/cc-backend/internal/auth"
 | 
					 | 
				
			||||||
	"github.com/ClusterCockpit/cc-backend/internal/graph/model"
 | 
						"github.com/ClusterCockpit/cc-backend/internal/graph/model"
 | 
				
			||||||
	"github.com/ClusterCockpit/cc-backend/internal/metricdata"
 | 
						"github.com/ClusterCockpit/cc-backend/internal/metricdata"
 | 
				
			||||||
	"github.com/ClusterCockpit/cc-backend/pkg/log"
 | 
						"github.com/ClusterCockpit/cc-backend/pkg/log"
 | 
				
			||||||
@@ -615,7 +614,7 @@ func (r *JobRepository) WaitForArchiving() {
 | 
				
			|||||||
	r.archivePending.Wait()
 | 
						r.archivePending.Wait()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (r *JobRepository) FindUserOrProjectOrJobname(user *auth.User, searchterm string) (jobid string, username string, project string, jobname string) {
 | 
					func (r *JobRepository) FindUserOrProjectOrJobname(user *schema.User, searchterm string) (jobid string, username string, project string, jobname string) {
 | 
				
			||||||
	if _, err := strconv.Atoi(searchterm); err == nil { // Return empty on successful conversion: parent method will redirect for integer jobId
 | 
						if _, err := strconv.Atoi(searchterm); err == nil { // Return empty on successful conversion: parent method will redirect for integer jobId
 | 
				
			||||||
		return searchterm, "", "", ""
 | 
							return searchterm, "", "", ""
 | 
				
			||||||
	} else { // Has to have letters and logged-in user for other guesses
 | 
						} else { // Has to have letters and logged-in user for other guesses
 | 
				
			||||||
@@ -644,14 +643,14 @@ func (r *JobRepository) FindUserOrProjectOrJobname(user *auth.User, searchterm s
 | 
				
			|||||||
var ErrNotFound = errors.New("no such jobname, project or user")
 | 
					var ErrNotFound = errors.New("no such jobname, project or user")
 | 
				
			||||||
var ErrForbidden = errors.New("not authorized")
 | 
					var ErrForbidden = errors.New("not authorized")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (r *JobRepository) FindColumnValue(user *auth.User, searchterm string, table string, selectColumn string, whereColumn string, isLike bool) (result string, err error) {
 | 
					func (r *JobRepository) FindColumnValue(user *schema.User, searchterm string, table string, selectColumn string, whereColumn string, isLike bool) (result string, err error) {
 | 
				
			||||||
	compareStr := " = ?"
 | 
						compareStr := " = ?"
 | 
				
			||||||
	query := searchterm
 | 
						query := searchterm
 | 
				
			||||||
	if isLike {
 | 
						if isLike {
 | 
				
			||||||
		compareStr = " LIKE ?"
 | 
							compareStr = " LIKE ?"
 | 
				
			||||||
		query = "%" + searchterm + "%"
 | 
							query = "%" + searchterm + "%"
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if user.HasAnyRole([]auth.Role{auth.RoleAdmin, auth.RoleSupport, auth.RoleManager}) {
 | 
						if user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport, schema.RoleManager}) {
 | 
				
			||||||
		theQuery := sq.Select(table+"."+selectColumn).Distinct().From(table).
 | 
							theQuery := sq.Select(table+"."+selectColumn).Distinct().From(table).
 | 
				
			||||||
			Where(table+"."+whereColumn+compareStr, query)
 | 
								Where(table+"."+whereColumn+compareStr, query)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -676,9 +675,9 @@ func (r *JobRepository) FindColumnValue(user *auth.User, searchterm string, tabl
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (r *JobRepository) FindColumnValues(user *auth.User, query string, table string, selectColumn string, whereColumn string) (results []string, err error) {
 | 
					func (r *JobRepository) FindColumnValues(user *schema.User, query string, table string, selectColumn string, whereColumn string) (results []string, err error) {
 | 
				
			||||||
	emptyResult := make([]string, 0)
 | 
						emptyResult := make([]string, 0)
 | 
				
			||||||
	if user.HasAnyRole([]auth.Role{auth.RoleAdmin, auth.RoleSupport, auth.RoleManager}) {
 | 
						if user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport, schema.RoleManager}) {
 | 
				
			||||||
		rows, err := sq.Select(table+"."+selectColumn).Distinct().From(table).
 | 
							rows, err := sq.Select(table+"."+selectColumn).Distinct().From(table).
 | 
				
			||||||
			Where(table+"."+whereColumn+" LIKE ?", fmt.Sprint("%", query, "%")).
 | 
								Where(table+"."+whereColumn+" LIKE ?", fmt.Sprint("%", query, "%")).
 | 
				
			||||||
			RunWith(r.stmtCache).Query()
 | 
								RunWith(r.stmtCache).Query()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,7 +12,6 @@ import (
 | 
				
			|||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/ClusterCockpit/cc-backend/internal/auth"
 | 
					 | 
				
			||||||
	"github.com/ClusterCockpit/cc-backend/internal/graph/model"
 | 
						"github.com/ClusterCockpit/cc-backend/internal/graph/model"
 | 
				
			||||||
	"github.com/ClusterCockpit/cc-backend/pkg/log"
 | 
						"github.com/ClusterCockpit/cc-backend/pkg/log"
 | 
				
			||||||
	"github.com/ClusterCockpit/cc-backend/pkg/schema"
 | 
						"github.com/ClusterCockpit/cc-backend/pkg/schema"
 | 
				
			||||||
@@ -130,20 +129,20 @@ func (r *JobRepository) CountJobs(
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func SecurityCheck(ctx context.Context, query sq.SelectBuilder) (sq.SelectBuilder, error) {
 | 
					func SecurityCheck(ctx context.Context, query sq.SelectBuilder) (sq.SelectBuilder, error) {
 | 
				
			||||||
	user := auth.GetUser(ctx)
 | 
						user := GetUserFromContext(ctx)
 | 
				
			||||||
	if user == nil {
 | 
						if user == nil {
 | 
				
			||||||
		var qnil sq.SelectBuilder
 | 
							var qnil sq.SelectBuilder
 | 
				
			||||||
		return qnil, fmt.Errorf("user context is nil!")
 | 
							return qnil, fmt.Errorf("user context is nil!")
 | 
				
			||||||
	} else if user.HasAnyRole([]auth.Role{auth.RoleAdmin, auth.RoleSupport, auth.RoleApi}) { // Admin & Co. : All jobs
 | 
						} else if user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport, schema.RoleApi}) { // Admin & Co. : All jobs
 | 
				
			||||||
		return query, nil
 | 
							return query, nil
 | 
				
			||||||
	} else if user.HasRole(auth.RoleManager) { // Manager : Add filter for managed projects' jobs only + personal jobs
 | 
						} else if user.HasRole(schema.RoleManager) { // Manager : Add filter for managed projects' jobs only + personal jobs
 | 
				
			||||||
		if len(user.Projects) != 0 {
 | 
							if len(user.Projects) != 0 {
 | 
				
			||||||
			return query.Where(sq.Or{sq.Eq{"job.project": user.Projects}, sq.Eq{"job.user": user.Username}}), nil
 | 
								return query.Where(sq.Or{sq.Eq{"job.project": user.Projects}, sq.Eq{"job.user": user.Username}}), nil
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			log.Debugf("Manager-User '%s' has no defined projects to lookup! Query only personal jobs ...", user.Username)
 | 
								log.Debugf("Manager-User '%s' has no defined projects to lookup! Query only personal jobs ...", user.Username)
 | 
				
			||||||
			return query.Where("job.user = ?", user.Username), nil
 | 
								return query.Where("job.user = ?", user.Username), nil
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	} else if user.HasRole(auth.RoleUser) { // User : Only personal jobs
 | 
						} else if user.HasRole(schema.RoleUser) { // User : Only personal jobs
 | 
				
			||||||
		return query.Where("job.user = ?", user.Username), nil
 | 
							return query.Where("job.user = ?", user.Username), nil
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		// Shortterm compatibility: Return User-Query if no roles:
 | 
							// Shortterm compatibility: Return User-Query if no roles:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,7 +10,6 @@ import (
 | 
				
			|||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/ClusterCockpit/cc-backend/internal/auth"
 | 
					 | 
				
			||||||
	"github.com/ClusterCockpit/cc-backend/internal/config"
 | 
						"github.com/ClusterCockpit/cc-backend/internal/config"
 | 
				
			||||||
	"github.com/ClusterCockpit/cc-backend/internal/graph/model"
 | 
						"github.com/ClusterCockpit/cc-backend/internal/graph/model"
 | 
				
			||||||
	"github.com/ClusterCockpit/cc-backend/pkg/log"
 | 
						"github.com/ClusterCockpit/cc-backend/pkg/log"
 | 
				
			||||||
@@ -86,7 +85,7 @@ func (r *JobRepository) buildStatsQuery(
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (r *JobRepository) getUserName(ctx context.Context, id string) string {
 | 
					func (r *JobRepository) getUserName(ctx context.Context, id string) string {
 | 
				
			||||||
	user := auth.GetUser(ctx)
 | 
						user := GetUserFromContext(ctx)
 | 
				
			||||||
	name, _ := r.FindColumnValue(user, id, "user", "name", "username", false)
 | 
						name, _ := r.FindColumnValue(user, id, "user", "name", "username", false)
 | 
				
			||||||
	if name != "" {
 | 
						if name != "" {
 | 
				
			||||||
		return name
 | 
							return name
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,7 +7,6 @@ package repository
 | 
				
			|||||||
import (
 | 
					import (
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/ClusterCockpit/cc-backend/internal/auth"
 | 
					 | 
				
			||||||
	"github.com/ClusterCockpit/cc-backend/pkg/archive"
 | 
						"github.com/ClusterCockpit/cc-backend/pkg/archive"
 | 
				
			||||||
	"github.com/ClusterCockpit/cc-backend/pkg/log"
 | 
						"github.com/ClusterCockpit/cc-backend/pkg/log"
 | 
				
			||||||
	"github.com/ClusterCockpit/cc-backend/pkg/schema"
 | 
						"github.com/ClusterCockpit/cc-backend/pkg/schema"
 | 
				
			||||||
@@ -68,7 +67,7 @@ func (r *JobRepository) CreateTag(tagType string, tagName string) (tagId int64,
 | 
				
			|||||||
	return res.LastInsertId()
 | 
						return res.LastInsertId()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (r *JobRepository) CountTags(user *auth.User) (tags []schema.Tag, counts map[string]int, err error) {
 | 
					func (r *JobRepository) CountTags(user *schema.User) (tags []schema.Tag, counts map[string]int, err error) {
 | 
				
			||||||
	tags = make([]schema.Tag, 0, 100)
 | 
						tags = make([]schema.Tag, 0, 100)
 | 
				
			||||||
	xrows, err := r.DB.Queryx("SELECT id, tag_type, tag_name FROM tag")
 | 
						xrows, err := r.DB.Queryx("SELECT id, tag_type, tag_name FROM tag")
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
@@ -88,10 +87,10 @@ func (r *JobRepository) CountTags(user *auth.User) (tags []schema.Tag, counts ma
 | 
				
			|||||||
		LeftJoin("jobtag jt ON t.id = jt.tag_id").
 | 
							LeftJoin("jobtag jt ON t.id = jt.tag_id").
 | 
				
			||||||
		GroupBy("t.tag_name")
 | 
							GroupBy("t.tag_name")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if user != nil && user.HasAnyRole([]auth.Role{auth.RoleAdmin, auth.RoleSupport}) { // ADMIN || SUPPORT: Count all jobs
 | 
						if user != nil && user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport}) { // ADMIN || SUPPORT: Count all jobs
 | 
				
			||||||
		log.Debug("CountTags: User Admin or Support -> Count all Jobs for Tags")
 | 
							log.Debug("CountTags: User Admin or Support -> Count all Jobs for Tags")
 | 
				
			||||||
		// Unchanged: Needs to be own case still, due to UserRole/NoRole compatibility handling in else case
 | 
							// Unchanged: Needs to be own case still, due to UserRole/NoRole compatibility handling in else case
 | 
				
			||||||
	} else if user != nil && user.HasRole(auth.RoleManager) { // MANAGER: Count own jobs plus project's jobs
 | 
						} else if user != nil && user.HasRole(schema.RoleManager) { // MANAGER: Count own jobs plus project's jobs
 | 
				
			||||||
		// Build ("project1", "project2", ...) list of variable length directly in SQL string
 | 
							// Build ("project1", "project2", ...) list of variable length directly in SQL string
 | 
				
			||||||
		q = q.Where("jt.job_id IN (SELECT id FROM job WHERE job.user = ? OR job.project IN (\""+strings.Join(user.Projects, "\",\"")+"\"))", user.Username)
 | 
							q = q.Where("jt.job_id IN (SELECT id FROM job WHERE job.user = ? OR job.project IN (\""+strings.Join(user.Projects, "\",\"")+"\"))", user.Username)
 | 
				
			||||||
	} else if user != nil { // USER OR NO ROLE (Compatibility): Only count own jobs
 | 
						} else if user != nil { // USER OR NO ROLE (Compatibility): Only count own jobs
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,137 +1,325 @@
 | 
				
			|||||||
// Copyright (C) 2022 NHR@FAU, University Erlangen-Nuremberg.
 | 
					// Copyright (C) 2023 NHR@FAU, University Erlangen-Nuremberg.
 | 
				
			||||||
// All rights reserved.
 | 
					// All rights reserved.
 | 
				
			||||||
// Use of this source code is governed by a MIT-style
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
// license that can be found in the LICENSE file.
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
package repository
 | 
					package repository
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"database/sql"
 | 
				
			||||||
	"encoding/json"
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
	"sync"
 | 
						"sync"
 | 
				
			||||||
	"time"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/ClusterCockpit/cc-backend/internal/auth"
 | 
						"github.com/ClusterCockpit/cc-backend/internal/graph/model"
 | 
				
			||||||
	"github.com/ClusterCockpit/cc-backend/internal/config"
 | 
					 | 
				
			||||||
	"github.com/ClusterCockpit/cc-backend/pkg/log"
 | 
						"github.com/ClusterCockpit/cc-backend/pkg/log"
 | 
				
			||||||
	"github.com/ClusterCockpit/cc-backend/pkg/lrucache"
 | 
						"github.com/ClusterCockpit/cc-backend/pkg/schema"
 | 
				
			||||||
 | 
						sq "github.com/Masterminds/squirrel"
 | 
				
			||||||
	"github.com/jmoiron/sqlx"
 | 
						"github.com/jmoiron/sqlx"
 | 
				
			||||||
 | 
						"golang.org/x/crypto/bcrypt"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var (
 | 
					var (
 | 
				
			||||||
	userCfgRepoOnce     sync.Once
 | 
						userRepoOnce     sync.Once
 | 
				
			||||||
	userCfgRepoInstance *UserCfgRepo
 | 
						userRepoInstance *UserRepository
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type UserCfgRepo struct {
 | 
					type UserRepository struct {
 | 
				
			||||||
	DB     *sqlx.DB
 | 
						DB     *sqlx.DB
 | 
				
			||||||
	Lookup     *sqlx.Stmt
 | 
						driver string
 | 
				
			||||||
	lock       sync.RWMutex
 | 
					 | 
				
			||||||
	uiDefaults map[string]interface{}
 | 
					 | 
				
			||||||
	cache      *lrucache.Cache
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func GetUserCfgRepo() *UserCfgRepo {
 | 
					func GetUserRepository() *UserRepository {
 | 
				
			||||||
	userCfgRepoOnce.Do(func() {
 | 
						userRepoOnce.Do(func() {
 | 
				
			||||||
		db := GetConnection()
 | 
							db := GetConnection()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		lookupConfigStmt, err := db.DB.Preparex(`SELECT confkey, value FROM configuration WHERE configuration.username = ?`)
 | 
							userRepoInstance = &UserRepository{
 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			log.Fatalf("db.DB.Preparex() error: %v", err)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		userCfgRepoInstance = &UserCfgRepo{
 | 
					 | 
				
			||||||
			DB:     db.DB,
 | 
								DB:     db.DB,
 | 
				
			||||||
			Lookup:     lookupConfigStmt,
 | 
								driver: db.Driver,
 | 
				
			||||||
			uiDefaults: config.Keys.UiDefaults,
 | 
					 | 
				
			||||||
			cache:      lrucache.New(1024),
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 | 
						return userRepoInstance
 | 
				
			||||||
	return userCfgRepoInstance
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Return the personalised UI config for the currently authenticated
 | 
					func (r *UserRepository) GetUser(username string) (*schema.User, error) {
 | 
				
			||||||
// user or return the plain default config.
 | 
						user := &schema.User{Username: username}
 | 
				
			||||||
func (uCfg *UserCfgRepo) GetUIConfig(user *auth.User) (map[string]interface{}, error) {
 | 
						var hashedPassword, name, rawRoles, email, rawProjects sql.NullString
 | 
				
			||||||
	if user == nil {
 | 
						if err := sq.Select("password", "ldap", "name", "roles", "email", "projects").From("user").
 | 
				
			||||||
		uCfg.lock.RLock()
 | 
							Where("user.username = ?", username).RunWith(r.DB).
 | 
				
			||||||
		copy := make(map[string]interface{}, len(uCfg.uiDefaults))
 | 
							QueryRow().Scan(&hashedPassword, &user.AuthSource, &name, &rawRoles, &email, &rawProjects); err != nil {
 | 
				
			||||||
		for k, v := range uCfg.uiDefaults {
 | 
							log.Warnf("Error while querying user '%v' from database", username)
 | 
				
			||||||
			copy[k] = v
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		uCfg.lock.RUnlock()
 | 
					 | 
				
			||||||
		return copy, nil
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	data := uCfg.cache.Get(user.Username, func() (interface{}, time.Duration, int) {
 | 
					 | 
				
			||||||
		uiconfig := make(map[string]interface{}, len(uCfg.uiDefaults))
 | 
					 | 
				
			||||||
		for k, v := range uCfg.uiDefaults {
 | 
					 | 
				
			||||||
			uiconfig[k] = v
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		rows, err := uCfg.Lookup.Query(user.Username)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			log.Warnf("Error while looking up user uiconfig for user '%v'", user.Username)
 | 
					 | 
				
			||||||
			return err, 0, 0
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		size := 0
 | 
					 | 
				
			||||||
		defer rows.Close()
 | 
					 | 
				
			||||||
		for rows.Next() {
 | 
					 | 
				
			||||||
			var key, rawval string
 | 
					 | 
				
			||||||
			if err := rows.Scan(&key, &rawval); err != nil {
 | 
					 | 
				
			||||||
				log.Warn("Error while scanning user uiconfig values")
 | 
					 | 
				
			||||||
				return err, 0, 0
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			var val interface{}
 | 
					 | 
				
			||||||
			if err := json.Unmarshal([]byte(rawval), &val); err != nil {
 | 
					 | 
				
			||||||
				log.Warn("Error while unmarshaling raw user uiconfig json")
 | 
					 | 
				
			||||||
				return err, 0, 0
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			size += len(key)
 | 
					 | 
				
			||||||
			size += len(rawval)
 | 
					 | 
				
			||||||
			uiconfig[key] = val
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Add global ShortRunningJobsDuration setting as plot_list_hideShortRunningJobs
 | 
					 | 
				
			||||||
		uiconfig["plot_list_hideShortRunningJobs"] = config.Keys.ShortRunningJobsDuration
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		return uiconfig, 24 * time.Hour, size
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
	if err, ok := data.(error); ok {
 | 
					 | 
				
			||||||
		log.Error("Error in returned dataset")
 | 
					 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return data.(map[string]interface{}), nil
 | 
						user.Password = hashedPassword.String
 | 
				
			||||||
 | 
						user.Name = name.String
 | 
				
			||||||
 | 
						user.Email = email.String
 | 
				
			||||||
 | 
						if rawRoles.Valid {
 | 
				
			||||||
 | 
							if err := json.Unmarshal([]byte(rawRoles.String), &user.Roles); err != nil {
 | 
				
			||||||
 | 
								log.Warn("Error while unmarshaling raw roles from DB")
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if rawProjects.Valid {
 | 
				
			||||||
 | 
							if err := json.Unmarshal([]byte(rawProjects.String), &user.Projects); err != nil {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return user, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// If the context does not have a user, update the global ui configuration
 | 
					func (r *UserRepository) AddUser(user *schema.User) error {
 | 
				
			||||||
// without persisting it!  If there is a (authenticated) user, update only his
 | 
						rolesJson, _ := json.Marshal(user.Roles)
 | 
				
			||||||
// configuration.
 | 
						projectsJson, _ := json.Marshal(user.Projects)
 | 
				
			||||||
func (uCfg *UserCfgRepo) UpdateConfig(
 | 
					 | 
				
			||||||
	key, value string,
 | 
					 | 
				
			||||||
	user *auth.User) error {
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if user == nil {
 | 
						cols := []string{"username", "roles", "projects"}
 | 
				
			||||||
		var val interface{}
 | 
						vals := []interface{}{user.Username, string(rolesJson), string(projectsJson)}
 | 
				
			||||||
		if err := json.Unmarshal([]byte(value), &val); err != nil {
 | 
					
 | 
				
			||||||
			log.Warn("Error while unmarshaling raw user config json")
 | 
						if user.Name != "" {
 | 
				
			||||||
 | 
							cols = append(cols, "name")
 | 
				
			||||||
 | 
							vals = append(vals, user.Name)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if user.Email != "" {
 | 
				
			||||||
 | 
							cols = append(cols, "email")
 | 
				
			||||||
 | 
							vals = append(vals, user.Email)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if user.Password != "" {
 | 
				
			||||||
 | 
							password, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Error("Error while encrypting new user password")
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							cols = append(cols, "password")
 | 
				
			||||||
 | 
							vals = append(vals, string(password))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if _, err := sq.Insert("user").Columns(cols...).Values(vals...).RunWith(r.DB).Exec(); err != nil {
 | 
				
			||||||
 | 
							log.Errorf("Error while inserting new user '%v' into DB", user.Username)
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		uCfg.lock.Lock()
 | 
						log.Infof("new user %#v created (roles: %s, auth-source: %d, projects: %s)", user.Username, rolesJson, user.AuthSource, projectsJson)
 | 
				
			||||||
		defer uCfg.lock.Unlock()
 | 
					 | 
				
			||||||
		uCfg.uiDefaults[key] = val
 | 
					 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (r *UserRepository) DelUser(username string) error {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						_, err := r.DB.Exec(`DELETE FROM user WHERE user.username = ?`, username)
 | 
				
			||||||
 | 
						log.Errorf("Error while deleting user '%s' from DB", username)
 | 
				
			||||||
 | 
						return err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (r *UserRepository) ListUsers(specialsOnly bool) ([]*schema.User, error) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						q := sq.Select("username", "name", "email", "roles", "projects").From("user")
 | 
				
			||||||
 | 
						if specialsOnly {
 | 
				
			||||||
 | 
							q = q.Where("(roles != '[\"user\"]' AND roles != '[]')")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if _, err := uCfg.DB.Exec(`REPLACE INTO configuration (username, confkey, value) VALUES (?, ?, ?)`, user.Username, key, value); err != nil {
 | 
						rows, err := q.RunWith(r.DB).Query()
 | 
				
			||||||
		log.Warnf("Error while replacing user config in DB for user '%v'", user.Username)
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Warn("Error while querying user list")
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						users := make([]*schema.User, 0)
 | 
				
			||||||
 | 
						defer rows.Close()
 | 
				
			||||||
 | 
						for rows.Next() {
 | 
				
			||||||
 | 
							rawroles := ""
 | 
				
			||||||
 | 
							rawprojects := ""
 | 
				
			||||||
 | 
							user := &schema.User{}
 | 
				
			||||||
 | 
							var name, email sql.NullString
 | 
				
			||||||
 | 
							if err := rows.Scan(&user.Username, &name, &email, &rawroles, &rawprojects); err != nil {
 | 
				
			||||||
 | 
								log.Warn("Error while scanning user list")
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err := json.Unmarshal([]byte(rawroles), &user.Roles); err != nil {
 | 
				
			||||||
 | 
								log.Warn("Error while unmarshaling raw role list")
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err := json.Unmarshal([]byte(rawprojects), &user.Projects); err != nil {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							user.Name = name.String
 | 
				
			||||||
 | 
							user.Email = email.String
 | 
				
			||||||
 | 
							users = append(users, user)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return users, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (r *UserRepository) AddRole(
 | 
				
			||||||
 | 
						ctx context.Context,
 | 
				
			||||||
 | 
						username string,
 | 
				
			||||||
 | 
						queryrole string) error {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						newRole := strings.ToLower(queryrole)
 | 
				
			||||||
 | 
						user, err := r.GetUser(username)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Warnf("Could not load user '%s'", username)
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	uCfg.cache.Del(user.Username)
 | 
						exists, valid := user.HasValidRole(newRole)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if !valid {
 | 
				
			||||||
 | 
							return fmt.Errorf("Supplied role is no valid option : %v", newRole)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if exists {
 | 
				
			||||||
 | 
							return fmt.Errorf("User %v already has role %v", username, newRole)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						roles, _ := json.Marshal(append(user.Roles, newRole))
 | 
				
			||||||
 | 
						if _, err := sq.Update("user").Set("roles", roles).Where("user.username = ?", username).RunWith(r.DB).Exec(); err != nil {
 | 
				
			||||||
 | 
							log.Errorf("Error while adding new role for user '%s'", user.Username)
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (r *UserRepository) RemoveRole(ctx context.Context, username string, queryrole string) error {
 | 
				
			||||||
 | 
						oldRole := strings.ToLower(queryrole)
 | 
				
			||||||
 | 
						user, err := r.GetUser(username)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Warnf("Could not load user '%s'", username)
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						exists, valid := user.HasValidRole(oldRole)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if !valid {
 | 
				
			||||||
 | 
							return fmt.Errorf("Supplied role is no valid option : %v", oldRole)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if !exists {
 | 
				
			||||||
 | 
							return fmt.Errorf("Role already deleted for user '%v': %v", username, oldRole)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if oldRole == schema.GetRoleString(schema.RoleManager) && len(user.Projects) != 0 {
 | 
				
			||||||
 | 
							return fmt.Errorf("Cannot remove role 'manager' while user %s still has assigned project(s) : %v", username, user.Projects)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var newroles []string
 | 
				
			||||||
 | 
						for _, r := range user.Roles {
 | 
				
			||||||
 | 
							if r != oldRole {
 | 
				
			||||||
 | 
								newroles = append(newroles, r) // Append all roles not matching requested to be deleted role
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var mroles, _ = json.Marshal(newroles)
 | 
				
			||||||
 | 
						if _, err := sq.Update("user").Set("roles", mroles).Where("user.username = ?", username).RunWith(r.DB).Exec(); err != nil {
 | 
				
			||||||
 | 
							log.Errorf("Error while removing role for user '%s'", user.Username)
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (r *UserRepository) AddProject(
 | 
				
			||||||
 | 
						ctx context.Context,
 | 
				
			||||||
 | 
						username string,
 | 
				
			||||||
 | 
						project string) error {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						user, err := r.GetUser(username)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if !user.HasRole(schema.RoleManager) {
 | 
				
			||||||
 | 
							return fmt.Errorf("user '%s' is not a manager!", username)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if user.HasProject(project) {
 | 
				
			||||||
 | 
							return fmt.Errorf("user '%s' already manages project '%s'", username, project)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						projects, _ := json.Marshal(append(user.Projects, project))
 | 
				
			||||||
 | 
						if _, err := sq.Update("user").Set("projects", projects).Where("user.username = ?", username).RunWith(r.DB).Exec(); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (r *UserRepository) RemoveProject(ctx context.Context, username string, project string) error {
 | 
				
			||||||
 | 
						user, err := r.GetUser(username)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if !user.HasRole(schema.RoleManager) {
 | 
				
			||||||
 | 
							return fmt.Errorf("user '%#v' is not a manager!", username)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if !user.HasProject(project) {
 | 
				
			||||||
 | 
							return fmt.Errorf("user '%#v': Cannot remove project '%#v' - Does not match!", username, project)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var exists bool
 | 
				
			||||||
 | 
						var newprojects []string
 | 
				
			||||||
 | 
						for _, p := range user.Projects {
 | 
				
			||||||
 | 
							if p != project {
 | 
				
			||||||
 | 
								newprojects = append(newprojects, p) // Append all projects not matching requested to be deleted project
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								exists = true
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if exists == true {
 | 
				
			||||||
 | 
							var result interface{}
 | 
				
			||||||
 | 
							if len(newprojects) == 0 {
 | 
				
			||||||
 | 
								result = "[]"
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								result, _ = json.Marshal(newprojects)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if _, err := sq.Update("user").Set("projects", result).Where("user.username = ?", username).RunWith(r.DB).Exec(); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							return fmt.Errorf("user %s already does not manage project %s", username, project)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ContextKey string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ContextUserKey ContextKey = "user"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func GetUserFromContext(ctx context.Context) *schema.User {
 | 
				
			||||||
 | 
						x := ctx.Value(ContextUserKey)
 | 
				
			||||||
 | 
						if x == nil {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return x.(*schema.User)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (r *UserRepository) FetchUserInCtx(ctx context.Context, username string) (*model.User, error) {
 | 
				
			||||||
 | 
						me := GetUserFromContext(ctx)
 | 
				
			||||||
 | 
						if me != nil && me.Username != username &&
 | 
				
			||||||
 | 
							me.HasNotRoles([]schema.Role{schema.RoleAdmin, schema.RoleSupport, schema.RoleManager}) {
 | 
				
			||||||
 | 
							return nil, errors.New("forbidden")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						user := &model.User{Username: username}
 | 
				
			||||||
 | 
						var name, email sql.NullString
 | 
				
			||||||
 | 
						if err := sq.Select("name", "email").From("user").Where("user.username = ?", username).
 | 
				
			||||||
 | 
							RunWith(r.DB).QueryRow().Scan(&name, &email); err != nil {
 | 
				
			||||||
 | 
							if err == sql.ErrNoRows {
 | 
				
			||||||
 | 
								/* This warning will be logged *often* for non-local users, i.e. users mentioned only in job-table or archive, */
 | 
				
			||||||
 | 
								/* since FetchUser will be called to retrieve full name and mail for every job in query/list									 */
 | 
				
			||||||
 | 
								// log.Warnf("User '%s' Not found in DB", username)
 | 
				
			||||||
 | 
								return nil, nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							log.Warnf("Error while fetching user '%s'", username)
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						user.Name = name.String
 | 
				
			||||||
 | 
						user.Email = email.String
 | 
				
			||||||
 | 
						return user, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										137
									
								
								internal/repository/userConfig.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								internal/repository/userConfig.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,137 @@
 | 
				
			|||||||
 | 
					// Copyright (C) 2022 NHR@FAU, University Erlangen-Nuremberg.
 | 
				
			||||||
 | 
					// All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					package repository
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"sync"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/ClusterCockpit/cc-backend/internal/config"
 | 
				
			||||||
 | 
						"github.com/ClusterCockpit/cc-backend/pkg/log"
 | 
				
			||||||
 | 
						"github.com/ClusterCockpit/cc-backend/pkg/lrucache"
 | 
				
			||||||
 | 
						"github.com/ClusterCockpit/cc-backend/pkg/schema"
 | 
				
			||||||
 | 
						"github.com/jmoiron/sqlx"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						userCfgRepoOnce     sync.Once
 | 
				
			||||||
 | 
						userCfgRepoInstance *UserCfgRepo
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type UserCfgRepo struct {
 | 
				
			||||||
 | 
						DB         *sqlx.DB
 | 
				
			||||||
 | 
						Lookup     *sqlx.Stmt
 | 
				
			||||||
 | 
						lock       sync.RWMutex
 | 
				
			||||||
 | 
						uiDefaults map[string]interface{}
 | 
				
			||||||
 | 
						cache      *lrucache.Cache
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func GetUserCfgRepo() *UserCfgRepo {
 | 
				
			||||||
 | 
						userCfgRepoOnce.Do(func() {
 | 
				
			||||||
 | 
							db := GetConnection()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							lookupConfigStmt, err := db.DB.Preparex(`SELECT confkey, value FROM configuration WHERE configuration.username = ?`)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Fatalf("db.DB.Preparex() error: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							userCfgRepoInstance = &UserCfgRepo{
 | 
				
			||||||
 | 
								DB:         db.DB,
 | 
				
			||||||
 | 
								Lookup:     lookupConfigStmt,
 | 
				
			||||||
 | 
								uiDefaults: config.Keys.UiDefaults,
 | 
				
			||||||
 | 
								cache:      lrucache.New(1024),
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return userCfgRepoInstance
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Return the personalised UI config for the currently authenticated
 | 
				
			||||||
 | 
					// user or return the plain default config.
 | 
				
			||||||
 | 
					func (uCfg *UserCfgRepo) GetUIConfig(user *schema.User) (map[string]interface{}, error) {
 | 
				
			||||||
 | 
						if user == nil {
 | 
				
			||||||
 | 
							uCfg.lock.RLock()
 | 
				
			||||||
 | 
							copy := make(map[string]interface{}, len(uCfg.uiDefaults))
 | 
				
			||||||
 | 
							for k, v := range uCfg.uiDefaults {
 | 
				
			||||||
 | 
								copy[k] = v
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							uCfg.lock.RUnlock()
 | 
				
			||||||
 | 
							return copy, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						data := uCfg.cache.Get(user.Username, func() (interface{}, time.Duration, int) {
 | 
				
			||||||
 | 
							uiconfig := make(map[string]interface{}, len(uCfg.uiDefaults))
 | 
				
			||||||
 | 
							for k, v := range uCfg.uiDefaults {
 | 
				
			||||||
 | 
								uiconfig[k] = v
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							rows, err := uCfg.Lookup.Query(user.Username)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Warnf("Error while looking up user uiconfig for user '%v'", user.Username)
 | 
				
			||||||
 | 
								return err, 0, 0
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							size := 0
 | 
				
			||||||
 | 
							defer rows.Close()
 | 
				
			||||||
 | 
							for rows.Next() {
 | 
				
			||||||
 | 
								var key, rawval string
 | 
				
			||||||
 | 
								if err := rows.Scan(&key, &rawval); err != nil {
 | 
				
			||||||
 | 
									log.Warn("Error while scanning user uiconfig values")
 | 
				
			||||||
 | 
									return err, 0, 0
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								var val interface{}
 | 
				
			||||||
 | 
								if err := json.Unmarshal([]byte(rawval), &val); err != nil {
 | 
				
			||||||
 | 
									log.Warn("Error while unmarshaling raw user uiconfig json")
 | 
				
			||||||
 | 
									return err, 0, 0
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								size += len(key)
 | 
				
			||||||
 | 
								size += len(rawval)
 | 
				
			||||||
 | 
								uiconfig[key] = val
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Add global ShortRunningJobsDuration setting as plot_list_hideShortRunningJobs
 | 
				
			||||||
 | 
							uiconfig["plot_list_hideShortRunningJobs"] = config.Keys.ShortRunningJobsDuration
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return uiconfig, 24 * time.Hour, size
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						if err, ok := data.(error); ok {
 | 
				
			||||||
 | 
							log.Error("Error in returned dataset")
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return data.(map[string]interface{}), nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// If the context does not have a user, update the global ui configuration
 | 
				
			||||||
 | 
					// without persisting it!  If there is a (authenticated) user, update only his
 | 
				
			||||||
 | 
					// configuration.
 | 
				
			||||||
 | 
					func (uCfg *UserCfgRepo) UpdateConfig(
 | 
				
			||||||
 | 
						key, value string,
 | 
				
			||||||
 | 
						user *schema.User) error {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if user == nil {
 | 
				
			||||||
 | 
							var val interface{}
 | 
				
			||||||
 | 
							if err := json.Unmarshal([]byte(value), &val); err != nil {
 | 
				
			||||||
 | 
								log.Warn("Error while unmarshaling raw user config json")
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							uCfg.lock.Lock()
 | 
				
			||||||
 | 
							defer uCfg.lock.Unlock()
 | 
				
			||||||
 | 
							uCfg.uiDefaults[key] = val
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if _, err := uCfg.DB.Exec(`REPLACE INTO configuration (username, confkey, value) VALUES (?, ?, ?)`, user.Username, key, value); err != nil {
 | 
				
			||||||
 | 
							log.Warnf("Error while replacing user config in DB for user '%v'", user.Username)
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						uCfg.cache.Del(user.Username)
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -9,9 +9,9 @@ import (
 | 
				
			|||||||
	"path/filepath"
 | 
						"path/filepath"
 | 
				
			||||||
	"testing"
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/ClusterCockpit/cc-backend/internal/auth"
 | 
					 | 
				
			||||||
	"github.com/ClusterCockpit/cc-backend/internal/config"
 | 
						"github.com/ClusterCockpit/cc-backend/internal/config"
 | 
				
			||||||
	"github.com/ClusterCockpit/cc-backend/pkg/log"
 | 
						"github.com/ClusterCockpit/cc-backend/pkg/log"
 | 
				
			||||||
 | 
						"github.com/ClusterCockpit/cc-backend/pkg/schema"
 | 
				
			||||||
	_ "github.com/mattn/go-sqlite3"
 | 
						_ "github.com/mattn/go-sqlite3"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -53,7 +53,7 @@ func setupUserTest(t *testing.T) *UserCfgRepo {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func TestGetUIConfig(t *testing.T) {
 | 
					func TestGetUIConfig(t *testing.T) {
 | 
				
			||||||
	r := setupUserTest(t)
 | 
						r := setupUserTest(t)
 | 
				
			||||||
	u := auth.User{Username: "demo"}
 | 
						u := schema.User{Username: "demo"}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	cfg, err := r.GetUIConfig(&u)
 | 
						cfg, err := r.GetUIConfig(&u)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
@@ -13,11 +13,11 @@ import (
 | 
				
			|||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/ClusterCockpit/cc-backend/internal/auth"
 | 
					 | 
				
			||||||
	"github.com/ClusterCockpit/cc-backend/internal/graph/model"
 | 
						"github.com/ClusterCockpit/cc-backend/internal/graph/model"
 | 
				
			||||||
	"github.com/ClusterCockpit/cc-backend/internal/repository"
 | 
						"github.com/ClusterCockpit/cc-backend/internal/repository"
 | 
				
			||||||
	"github.com/ClusterCockpit/cc-backend/internal/util"
 | 
						"github.com/ClusterCockpit/cc-backend/internal/util"
 | 
				
			||||||
	"github.com/ClusterCockpit/cc-backend/pkg/log"
 | 
						"github.com/ClusterCockpit/cc-backend/pkg/log"
 | 
				
			||||||
 | 
						"github.com/ClusterCockpit/cc-backend/pkg/schema"
 | 
				
			||||||
	"github.com/ClusterCockpit/cc-backend/web"
 | 
						"github.com/ClusterCockpit/cc-backend/web"
 | 
				
			||||||
	"github.com/gorilla/mux"
 | 
						"github.com/gorilla/mux"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
@@ -81,12 +81,11 @@ func setupJobRoute(i InfoType, r *http.Request) InfoType {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func setupUserRoute(i InfoType, r *http.Request) InfoType {
 | 
					func setupUserRoute(i InfoType, r *http.Request) InfoType {
 | 
				
			||||||
	jobRepo := repository.GetJobRepository()
 | 
					 | 
				
			||||||
	username := mux.Vars(r)["id"]
 | 
						username := mux.Vars(r)["id"]
 | 
				
			||||||
	i["id"] = username
 | 
						i["id"] = username
 | 
				
			||||||
	i["username"] = username
 | 
						i["username"] = username
 | 
				
			||||||
	// TODO: If forbidden (== err exists), redirect to error page
 | 
						// TODO: If forbidden (== err exists), redirect to error page
 | 
				
			||||||
	if user, _ := auth.FetchUser(r.Context(), jobRepo.DB, username); user != nil {
 | 
						if user, _ := repository.GetUserRepository().FetchUserInCtx(r.Context(), username); user != nil {
 | 
				
			||||||
		i["name"] = user.Name
 | 
							i["name"] = user.Name
 | 
				
			||||||
		i["email"] = user.Email
 | 
							i["email"] = user.Email
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -125,7 +124,7 @@ func setupAnalysisRoute(i InfoType, r *http.Request) InfoType {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func setupTaglistRoute(i InfoType, r *http.Request) InfoType {
 | 
					func setupTaglistRoute(i InfoType, r *http.Request) InfoType {
 | 
				
			||||||
	jobRepo := repository.GetJobRepository()
 | 
						jobRepo := repository.GetJobRepository()
 | 
				
			||||||
	user := auth.GetUser(r.Context())
 | 
						user := repository.GetUserFromContext(r.Context())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	tags, counts, err := jobRepo.CountTags(user)
 | 
						tags, counts, err := jobRepo.CountTags(user)
 | 
				
			||||||
	tagMap := make(map[string][]map[string]interface{})
 | 
						tagMap := make(map[string][]map[string]interface{})
 | 
				
			||||||
@@ -255,7 +254,7 @@ func SetupRoutes(router *mux.Router, buildInfo web.Build) {
 | 
				
			|||||||
	for _, route := range routes {
 | 
						for _, route := range routes {
 | 
				
			||||||
		route := route
 | 
							route := route
 | 
				
			||||||
		router.HandleFunc(route.Route, func(rw http.ResponseWriter, r *http.Request) {
 | 
							router.HandleFunc(route.Route, func(rw http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
			conf, err := userCfgRepo.GetUIConfig(auth.GetUser(r.Context()))
 | 
								conf, err := userCfgRepo.GetUIConfig(repository.GetUserFromContext(r.Context()))
 | 
				
			||||||
			if err != nil {
 | 
								if err != nil {
 | 
				
			||||||
				http.Error(rw, err.Error(), http.StatusInternalServerError)
 | 
									http.Error(rw, err.Error(), http.StatusInternalServerError)
 | 
				
			||||||
				return
 | 
									return
 | 
				
			||||||
@@ -268,9 +267,9 @@ func SetupRoutes(router *mux.Router, buildInfo web.Build) {
 | 
				
			|||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// Get User -> What if NIL?
 | 
								// Get User -> What if NIL?
 | 
				
			||||||
			user := auth.GetUser(r.Context())
 | 
								user := repository.GetUserFromContext(r.Context())
 | 
				
			||||||
			// Get Roles
 | 
								// Get Roles
 | 
				
			||||||
			availableRoles, _ := auth.GetValidRolesMap(user)
 | 
								availableRoles, _ := schema.GetValidRolesMap(user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			page := web.Page{
 | 
								page := web.Page{
 | 
				
			||||||
				Title:  title,
 | 
									Title:  title,
 | 
				
			||||||
@@ -285,14 +284,14 @@ func SetupRoutes(router *mux.Router, buildInfo web.Build) {
 | 
				
			|||||||
				page.FilterPresets = buildFilterPresets(r.URL.Query())
 | 
									page.FilterPresets = buildFilterPresets(r.URL.Query())
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			web.RenderTemplate(rw, r, route.Template, &page)
 | 
								web.RenderTemplate(rw, route.Template, &page)
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func HandleSearchBar(rw http.ResponseWriter, r *http.Request, buildInfo web.Build) {
 | 
					func HandleSearchBar(rw http.ResponseWriter, r *http.Request, buildInfo web.Build) {
 | 
				
			||||||
	user := auth.GetUser(r.Context())
 | 
						user := repository.GetUserFromContext(r.Context())
 | 
				
			||||||
	availableRoles, _ := auth.GetValidRolesMap(user)
 | 
						availableRoles, _ := schema.GetValidRolesMap(user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if search := r.URL.Query().Get("searchId"); search != "" {
 | 
						if search := r.URL.Query().Get("searchId"); search != "" {
 | 
				
			||||||
		repo := repository.GetJobRepository()
 | 
							repo := repository.GetJobRepository()
 | 
				
			||||||
@@ -309,10 +308,10 @@ func HandleSearchBar(rw http.ResponseWriter, r *http.Request, buildInfo web.Buil
 | 
				
			|||||||
			case "arrayJobId":
 | 
								case "arrayJobId":
 | 
				
			||||||
				http.Redirect(rw, r, "/monitoring/jobs/?arrayJobId="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusFound) // All Users: Redirect to Tablequery
 | 
									http.Redirect(rw, r, "/monitoring/jobs/?arrayJobId="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusFound) // All Users: Redirect to Tablequery
 | 
				
			||||||
			case "username":
 | 
								case "username":
 | 
				
			||||||
				if user.HasAnyRole([]auth.Role{auth.RoleAdmin, auth.RoleSupport, auth.RoleManager}) {
 | 
									if user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport, schema.RoleManager}) {
 | 
				
			||||||
					http.Redirect(rw, r, "/monitoring/users/?user="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusFound)
 | 
										http.Redirect(rw, r, "/monitoring/users/?user="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusFound)
 | 
				
			||||||
				} else {
 | 
									} else {
 | 
				
			||||||
					web.RenderTemplate(rw, r, "message.tmpl", &web.Page{Title: "Error", MsgType: "alert-danger", Message: "Missing Access Rights", User: *user, Roles: availableRoles, Build: buildInfo})
 | 
										web.RenderTemplate(rw, "message.tmpl", &web.Page{Title: "Error", MsgType: "alert-danger", Message: "Missing Access Rights", User: *user, Roles: availableRoles, Build: buildInfo})
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			case "name":
 | 
								case "name":
 | 
				
			||||||
				usernames, _ := repo.FindColumnValues(user, strings.Trim(splitSearch[1], " "), "user", "username", "name")
 | 
									usernames, _ := repo.FindColumnValues(user, strings.Trim(splitSearch[1], " "), "user", "username", "name")
 | 
				
			||||||
@@ -320,14 +319,14 @@ func HandleSearchBar(rw http.ResponseWriter, r *http.Request, buildInfo web.Buil
 | 
				
			|||||||
					joinedNames := strings.Join(usernames, "&user=")
 | 
										joinedNames := strings.Join(usernames, "&user=")
 | 
				
			||||||
					http.Redirect(rw, r, "/monitoring/users/?user="+joinedNames, http.StatusFound)
 | 
										http.Redirect(rw, r, "/monitoring/users/?user="+joinedNames, http.StatusFound)
 | 
				
			||||||
				} else {
 | 
									} else {
 | 
				
			||||||
					if user.HasAnyRole([]auth.Role{auth.RoleAdmin, auth.RoleSupport, auth.RoleManager}) {
 | 
										if user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport, schema.RoleManager}) {
 | 
				
			||||||
						http.Redirect(rw, r, "/monitoring/users/?user=NoUserNameFound", http.StatusPermanentRedirect)
 | 
											http.Redirect(rw, r, "/monitoring/users/?user=NoUserNameFound", http.StatusPermanentRedirect)
 | 
				
			||||||
					} else {
 | 
										} else {
 | 
				
			||||||
						web.RenderTemplate(rw, r, "message.tmpl", &web.Page{Title: "Error", MsgType: "alert-danger", Message: "Missing Access Rights", User: *user, Roles: availableRoles, Build: buildInfo})
 | 
											web.RenderTemplate(rw, "message.tmpl", &web.Page{Title: "Error", MsgType: "alert-danger", Message: "Missing Access Rights", User: *user, Roles: availableRoles, Build: buildInfo})
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			default:
 | 
								default:
 | 
				
			||||||
				web.RenderTemplate(rw, r, "message.tmpl", &web.Page{Title: "Warning", MsgType: "alert-warning", Message: fmt.Sprintf("Unknown search type: %s", strings.Trim(splitSearch[0], " ")), User: *user, Roles: availableRoles, Build: buildInfo})
 | 
									web.RenderTemplate(rw, "message.tmpl", &web.Page{Title: "Warning", MsgType: "alert-warning", Message: fmt.Sprintf("Unknown search type: %s", strings.Trim(splitSearch[0], " ")), User: *user, Roles: availableRoles, Build: buildInfo})
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		} else if len(splitSearch) == 1 {
 | 
							} else if len(splitSearch) == 1 {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -342,13 +341,13 @@ func HandleSearchBar(rw http.ResponseWriter, r *http.Request, buildInfo web.Buil
 | 
				
			|||||||
			} else if jobname != "" {
 | 
								} else if jobname != "" {
 | 
				
			||||||
				http.Redirect(rw, r, "/monitoring/jobs/?jobName="+url.QueryEscape(jobname), http.StatusFound) // JobName (contains)
 | 
									http.Redirect(rw, r, "/monitoring/jobs/?jobName="+url.QueryEscape(jobname), http.StatusFound) // JobName (contains)
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				web.RenderTemplate(rw, r, "message.tmpl", &web.Page{Title: "Info", MsgType: "alert-info", Message: "Search without result", User: *user, Roles: availableRoles, Build: buildInfo})
 | 
									web.RenderTemplate(rw, "message.tmpl", &web.Page{Title: "Info", MsgType: "alert-info", Message: "Search without result", User: *user, Roles: availableRoles, Build: buildInfo})
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			web.RenderTemplate(rw, r, "message.tmpl", &web.Page{Title: "Error", MsgType: "alert-danger", Message: "Searchbar query parameters malformed", User: *user, Roles: availableRoles, Build: buildInfo})
 | 
								web.RenderTemplate(rw, "message.tmpl", &web.Page{Title: "Error", MsgType: "alert-danger", Message: "Searchbar query parameters malformed", User: *user, Roles: availableRoles, Build: buildInfo})
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		web.RenderTemplate(rw, r, "message.tmpl", &web.Page{Title: "Warning", MsgType: "alert-warning", Message: "Empty search", User: *user, Roles: availableRoles, Build: buildInfo})
 | 
							web.RenderTemplate(rw, "message.tmpl", &web.Page{Title: "Warning", MsgType: "alert-warning", Message: "Empty search", User: *user, Roles: availableRoles, Build: buildInfo})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,6 +17,8 @@ type LdapConfig struct {
 | 
				
			|||||||
	UserFilter      string `json:"user_filter"`
 | 
						UserFilter      string `json:"user_filter"`
 | 
				
			||||||
	SyncInterval    string `json:"sync_interval"` // Parsed using time.ParseDuration.
 | 
						SyncInterval    string `json:"sync_interval"` // Parsed using time.ParseDuration.
 | 
				
			||||||
	SyncDelOldUsers bool   `json:"sync_del_old_users"`
 | 
						SyncDelOldUsers bool   `json:"sync_del_old_users"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Should an non-existent user be added to the DB if user exists in ldap directory
 | 
				
			||||||
	SyncUserOnLogin bool `json:"syncUserOnLogin"`
 | 
						SyncUserOnLogin bool `json:"syncUserOnLogin"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -30,10 +32,13 @@ type JWTAuthConfig struct {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	// Deny login for users not in database (but defined in JWT).
 | 
						// Deny login for users not in database (but defined in JWT).
 | 
				
			||||||
	// Ignore user roles defined in JWTs ('roles' claim), get them from db.
 | 
						// Ignore user roles defined in JWTs ('roles' claim), get them from db.
 | 
				
			||||||
	ForceJWTValidationViaDatabase bool `json:"forceJWTValidationViaDatabase"`
 | 
						ValidateUser bool `json:"validateUser"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Specifies which issuer should be accepted when validating external JWTs ('iss' claim)
 | 
						// Specifies which issuer should be accepted when validating external JWTs ('iss' claim)
 | 
				
			||||||
	TrustedExternalIssuer string `json:"trustedExternalIssuer"`
 | 
						TrustedIssuer string `json:"trustedIssuer"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Should an non-existent user be added to the DB based on the information in the token
 | 
				
			||||||
 | 
						SyncUserOnLogin bool `json:"syncUserOnLogin"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type IntRange struct {
 | 
					type IntRange struct {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,7 +2,7 @@
 | 
				
			|||||||
// All rights reserved.
 | 
					// All rights reserved.
 | 
				
			||||||
// Use of this source code is governed by a MIT-style
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
// license that can be found in the LICENSE file.
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
package auth
 | 
					package schema
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
@@ -21,6 +21,41 @@ const (
 | 
				
			|||||||
	RoleError
 | 
						RoleError
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type AuthSource int
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						AuthViaLocalPassword AuthSource = iota
 | 
				
			||||||
 | 
						AuthViaLDAP
 | 
				
			||||||
 | 
						AuthViaToken
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type AuthType int
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						AuthToken AuthType = iota
 | 
				
			||||||
 | 
						AuthSession
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type User struct {
 | 
				
			||||||
 | 
						Username   string     `json:"username"`
 | 
				
			||||||
 | 
						Password   string     `json:"-"`
 | 
				
			||||||
 | 
						Name       string     `json:"name"`
 | 
				
			||||||
 | 
						Roles      []string   `json:"roles"`
 | 
				
			||||||
 | 
						AuthType   AuthType   `json:"authType"`
 | 
				
			||||||
 | 
						AuthSource AuthSource `json:"authSource"`
 | 
				
			||||||
 | 
						Email      string     `json:"email"`
 | 
				
			||||||
 | 
						Projects   []string   `json:"projects"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (u *User) HasProject(project string) bool {
 | 
				
			||||||
 | 
						for _, p := range u.Projects {
 | 
				
			||||||
 | 
							if p == project {
 | 
				
			||||||
 | 
								return true
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func GetRoleString(roleInt Role) string {
 | 
					func GetRoleString(roleInt Role) string {
 | 
				
			||||||
	return [6]string{"anonymous", "api", "user", "manager", "support", "admin"}[roleInt]
 | 
						return [6]string{"anonymous", "api", "user", "manager", "support", "admin"}[roleInt]
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -44,12 +79,12 @@ func getRoleEnum(roleStr string) Role {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func isValidRole(role string) bool {
 | 
					func IsValidRole(role string) bool {
 | 
				
			||||||
	return getRoleEnum(role) != RoleError
 | 
						return getRoleEnum(role) != RoleError
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (u *User) HasValidRole(role string) (hasRole bool, isValid bool) {
 | 
					func (u *User) HasValidRole(role string) (hasRole bool, isValid bool) {
 | 
				
			||||||
	if isValidRole(role) {
 | 
						if IsValidRole(role) {
 | 
				
			||||||
		for _, r := range u.Roles {
 | 
							for _, r := range u.Roles {
 | 
				
			||||||
			if r == role {
 | 
								if r == role {
 | 
				
			||||||
				return true, true
 | 
									return true, true
 | 
				
			||||||
@@ -2,7 +2,7 @@
 | 
				
			|||||||
// All rights reserved.
 | 
					// All rights reserved.
 | 
				
			||||||
// Use of this source code is governed by a MIT-style
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
// license that can be found in the LICENSE file.
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
package auth
 | 
					package schema
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"testing"
 | 
						"testing"
 | 
				
			||||||
@@ -11,7 +11,6 @@ import (
 | 
				
			|||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/ClusterCockpit/cc-backend/internal/auth"
 | 
					 | 
				
			||||||
	"github.com/ClusterCockpit/cc-backend/internal/config"
 | 
						"github.com/ClusterCockpit/cc-backend/internal/config"
 | 
				
			||||||
	"github.com/ClusterCockpit/cc-backend/internal/util"
 | 
						"github.com/ClusterCockpit/cc-backend/internal/util"
 | 
				
			||||||
	"github.com/ClusterCockpit/cc-backend/pkg/log"
 | 
						"github.com/ClusterCockpit/cc-backend/pkg/log"
 | 
				
			||||||
@@ -92,8 +91,8 @@ type Page struct {
 | 
				
			|||||||
	Title         string                 // Page title
 | 
						Title         string                 // Page title
 | 
				
			||||||
	MsgType       string                 // For generic use in message boxes
 | 
						MsgType       string                 // For generic use in message boxes
 | 
				
			||||||
	Message       string                 // For generic use in message boxes
 | 
						Message       string                 // For generic use in message boxes
 | 
				
			||||||
	User          auth.User              // Information about the currently logged in user (Full User Info)
 | 
						User          schema.User            // Information about the currently logged in user (Full User Info)
 | 
				
			||||||
	Roles         map[string]auth.Role   // Available roles for frontend render checks
 | 
						Roles         map[string]schema.Role // Available roles for frontend render checks
 | 
				
			||||||
	Build         Build                  // Latest information about the application
 | 
						Build         Build                  // Latest information about the application
 | 
				
			||||||
	Clusters      []schema.ClusterConfig // List of all clusters for use in the Header
 | 
						Clusters      []schema.ClusterConfig // List of all clusters for use in the Header
 | 
				
			||||||
	FilterPresets map[string]interface{} // For pages with the Filter component, this can be used to set initial filters.
 | 
						FilterPresets map[string]interface{} // For pages with the Filter component, this can be used to set initial filters.
 | 
				
			||||||
@@ -101,7 +100,7 @@ type Page struct {
 | 
				
			|||||||
	Config        map[string]interface{} // UI settings for the currently logged in user (e.g. line width, ...)
 | 
						Config        map[string]interface{} // UI settings for the currently logged in user (e.g. line width, ...)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func RenderTemplate(rw http.ResponseWriter, r *http.Request, file string, page *Page) {
 | 
					func RenderTemplate(rw http.ResponseWriter, file string, page *Page) {
 | 
				
			||||||
	t, ok := templates[file]
 | 
						t, ok := templates[file]
 | 
				
			||||||
	if !ok {
 | 
						if !ok {
 | 
				
			||||||
		log.Errorf("WEB/WEB > template '%s' not found", file)
 | 
							log.Errorf("WEB/WEB > template '%s' not found", file)
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user