mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2024-12-26 05:19:05 +01:00
Merge branch '105_modify_user_via_api' into 189-refactor-authentication-module
This commit is contained in:
commit
65cf86586a
@ -1,4 +1,4 @@
|
|||||||
// 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.
|
||||||
@ -20,11 +20,13 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ClusterCockpit/cc-backend/internal/auth"
|
"github.com/ClusterCockpit/cc-backend/internal/auth"
|
||||||
|
"github.com/ClusterCockpit/cc-backend/internal/config"
|
||||||
"github.com/ClusterCockpit/cc-backend/internal/graph"
|
"github.com/ClusterCockpit/cc-backend/internal/graph"
|
||||||
"github.com/ClusterCockpit/cc-backend/internal/graph/model"
|
"github.com/ClusterCockpit/cc-backend/internal/graph/model"
|
||||||
"github.com/ClusterCockpit/cc-backend/internal/importer"
|
"github.com/ClusterCockpit/cc-backend/internal/importer"
|
||||||
"github.com/ClusterCockpit/cc-backend/internal/metricdata"
|
"github.com/ClusterCockpit/cc-backend/internal/metricdata"
|
||||||
"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/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"
|
||||||
@ -75,6 +77,13 @@ func (api *RestApi) MountRoutes(r *mux.Router) {
|
|||||||
r.HandleFunc("/jobs/delete_job/", api.deleteJobByRequest).Methods(http.MethodDelete)
|
r.HandleFunc("/jobs/delete_job/", api.deleteJobByRequest).Methods(http.MethodDelete)
|
||||||
r.HandleFunc("/jobs/delete_job/{id}", api.deleteJobById).Methods(http.MethodDelete)
|
r.HandleFunc("/jobs/delete_job/{id}", api.deleteJobById).Methods(http.MethodDelete)
|
||||||
r.HandleFunc("/jobs/delete_job_before/{ts}", api.deleteJobBefore).Methods(http.MethodDelete)
|
r.HandleFunc("/jobs/delete_job_before/{ts}", api.deleteJobBefore).Methods(http.MethodDelete)
|
||||||
|
// r.HandleFunc("/secured/addProject/{id}/{project}", api.secureUpdateUser).Methods(http.MethodPost)
|
||||||
|
// r.HandleFunc("/secured/addRole/{id}/{role}", api.secureUpdateUser).Methods(http.MethodPost)
|
||||||
|
|
||||||
|
if api.MachineStateDir != "" {
|
||||||
|
r.HandleFunc("/machine_state/{cluster}/{host}", api.getMachineState).Methods(http.MethodGet)
|
||||||
|
r.HandleFunc("/machine_state/{cluster}/{host}", api.putMachineState).Methods(http.MethodPut, http.MethodPost)
|
||||||
|
}
|
||||||
|
|
||||||
if api.Authentication != nil {
|
if api.Authentication != nil {
|
||||||
r.HandleFunc("/jwt/", api.getJWT).Methods(http.MethodGet)
|
r.HandleFunc("/jwt/", api.getJWT).Methods(http.MethodGet)
|
||||||
@ -85,11 +94,6 @@ func (api *RestApi) MountRoutes(r *mux.Router) {
|
|||||||
r.HandleFunc("/user/{id}", api.updateUser).Methods(http.MethodPost)
|
r.HandleFunc("/user/{id}", api.updateUser).Methods(http.MethodPost)
|
||||||
r.HandleFunc("/configuration/", api.updateConfiguration).Methods(http.MethodPost)
|
r.HandleFunc("/configuration/", api.updateConfiguration).Methods(http.MethodPost)
|
||||||
}
|
}
|
||||||
|
|
||||||
if api.MachineStateDir != "" {
|
|
||||||
r.HandleFunc("/machine_state/{cluster}/{host}", api.getMachineState).Methods(http.MethodGet)
|
|
||||||
r.HandleFunc("/machine_state/{cluster}/{host}", api.putMachineState).Methods(http.MethodPut, http.MethodPost)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// StartJobApiResponse model
|
// StartJobApiResponse model
|
||||||
@ -103,6 +107,11 @@ type DeleteJobApiResponse struct {
|
|||||||
Message string `json:"msg"`
|
Message string `json:"msg"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateUserApiResponse model
|
||||||
|
type UpdateUserApiResponse struct {
|
||||||
|
Message string `json:"msg"`
|
||||||
|
}
|
||||||
|
|
||||||
// StopJobApiRequest model
|
// StopJobApiRequest model
|
||||||
type StopJobApiRequest struct {
|
type StopJobApiRequest struct {
|
||||||
// Stop Time of job as epoch
|
// Stop Time of job as epoch
|
||||||
@ -172,6 +181,36 @@ func decode(r io.Reader, val interface{}) error {
|
|||||||
return dec.Decode(val)
|
return dec.Decode(val)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func securedCheck(r *http.Request) error {
|
||||||
|
user := auth.GetUser(r.Context())
|
||||||
|
if user == nil {
|
||||||
|
return fmt.Errorf("no user in context")
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.AuthType == auth.AuthToken {
|
||||||
|
// If nothing declared in config: deny all request to this endpoint
|
||||||
|
if config.Keys.ApiAllowedIPs == nil || len(config.Keys.ApiAllowedIPs) == 0 {
|
||||||
|
return fmt.Errorf("missing configuration key ApiAllowedIPs")
|
||||||
|
}
|
||||||
|
|
||||||
|
// extract IP address
|
||||||
|
IPAddress := r.Header.Get("X-Real-Ip")
|
||||||
|
if IPAddress == "" {
|
||||||
|
IPAddress = r.Header.Get("X-Forwarded-For")
|
||||||
|
}
|
||||||
|
if IPAddress == "" {
|
||||||
|
IPAddress = r.RemoteAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if IP is allowed
|
||||||
|
if !util.Contains(config.Keys.ApiAllowedIPs, IPAddress) {
|
||||||
|
return fmt.Errorf("unknown ip: %v", IPAddress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// getJobs godoc
|
// getJobs godoc
|
||||||
// @summary Lists all jobs
|
// @summary Lists all jobs
|
||||||
// @tags query
|
// @tags query
|
||||||
@ -892,6 +931,11 @@ func (api *RestApi) getJobMetrics(rw http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (api *RestApi) getJWT(rw http.ResponseWriter, r *http.Request) {
|
func (api *RestApi) getJWT(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
err := securedCheck(r)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(rw, err.Error(), http.StatusForbidden)
|
||||||
|
}
|
||||||
|
|
||||||
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 := auth.GetUser(r.Context())
|
||||||
@ -920,6 +964,11 @@ func (api *RestApi) getJWT(rw http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (api *RestApi) createUser(rw http.ResponseWriter, r *http.Request) {
|
func (api *RestApi) createUser(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
err := securedCheck(r)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(rw, err.Error(), http.StatusForbidden)
|
||||||
|
}
|
||||||
|
|
||||||
rw.Header().Set("Content-Type", "text/plain")
|
rw.Header().Set("Content-Type", "text/plain")
|
||||||
me := auth.GetUser(r.Context())
|
me := auth.GetUser(r.Context())
|
||||||
if !me.HasRole(auth.RoleAdmin) {
|
if !me.HasRole(auth.RoleAdmin) {
|
||||||
@ -927,17 +976,22 @@ func (api *RestApi) createUser(rw http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
username, password, role, name, email, project := r.FormValue("username"), r.FormValue("password"), r.FormValue("role"), r.FormValue("name"), r.FormValue("email"), r.FormValue("project")
|
username, password, role, name, email, project := r.FormValue("username"),
|
||||||
|
r.FormValue("password"), r.FormValue("role"), r.FormValue("name"),
|
||||||
|
r.FormValue("email"), r.FormValue("project")
|
||||||
|
|
||||||
if len(password) == 0 && role != auth.GetRoleString(auth.RoleApi) {
|
if len(password) == 0 && role != auth.GetRoleString(auth.RoleApi) {
|
||||||
http.Error(rw, "Only API users are allowed to have a blank password (login will be impossible)", http.StatusBadRequest)
|
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 != auth.GetRoleString(auth.RoleManager) {
|
||||||
http.Error(rw, "only managers require a project (can be changed later)", http.StatusBadRequest)
|
http.Error(rw, "only managers require a project (can be changed later)",
|
||||||
|
http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
} else if len(project) == 0 && role == auth.GetRoleString(auth.RoleManager) {
|
} else if len(project) == 0 && role == auth.GetRoleString(auth.RoleManager) {
|
||||||
http.Error(rw, "managers require a project to manage (can be changed later)", http.StatusBadRequest)
|
http.Error(rw, "managers require a project to manage (can be changed later)",
|
||||||
|
http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -956,6 +1010,11 @@ func (api *RestApi) createUser(rw http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (api *RestApi) deleteUser(rw http.ResponseWriter, r *http.Request) {
|
func (api *RestApi) deleteUser(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
err := securedCheck(r)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(rw, err.Error(), http.StatusForbidden)
|
||||||
|
}
|
||||||
|
|
||||||
if user := auth.GetUser(r.Context()); !user.HasRole(auth.RoleAdmin) {
|
if user := auth.GetUser(r.Context()); !user.HasRole(auth.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
|
||||||
@ -971,6 +1030,11 @@ func (api *RestApi) deleteUser(rw http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (api *RestApi) getUsers(rw http.ResponseWriter, r *http.Request) {
|
func (api *RestApi) getUsers(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
err := securedCheck(r)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(rw, err.Error(), http.StatusForbidden)
|
||||||
|
}
|
||||||
|
|
||||||
if user := auth.GetUser(r.Context()); !user.HasRole(auth.RoleAdmin) {
|
if user := auth.GetUser(r.Context()); !user.HasRole(auth.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
|
||||||
@ -986,6 +1050,11 @@ func (api *RestApi) getUsers(rw http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (api *RestApi) getRoles(rw http.ResponseWriter, r *http.Request) {
|
func (api *RestApi) getRoles(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
err := securedCheck(r)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(rw, err.Error(), http.StatusForbidden)
|
||||||
|
}
|
||||||
|
|
||||||
user := auth.GetUser(r.Context())
|
user := auth.GetUser(r.Context())
|
||||||
if !user.HasRole(auth.RoleAdmin) {
|
if !user.HasRole(auth.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)
|
||||||
@ -1002,6 +1071,11 @@ func (api *RestApi) getRoles(rw http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (api *RestApi) updateUser(rw http.ResponseWriter, r *http.Request) {
|
func (api *RestApi) updateUser(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
err := securedCheck(r)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(rw, err.Error(), http.StatusForbidden)
|
||||||
|
}
|
||||||
|
|
||||||
if user := auth.GetUser(r.Context()); !user.HasRole(auth.RoleAdmin) {
|
if user := auth.GetUser(r.Context()); !user.HasRole(auth.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
|
||||||
@ -1043,6 +1117,71 @@ func (api *RestApi) updateUser(rw http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// func (api *RestApi) secureUpdateUser(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
// if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) {
|
||||||
|
// handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // IP CHECK HERE (WIP)
|
||||||
|
// // Probably better as private routine
|
||||||
|
// IPAddress := r.Header.Get("X-Real-Ip")
|
||||||
|
// if IPAddress == "" {
|
||||||
|
// IPAddress = r.Header.Get("X-Forwarded-For")
|
||||||
|
// }
|
||||||
|
// if IPAddress == "" {
|
||||||
|
// IPAddress = r.RemoteAddr
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Also This
|
||||||
|
// ipOk := false
|
||||||
|
// for _, a := range config.Keys.ApiAllowedAddrs {
|
||||||
|
// if a == IPAddress {
|
||||||
|
// ipOk = true
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if IPAddress == "" || ipOk == false {
|
||||||
|
// handleError(fmt.Errorf("unknown ip: %v", IPAddress), http.StatusForbidden, rw)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// // IP CHECK END
|
||||||
|
//
|
||||||
|
// // Get Values
|
||||||
|
// id := mux.Vars(r)["id"]
|
||||||
|
// newproj := mux.Vars(r)["project"]
|
||||||
|
// newrole := mux.Vars(r)["role"]
|
||||||
|
//
|
||||||
|
// // TODO: Handle anything but roles...
|
||||||
|
// if newrole != "" {
|
||||||
|
// if err := api.Authentication.AddRole(r.Context(), id, newrole); err != nil {
|
||||||
|
// handleError(errors.New(err.Error()), http.StatusUnprocessableEntity, rw)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// rw.Header().Add("Content-Type", "application/json")
|
||||||
|
// rw.WriteHeader(http.StatusOK)
|
||||||
|
// json.NewEncoder(rw).Encode(UpdateUserApiResponse{
|
||||||
|
// Message: fmt.Sprintf("Successfully added role %s to %s", newrole, id),
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
// } else if newproj != "" {
|
||||||
|
// if err := api.Authentication.AddProject(r.Context(), id, newproj); err != nil {
|
||||||
|
// handleError(errors.New(err.Error()), http.StatusUnprocessableEntity, rw)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// rw.Header().Add("Content-Type", "application/json")
|
||||||
|
// rw.WriteHeader(http.StatusOK)
|
||||||
|
// json.NewEncoder(rw).Encode(UpdateUserApiResponse{
|
||||||
|
// Message: fmt.Sprintf("Successfully added project %s to %s", newproj, id),
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
// } else {
|
||||||
|
// handleError(errors.New("Not Add [role|project]?"), http.StatusBadRequest, rw)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
func (api *RestApi) updateConfiguration(rw http.ResponseWriter, r *http.Request) {
|
func (api *RestApi) updateConfiguration(rw http.ResponseWriter, r *http.Request) {
|
||||||
rw.Header().Set("Content-Type", "text/plain")
|
rw.Header().Set("Content-Type", "text/plain")
|
||||||
key, value := r.FormValue("key"), r.FormValue("value")
|
key, value := r.FormValue("key"), r.FormValue("value")
|
||||||
|
@ -158,13 +158,14 @@ func Init(db *sqlx.DB,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if config, ok := configs["ldap"]; ok {
|
if config, ok := configs["ldap"]; ok {
|
||||||
auth.LdapAuth = &LdapAuthenticator{}
|
ldapAuth := &LdapAuthenticator{}
|
||||||
if err := auth.LdapAuth.Init(auth, config); err != nil {
|
if err := ldapAuth.Init(auth, config); err != nil {
|
||||||
log.Error("Error while initializing authentication -> ldapAuth init failed")
|
log.Warn("Error while initializing authentication -> ldapAuth init failed")
|
||||||
return nil, err
|
} else {
|
||||||
}
|
auth.LdapAuth = ldapAuth
|
||||||
auth.authenticators = append(auth.authenticators, auth.LdapAuth)
|
auth.authenticators = append(auth.authenticators, auth.LdapAuth)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
jwtSessionAuth := &JWTSessionAuthenticator{}
|
jwtSessionAuth := &JWTSessionAuthenticator{}
|
||||||
if err := jwtSessionAuth.Init(auth, configs["jwt"]); err != nil {
|
if err := jwtSessionAuth.Init(auth, configs["jwt"]); err != nil {
|
||||||
@ -174,7 +175,7 @@ func Init(db *sqlx.DB,
|
|||||||
}
|
}
|
||||||
|
|
||||||
jwtCookieSessionAuth := &JWTCookieSessionAuthenticator{}
|
jwtCookieSessionAuth := &JWTCookieSessionAuthenticator{}
|
||||||
if err := jwtSessionAuth.Init(auth, configs["jwt"]); err != nil {
|
if err := jwtCookieSessionAuth.Init(auth, configs["jwt"]); err != nil {
|
||||||
log.Warn("Error while initializing authentication -> jwtCookieSessionAuth init failed")
|
log.Warn("Error while initializing authentication -> jwtCookieSessionAuth init failed")
|
||||||
} else {
|
} else {
|
||||||
auth.authenticators = append(auth.authenticators, jwtCookieSessionAuth)
|
auth.authenticators = append(auth.authenticators, jwtCookieSessionAuth)
|
||||||
|
@ -59,6 +59,8 @@ func (la *LdapAuthenticator) Init(
|
|||||||
log.Print("sync done")
|
log.Print("sync done")
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("missing LDAP configuration")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -73,7 +75,7 @@ func (la *LdapAuthenticator) CanLogin(
|
|||||||
if user != nil && user.AuthSource == AuthViaLDAP {
|
if user != nil && user.AuthSource == AuthViaLDAP {
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
if la.config.SyncUserOnLogin {
|
if la.config != nil && la.config.SyncUserOnLogin {
|
||||||
l, err := la.getLdapConnection(true)
|
l, err := la.getLdapConnection(true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("LDAP connection error")
|
log.Error("LDAP connection error")
|
||||||
|
14
internal/util/array.go
Normal file
14
internal/util/array.go
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
// Copyright (C) 2023 NHR@FAU, University Erlangen-Nuremberg.
|
||||||
|
// All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
package util
|
||||||
|
|
||||||
|
func Contains[T comparable](items []T, item T) bool {
|
||||||
|
for _, v := range items {
|
||||||
|
if v == item {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
@ -70,6 +70,9 @@ type ProgramConfig struct {
|
|||||||
// Address where the http (or https) server will listen on (for example: 'localhost:80').
|
// Address where the http (or https) server will listen on (for example: 'localhost:80').
|
||||||
Addr string `json:"addr"`
|
Addr string `json:"addr"`
|
||||||
|
|
||||||
|
// Addresses from which the /api/secured/* API endpoints can be reached
|
||||||
|
ApiAllowedIPs []string `json:"apiAllowedIPs"`
|
||||||
|
|
||||||
// Drop root permissions once .env was read and the port was taken.
|
// Drop root permissions once .env was read and the port was taken.
|
||||||
User string `json:"user"`
|
User string `json:"user"`
|
||||||
Group string `json:"group"`
|
Group string `json:"group"`
|
||||||
|
Loading…
Reference in New Issue
Block a user