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 all roles // @Tags acl/roles // @Produce json // @Success 200 {array} getRolesResponse // @Failure 500 {object} ProblemDetails // @Router /api/acl/roles [get] func (h *aclAdminHandler) getRoles(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") roles, err := h.a.GetRoles() 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) default: slog.Error("unexpected server error", "error", err.Error()) writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r) } return } type R struct { ID uint `json:"id" example:"1"` Name string `json:"name" example:"admin"` } resp := make([]R, 0, len(roles)) for _, role := range roles { resp = append(resp, R{ID: role.ID, Name: role.Name}) } _ = 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 acl/roles // @Produce json // @Param roleId path int true "Role ID" example(1) // @Success 200 {array} getRoleUsersResponse // @Failure 400 {object} ProblemDetails // @Failure 404 {object} ProblemDetails // @Failure 500 {object} ProblemDetails // @Router /api/acl/roles/{roleId}/users [get] func (h *aclAdminHandler) getRoleUsers(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 } if len(role.Users) == 0 { writeProblem(w, http.StatusNotFound, "/errors/acl/role-has-no-users", "Role has no users", "Role has no users", r) return } var respUsers getRoleUsersResponse for _, user := range role.Users { respUsers = append(respUsers, getRoleUser{ ID: user.ID, Name: user.Username, Email: user.Email, }) } _ = json.NewEncoder(w).Encode(respUsers) } // @Summary Get role resources // @Tags acl/roles // @Produce json // @Param roleId path int true "Role ID" example(1) // @Success 200 {array} getRoleResourcesResponse // @Failure 400 {object} ProblemDetails // @Failure 404 {object} ProblemDetails // @Failure 500 {object} ProblemDetails // @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) 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 } 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 acl/roles // @Accept json // @Produce json // @Param request body createRoleRequest true "Role" // @Success 201 {object} createRoleResponse // @Failure 400 {object} ProblemDetails // @Failure 409 {object} ProblemDetails // @Failure 500 {object} ProblemDetails // @Router /api/acl/roles [post] func (h *aclAdminHandler) createRole(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") var req createRoleRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeProblem(w, http.StatusBadRequest, "/errors/invalid-request-body", "Invalid request body", "Body is not valid JSON", r) return } roleID, err := h.a.CreateRole(req.Name) if err != nil { slog.Error("Failed to create 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.ErrInvalidRoleName: writeProblem(w, http.StatusBadRequest, "/errors/acl/invalid-role-name", "Invalid role name", "Role name must be non-empty", r) case acl.ErrRoleAlreadyExists: writeProblem(w, http.StatusConflict, "/errors/acl/role-already-exists", "Role already exists", "Role '"+req.Name+"' already exists", r) default: writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r) } return } w.WriteHeader(http.StatusCreated) _ = json.NewEncoder(w).Encode(createRoleResponse{ ID: roleID, Name: req.Name, }) } // @Summary Update role // @Tags acl/roles // @Accept json // @Produce json // @Param roleId path int true "Role ID" example(1) // @Param request body updateRoleRequest true "Role" // @Success 200 {object} updateRoleResponse // @Failure 400 {object} ProblemDetails // @Failure 404 {object} ProblemDetails // @Failure 409 {object} ProblemDetails // @Failure 500 {object} ProblemDetails // @Router /api/acl/roles/{roleId} [patch] func (h *aclAdminHandler) updateRole(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") var req updateRoleRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeProblem(w, http.StatusBadRequest, "/errors/invalid-request-body", "Invalid request body", "Body is not valid JSON", 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.UpdateRole(uint(roleID), req.Name) if err != nil { slog.Error("Failed to update 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.ErrInvalidRoleName: writeProblem(w, http.StatusBadRequest, "/errors/acl/invalid-role-name", "Invalid role name", "Role name must be non-empty", r) case acl.ErrRoleNotFound: writeProblem(w, http.StatusNotFound, "/errors/acl/role-not-found", "Role not found", "No role with ID "+roleIDStr, r) case acl.ErrSameRoleName: writeProblem(w, http.StatusConflict, "/errors/acl/role-name-already-exists", "Role name already exists", "Role '"+req.Name+"' already exists", r) default: writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r) } return } _ = json.NewEncoder(w).Encode(updateRoleResponse{ ID: uint(roleID), Name: req.Name, }) } // @Summary Delete role // @Tags acl/roles // @Produce json // @Param roleId path int true "Role ID" example(1) // @Success 204 // @Failure 400 {object} ProblemDetails // @Failure 404 {object} ProblemDetails // @Failure 409 {object} ProblemDetails // @Failure 500 {object} ProblemDetails // @Router /api/acl/roles/{roleId} [delete] func (h *aclAdminHandler) deleteRole(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 } err = h.a.DeleteRole(uint(roleID)) if err != nil { slog.Error("Failed to delete 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.ErrRoleInUse: writeProblem(w, http.StatusConflict, "/errors/acl/role-in-use", "Role in use", "Role "+roleIDStr+" is assigned to at least one user and cannot be deleted", r) default: writeProblem(w, http.StatusInternalServerError, "/errors/internal-server-error", "Internal Server Error", "unexpected error", r) } return } 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) }