/
t3
/
iam-server
Обзор
Документация
Войти
/
t3
/
iam-server
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
master
internal/api/role_handler.go
591 строка
15 KB
Ivan Shibkikh
fix P2 medium
20 июл 2026, 18:39
20 июл 2026, 18:39
6c5606a
Код
Авторство
О чём код?
package api import ( "context" "encoding/json" "errors" "net/http" "gitverse.ru/t3/iam-server/internal/service" "gitverse.ru/t3/iam-server/internal/storage" ) type RoleHandler struct { roleRepo *storage.RoleRepo permRepo *storage.PermissionRepo userRepo *storage.UserRepo auditSvc *service.AuditService } func NewRoleHandler(rr *storage.RoleRepo, pr *storage.PermissionRepo, ur *storage.UserRepo, as *service.AuditService) *RoleHandler { return &RoleHandler{roleRepo: rr, permRepo: pr, userRepo: ur, auditSvc: as} } func (h *RoleHandler) audit(ctx context.Context, action, resourceType, resourceID, outcome string, detail any) { actor := ActorFromContext(ctx) h.auditSvc.Record(ctx, service.AuditContext{ RequestID: RequestIDFromContext(ctx), ActorType: actor.Type, ActorID: actor.ID, ActorIP: actor.IP, Action: action, ResourceType: resourceType, ResourceID: resourceID, Outcome: outcome, Detail: detail, }) } // Update changes the name and/or description of a role. // Supports optional If-Match for optimistic locking. func (h *RoleHandler) Update(w http.ResponseWriter, r *http.Request) { if !requireScope(w, r, "role.update") { return } id := r.PathValue("id") var req UpdateRoleRequest if err := decodeJSON(r, &req); err != nil { writeDecodeError(w, err) return } if errs := validateUpdateRole(req); len(errs) > 0 { writeValidationError(w, "invalid request", errs) return } var ifVersion *int switch v, ok, err := parseIfMatch(r); { case err != nil: writeBadRequest(w, err.Error()) return case ok: ifVersion = &v } newVersion, err := h.roleRepo.Update(r.Context(), id, req.Name, req.Description, ifVersion) if err != nil { if writeStorageError(w, err) { h.audit(r.Context(), "role.update", "role", id, "error", map[string]any{"error": err.Error()}) return } writeInternalError(w) return } // Fetch the updated role to return role, err := h.roleRepo.GetByID(r.Context(), id) if err != nil { writeInternalError(w) return } resp := RoleResponse{ ID: role.ID, Name: role.Name, Description: role.Description, Version: newVersion, CreatedAt: role.CreatedAt, } setVersionHeaders(w, newVersion) jsonEncode(w, resp) h.audit(r.Context(), "role.update", "role", id, "success", map[string]any{"name": req.Name, "description": req.Description, "version": newVersion}) } func (h *RoleHandler) Create(w http.ResponseWriter, r *http.Request) { if !requireScope(w, r, "role.create") { return } var req CreateRoleRequest if err := decodeJSON(r, &req); err != nil { writeDecodeError(w, err) return } if errs := validateCreateRole(req); len(errs) > 0 { writeValidationError(w, "invalid request", errs) return } role, err := h.roleRepo.Create(r.Context(), req.Name, req.Description) if err != nil { if writeStorageError(w, err) { return } writeInternalError(w) return } resp := RoleResponse{ ID: role.ID, Name: role.Name, Description: role.Description, Version: role.Version, CreatedAt: role.CreatedAt, } w.WriteHeader(http.StatusCreated) jsonEncode(w, resp) h.audit(r.Context(), "role.create", "role", role.ID, "success", map[string]any{"name": req.Name}) } // AssignToUser replaces all roles for a user (PUT). // Supports optional If-Match for optimistic locking. func (h *RoleHandler) AssignToUser(w http.ResponseWriter, r *http.Request) { if !requireScope(w, r, "role.assign") { return } userID := r.PathValue("id") var req AssignRolesRequest if err := decodeJSON(r, &req); err != nil { writeDecodeError(w, err) return } if errs := validateAssignRoles(req); len(errs) > 0 { writeValidationError(w, "invalid request", errs) return } // Verify user exists if _, err := h.userRepo.GetByID(r.Context(), userID); err != nil { if writeStorageError(w, err) { return } writeInternalError(w) return } var ifVersion *int switch v, ok, err := parseIfMatch(r); { case err != nil: writeBadRequest(w, err.Error()) return case ok: ifVersion = &v } newVersion, err := h.roleRepo.AssignRolesToUser(r.Context(), userID, req.RoleIDs, ifVersion) if err != nil { if writeStorageError(w, err) { h.audit(r.Context(), "role.assign", "user", userID, "error", map[string]any{"role_ids": req.RoleIDs, "error": err.Error()}) return } writeInternalError(w) return } setVersionHeaders(w, newVersion) w.WriteHeader(http.StatusNoContent) h.audit(r.Context(), "role.assign", "user", userID, "success", map[string]any{"role_ids": req.RoleIDs, "version": newVersion}) } // AddRolesToUser adds roles to a user without removing existing assignments (POST). func (h *RoleHandler) AddRolesToUser(w http.ResponseWriter, r *http.Request) { if !requireScope(w, r, "role.assign") { return } userID := r.PathValue("id") var req AddRemoveRolesRequest if err := decodeJSON(r, &req); err != nil { writeDecodeError(w, err) return } if errs := validateAssignRoles(AssignRolesRequest{RoleIDs: req.RoleIDs}); len(errs) > 0 { writeValidationError(w, "invalid request", errs) return } if _, err := h.userRepo.GetByID(r.Context(), userID); err != nil { if writeStorageError(w, err) { return } writeInternalError(w) return } newVersion, err := h.roleRepo.AddRolesToUser(r.Context(), userID, req.RoleIDs) if err != nil { if writeStorageError(w, err) { h.audit(r.Context(), "role.add", "user", userID, "error", map[string]any{"role_ids": req.RoleIDs, "error": err.Error()}) return } writeInternalError(w) return } setVersionHeaders(w, newVersion) w.WriteHeader(http.StatusNoContent) h.audit(r.Context(), "role.add", "user", userID, "success", map[string]any{"role_ids": req.RoleIDs, "version": newVersion}) } // RemoveRolesFromUser removes specific roles from a user (DELETE with body). func (h *RoleHandler) RemoveRolesFromUser(w http.ResponseWriter, r *http.Request) { if !requireScope(w, r, "role.assign") { return } userID := r.PathValue("id") var req AddRemoveRolesRequest if err := decodeJSON(r, &req); err != nil { writeDecodeError(w, err) return } if len(req.RoleIDs) == 0 { writeBadRequest(w, "role_ids must not be empty") return } // Verify user exists if _, err := h.userRepo.GetByID(r.Context(), userID); err != nil { if writeStorageError(w, err) { return } writeInternalError(w) return } newVersion, err := h.roleRepo.RemoveRolesFromUser(r.Context(), userID, req.RoleIDs) if err != nil { if writeStorageError(w, err) { h.audit(r.Context(), "role.remove", "user", userID, "error", map[string]any{"role_ids": req.RoleIDs, "error": err.Error()}) return } writeInternalError(w) return } setVersionHeaders(w, newVersion) w.WriteHeader(http.StatusNoContent) h.audit(r.Context(), "role.remove", "user", userID, "success", map[string]any{"role_ids": req.RoleIDs, "version": newVersion}) } // GetUserRoles returns all roles assigned to a user along with the collection version. func (h *RoleHandler) GetUserRoles(w http.ResponseWriter, r *http.Request) { if !requireScope(w, r, "user.read") { return } id := r.PathValue("id") // Verify user exists if _, err := h.userRepo.GetByID(r.Context(), id); err != nil { if writeStorageError(w, err) { return } writeInternalError(w) return } roles, err := h.permRepo.GetUserRoles(r.Context(), id) if err != nil { writeInternalError(w) return } version, err := h.roleRepo.GetUserRolesVersion(r.Context(), id) if err != nil { writeInternalError(w) return } var rolesResp []RoleResponse for _, role := range roles { rolesResp = append(rolesResp, RoleResponse{ ID: role.ID, Name: role.Name, Description: role.Description, Version: role.Version, CreatedAt: role.CreatedAt, }) } resp := UserRolesResponse{ Version: version, Roles: rolesResp, } setVersionHeaders(w, version) jsonEncode(w, resp) } // SetPermissions replaces all permissions for a role (PUT). // Supports optional If-Match for optimistic locking. func (h *RoleHandler) SetPermissions(w http.ResponseWriter, r *http.Request) { if !requireScope(w, r, "role.update") { return } roleID := r.PathValue("id") var req SetPermissionsRequest if err := decodeJSON(r, &req); err != nil { writeDecodeError(w, err) return } if errs := validateSetPermissions(req); len(errs) > 0 { writeValidationError(w, "invalid request", errs) return } // Verify role exists and is not deleted if _, err := h.roleRepo.GetByID(r.Context(), roleID); err != nil { if writeStorageError(w, err) { return } writeInternalError(w) return } // Маппим DTO в модели БД var perms []storage.Permission for _, p := range req.Permissions { perms = append(perms, storage.Permission{ Effect: p.Effect, Action: p.Action, Resource: p.Resource, }) } var ifVersion *int switch v, ok, err := parseIfMatch(r); { case err != nil: writeBadRequest(w, err.Error()) return case ok: ifVersion = &v } newVersion, err := h.permRepo.SetRolePermissions(r.Context(), roleID, perms, ifVersion) if err != nil { if writeStorageError(w, err) { return } writeInternalError(w) return } setVersionHeaders(w, newVersion) w.WriteHeader(http.StatusNoContent) // Record without secrets — only effect/action/resource patterns permSummaries := make([]map[string]string, 0, len(req.Permissions)) for _, p := range req.Permissions { permSummaries = append(permSummaries, map[string]string{ "effect": p.Effect, "action": p.Action, "resource": p.Resource, }) } h.audit(r.Context(), "role.set_permissions", "role", roleID, "success", map[string]any{"permissions": permSummaries, "version": newVersion}) } // AddRolePermissions adds permissions to a role without removing existing ones (POST). func (h *RoleHandler) AddRolePermissions(w http.ResponseWriter, r *http.Request) { if !requireScope(w, r, "role.update") { return } roleID := r.PathValue("id") var req PermissionsRequest if err := decodeJSON(r, &req); err != nil { writeDecodeError(w, err) return } if errs := validateSetPermissions(SetPermissionsRequest{Permissions: req.Permissions}); len(errs) > 0 { writeValidationError(w, "invalid request", errs) return } if _, err := h.roleRepo.GetByID(r.Context(), roleID); err != nil { if writeStorageError(w, err) { return } writeInternalError(w) return } var perms []storage.Permission for _, p := range req.Permissions { perms = append(perms, storage.Permission{ Effect: p.Effect, Action: p.Action, Resource: p.Resource, }) } newVersion, err := h.permRepo.AddRolePermissions(r.Context(), roleID, perms) if err != nil { if writeStorageError(w, err) { return } writeInternalError(w) return } setVersionHeaders(w, newVersion) w.WriteHeader(http.StatusNoContent) permSummaries := make([]map[string]string, 0, len(req.Permissions)) for _, p := range req.Permissions { permSummaries = append(permSummaries, map[string]string{ "effect": p.Effect, "action": p.Action, "resource": p.Resource, }) } h.audit(r.Context(), "role.add_permissions", "role", roleID, "success", map[string]any{"permissions": permSummaries, "version": newVersion}) } // RemoveRolePermissions removes specific permissions from a role (DELETE with body). func (h *RoleHandler) RemoveRolePermissions(w http.ResponseWriter, r *http.Request) { if !requireScope(w, r, "role.update") { return } roleID := r.PathValue("id") var req PermissionsRequest if err := decodeJSON(r, &req); err != nil { writeDecodeError(w, err) return } if len(req.Permissions) == 0 { writeBadRequest(w, "permissions must not be empty") return } // Verify role exists and is not deleted if _, err := h.roleRepo.GetByID(r.Context(), roleID); err != nil { if writeStorageError(w, err) { return } writeInternalError(w) return } var perms []storage.Permission for _, p := range req.Permissions { perms = append(perms, storage.Permission{ Effect: p.Effect, Action: p.Action, Resource: p.Resource, }) } newVersion, err := h.permRepo.RemoveRolePermissions(r.Context(), roleID, perms) if err != nil { if writeStorageError(w, err) { return } writeInternalError(w) return } setVersionHeaders(w, newVersion) w.WriteHeader(http.StatusNoContent) permSummaries := make([]map[string]string, 0, len(req.Permissions)) for _, p := range req.Permissions { permSummaries = append(permSummaries, map[string]string{ "effect": p.Effect, "action": p.Action, "resource": p.Resource, }) } h.audit(r.Context(), "role.remove_permissions", "role", roleID, "success", map[string]any{"permissions": permSummaries, "version": newVersion}) } func (h *RoleHandler) List(w http.ResponseWriter, r *http.Request) { if !requireScope(w, r, "role.read") { return } roles, err := h.roleRepo.List(r.Context()) if err != nil { writeInternalError(w) return } var resp []RoleResponse for _, role := range roles { resp = append(resp, RoleResponse{ ID: role.ID, Name: role.Name, Description: role.Description, Version: role.Version, CreatedAt: role.CreatedAt, }) } jsonEncode(w, resp) } func (h *RoleHandler) Delete(w http.ResponseWriter, r *http.Request) { if !requireScope(w, r, "role.delete") { return } id := r.PathValue("id") if err := h.roleRepo.Delete(r.Context(), id); err != nil { if writeStorageError(w, err) { h.audit(r.Context(), "role.delete", "role", id, "error", map[string]any{"error": err.Error()}) return } writeInternalError(w) return } w.WriteHeader(http.StatusNoContent) h.audit(r.Context(), "role.delete", "role", id, "success", nil) } func (h *RoleHandler) Restore(w http.ResponseWriter, r *http.Request) { if !requireScope(w, r, "role.delete") { return } id := r.PathValue("id") role, err := h.roleRepo.Restore(r.Context(), id) if err != nil { if writeStorageError(w, err) { h.audit(r.Context(), "role.restore", "role", id, "error", map[string]any{"error": err.Error()}) return } writeInternalError(w) return } resp := RoleResponse{ ID: role.ID, Name: role.Name, Description: role.Description, Version: role.Version, CreatedAt: role.CreatedAt, } jsonEncode(w, resp) h.audit(r.Context(), "role.restore", "role", id, "success", map[string]any{"name": role.Name}) } func (h *RoleHandler) GetPermissions(w http.ResponseWriter, r *http.Request) { if !requireScope(w, r, "role.read") { return } id := r.PathValue("id") perms, err := h.permRepo.GetRolePermissions(r.Context(), id) if err != nil { writeInternalError(w) return } version, err := h.roleRepo.GetRolePermissionsVersion(r.Context(), id) if err != nil { writeInternalError(w) return } var items []PermissionItem for _, p := range perms { items = append(items, PermissionItem{ Effect: p.Effect, Action: p.Action, Resource: p.Resource, }) } resp := RolePermissionsResponse{ Version: version, Permissions: items, } setVersionHeaders(w, version) jsonEncode(w, resp) } type PermissionsRequest struct { Permissions []PermissionItem `json:"permissions"` } // Ensure json and errors are used (for future imports). var _ = json.NewEncoder var _ = errors.Is