/
t3
/
iam-server
Обзор
Документация
Войти
/
t3
/
iam-server
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
master
internal/api/auth_handler.go
267 строк
7 KB
Ivan Shibkikh
s3 integration
23 июл 2026, 21:55
23 июл 2026, 21:55
6df7382
Код
Авторство
О чём код?
package api import ( "context" "encoding/json" "net" "net/http" "time" "gitverse.ru/t3/iam-server/internal/service" "gitverse.ru/t3/iam-server/internal/storage" ) type AuthHandler struct { accessKeyRepo *storage.AccessKeyRepo userRepo *storage.UserRepo policyEngine *service.PolicyEngine cryptoSvc *service.CryptoService sigv4Verifier *service.SigV4Verifier replayProtector *service.ReplayProtector auditSvc *service.AuditService } func NewAuthHandler( akr *storage.AccessKeyRepo, ur *storage.UserRepo, pe *service.PolicyEngine, cs *service.CryptoService, sv *service.SigV4Verifier, rp *service.ReplayProtector, as *service.AuditService, ) *AuthHandler { return &AuthHandler{ accessKeyRepo: akr, userRepo: ur, policyEngine: pe, cryptoSvc: cs, sigv4Verifier: sv, replayProtector: rp, auditSvc: as, } } func (h *AuthHandler) 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 *AuthHandler) Check(w http.ResponseWriter, r *http.Request) { actor := ActorFromContext(r.Context()) // Service-to-service path: skip SigV4, use access_key from request body. if actor.Type == "service" { var req AuthCheckRequest if err := decodeJSON(r, &req); err != nil { writeDecodeError(w, err) return } if errs := validateAuthCheck(req); len(errs) > 0 { writeValidationError(w, "invalid request", errs) return } if req.AccessKey == "" { writeBadRequest(w, "access_key is required for service-to-service auth check") return } key, err := h.accessKeyRepo.GetByAccessKey(r.Context(), req.AccessKey) if err != nil { if err == storage.ErrNotFound { resp := AuthCheckResponse{Allowed: false} jsonEncode(w, resp) h.audit(r.Context(), "auth.check", "auth", req.AccessKey, "denied", map[string]any{ "reason": "key_not_found", }) return } writeInternalError(w) return } if key.Status != "Active" { resp := AuthCheckResponse{Allowed: false} jsonEncode(w, resp) h.audit(r.Context(), "auth.check", "auth", key.UserID, "denied", map[string]any{ "reason": "key_inactive", "key_id": key.ID, }) return } if key.ExpiresAt != nil && time.Now().UTC().After(*key.ExpiresAt) { resp := AuthCheckResponse{Allowed: false} jsonEncode(w, resp) h.audit(r.Context(), "auth.check", "auth", key.UserID, "denied", map[string]any{ "reason": "key_expired", "key_id": key.ID, }) return } decision, err := h.policyEngine.Evaluate(r.Context(), key.UserID, req.Action, req.Resource) if err != nil { writeInternalError(w) return } outcome := "denied" if decision.Allowed { outcome = "allowed" } auditDetail := service.AuditDecisionFromDecision(decision, req.Action, req.Resource) h.audit(r.Context(), "auth.check", "auth", key.UserID, outcome, auditDetail) displayName := "" if user, err := h.userRepo.GetByID(r.Context(), key.UserID); err == nil { displayName = user.DisplayName } resp := AuthCheckResponse{ Allowed: decision.Allowed, UserID: key.UserID, DisplayName: displayName, } jsonEncode(w, resp) return } // Standard SigV4 path (user-facing). // 1. Validate X-Amz-Nonce for replay protection (no body access required) nonce := r.Header.Get("X-Amz-Nonce") amzDate := r.Header.Get("X-Amz-Date") if amzDate == "" { writeBadRequest(w, "missing X-Amz-Date header") return } ok, err := h.replayProtector.Check(r.Context(), amzDate, nonce) if err != nil { writeForbidden(w, err.Error()) return } if !ok { writeForbidden(w, "replay detected") return } // 2. Load user by access_key (extracted from SigV4 signature) accessKey, err := h.sigv4Verifier.ExtractAccessKey(r) if err != nil { writeUnauthorized(w, "invalid Authorization header") return } key, err := h.accessKeyRepo.GetByAccessKey(r.Context(), accessKey) if err != nil { if err == storage.ErrNotFound { resp := AuthCheckResponse{Allowed: false} jsonEncode(w, resp) h.audit(r.Context(), "auth.check", "auth", accessKey, "denied", map[string]any{ "reason": "key_not_found", }) return } writeInternalError(w) return } // 3. Reject inactive keys if key.Status != "Active" { resp := AuthCheckResponse{Allowed: false} jsonEncode(w, resp) h.audit(r.Context(), "auth.check", "auth", key.UserID, "denied", map[string]any{ "reason": "key_inactive", "key_id": key.ID, }) return } // 4. Reject expired keys if key.ExpiresAt != nil && time.Now().UTC().After(*key.ExpiresAt) { resp := AuthCheckResponse{Allowed: false} jsonEncode(w, resp) h.audit(r.Context(), "auth.check", "auth", key.UserID, "denied", map[string]any{ "reason": "key_expired", "key_id": key.ID, }) return } // 5. Decrypt the user's access_secret for signature verification secretPlain, err := h.cryptoSvc.Decrypt(key.AccessSecretEncrypted) if err != nil { writeInternalError(w) return } // 6. Verify SigV4 signature with the decrypted secret. // This buffers and verifies the request body hash BEFORE the body is parsed // or used for policy decisions, then re-wraps r.Body for the decode below. verifiedKey, err := h.sigv4Verifier.Verify(r, secretPlain) if err != nil { writeUnauthorized(w, "signature verification failed: "+err.Error()) return } if verifiedKey != accessKey { writeUnauthorized(w, "access key mismatch") return } // 7. Parse request body (action + resource) — only after the signature has // bound the body bytes. Verify re-wrapped r.Body so it is readable here. var req AuthCheckRequest if err := decodeJSON(r, &req); err != nil { writeDecodeError(w, err) return } if errs := validateAuthCheck(req); len(errs) > 0 { writeValidationError(w, "invalid request", errs) return } // 8. Record last-used (best-effort) remoteIP, _, _ := net.SplitHostPort(r.RemoteAddr) if remoteIP == "" { remoteIP = r.RemoteAddr } _ = h.accessKeyRepo.UpdateLastUsed(r.Context(), key.ID, remoteIP) // 9. Evaluate policy with decision reasoning decision, err := h.policyEngine.Evaluate(r.Context(), key.UserID, req.Action, req.Resource) if err != nil { writeInternalError(w) return } outcome := "denied" if decision.Allowed { outcome = "allowed" } // Record audit with decision detail (no secrets) auditDetail := service.AuditDecisionFromDecision(decision, req.Action, req.Resource) h.audit(r.Context(), "auth.check", "auth", key.UserID, outcome, auditDetail) displayName := "" if user, err := h.userRepo.GetByID(r.Context(), key.UserID); err == nil { displayName = user.DisplayName } resp := AuthCheckResponse{ Allowed: decision.Allowed, UserID: key.UserID, DisplayName: displayName, } jsonEncode(w, resp) } // Ensure json is used. var _ = json.NewEncoder