diff --git a/api/swagger.json b/api/swagger.json index c9c36de1..cbad2856 100644 --- a/api/swagger.json +++ b/api/swagger.json @@ -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": { diff --git a/api/swagger.yaml b/api/swagger.yaml index def939dd..7aa215eb 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -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: diff --git a/internal/api/docs.go b/internal/api/docs.go index de3cf506..2c3b0540 100644 --- a/internal/api/docs.go +++ b/internal/api/docs.go @@ -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": { diff --git a/internal/api/rest.go b/internal/api/rest.go index cb041012..93a7ab52 100644 --- a/internal/api/rest.go +++ b/internal/api/rest.go @@ -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) diff --git a/internal/api/user.go b/internal/api/user.go index e2f78165..a81eb029 100644 --- a/internal/api/user.go +++ b/internal/api/user.go @@ -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 -// @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" +// @accept json +// @produce json +// @param id path string true "Username" +// @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) { diff --git a/internal/auth/auth.go b/internal/auth/auth.go index d1c004bd..04d9e47a 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -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 }