/
t3
/
iam-server
Обзор
Документация
Войти
/
t3
/
iam-server
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
master
internal/api/user_handler.go
776 строк
19 KB
Ivan Shibkikh
fix P2 medium
20 июл 2026, 18:39
20 июл 2026, 18:39
6c5606a
Код
Авторство
О чём код?
package api import ( "context" "fmt" "net" "net/http" "time" "gitverse.ru/t3/iam-server/internal/service" "gitverse.ru/t3/iam-server/internal/storage" ) type UserHandler struct { userRepo *storage.UserRepo permRepo *storage.PermissionRepo accessKeyRepo *storage.AccessKeyRepo cryptoSvc *service.CryptoService auditSvc *service.AuditService } func NewUserHandler(ur *storage.UserRepo, pr *storage.PermissionRepo, akr *storage.AccessKeyRepo, cs *service.CryptoService, as *service.AuditService) *UserHandler { return &UserHandler{userRepo: ur, cryptoSvc: cs, permRepo: pr, accessKeyRepo: akr, auditSvc: as} } const maxActiveKeys = storage.MaxActiveAccessKeys // audit is a helper for writing audit records from handler methods. func (h *UserHandler) 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, }) } func (h *UserHandler) Create(w http.ResponseWriter, r *http.Request) { if !requireScope(w, r, "user.create") { return } var req CreateUserRequest if err := decodeJSON(r, &req); err != nil { writeDecodeError(w, err) return } if errs := validateCreateUser(req); len(errs) > 0 { writeValidationError(w, "invalid request", errs) return } // Generate and encrypt the initial access key BEFORE touching the database, // so that user creation and the first key are committed together in a single // transaction. This avoids orphaned users (created but without a usable key) // if key generation/encryption were to fail after the user row was inserted. accessKey, secretKey, err := generateKeyPair() if err != nil { writeInternalError(w) return } encryptedSecret, err := h.cryptoSvc.Encrypt(secretKey) if err != nil { writeInternalError(w) return } user, _, err := h.userRepo.CreateWithInitialKey(r.Context(), req.DisplayName, h.accessKeyRepo, accessKey, encryptedSecret, nil) if err != nil { if writeStorageError(w, storage.ClassifyError(err)) { return } writeInternalError(w) return } resp := CreateUserResponse{ UserResponse: UserResponse{ ID: user.ID, DisplayName: user.DisplayName, Version: user.Version, CreatedAt: user.CreatedAt, }, AccessKey: accessKey, AccessSecret: secretKey, } w.WriteHeader(http.StatusCreated) jsonEncode(w, resp) h.audit(r.Context(), "user.create", "user", user.ID, "success", map[string]any{"display_name": req.DisplayName}) } func generateKeyPair() (accessKey, secretKey string, err error) { accessKey, err = service.GenerateAccessKey() if err != nil { return "", "", err } secretKey, err = service.GenerateSecretKey() if err != nil { return "", "", err } return accessKey, secretKey, nil } func (h *UserHandler) List(w http.ResponseWriter, r *http.Request) { if !requireScope(w, r, "user.read") { return } users, err := h.userRepo.List(r.Context()) if err != nil { writeInternalError(w) return } var resp []UserResponse for _, u := range users { resp = append(resp, UserResponse{ ID: u.ID, DisplayName: u.DisplayName, Version: u.Version, CreatedAt: u.CreatedAt, }) } jsonEncode(w, resp) } func (h *UserHandler) Update(w http.ResponseWriter, r *http.Request) { if !requireScope(w, r, "user.update") { return } id := r.PathValue("id") var req UpdateUserRequest if err := decodeJSON(r, &req); err != nil { writeDecodeError(w, err) return } if errs := validateUpdateUser(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.userRepo.Update(r.Context(), id, req.DisplayName, ifVersion) if err != nil { if writeStorageError(w, err) { h.audit(r.Context(), "user.update", "user", id, "error", map[string]any{"error": err.Error()}) return } writeInternalError(w) return } setVersionHeaders(w, newVersion) w.WriteHeader(http.StatusNoContent) h.audit(r.Context(), "user.update", "user", id, "success", map[string]any{"display_name": req.DisplayName}) } func (h *UserHandler) Delete(w http.ResponseWriter, r *http.Request) { if !requireScope(w, r, "user.delete") { return } id := r.PathValue("id") if err := h.userRepo.Delete(r.Context(), id); err != nil { if writeStorageError(w, err) { h.audit(r.Context(), "user.delete", "user", id, "error", map[string]any{"error": err.Error()}) return } writeInternalError(w) return } w.WriteHeader(http.StatusNoContent) h.audit(r.Context(), "user.delete", "user", id, "success", nil) } func (h *UserHandler) Inspect(w http.ResponseWriter, r *http.Request) { if !requireScope(w, r, "user.read") { return } id := r.PathValue("id") user, err := h.userRepo.GetByID(r.Context(), id) if err != nil { if writeStorageError(w, err) { return } writeInternalError(w) return } // Get access keys keys, err := h.accessKeyRepo.ListByUser(r.Context(), id) if err != nil { writeInternalError(w) return } var keysResp []AccessKeyResponse for _, k := range keys { keysResp = append(keysResp, AccessKeyResponse{ ID: k.ID, AccessKey: k.AccessKey, Status: k.Status, CreatedAt: k.CreatedAt, UpdatedAt: k.UpdatedAt, ExpiresAt: k.ExpiresAt, LastUsedAt: k.LastUsedAt, LastUsedIP: k.LastUsedIP, }) } // Получаем роли пользователя roles, err := h.permRepo.GetUserRoles(r.Context(), id) if err != nil { writeInternalError(w) return } var rolesResp []RoleWithPermissions for _, role := range roles { // Получаем права для каждой роли perms, err := h.permRepo.GetRolePermissions(r.Context(), role.ID) if err != nil { writeInternalError(w) return } var permsResp []PermissionItem for _, p := range perms { permsResp = append(permsResp, PermissionItem{ Effect: p.Effect, Action: p.Action, Resource: p.Resource, }) } rolesResp = append(rolesResp, RoleWithPermissions{ Role: RoleResponse{ ID: role.ID, Name: role.Name, Description: role.Description, CreatedAt: role.CreatedAt, }, Permissions: permsResp, }) } resp := InspectUserResponse{ UserResponse: UserResponse{ ID: user.ID, DisplayName: user.DisplayName, Version: user.Version, CreatedAt: user.CreatedAt, }, Keys: keysResp, Roles: rolesResp, } jsonEncode(w, resp) } // CreateAccessKey creates a new access key for a user. func (h *UserHandler) CreateAccessKey(w http.ResponseWriter, r *http.Request) { if !requireScope(w, r, "user.update") { return } userID := r.PathValue("id") var req CreateAccessKeyRequest if err := decodeJSON(r, &req); err != nil { writeDecodeError(w, err) return } if errs := validateCreateAccessKey(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 } // Enforce max keys count, err := h.accessKeyRepo.CountActiveByUser(r.Context(), userID) if err != nil { writeInternalError(w) return } if count >= maxActiveKeys { writeConflict(w, fmt.Sprintf("maximum %d active access keys allowed", maxActiveKeys)) return } var expiresAt *time.Time if req.ExpiresAt != nil && *req.ExpiresAt != "" { t, _ := time.Parse(time.RFC3339, *req.ExpiresAt) expiresAt = &t } accessKey, secretKey, err := generateKeyPair() if err != nil { writeInternalError(w) return } encryptedSecret, err := h.cryptoSvc.Encrypt(secretKey) if err != nil { writeInternalError(w) return } k, err := h.accessKeyRepo.Create(r.Context(), userID, accessKey, encryptedSecret, expiresAt) if err != nil { if writeStorageError(w, storage.ClassifyError(err)) { return } writeInternalError(w) return } resp := CreateAccessKeyResponse{ ID: k.ID, AccessKey: k.AccessKey, AccessSecret: secretKey, Status: k.Status, ExpiresAt: k.ExpiresAt, CreatedAt: k.CreatedAt, } w.WriteHeader(http.StatusCreated) jsonEncode(w, resp) h.audit(r.Context(), "access_key.create", "access_key", k.ID, "success", map[string]any{"user_id": userID}) } // RotateAccessKey atomically creates a new key and optionally deactivates an old one. func (h *UserHandler) RotateAccessKey(w http.ResponseWriter, r *http.Request) { if !requireScope(w, r, "user.update") { return } userID := r.PathValue("id") var req RotateAccessKeyRequest if err := decodeJSON(r, &req); err != nil { writeDecodeError(w, err) return } if errs := validateRotateAccessKey(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 expiresAt *time.Time if req.ExpiresAt != nil && *req.ExpiresAt != "" { t, err := time.Parse(time.RFC3339, *req.ExpiresAt) if err != nil { writeValidationError(w, "invalid request", []FieldError{ {Field: "expires_at", Issue: "must be a valid RFC3339 timestamp"}, }) return } expiresAt = &t } accessKey, secretKey, err := generateKeyPair() if err != nil { writeInternalError(w) return } encryptedSecret, err := h.cryptoSvc.Encrypt(secretKey) if err != nil { writeInternalError(w) return } k, err := h.accessKeyRepo.RotateAccessKey(r.Context(), userID, accessKey, encryptedSecret, expiresAt, req.DeactivateKeyID) if err != nil { if writeStorageError(w, err) { h.audit(r.Context(), "access_key.rotate", "access_key", userID, "error", map[string]any{"error": err.Error()}) return } writeInternalError(w) return } resp := CreateAccessKeyResponse{ ID: k.ID, AccessKey: k.AccessKey, AccessSecret: secretKey, Status: k.Status, ExpiresAt: k.ExpiresAt, CreatedAt: k.CreatedAt, } w.WriteHeader(http.StatusCreated) jsonEncode(w, resp) h.audit(r.Context(), "access_key.rotate", "access_key", userID, "success", map[string]any{"new_key_id": k.ID, "deactivated_key_id": req.DeactivateKeyID}) } // ListAccessKeys returns all access keys for a user. func (h *UserHandler) ListAccessKeys(w http.ResponseWriter, r *http.Request) { if !requireScope(w, r, "user.read") { return } userID := r.PathValue("id") // Verify user exists if _, err := h.userRepo.GetByID(r.Context(), userID); err != nil { if writeStorageError(w, err) { return } writeInternalError(w) return } keys, err := h.accessKeyRepo.ListByUser(r.Context(), userID) if err != nil { writeInternalError(w) return } var resp []AccessKeyResponse for _, k := range keys { resp = append(resp, AccessKeyResponse{ ID: k.ID, AccessKey: k.AccessKey, Status: k.Status, CreatedAt: k.CreatedAt, UpdatedAt: k.UpdatedAt, ExpiresAt: k.ExpiresAt, LastUsedAt: k.LastUsedAt, LastUsedIP: k.LastUsedIP, }) } jsonEncode(w, resp) } // UpdateAccessKey updates the status of an access key. func (h *UserHandler) UpdateAccessKey(w http.ResponseWriter, r *http.Request) { if !requireScope(w, r, "user.update") { return } userID := r.PathValue("id") keyID := r.PathValue("keyId") var req UpdateAccessKeyRequest if err := decodeJSON(r, &req); err != nil { writeDecodeError(w, err) return } if errs := validateUpdateAccessKey(req); len(errs) > 0 { writeValidationError(w, "invalid request", errs) return } // Verify key belongs to user k, err := h.accessKeyRepo.GetByID(r.Context(), keyID) if err != nil { if writeStorageError(w, err) { return } writeInternalError(w) return } if k.UserID != userID { writeNotFound(w, "access key") return } // Enforce max active keys on activation if req.Status == "Active" { count, err := h.accessKeyRepo.CountActiveByUser(r.Context(), userID) if err != nil { writeInternalError(w) return } if count >= maxActiveKeys { writeConflict(w, fmt.Sprintf("maximum %d active access keys allowed", maxActiveKeys)) return } } if err := h.accessKeyRepo.UpdateStatus(r.Context(), keyID, req.Status); err != nil { if writeStorageError(w, err) { return } writeInternalError(w) return } w.WriteHeader(http.StatusNoContent) h.audit(r.Context(), "access_key.update", "access_key", keyID, "success", map[string]any{"status": req.Status, "user_id": userID}) } // DeleteAccessKey permanently removes an access key. func (h *UserHandler) DeleteAccessKey(w http.ResponseWriter, r *http.Request) { if !requireScope(w, r, "user.update") { return } userID := r.PathValue("id") keyID := r.PathValue("keyId") // Verify key belongs to user k, err := h.accessKeyRepo.GetByID(r.Context(), keyID) if err != nil { if writeStorageError(w, err) { return } writeInternalError(w) return } if k.UserID != userID { writeNotFound(w, "access key") return } if err := h.accessKeyRepo.Delete(r.Context(), keyID); err != nil { if writeStorageError(w, err) { return } writeInternalError(w) return } w.WriteHeader(http.StatusNoContent) h.audit(r.Context(), "access_key.delete", "access_key", keyID, "success", map[string]any{"user_id": userID}) } // --- Direct user permissions (version-aware) --- // GetUserPermissions returns direct permissions for a user with collection version. func (h *UserHandler) GetUserPermissions(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 } perms, err := h.permRepo.GetUserPermissions(r.Context(), id) if err != nil { writeInternalError(w) return } version, err := h.permRepo.GetUserPermissionsVersion(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 := UserPermissionsResponse{ Version: version, Permissions: items, } setVersionHeaders(w, version) jsonEncode(w, resp) } // SetUserPermissions replaces all direct permissions for a user (PUT). // Supports optional If-Match for optimistic locking. func (h *UserHandler) SetUserPermissions(w http.ResponseWriter, r *http.Request) { if !requireScope(w, r, "user.update") { return } userID := 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 } if _, err := h.userRepo.GetByID(r.Context(), userID); 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, }) } 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.SetUserPermissions(r.Context(), userID, perms, ifVersion) 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(), "user.set_permissions", "user", userID, "success", map[string]any{"permissions": permSummaries, "version": newVersion}) } // AddUserPermissions adds direct permissions to a user (POST). func (h *UserHandler) AddUserPermissions(w http.ResponseWriter, r *http.Request) { if !requireScope(w, r, "user.update") { return } userID := 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.userRepo.GetByID(r.Context(), userID); 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.AddUserPermissions(r.Context(), userID, 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(), "user.add_permissions", "user", userID, "success", map[string]any{"permissions": permSummaries, "version": newVersion}) } // RemoveUserPermissions removes specific direct permissions from a user (DELETE with body). func (h *UserHandler) RemoveUserPermissions(w http.ResponseWriter, r *http.Request) { if !requireScope(w, r, "user.update") { return } userID := 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 user exists if _, err := h.userRepo.GetByID(r.Context(), userID); 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.RemoveUserPermissions(r.Context(), userID, 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(), "user.remove_permissions", "user", userID, "success", map[string]any{"permissions": permSummaries, "version": newVersion}) } // Ensure net is imported. var _ = net.IPv4len