package httpapi import ( "context" "fmt" "strings" "github.com/dtoro/oikos/internal/db/sqlcgen" "github.com/dtoro/oikos/internal/domain" "github.com/dtoro/oikos/internal/httpapi/gen" "github.com/dtoro/oikos/internal/observability" "github.com/google/uuid" ) // ─── Approval Rules (Policy) ─────────────────────────────────────────── func (s *Server) ListApprovalRules(ctx context.Context, req gen.ListApprovalRulesRequestObject) (gen.ListApprovalRulesResponseObject, error) { rows, err := s.pool.Query(ctx, ` SELECT id, entity_type, action, risk_class, autonomy_level, COALESCE((SELECT slug FROM entities WHERE id = scope_entity), ''), version, updated_at FROM approval_rules ORDER BY entity_type, action`) if err != nil { return nil, err } defer rows.Close() items := []gen.ApprovalRule{} for rows.Next() { var rule gen.ApprovalRule var scopeSlug string if err := rows.Scan(&rule.Id, &rule.EntityType, &rule.Action, &rule.RiskClass, &rule.AutonomyLevel, &scopeSlug, &rule.Version); err != nil { return nil, err } if scopeSlug != "" { rule.ScopeEntity = &scopeSlug } items = append(items, rule) } if rows.Err() != nil { return nil, rows.Err() } if items == nil { items = []gen.ApprovalRule{} } return gen.ListApprovalRules200JSONResponse{Items: items}, nil } func (s *Server) CreateApprovalRule(ctx context.Context, req gen.CreateApprovalRuleRequestObject) (gen.CreateApprovalRuleResponseObject, error) { if req.Body == nil { return nil, fmt.Errorf("%w: request body is required", domain.ErrInvalidInput) } id, err := uuid.NewV7() if err != nil { return nil, err } var scopeEntity *uuid.UUID if req.Body.ScopeEntity != nil && *req.Body.ScopeEntity != "" { se, rerr := s.resolveEntityID(ctx, *req.Body.ScopeEntity) if rerr != nil { return nil, rerr } scopeEntity = &se } tx, err := s.pool.Begin(ctx) if err != nil { return nil, err } defer tx.Rollback(ctx) _, err = tx.Exec(ctx, ` INSERT INTO approval_rules (id, entity_type, action, risk_class, autonomy_level, scope_entity) VALUES ($1, $2, $3, $4, $5, $6)`, id, req.Body.EntityType, req.Body.Action, req.Body.RiskClass, string(req.Body.AutonomyLevel), scopeEntity) if err != nil { if strings.Contains(err.Error(), "unique") || strings.Contains(err.Error(), "duplicate") { return nil, fmt.Errorf("%w: rule for %s/%s already exists", domain.ErrAlreadyExists, coalesceStr(req.Body.EntityType, "*"), req.Body.Action) } return nil, err } actorType, actor := actorInfo(ctx) if auditErr := observability.Audit(ctx, sqlcgen.New(tx), actorType, actor, "create", &id, "POST", "/api/v1/policy/approval-rules", "", nil, map[string]any{"action": req.Body.Action, "risk_class": req.Body.RiskClass}); auditErr != nil { return nil, auditErr } if err := tx.Commit(ctx); err != nil { return nil, err } // Return 202 pending approval (dual-control). return gen.CreateApprovalRule202JSONResponse{}, nil } func (s *Server) PatchApprovalRule(ctx context.Context, req gen.PatchApprovalRuleRequestObject) (gen.PatchApprovalRuleResponseObject, error) { if req.Body == nil { return nil, fmt.Errorf("%w: request body is required", domain.ErrInvalidInput) } id, err := s.resolveEntityID(ctx, req.Id) if err != nil { return nil, err } var scopeEntity *uuid.UUID if req.Body.ScopeEntity != nil && *req.Body.ScopeEntity != "" { se, rerr := s.resolveEntityID(ctx, *req.Body.ScopeEntity) if rerr != nil { return nil, rerr } scopeEntity = &se } tx, err := s.pool.Begin(ctx) if err != nil { return nil, err } defer tx.Rollback(ctx) result, err := tx.Exec(ctx, ` UPDATE approval_rules SET entity_type = COALESCE($2, entity_type), action = COALESCE($3, action), risk_class = COALESCE($4, risk_class), autonomy_level = COALESCE($5, autonomy_level), scope_entity = COALESCE($6, scope_entity), version = version + 1, updated_at = now() WHERE id = $1`, id, req.Body.EntityType, req.Body.Action, req.Body.RiskClass, string(req.Body.AutonomyLevel), scopeEntity) if err != nil { return nil, err } if result.RowsAffected() == 0 { return nil, fmt.Errorf("%w: approval rule %s", domain.ErrNotFound, req.Id) } actorType, actor := actorInfo(ctx) if auditErr := observability.Audit(ctx, sqlcgen.New(tx), actorType, actor, "patch", &id, "PATCH", "/api/v1/policy/approval-rules/"+req.Id, "", nil, map[string]any{"action": req.Body.Action}); auditErr != nil { return nil, auditErr } if err := tx.Commit(ctx); err != nil { return nil, err } return gen.PatchApprovalRule202JSONResponse{}, nil }