fully implement acl backend and interface

This commit is contained in:
2025-12-21 22:18:29 +02:00
parent 85f8ac60e7
commit e9d8877fbf
15 changed files with 1567 additions and 166 deletions

View File

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

View File

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

View File

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

View File

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

136
api/acl_admin/users.go Normal file
View File

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

View File

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