diff --git a/api/acl_admin/handle.go b/api/acl_admin/handle.go index a4c1c71..837c3e7 100644 --- a/api/acl_admin/handle.go +++ b/api/acl_admin/handle.go @@ -51,20 +51,28 @@ func MustRoute(config *config.Config, aclService *acl.Service, authService *auth // DELETE /roles/{roleId}/resources/{resId} — убрать ресурс return func(r chi.Router) { // Roles - r.Get("/roles", h.getRoles) // list all roles - r.Post("/roles", h.createRole) // create a new role - r.Get("/roles/{roleId}", h.getRole) // get a role by ID - r.Get("/roles/{roleId}/users", h.getRoleUsers) // get all assigned users to a role - r.Patch("/roles/{roleId}", h.updateRole) // update a role by ID - r.Delete("/roles/{roleId}", h.deleteRole) // delete a role by ID + r.Get("/roles", h.getRoles) // list all roles + r.Post("/roles", h.createRole) // create a new role + r.Get("/roles/{roleId}", h.getRole) // get a role by ID + r.Get("/roles/{roleId}/users", h.getRoleUsers) // get all assigned users to a role + r.Get("/roles/{roleId}/resources", h.getRoleResources) // get all resources assigned to a role + r.Patch("/roles/{roleId}", h.updateRole) // update a role by ID + r.Delete("/roles/{roleId}", h.deleteRole) // delete a role by ID + r.Post("/roles/{roleId}/resources", h.assignResourceToRole) // assign a resource to a role + r.Delete("/roles/{roleId}/resources/{resId}", h.removeResourceFromRole) // remove a resource from a role - // // Resources + // Resources r.Get("/resources", h.getResources) // list all resources r.Post("/resources", h.createResource) // create a new resource r.Get("/resources/{resourceId}", h.getResource) // get a resource by ID r.Patch("/resources/{resourceId}", h.updateResource) // update a resource by ID r.Delete("/resources/{resourceId}", h.deleteResource) // delete a resource by ID + // Users + r.Get("/users/{userId}/roles", h.getUserRoles) // get all roles for a user + r.Post("/users/{userId}/roles", h.assignRoleToUser) // assign a role to a user + r.Delete("/users/{userId}/roles/{roleId}", h.removeRoleFromUser) // remove a role from a user + // Users // r.Get("/users/{userId}/roles", h.getUserRoles) // get all roles for a user // r.Post("/users/{userId}/roles", h.assignRoleToUser) // assign a role to a user diff --git a/api/acl_admin/resources.go b/api/acl_admin/resources.go index 6986ebb..65ed748 100644 --- a/api/acl_admin/resources.go +++ b/api/acl_admin/resources.go @@ -11,7 +11,7 @@ import ( ) // @Summary Get all resources -// @Tags resources +// @Tags acl/resources // @Produce json // @Success 200 {array} getResourcesResponse // @Failure 500 {object} ProblemDetails @@ -45,7 +45,7 @@ func (h *aclAdminHandler) getResources(w http.ResponseWriter, r *http.Request) { } // @Summary Get resource by ID -// @Tags resources +// @Tags acl/resources // @Produce json // @Param resourceId path int true "Resource ID" example(1) // @Success 200 {object} getResourceResponse @@ -84,7 +84,7 @@ func (h *aclAdminHandler) getResource(w http.ResponseWriter, r *http.Request) { } // @Summary Create resource -// @Tags resources +// @Tags acl/resources // @Accept json // @Produce json // @Param request body createResourceRequest true "Resource" @@ -128,7 +128,7 @@ func (h *aclAdminHandler) createResource(w http.ResponseWriter, r *http.Request) } // @Summary Update resource -// @Tags resources +// @Tags acl/resources // @Accept json // @Produce json // @Param resourceId path int true "Resource ID" example(1) @@ -182,7 +182,7 @@ func (h *aclAdminHandler) updateResource(w http.ResponseWriter, r *http.Request) } // @Summary Delete resource -// @Tags resources +// @Tags acl/resources // @Produce json // @Param resourceId path int true "Resource ID" example(1) // @Success 200 diff --git a/api/acl_admin/roles.go b/api/acl_admin/roles.go index c0dbb88..d870f46 100644 --- a/api/acl_admin/roles.go +++ b/api/acl_admin/roles.go @@ -11,7 +11,7 @@ import ( ) // @Summary Get all roles -// @Tags roles +// @Tags acl/roles // @Produce json // @Success 200 {array} getRolesResponse // @Failure 500 {object} ProblemDetails @@ -43,8 +43,46 @@ func (h *aclAdminHandler) getRoles(w http.ResponseWriter, r *http.Request) { _ = json.NewEncoder(w).Encode(resp) } +// @Summary Get role by ID +// @Tags acl/roles +// @Produce json +// @Param roleId path int true "Role ID" example(1) +// @Success 200 {object} getRoleResponse +// @Failure 400 {object} ProblemDetails +// @Failure 404 {object} ProblemDetails +// @Failure 500 {object} ProblemDetails +// @Router /api/acl/roles/{roleId} [get] +func (h *aclAdminHandler) getRole(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + roleIDStr := chi.URLParam(r, "roleId") + roleID, err := strconv.Atoi(roleIDStr) + if err != nil || roleID < 0 { + writeProblem(w, http.StatusBadRequest, "/errors/acl/invalid-role-id", "Invalid role ID", "Role ID must be positive integer", r) + return + } + + role, err := h.a.GetRoleByID(uint(roleID)) + if err != nil { + switch err { + case acl.ErrNotInitialized: + writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "ACL service is not initialized", r) + case acl.ErrRoleNotFound: + writeProblem(w, http.StatusNotFound, "/errors/acl/role-not-found", "Role not found", "No role with ID "+roleIDStr, r) + default: + slog.Error("unexpected server error", "error", err.Error()) + writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r) + } + return + } + + _ = json.NewEncoder(w).Encode(getRoleResponse{ + ID: role.ID, + Name: role.Name, + }) +} + // @Summary Get role users -// @Tags roles +// @Tags acl/roles // @Produce json // @Param roleId path int true "Role ID" example(1) // @Success 200 {array} getRoleUsersResponse @@ -89,16 +127,16 @@ func (h *aclAdminHandler) getRoleUsers(w http.ResponseWriter, r *http.Request) { _ = json.NewEncoder(w).Encode(respUsers) } -// @Summary Get role by ID -// @Tags roles +// @Summary Get role resources +// @Tags acl/roles // @Produce json // @Param roleId path int true "Role ID" example(1) -// @Success 200 {object} getRoleResponse +// @Success 200 {array} getRoleResourcesResponse // @Failure 400 {object} ProblemDetails // @Failure 404 {object} ProblemDetails // @Failure 500 {object} ProblemDetails -// @Router /api/acl/roles/{roleId} [get] -func (h *aclAdminHandler) getRole(w http.ResponseWriter, r *http.Request) { +// @Router /api/acl/roles/{roleId}/resources [get] +func (h *aclAdminHandler) getRoleResources(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") roleIDStr := chi.URLParam(r, "roleId") roleID, err := strconv.Atoi(roleIDStr) @@ -106,7 +144,6 @@ func (h *aclAdminHandler) getRole(w http.ResponseWriter, r *http.Request) { writeProblem(w, http.StatusBadRequest, "/errors/acl/invalid-role-id", "Invalid role ID", "Role ID must be positive integer", r) return } - role, err := h.a.GetRoleByID(uint(roleID)) if err != nil { switch err { @@ -120,15 +157,22 @@ func (h *aclAdminHandler) getRole(w http.ResponseWriter, r *http.Request) { } return } - - _ = json.NewEncoder(w).Encode(getRoleResponse{ - ID: role.ID, - Name: role.Name, - }) + if len(role.Resources) == 0 { + writeProblem(w, http.StatusNotFound, "/errors/acl/role-has-no-users", "Role has no users", "Role has no users", r) + return + } + var respResources getRoleResourcesResponse + for _, user := range role.Resources { + respResources = append(respResources, getRoleResource{ + ID: user.ID, + Name: user.Key, + }) + } + _ = json.NewEncoder(w).Encode(respResources) } // @Summary Create role -// @Tags roles +// @Tags acl/roles // @Accept json // @Produce json // @Param request body createRoleRequest true "Role" @@ -170,7 +214,7 @@ func (h *aclAdminHandler) createRole(w http.ResponseWriter, r *http.Request) { } // @Summary Update role -// @Tags roles +// @Tags acl/roles // @Accept json // @Produce json // @Param roleId path int true "Role ID" example(1) @@ -222,10 +266,10 @@ func (h *aclAdminHandler) updateRole(w http.ResponseWriter, r *http.Request) { } // @Summary Delete role -// @Tags roles +// @Tags acl/roles // @Produce json // @Param roleId path int true "Role ID" example(1) -// @Success 200 +// @Success 204 // @Failure 400 {object} ProblemDetails // @Failure 404 {object} ProblemDetails // @Failure 409 {object} ProblemDetails @@ -256,5 +300,91 @@ func (h *aclAdminHandler) deleteRole(w http.ResponseWriter, r *http.Request) { return } - w.WriteHeader(http.StatusOK) + w.WriteHeader(http.StatusNoContent) +} + +// @Summary Assign resource to role +// @Tags acl/roles +// @Produce json +// @Param roleId path int true "Role ID" example(1) +// @Param request body assignResourceToRoleRequest true "Resource" +// @Success 201 +// @Failure 400 {object} ProblemDetails +// @Failure 404 {object} ProblemDetails +// @Failure 409 {object} ProblemDetails +// @Failure 500 {object} ProblemDetails +// @Router /api/acl/roles/{roleId}/resources [post] +func (h *aclAdminHandler) assignResourceToRole(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + roleIDStr := chi.URLParam(r, "roleId") + roleID, err := strconv.Atoi(roleIDStr) + if err != nil || roleID < 0 { + writeProblem(w, http.StatusBadRequest, "/errors/acl/invalid-role-id", "Invalid role ID", "Role ID must be positive integer", r) + return + } + var req assignResourceToRoleRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeProblem(w, http.StatusBadRequest, "/errors/acl/invalid-request-body", "Invalid request body", "Invalid JSON body", r) + return + } + if err := h.a.AssignResourceToRole(uint(roleID), req.ResourceID); err != nil { + slog.Error("Failed to assign resource to role", "error", err.Error()) + switch err { + case acl.ErrNotInitialized: + writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "ACL service is not initialized", r) + case acl.ErrRoleNotFound: + writeProblem(w, http.StatusNotFound, "/errors/acl/role-not-found", "Role not found", "No role with ID "+roleIDStr, r) + case acl.ErrResourceNotFound: + writeProblem(w, http.StatusNotFound, "/errors/acl/resource-not-found", "Resource not found", "No resource with ID "+strconv.Itoa(int(req.ResourceID)), r) + case acl.ErrResourceAlreadyAssigned: + writeProblem(w, http.StatusConflict, "/errors/acl/resource-already-assigned", "Resource already assigned", "Resource with ID "+strconv.Itoa(int(req.ResourceID))+" is already assigned to role with ID "+roleIDStr, r) + default: + writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r) + } + return + } + w.WriteHeader(http.StatusCreated) +} + +// @Summary Remove resource from role +// @Tags acl/roles +// @Produce json +// @Param roleId path int true "Role ID" example(1) +// @Param resId path int true "Resource ID" example(1) +// @Success 204 +// @Failure 400 {object} ProblemDetails +// @Failure 404 {object} ProblemDetails +// @Failure 500 {object} ProblemDetails +// @Router /api/acl/roles/{roleId}/resources/{resId} [delete] +func (h *aclAdminHandler) removeResourceFromRole(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + roleIDStr := chi.URLParam(r, "roleId") + roleID, err := strconv.Atoi(roleIDStr) + if err != nil || roleID < 0 { + writeProblem(w, http.StatusBadRequest, "/errors/acl/invalid-role-id", "Invalid role ID", "Role ID must be positive integer", r) + return + } + resourceIDStr := chi.URLParam(r, "resId") + resourceID, err := strconv.Atoi(resourceIDStr) + if err != nil || resourceID < 0 { + writeProblem(w, http.StatusBadRequest, "/errors/acl/invalid-resource-id", "Invalid resource ID", "Resource ID must be positive integer", r) + return + } + if err := h.a.RemoveResourceFromRole(uint(roleID), uint(resourceID)); err != nil { + slog.Error("Failed to remove resource from role", "error", err.Error()) + switch err { + case acl.ErrNotInitialized: + writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "ACL service is not initialized", r) + case acl.ErrRoleNotFound: + writeProblem(w, http.StatusNotFound, "/errors/acl/role-not-found", "Role not found", "No role with ID "+roleIDStr, r) + case acl.ErrResourceNotFound: + writeProblem(w, http.StatusNotFound, "/errors/acl/resource-not-found", "Resource not found", "No resource with ID "+strconv.Itoa(int(resourceID)), r) + case acl.ErrRoleResourceNotFound: + writeProblem(w, http.StatusNotFound, "/errors/acl/role-resource-not-found", "Role resource not found", "No role-resource pair with role ID "+roleIDStr+" and resource ID "+strconv.Itoa(int(resourceID)), r) + default: + writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r) + } + return + } + w.WriteHeader(http.StatusNoContent) } diff --git a/api/acl_admin/roles_models.go b/api/acl_admin/roles_models.go index 47e03d1..8fb8fee 100644 --- a/api/acl_admin/roles_models.go +++ b/api/acl_admin/roles_models.go @@ -25,6 +25,14 @@ type getRoleUser struct { } type getRoleUsersResponse []getRoleUser +/*******************************************************************/ +// used in getRoleResources() +type getRoleResource struct { + ID uint `json:"id" example:"1"` + Name string `json:"name" example:"*"` +} +type getRoleResourcesResponse []getRoleResource + /*******************************************************************/ // used in createRole() type createRoleRequest struct { @@ -46,3 +54,9 @@ type updateRoleResponse struct { ID uint `json:"id" example:"1"` Name string `json:"name" example:"admin"` } + +/*******************************************************************/ +// used in assignResourceToRole() +type assignResourceToRoleRequest struct { + ResourceID uint `json:"resourceId" example:"1"` +} diff --git a/api/acl_admin/users.go b/api/acl_admin/users.go new file mode 100644 index 0000000..83595cb --- /dev/null +++ b/api/acl_admin/users.go @@ -0,0 +1,136 @@ +package api_acladmin + +import ( + "encoding/json" + "log/slog" + "net/http" + "strconv" + + "git.oblat.lv/alex/triggerssmith/internal/acl" + "github.com/go-chi/chi/v5" +) + +// @Summary Get user roles by user ID +// @Tags acl/users +// @Produce json +// @Param userId path int true "User ID" example(1) +// @Success 200 {object} getUserRolesResponse +// @Failure 400 {object} ProblemDetails +// @Failure 404 {object} ProblemDetails +// @Failure 500 {object} ProblemDetails +// @Router /api/acl/users/{userId}/roles [get] +func (h *aclAdminHandler) getUserRoles(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + userIDStr := chi.URLParam(r, "userId") + userID, err := strconv.Atoi(userIDStr) + if err != nil { + writeProblem(w, http.StatusBadRequest, "/errors/acl/invalid-user-id", "Invalid user ID", "User ID must be positive integer", r) + return + } + roles, err := h.a.GetUserRoles(uint(userID)) + if err != nil { + switch err { + case acl.ErrNotInitialized: + writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "ACL service is not initialized", r) + case acl.ErrUserNotFound: + writeProblem(w, http.StatusNotFound, "/errors/acl/user-not-found", "User not found", "User not found", r) + case acl.ErrRoleNotFound: + writeProblem(w, http.StatusNotFound, "/errors/acl/no-role-found", "No role found", "No role found for user "+strconv.Itoa(userID), r) + default: + slog.Error("unexpected server error", "error", err.Error()) + writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r) + } + return + } + resp := make(getUserRolesResponse, 0, len(roles)) + for _, role := range roles { + resp = append(resp, getUserRole{ID: role.ID, Name: role.Name}) + } + _ = json.NewEncoder(w).Encode(resp) +} + +// @Summary Assign role to user +// @Tags acl/users +// @Produce json +// @Param userId path int true "User ID" example(1) +// @Param body body assignRoleToUserRequest true "Role ID" +// @Success 201 +// @Failure 400 {object} ProblemDetails +// @Failure 404 {object} ProblemDetails +// @Failure 409 {object} ProblemDetails +// @Failure 500 {object} ProblemDetails +// @Router /api/acl/users/{userId}/roles [post] +func (h *aclAdminHandler) assignRoleToUser(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + userIDStr := chi.URLParam(r, "userId") + userID, err := strconv.Atoi(userIDStr) + if err != nil || userID < 0 { + writeProblem(w, http.StatusBadRequest, "/errors/acl/invalid-user-id", "Invalid user ID", "User ID must be positive integer", r) + return + } + var req assignRoleToUserRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeProblem(w, http.StatusBadRequest, "/errors/acl/invalid-request-body", "Invalid request body", "Invalid JSON body", r) + return + } + if err := h.a.AssignRoleToUser(req.RoleID, uint(userID)); err != nil { + slog.Error("Failed to assign role to user", "error", err.Error()) + switch err { + case acl.ErrNotInitialized: + writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "ACL service is not initialized", r) + case acl.ErrUserNotFound: + writeProblem(w, http.StatusNotFound, "/errors/acl/user-not-found", "User not found", "User not found", r) + case acl.ErrRoleNotFound: + writeProblem(w, http.StatusNotFound, "/errors/acl/no-role-found", "No role found", "No role found for user "+strconv.Itoa(userID), r) + case acl.ErrRoleAlreadyAssigned: + writeProblem(w, http.StatusConflict, "/errors/acl/role-already-assigned", "Role already assigned", "Role with ID "+strconv.Itoa(int(req.RoleID))+" is already assigned to user "+strconv.Itoa(userID), r) + default: + writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r) + } + return + } + w.WriteHeader(http.StatusCreated) +} + +// @Summary Remove role from user +// @Tags acl/users +// @Produce json +// @Param userId path int true "User ID" example(1) +// @Param roleId path int true "Role ID" example(1) +// @Success 204 +// @Failure 400 {object} ProblemDetails +// @Failure 404 {object} ProblemDetails +// @Failure 500 {object} ProblemDetails +// @Router /api/acl/users/{userId}/roles/{roleId} [delete] +func (h *aclAdminHandler) removeRoleFromUser(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + userIDStr := chi.URLParam(r, "userId") + userID, err := strconv.Atoi(userIDStr) + if err != nil || userID < 0 { + writeProblem(w, http.StatusBadRequest, "/errors/acl/invalid-user-id", "Invalid user ID", "User ID must be positive integer", r) + return + } + roleIDStr := chi.URLParam(r, "roleId") + roleID, err := strconv.Atoi(roleIDStr) + if err != nil || roleID < 0 { + writeProblem(w, http.StatusBadRequest, "/errors/acl/invalid-role-id", "Invalid role ID", "Role ID must be positive integer", r) + return + } + err = h.a.RemoveRoleFromUser(uint(roleID), uint(userID)) + if err != nil { + slog.Error("Failed to remove role from user", "error", err.Error()) + switch err { + case acl.ErrNotInitialized: + writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "ACL service is not initialized", r) + case acl.ErrUserNotFound: + writeProblem(w, http.StatusNotFound, "/errors/acl/user-not-found", "User not found", "User not found", r) + case acl.ErrRoleNotFound: + writeProblem(w, http.StatusNotFound, "/errors/acl/no-role-found", "No role found", "No role found for user "+strconv.Itoa(userID), r) + case acl.ErrUserRoleNotFound: + writeProblem(w, http.StatusNotFound, "/errors/acl/user-role-not-found", "User role not found", "User "+strconv.Itoa(userID)+" does not have role "+strconv.Itoa(roleID), r) + default: + writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r) + } + } + w.WriteHeader(http.StatusNoContent) +} diff --git a/api/acl_admin/users_models.go b/api/acl_admin/users_models.go new file mode 100644 index 0000000..25a1dff --- /dev/null +++ b/api/acl_admin/users_models.go @@ -0,0 +1,16 @@ +package api_acladmin + +/*******************************************************************/ +// used in getUserRoles() +type getUserRole struct { + ID uint `json:"id" example:"1"` + Name string `json:"name" example:"*"` +} + +type getUserRolesResponse []getUserRole + +/*******************************************************************/ +// used in assignRoleToUser() +type assignRoleToUserRequest struct { + RoleID uint `json:"roleId" example:"1"` +} diff --git a/docs/docs.go b/docs/docs.go index cfc731f..39301b1 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -21,7 +21,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "resources" + "acl/resources" ], "summary": "Get all resources", "responses": { @@ -63,7 +63,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "resources" + "acl/resources" ], "summary": "Create resource", "parameters": [ @@ -111,7 +111,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "resources" + "acl/resources" ], "summary": "Get resource by ID", "parameters": [ @@ -156,7 +156,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "resources" + "acl/resources" ], "summary": "Delete resource", "parameters": [ @@ -207,7 +207,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "resources" + "acl/resources" ], "summary": "Update resource", "parameters": [ @@ -269,7 +269,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "roles" + "acl/roles" ], "summary": "Get all roles", "responses": { @@ -311,7 +311,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "roles" + "acl/roles" ], "summary": "Create role", "parameters": [ @@ -359,7 +359,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "roles" + "acl/roles" ], "summary": "Get role by ID", "parameters": [ @@ -404,7 +404,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "roles" + "acl/roles" ], "summary": "Delete role", "parameters": [ @@ -418,8 +418,8 @@ const docTemplate = `{ } ], "responses": { - "200": { - "description": "OK" + "204": { + "description": "No Content" }, "400": { "description": "Bad Request", @@ -455,7 +455,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "roles" + "acl/roles" ], "summary": "Update role", "parameters": [ @@ -511,13 +511,175 @@ const docTemplate = `{ } } }, + "/api/acl/roles/{roleId}/resources": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "acl/roles" + ], + "summary": "Get role resources", + "parameters": [ + { + "type": "integer", + "example": 1, + "description": "Role ID", + "name": "roleId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/api_acladmin.getRoleResource" + } + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api_acladmin.ProblemDetails" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/api_acladmin.ProblemDetails" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api_acladmin.ProblemDetails" + } + } + } + }, + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "acl/roles" + ], + "summary": "Assign resource to role", + "parameters": [ + { + "type": "integer", + "example": 1, + "description": "Role ID", + "name": "roleId", + "in": "path", + "required": true + }, + { + "description": "Resource", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api_acladmin.assignResourceToRoleRequest" + } + } + ], + "responses": { + "201": { + "description": "Created" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api_acladmin.ProblemDetails" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/api_acladmin.ProblemDetails" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/api_acladmin.ProblemDetails" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api_acladmin.ProblemDetails" + } + } + } + } + }, + "/api/acl/roles/{roleId}/resources/{resId}": { + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "acl/roles" + ], + "summary": "Remove resource from role", + "parameters": [ + { + "type": "integer", + "example": 1, + "description": "Role ID", + "name": "roleId", + "in": "path", + "required": true + }, + { + "type": "integer", + "example": 1, + "description": "Resource ID", + "name": "resId", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api_acladmin.ProblemDetails" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/api_acladmin.ProblemDetails" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api_acladmin.ProblemDetails" + } + } + } + } + }, "/api/acl/roles/{roleId}/users": { "get": { "produces": [ "application/json" ], "tags": [ - "roles" + "acl/roles" ], "summary": "Get role users", "parameters": [ @@ -563,6 +725,165 @@ const docTemplate = `{ } } } + }, + "/api/acl/users/{userId}/roles": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "acl/users" + ], + "summary": "Get user roles by user ID", + "parameters": [ + { + "type": "integer", + "example": 1, + "description": "User ID", + "name": "userId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/api_acladmin.getUserRole" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api_acladmin.ProblemDetails" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/api_acladmin.ProblemDetails" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api_acladmin.ProblemDetails" + } + } + } + }, + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "acl/users" + ], + "summary": "Assign role to user", + "parameters": [ + { + "type": "integer", + "example": 1, + "description": "User ID", + "name": "userId", + "in": "path", + "required": true + }, + { + "description": "Role ID", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api_acladmin.assignRoleToUserRequest" + } + } + ], + "responses": { + "201": { + "description": "Created" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api_acladmin.ProblemDetails" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/api_acladmin.ProblemDetails" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/api_acladmin.ProblemDetails" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api_acladmin.ProblemDetails" + } + } + } + } + }, + "/api/acl/users/{userId}/roles/{roleId}": { + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "acl/users" + ], + "summary": "Remove role from user", + "parameters": [ + { + "type": "integer", + "example": 1, + "description": "User ID", + "name": "userId", + "in": "path", + "required": true + }, + { + "type": "integer", + "example": 1, + "description": "Role ID", + "name": "roleId", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api_acladmin.ProblemDetails" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/api_acladmin.ProblemDetails" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api_acladmin.ProblemDetails" + } + } + } + } } }, "definitions": { @@ -591,6 +912,24 @@ const docTemplate = `{ } } }, + "api_acladmin.assignResourceToRoleRequest": { + "type": "object", + "properties": { + "resourceId": { + "type": "integer", + "example": 1 + } + } + }, + "api_acladmin.assignRoleToUserRequest": { + "type": "object", + "properties": { + "roleId": { + "type": "integer", + "example": 1 + } + } + }, "api_acladmin.createResourceRequest": { "type": "object", "properties": { @@ -648,6 +987,19 @@ const docTemplate = `{ } } }, + "api_acladmin.getRoleResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "name": { + "type": "string", + "example": "*" + } + } + }, "api_acladmin.getRoleResponse": { "type": "object", "properties": { @@ -664,20 +1016,33 @@ const docTemplate = `{ "api_acladmin.getRoleUser": { "type": "object", "properties": { - "userEmail": { + "email": { "type": "string", "example": "admin@triggerssmith.com" }, - "userId": { + "id": { "type": "integer", "example": 1 }, - "userName": { + "username": { "type": "string", "example": "admin" } } }, + "api_acladmin.getUserRole": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "name": { + "type": "string", + "example": "*" + } + } + }, "api_acladmin.updateResourceRequest": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 69f3f69..c5e8cab 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -10,7 +10,7 @@ "application/json" ], "tags": [ - "resources" + "acl/resources" ], "summary": "Get all resources", "responses": { @@ -52,7 +52,7 @@ "application/json" ], "tags": [ - "resources" + "acl/resources" ], "summary": "Create resource", "parameters": [ @@ -100,7 +100,7 @@ "application/json" ], "tags": [ - "resources" + "acl/resources" ], "summary": "Get resource by ID", "parameters": [ @@ -145,7 +145,7 @@ "application/json" ], "tags": [ - "resources" + "acl/resources" ], "summary": "Delete resource", "parameters": [ @@ -196,7 +196,7 @@ "application/json" ], "tags": [ - "resources" + "acl/resources" ], "summary": "Update resource", "parameters": [ @@ -258,7 +258,7 @@ "application/json" ], "tags": [ - "roles" + "acl/roles" ], "summary": "Get all roles", "responses": { @@ -300,7 +300,7 @@ "application/json" ], "tags": [ - "roles" + "acl/roles" ], "summary": "Create role", "parameters": [ @@ -348,7 +348,7 @@ "application/json" ], "tags": [ - "roles" + "acl/roles" ], "summary": "Get role by ID", "parameters": [ @@ -393,7 +393,7 @@ "application/json" ], "tags": [ - "roles" + "acl/roles" ], "summary": "Delete role", "parameters": [ @@ -407,8 +407,8 @@ } ], "responses": { - "200": { - "description": "OK" + "204": { + "description": "No Content" }, "400": { "description": "Bad Request", @@ -444,7 +444,7 @@ "application/json" ], "tags": [ - "roles" + "acl/roles" ], "summary": "Update role", "parameters": [ @@ -500,13 +500,175 @@ } } }, + "/api/acl/roles/{roleId}/resources": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "acl/roles" + ], + "summary": "Get role resources", + "parameters": [ + { + "type": "integer", + "example": 1, + "description": "Role ID", + "name": "roleId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/api_acladmin.getRoleResource" + } + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api_acladmin.ProblemDetails" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/api_acladmin.ProblemDetails" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api_acladmin.ProblemDetails" + } + } + } + }, + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "acl/roles" + ], + "summary": "Assign resource to role", + "parameters": [ + { + "type": "integer", + "example": 1, + "description": "Role ID", + "name": "roleId", + "in": "path", + "required": true + }, + { + "description": "Resource", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api_acladmin.assignResourceToRoleRequest" + } + } + ], + "responses": { + "201": { + "description": "Created" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api_acladmin.ProblemDetails" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/api_acladmin.ProblemDetails" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/api_acladmin.ProblemDetails" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api_acladmin.ProblemDetails" + } + } + } + } + }, + "/api/acl/roles/{roleId}/resources/{resId}": { + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "acl/roles" + ], + "summary": "Remove resource from role", + "parameters": [ + { + "type": "integer", + "example": 1, + "description": "Role ID", + "name": "roleId", + "in": "path", + "required": true + }, + { + "type": "integer", + "example": 1, + "description": "Resource ID", + "name": "resId", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api_acladmin.ProblemDetails" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/api_acladmin.ProblemDetails" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api_acladmin.ProblemDetails" + } + } + } + } + }, "/api/acl/roles/{roleId}/users": { "get": { "produces": [ "application/json" ], "tags": [ - "roles" + "acl/roles" ], "summary": "Get role users", "parameters": [ @@ -552,6 +714,165 @@ } } } + }, + "/api/acl/users/{userId}/roles": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "acl/users" + ], + "summary": "Get user roles by user ID", + "parameters": [ + { + "type": "integer", + "example": 1, + "description": "User ID", + "name": "userId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/api_acladmin.getUserRole" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api_acladmin.ProblemDetails" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/api_acladmin.ProblemDetails" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api_acladmin.ProblemDetails" + } + } + } + }, + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "acl/users" + ], + "summary": "Assign role to user", + "parameters": [ + { + "type": "integer", + "example": 1, + "description": "User ID", + "name": "userId", + "in": "path", + "required": true + }, + { + "description": "Role ID", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api_acladmin.assignRoleToUserRequest" + } + } + ], + "responses": { + "201": { + "description": "Created" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api_acladmin.ProblemDetails" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/api_acladmin.ProblemDetails" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/api_acladmin.ProblemDetails" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api_acladmin.ProblemDetails" + } + } + } + } + }, + "/api/acl/users/{userId}/roles/{roleId}": { + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "acl/users" + ], + "summary": "Remove role from user", + "parameters": [ + { + "type": "integer", + "example": 1, + "description": "User ID", + "name": "userId", + "in": "path", + "required": true + }, + { + "type": "integer", + "example": 1, + "description": "Role ID", + "name": "roleId", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api_acladmin.ProblemDetails" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/api_acladmin.ProblemDetails" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api_acladmin.ProblemDetails" + } + } + } + } } }, "definitions": { @@ -580,6 +901,24 @@ } } }, + "api_acladmin.assignResourceToRoleRequest": { + "type": "object", + "properties": { + "resourceId": { + "type": "integer", + "example": 1 + } + } + }, + "api_acladmin.assignRoleToUserRequest": { + "type": "object", + "properties": { + "roleId": { + "type": "integer", + "example": 1 + } + } + }, "api_acladmin.createResourceRequest": { "type": "object", "properties": { @@ -637,6 +976,19 @@ } } }, + "api_acladmin.getRoleResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "name": { + "type": "string", + "example": "*" + } + } + }, "api_acladmin.getRoleResponse": { "type": "object", "properties": { @@ -653,20 +1005,33 @@ "api_acladmin.getRoleUser": { "type": "object", "properties": { - "userEmail": { + "email": { "type": "string", "example": "admin@triggerssmith.com" }, - "userId": { + "id": { "type": "integer", "example": 1 }, - "userName": { + "username": { "type": "string", "example": "admin" } } }, + "api_acladmin.getUserRole": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "name": { + "type": "string", + "example": "*" + } + } + }, "api_acladmin.updateResourceRequest": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 9af92f5..dc03ad4 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -17,6 +17,18 @@ definitions: example: https://api.triggerssmith.com/errors/role-not-found type: string type: object + api_acladmin.assignResourceToRoleRequest: + properties: + resourceId: + example: 1 + type: integer + type: object + api_acladmin.assignRoleToUserRequest: + properties: + roleId: + example: 1 + type: integer + type: object api_acladmin.createResourceRequest: properties: key: @@ -56,6 +68,15 @@ definitions: example: html.view type: string type: object + api_acladmin.getRoleResource: + properties: + id: + example: 1 + type: integer + name: + example: '*' + type: string + type: object api_acladmin.getRoleResponse: properties: id: @@ -67,16 +88,25 @@ definitions: type: object api_acladmin.getRoleUser: properties: - userEmail: + email: example: admin@triggerssmith.com type: string - userId: + id: example: 1 type: integer - userName: + username: example: admin type: string type: object + api_acladmin.getUserRole: + properties: + id: + example: 1 + type: integer + name: + example: '*' + type: string + type: object api_acladmin.updateResourceRequest: properties: key: @@ -136,7 +166,7 @@ paths: $ref: '#/definitions/api_acladmin.ProblemDetails' summary: Get all resources tags: - - resources + - acl/resources post: consumes: - application/json @@ -168,7 +198,7 @@ paths: $ref: '#/definitions/api_acladmin.ProblemDetails' summary: Create resource tags: - - resources + - acl/resources /api/acl/resources/{resourceId}: delete: parameters: @@ -201,7 +231,7 @@ paths: $ref: '#/definitions/api_acladmin.ProblemDetails' summary: Delete resource tags: - - resources + - acl/resources get: parameters: - description: Resource ID @@ -231,7 +261,7 @@ paths: $ref: '#/definitions/api_acladmin.ProblemDetails' summary: Get resource by ID tags: - - resources + - acl/resources patch: consumes: - application/json @@ -273,7 +303,7 @@ paths: $ref: '#/definitions/api_acladmin.ProblemDetails' summary: Update resource tags: - - resources + - acl/resources /api/acl/roles: get: produces: @@ -300,7 +330,7 @@ paths: $ref: '#/definitions/api_acladmin.ProblemDetails' summary: Get all roles tags: - - roles + - acl/roles post: consumes: - application/json @@ -332,7 +362,7 @@ paths: $ref: '#/definitions/api_acladmin.ProblemDetails' summary: Create role tags: - - roles + - acl/roles /api/acl/roles/{roleId}: delete: parameters: @@ -345,8 +375,8 @@ paths: produces: - application/json responses: - "200": - description: OK + "204": + description: No Content "400": description: Bad Request schema: @@ -365,7 +395,7 @@ paths: $ref: '#/definitions/api_acladmin.ProblemDetails' summary: Delete role tags: - - roles + - acl/roles get: parameters: - description: Role ID @@ -395,7 +425,7 @@ paths: $ref: '#/definitions/api_acladmin.ProblemDetails' summary: Get role by ID tags: - - roles + - acl/roles patch: consumes: - application/json @@ -437,7 +467,115 @@ paths: $ref: '#/definitions/api_acladmin.ProblemDetails' summary: Update role tags: - - roles + - acl/roles + /api/acl/roles/{roleId}/resources: + get: + parameters: + - description: Role ID + example: 1 + in: path + name: roleId + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + items: + $ref: '#/definitions/api_acladmin.getRoleResource' + type: array + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/api_acladmin.ProblemDetails' + "404": + description: Not Found + schema: + $ref: '#/definitions/api_acladmin.ProblemDetails' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/api_acladmin.ProblemDetails' + summary: Get role resources + tags: + - acl/roles + post: + parameters: + - description: Role ID + example: 1 + in: path + name: roleId + required: true + type: integer + - description: Resource + in: body + name: request + required: true + schema: + $ref: '#/definitions/api_acladmin.assignResourceToRoleRequest' + produces: + - application/json + responses: + "201": + description: Created + "400": + description: Bad Request + schema: + $ref: '#/definitions/api_acladmin.ProblemDetails' + "404": + description: Not Found + schema: + $ref: '#/definitions/api_acladmin.ProblemDetails' + "409": + description: Conflict + schema: + $ref: '#/definitions/api_acladmin.ProblemDetails' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/api_acladmin.ProblemDetails' + summary: Assign resource to role + tags: + - acl/roles + /api/acl/roles/{roleId}/resources/{resId}: + delete: + parameters: + - description: Role ID + example: 1 + in: path + name: roleId + required: true + type: integer + - description: Resource ID + example: 1 + in: path + name: resId + required: true + type: integer + produces: + - application/json + responses: + "204": + description: No Content + "400": + description: Bad Request + schema: + $ref: '#/definitions/api_acladmin.ProblemDetails' + "404": + description: Not Found + schema: + $ref: '#/definitions/api_acladmin.ProblemDetails' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/api_acladmin.ProblemDetails' + summary: Remove resource from role + tags: + - acl/roles /api/acl/roles/{roleId}/users: get: parameters: @@ -472,5 +610,111 @@ paths: $ref: '#/definitions/api_acladmin.ProblemDetails' summary: Get role users tags: - - roles + - acl/roles + /api/acl/users/{userId}/roles: + get: + parameters: + - description: User ID + example: 1 + in: path + name: userId + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/api_acladmin.getUserRole' + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/api_acladmin.ProblemDetails' + "404": + description: Not Found + schema: + $ref: '#/definitions/api_acladmin.ProblemDetails' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/api_acladmin.ProblemDetails' + summary: Get user roles by user ID + tags: + - acl/users + post: + parameters: + - description: User ID + example: 1 + in: path + name: userId + required: true + type: integer + - description: Role ID + in: body + name: body + required: true + schema: + $ref: '#/definitions/api_acladmin.assignRoleToUserRequest' + produces: + - application/json + responses: + "201": + description: Created + "400": + description: Bad Request + schema: + $ref: '#/definitions/api_acladmin.ProblemDetails' + "404": + description: Not Found + schema: + $ref: '#/definitions/api_acladmin.ProblemDetails' + "409": + description: Conflict + schema: + $ref: '#/definitions/api_acladmin.ProblemDetails' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/api_acladmin.ProblemDetails' + summary: Assign role to user + tags: + - acl/users + /api/acl/users/{userId}/roles/{roleId}: + delete: + parameters: + - description: User ID + example: 1 + in: path + name: userId + required: true + type: integer + - description: Role ID + example: 1 + in: path + name: roleId + required: true + type: integer + produces: + - application/json + responses: + "204": + description: No Content + "400": + description: Bad Request + schema: + $ref: '#/definitions/api_acladmin.ProblemDetails' + "404": + description: Not Found + schema: + $ref: '#/definitions/api_acladmin.ProblemDetails' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/api_acladmin.ProblemDetails' + summary: Remove role from user + tags: + - acl/users swagger: "2.0" diff --git a/internal/acl/errors.go b/internal/acl/errors.go index 21b2074..64c2193 100644 --- a/internal/acl/errors.go +++ b/internal/acl/errors.go @@ -7,15 +7,21 @@ import "fmt" var ( ErrNotInitialized = fmt.Errorf("acl service is not initialized") - ErrRoleNotFound = fmt.Errorf("role not found") - ErrRoleAlreadyExists = fmt.Errorf("role already exists") - ErrInvalidRoleName = fmt.Errorf("role name is invalid") - ErrSameRoleName = fmt.Errorf("role name is the same as another role") - ErrRoleInUse = fmt.Errorf("role is in use") + ErrRoleNotFound = fmt.Errorf("role not found") + ErrRoleAlreadyExists = fmt.Errorf("role already exists") + ErrInvalidRoleName = fmt.Errorf("role name is invalid") + ErrSameRoleName = fmt.Errorf("role name is the same as another role") + ErrRoleInUse = fmt.Errorf("role is in use") + ErrRoleAlreadyAssigned = fmt.Errorf("role is already assigned to user") - ErrResourceNotFound = fmt.Errorf("resource not found") - ErrResourceAlreadyExists = fmt.Errorf("resource already exists") - ErrInvalidResourceKey = fmt.Errorf("invalid resource key") - ErrResourceInUse = fmt.Errorf("resource is in use") - ErrSameResourceKey = fmt.Errorf("resource key is the same as another resource") + ErrResourceNotFound = fmt.Errorf("resource not found") + ErrResourceAlreadyExists = fmt.Errorf("resource already exists") + ErrInvalidResourceKey = fmt.Errorf("invalid resource key") + ErrResourceInUse = fmt.Errorf("resource is in use") + ErrSameResourceKey = fmt.Errorf("resource key is the same as another resource") + ErrResourceAlreadyAssigned = fmt.Errorf("resource is already assigned to role") + ErrRoleResourceNotFound = fmt.Errorf("assigned resource to role is not found") + + ErrUserNotFound = fmt.Errorf("user not found") + ErrUserRoleNotFound = fmt.Errorf("user role not found") ) diff --git a/internal/acl/resources.go b/internal/acl/resources.go index 4cc7b60..13e03e7 100644 --- a/internal/acl/resources.go +++ b/internal/acl/resources.go @@ -6,6 +6,7 @@ import ( "strings" "gorm.io/gorm" + "gorm.io/gorm/clause" ) // GetResources returns all resources. @@ -138,3 +139,82 @@ func (s *Service) DeleteResource(resourceID uint) error { return nil } + +// AssignResourceToRole assigns a resource to a role +// May return [ErrNotInitialized], [ErrRoleNotFound], [ErrResourceNotFound], [ErrAlreadyAssigned] or db error. +func (s *Service) AssignResourceToRole(roleID, resourceID uint) error { + if !s.isInitialized() { + return ErrNotInitialized + } + + // check role exists + var r Role + if err := s.db.First(&r, roleID).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrRoleNotFound + } + return fmt.Errorf("failed to fetch role: %w", err) + } + + // check resource exists + var res Resource + if err := s.db.First(&res, resourceID).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrResourceNotFound + } + return fmt.Errorf("failed to fetch resource: %w", err) + } + + rr := RoleResource{ + RoleID: roleID, + ResourceID: resourceID, + } + + tx := s.db.Clauses(clause.OnConflict{DoNothing: true}).Create(&rr) + if tx.Error != nil { + return fmt.Errorf("failed to assign resource to role: %w", tx.Error) + } + + // if nothing inserted — already assigned + if tx.RowsAffected == 0 { + return ErrResourceAlreadyAssigned + } + + return nil +} + +// RemoveResourceFromRole removes a resource from a role +// May return [ErrNotInitialized], [ErrRoleNotFound], [ErrResourceNotFound], [ErrRoleResourceNotFound] or db error. +func (s *Service) RemoveResourceFromRole(roleID, resourceID uint) error { + if !s.isInitialized() { + return ErrNotInitialized + } + // check role exists + var r Role + if err := s.db.First(&r, roleID).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrRoleNotFound + } + return fmt.Errorf("failed to fetch role: %w", err) + } + + // check resource exists + var res Resource + if err := s.db.First(&res, resourceID).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrResourceNotFound + } + return fmt.Errorf("failed to fetch resource: %w", err) + } + + tx := s.db.Where("role_id = ? AND resource_id = ?", roleID, resourceID).Delete(&RoleResource{}) + if tx.Error != nil { + return fmt.Errorf("failed to remove resource from role: %w", tx.Error) + } + + if tx.RowsAffected == 0 { + return ErrRoleResourceNotFound + } + + return nil +} diff --git a/internal/acl/roles.go b/internal/acl/roles.go index c813a93..5b7db94 100644 --- a/internal/acl/roles.go +++ b/internal/acl/roles.go @@ -5,7 +5,9 @@ import ( "fmt" "strings" + "git.oblat.lv/alex/triggerssmith/internal/user" "gorm.io/gorm" + "gorm.io/gorm/clause" ) // GetRoles returns all roles. @@ -134,3 +136,105 @@ func (s *Service) DeleteRole(roleID uint) error { return nil } + +// GetUserRoles returns all roles for a given user. +// May return [ErrNotInitialized], [ErrUserNotFound], [ErrRoleNotFound] or db error. +func (s *Service) GetUserRoles(userID uint) ([]Role, error) { + if !s.isInitialized() { + return nil, ErrNotInitialized + } + var user user.User + if err := s.db.First(&user, userID).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrUserNotFound + } + return nil, fmt.Errorf("failed to fetch user: %w", err) + } + + var roles []Role + err := s.db. + Joins("JOIN user_roles ur ON ur.role_id = roles.id"). + Where("ur.user_id = ?", userID). + Find(&roles).Error + if err != nil { + return nil, fmt.Errorf("failed to get user roles: %w", err) + } + + if len(roles) == 0 { + return nil, ErrRoleNotFound + } + return roles, nil +} + +// AssignRoleToUser assigns a role to a user. +// May return [ErrNotInitialized], [ErrUserNotFound], [ErrRoleNotFound], [ErrRoleAlreadyAssigned] or db error. +func (s *Service) AssignRoleToUser(roleID, userID uint) error { + if !s.isInitialized() { + return ErrNotInitialized + } + var user user.User + if err := s.db.First(&user, userID).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrUserNotFound + } + return fmt.Errorf("failed to fetch user: %w", err) + } + + var r Role + if err := s.db.First(&r, roleID).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrRoleNotFound + } + return fmt.Errorf("failed to fetch role: %w", err) + } + + ur := UserRole{ + UserID: userID, + RoleID: roleID, + } + + tx := s.db.Clauses(clause.OnConflict{DoNothing: true}).Create(&ur) + if tx.Error != nil { + return fmt.Errorf("failed to assign resource to role: %w", tx.Error) + } + if tx.RowsAffected == 0 { + return ErrRoleAlreadyAssigned + } + + return nil +} + +// RemoveRoleFromUser removes a role from a user. +// May return [ErrNotInitialized], [ErrUserNotFound], [ErrRoleNotFound], [ErrUserRoleNotFound] or db error. +func (s *Service) RemoveRoleFromUser(roleID, userID uint) error { + if !s.isInitialized() { + return ErrNotInitialized + } + + var user user.User + if err := s.db.First(&user, userID).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrUserNotFound + } + return fmt.Errorf("failed to fetch user: %w", err) + } + + var r Role + if err := s.db.First(&r, roleID).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrRoleNotFound + } + return fmt.Errorf("failed to fetch role: %w", err) + } + + tx := s.db.Where("role_id = ? AND user_id = ?", roleID, userID).Delete(&UserRole{}) + if tx.Error != nil { + return fmt.Errorf("failed to remove role from user: %w", tx.Error) + } + + if tx.RowsAffected == 0 { + return ErrUserRoleNotFound + } + + return nil +} diff --git a/internal/acl/service.go b/internal/acl/service.go index 17dfba7..8c53f56 100644 --- a/internal/acl/service.go +++ b/internal/acl/service.go @@ -1,7 +1,6 @@ package acl import ( - "errors" "fmt" "gorm.io/gorm" @@ -40,75 +39,3 @@ func (s *Service) Init() error { s.initialized = true return nil } - -// Admin crud functions // - -// Resources - -// AssignResourceToRole assigns a resource to a role -func (s *Service) AssignResourceToRole(roleID, resourceID uint) error { - if !s.isInitialized() { - return ErrNotInitialized - } - rr := RoleResource{ - RoleID: roleID, - ResourceID: resourceID, - } - return s.db.FirstOrCreate(&rr, RoleResource{RoleID: roleID, ResourceID: resourceID}).Error -} - -// AssignRoleToUser assigns a role to a user -func (s *Service) AssignRoleToUser(roleID, userID uint) error { - if !s.isInitialized() { - return ErrNotInitialized - } - ur := UserRole{ - UserID: userID, - RoleID: roleID, - } - if err := s.db.Create(&ur).Error; err != nil { - if errors.Is(err, gorm.ErrDuplicatedKey) { - return fmt.Errorf("role already assigned to user") - } - return err - } - return nil -} - -// RemoveResourceFromRole removes a resource from a role -func (s *Service) RemoveResourceFromRole(roleID, resourceID uint) error { - if !s.isInitialized() { - return ErrNotInitialized - } - return s.db.Where("role_id = ? AND resource_id = ?", roleID, resourceID).Delete(&RoleResource{}).Error -} - -// RemoveRoleFromUser removes a role from a user -func (s *Service) RemoveRoleFromUser(roleID, userID uint) error { - if !s.isInitialized() { - return ErrNotInitialized - } - return s.db.Where("role_id = ? AND user_id = ?", roleID, userID).Delete(&UserRole{}).Error -} - -// GetRoleResources returns all resources for a given role -func (s *Service) GetRoleResources(roleID uint) ([]Resource, error) { - if !s.isInitialized() { - return nil, ErrNotInitialized - } - var resources []Resource - err := s.db.Joins("JOIN role_resources rr ON rr.resource_id = resources.id"). - Where("rr.role_id = ?", roleID).Find(&resources).Error - return resources, err -} - -// GetUserRoles returns all roles for a given user -func (s *Service) GetUserRoles(userID uint) ([]Role, error) { - if !s.isInitialized() { - return nil, ErrNotInitialized - } - var roles []Role - err := s.db.Joins("JOIN user_roles ur ON ur.role_id = roles.id"). - Where("ur.user_id = ?", userID).Find(&roles).Error - return roles, err -} diff --git a/internal/user/errors.go b/internal/user/errors.go new file mode 100644 index 0000000..af47ecf --- /dev/null +++ b/internal/user/errors.go @@ -0,0 +1,7 @@ +package user + +import "fmt" + +var ( + ErrUserNotFound = fmt.Errorf("user not found") +) diff --git a/internal/user/model.go b/internal/user/model.go index 06c40c3..378a064 100644 --- a/internal/user/model.go +++ b/internal/user/model.go @@ -5,10 +5,9 @@ import ( ) type User struct { - ID uint `gorm:"primaryKey"` - Username string `gorm:"uniqueIndex;not null"` - Email string `gorm:"uniqueIndex;not null"` - Password string `gorm:"not null"` - //Roles []acl.Role `gorm:"many2many:user_roles"` + ID uint `gorm:"primaryKey"` + Username string `gorm:"uniqueIndex;not null"` + Email string `gorm:"uniqueIndex;not null"` + Password string `gorm:"not null"` DeletedAt gorm.DeletedAt `gorm:"index"` }