Merge branch '105_modify_user_via_api' into 189-refactor-authentication-module

This commit is contained in:
Jan Eitzinger 2023-08-16 09:46:41 +02:00
commit 65cf86586a
5 changed files with 175 additions and 16 deletions

View File

@ -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")

View File

@ -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)

View File

@ -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
View 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
}

View File

@ -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"`