Merge pull request #552 from ClusterCockpit/fix/add-user-edit-api

Reintroduce user update api path
This commit is contained in:
Jan Eitzinger
2026-06-04 08:27:23 +02:00
committed by GitHub
6 changed files with 194 additions and 79 deletions

View File

@@ -958,8 +958,11 @@
"/api/user/{id}": {
"post": {
"description": "Allows admins to add/remove roles and projects for a user",
"consumes": [
"application/json"
],
"produces": [
"text/plain"
"application/json"
],
"tags": [
"User"
@@ -974,35 +977,26 @@
"required": true
},
{
"type": "string",
"description": "Role to add",
"name": "add-role",
"in": "formData"
},
{
"type": "string",
"description": "Role to remove",
"name": "remove-role",
"in": "formData"
},
{
"type": "string",
"description": "Project to add",
"name": "add-project",
"in": "formData"
},
{
"type": "string",
"description": "Project to remove",
"name": "remove-project",
"in": "formData"
"description": "Single Field Changes",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/api.UpdateUserAPIRequest"
}
}
],
"responses": {
"200": {
"description": "Success message",
"description": "OK",
"schema": {
"type": "string"
"$ref": "#/definitions/api.DefaultAPIResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"403": {
@@ -1933,6 +1927,31 @@
}
}
},
"api.UpdateUserAPIRequest": {
"type": "object",
"properties": {
"add-role": {
"description": "Role to add to user $ID",
"type": "string",
"example": "user"
},
"remove-role": {
"description": "Role to remove from user $ID",
"type": "string",
"example": "user"
},
"add-project": {
"description": "Project to add to user $ID managed array",
"type": "string",
"example": "abcd100"
},
"remove-project": {
"description": "Project to remove from user $ID managed array",
"type": "string",
"example": "abcd100"
}
}
},
"api.DefaultAPIResponse": {
"type": "object",
"properties": {

View File

@@ -31,6 +31,25 @@ definitions:
example: Debug
type: string
type: object
api.UpdateUserAPIRequest:
properties:
add-project:
description: Project to add to user $ID managed array
example: abcd100
type: string
add-role:
description: Role to add to user $ID
example: user
type: string
remove-project:
description: Project to remove from user $ID managed array
example: abcd100
type: string
remove-role:
description: Role to remove from user $ID
example: user
type: string
type: object
api.DefaultAPIResponse:
properties:
msg:
@@ -1388,6 +1407,8 @@ paths:
- Nodestates
/api/user/{id}:
post:
consumes:
- application/json
description: Allows admins to add/remove roles and projects for a user
parameters:
- description: Username
@@ -1395,29 +1416,23 @@ paths:
name: id
required: true
type: string
- description: Role to add
in: formData
name: add-role
type: string
- description: Role to remove
in: formData
name: remove-role
type: string
- description: Project to add
in: formData
name: add-project
type: string
- description: Project to remove
in: formData
name: remove-project
type: string
- description: Single Field Changes
in: body
name: request
required: true
schema:
$ref: '#/definitions/api.UpdateUserAPIRequest'
produces:
- text/plain
- application/json
responses:
"200":
description: Success message
description: OK
schema:
type: string
$ref: '#/definitions/api.DefaultAPIResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/api.ErrorResponse'
"403":
description: Forbidden
schema:

View File

@@ -965,8 +965,11 @@ const docTemplate = `{
"/api/user/{id}": {
"post": {
"description": "Allows admins to add/remove roles and projects for a user",
"consumes": [
"application/json"
],
"produces": [
"text/plain"
"application/json"
],
"tags": [
"User"
@@ -981,35 +984,26 @@ const docTemplate = `{
"required": true
},
{
"type": "string",
"description": "Role to add",
"name": "add-role",
"in": "formData"
},
{
"type": "string",
"description": "Role to remove",
"name": "remove-role",
"in": "formData"
},
{
"type": "string",
"description": "Project to add",
"name": "add-project",
"in": "formData"
},
{
"type": "string",
"description": "Project to remove",
"name": "remove-project",
"in": "formData"
"description": "Single Field Changes",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/api.UpdateUserAPIRequest"
}
}
],
"responses": {
"200": {
"description": "Success message",
"description": "OK",
"schema": {
"type": "string"
"$ref": "#/definitions/api.DefaultAPIResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"403": {
@@ -1940,6 +1934,31 @@ const docTemplate = `{
}
}
},
"api.UpdateUserAPIRequest": {
"type": "object",
"properties": {
"add-role": {
"description": "Role to add to user $ID",
"type": "string",
"example": "user"
},
"remove-role": {
"description": "Role to remove from user $ID",
"type": "string",
"example": "user"
},
"add-project": {
"description": "Project to add to user $ID managed array",
"type": "string",
"example": "abcd100"
},
"remove-project": {
"description": "Project to remove from user $ID managed array",
"type": "string",
"example": "abcd100"
}
}
},
"api.DefaultAPIResponse": {
"type": "object",
"properties": {

View File

@@ -79,6 +79,8 @@ func (api *RestAPI) MountAPIRoutes(r chi.Router) {
// REST API Uses TokenAuth
// User List
r.Get("/users/", api.getUsers)
// User Edit
r.Post("/user/{id}", api.updateUserByRequest)
// Cluster List
r.Get("/clusters/", api.getClusters)
// Slurm node state
@@ -152,7 +154,7 @@ func (api *RestAPI) MountConfigAPIRoutes(r chi.Router) {
r.Put("/config/users/", api.createUser)
r.Get("/config/users/", api.getUsers)
r.Delete("/config/users/", api.deleteUser)
r.Post("/config/user/{id}", api.updateUser)
r.Post("/config/user/{id}", api.updateUserByForm)
r.Post("/config/notice/", api.editNotice)
r.Get("/config/taggers/", api.getTaggers)
r.Post("/config/taggers/run/", api.runTagger)

View File

@@ -24,6 +24,14 @@ type APIReturnedUser struct {
Projects []string `json:"projects"`
}
// UpdateUserAPIRequest model
type UpdateUserAPIRequest struct {
NewRole string `json:"add-role" example:"user"` // Role to add to user $ID
DelRole string `json:"remove-role" example:"user"` // Role to remove from user $ID
NewProj string `json:"add-project" example:"abcd100"` // Project to add to user $ID managed array
DelProj string `json:"remove-project" example:"abcd100"` // Project to remove from user $ID managed array
}
// getUsers godoc
// @summary Returns a list of users
// @tags User
@@ -58,22 +66,74 @@ func (api *RestAPI) getUsers(rw http.ResponseWriter, r *http.Request) {
}
}
// updateUser godoc
// updateUserByRequest godoc
// @summary Update user roles and projects
// @tags User
// @description Allows admins to add/remove roles and projects for a user
// @produce plain
// @accept json
// @produce json
// @param id path string true "Username"
// @param add-role formData string false "Role to add"
// @param remove-role formData string false "Role to remove"
// @param add-project formData string false "Project to add"
// @param remove-project formData string false "Project to remove"
// @param request body api.UpdateUserAPIRequest true "Single Field Changes"
// @success 200 {string} string "Success message"
// @failure 403 {object} api.ErrorResponse "Forbidden"
// @failure 422 {object} api.ErrorResponse "Unprocessable Entity"
// @security ApiKeyAuth
// @router /api/user/{id} [post]
func (api *RestAPI) updateUser(rw http.ResponseWriter, r *http.Request) {
func (api *RestAPI) updateUserByRequest(rw http.ResponseWriter, r *http.Request) {
if user := repository.GetUserFromContext(r.Context()); !user.HasRole(schema.RoleAdmin) {
handleError(fmt.Errorf("only admins are allowed to update a user"), http.StatusForbidden, rw)
return
}
// Get Values
var req UpdateUserAPIRequest
if err := decode(r.Body, &req); err != nil {
handleError(fmt.Errorf("decoding request failed: %w", err), http.StatusBadRequest, rw)
return
}
rw.Header().Set("Content-Type", "application/json")
// Handle role updates
if req.NewRole != "" {
if err := repository.GetUserRepository().AddRole(r.Context(), chi.URLParam(r, "id"), req.NewRole); err != nil {
handleError(fmt.Errorf("adding role failed: %w", err), http.StatusUnprocessableEntity, rw)
return
}
if err := json.NewEncoder(rw).Encode(DefaultAPIResponse{Message: fmt.Sprintf("Add Role Success for user %s", chi.URLParam(r, "id"))}); err != nil {
cclog.Errorf("Failed to encode response: %v", err)
}
} else if req.DelRole != "" {
if err := repository.GetUserRepository().RemoveRole(r.Context(), chi.URLParam(r, "id"), req.DelRole); err != nil {
handleError(fmt.Errorf("removing role failed: %w", err), http.StatusUnprocessableEntity, rw)
return
}
if err := json.NewEncoder(rw).Encode(DefaultAPIResponse{Message: fmt.Sprintf("Remove Role Success for user %s", chi.URLParam(r, "id"))}); err != nil {
cclog.Errorf("Failed to encode response: %v", err)
}
} else if req.NewProj != "" {
if err := repository.GetUserRepository().AddProject(r.Context(), chi.URLParam(r, "id"), req.NewProj); err != nil {
handleError(fmt.Errorf("adding project failed: %w", err), http.StatusUnprocessableEntity, rw)
return
}
if err := json.NewEncoder(rw).Encode(DefaultAPIResponse{Message: fmt.Sprintf("Add Project Success for user %s", chi.URLParam(r, "id"))}); err != nil {
cclog.Errorf("Failed to encode response: %v", err)
}
} else if req.DelProj != "" {
if err := repository.GetUserRepository().RemoveProject(r.Context(), chi.URLParam(r, "id"), req.DelProj); err != nil {
handleError(fmt.Errorf("removing project failed: %w", err), http.StatusUnprocessableEntity, rw)
return
}
if err := json.NewEncoder(rw).Encode(DefaultAPIResponse{Message: fmt.Sprintf("Remove Project Success for user %s", chi.URLParam(r, "id"))}); err != nil {
cclog.Errorf("Failed to encode response: %v", err)
}
} else {
handleError(fmt.Errorf("no operation specified: must provide add-role, remove-role, add-project, or remove-project"), http.StatusBadRequest, rw)
}
}
func (api *RestAPI) updateUserByForm(rw http.ResponseWriter, r *http.Request) {
// SecuredCheck() only worked with TokenAuth: Removed
if user := repository.GetUserFromContext(r.Context()); !user.HasRole(schema.RoleAdmin) {

View File

@@ -632,7 +632,7 @@ func securedCheck(user *schema.User, r *http.Request) error {
}
// If SplitHostPort fails, IPAddress is already just a host (no port)
// If nothing declared in config: Continue
// If nothing declared in config: Continue // FIXME: Allow All If Not Declared?
if len(config.Keys.APIAllowedIPs) == 0 {
return nil
}