package policy import ( "context" "fmt" "time" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) // Store provides database operations for the policy system. type Store struct { pool *pgxpool.Pool } // NewStore creates a new Store instance. func NewStore(pool *pgxpool.Pool) *Store { return &Store{pool: pool} } // ============================================================================= // SOURCE POLICIES // ============================================================================= // CreatePolicy creates a new source policy. func (s *Store) CreatePolicy(ctx context.Context, req *CreateSourcePolicyRequest) (*SourcePolicy, error) { policy := &SourcePolicy{ ID: uuid.New(), Version: 1, Name: req.Name, Description: req.Description, Bundesland: req.Bundesland, IsActive: true, CreatedAt: time.Now(), UpdatedAt: time.Now(), } query := ` INSERT INTO source_policies (id, version, name, description, bundesland, is_active, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id, version, name, description, bundesland, is_active, created_at, updated_at` err := s.pool.QueryRow(ctx, query, policy.ID, policy.Version, policy.Name, policy.Description, policy.Bundesland, policy.IsActive, policy.CreatedAt, policy.UpdatedAt, ).Scan( &policy.ID, &policy.Version, &policy.Name, &policy.Description, &policy.Bundesland, &policy.IsActive, &policy.CreatedAt, &policy.UpdatedAt, ) if err != nil { return nil, fmt.Errorf("failed to create policy: %w", err) } return policy, nil } // GetPolicy retrieves a policy by ID. func (s *Store) GetPolicy(ctx context.Context, id uuid.UUID) (*SourcePolicy, error) { query := ` SELECT id, version, name, description, bundesland, is_active, created_at, updated_at, approved_by, approved_at FROM source_policies WHERE id = $1` policy := &SourcePolicy{} err := s.pool.QueryRow(ctx, query, id).Scan( &policy.ID, &policy.Version, &policy.Name, &policy.Description, &policy.Bundesland, &policy.IsActive, &policy.CreatedAt, &policy.UpdatedAt, &policy.ApprovedBy, &policy.ApprovedAt, ) if err == pgx.ErrNoRows { return nil, nil } if err != nil { return nil, fmt.Errorf("failed to get policy: %w", err) } return policy, nil } // ListPolicies retrieves policies with optional filters. func (s *Store) ListPolicies(ctx context.Context, filter *PolicyListFilter) ([]SourcePolicy, int, error) { baseQuery := `FROM source_policies WHERE 1=1` args := []interface{}{} argCount := 0 if filter.Bundesland != nil { argCount++ baseQuery += fmt.Sprintf(" AND bundesland = $%d", argCount) args = append(args, *filter.Bundesland) } if filter.IsActive != nil { argCount++ baseQuery += fmt.Sprintf(" AND is_active = $%d", argCount) args = append(args, *filter.IsActive) } // Count query var total int countQuery := "SELECT COUNT(*) " + baseQuery err := s.pool.QueryRow(ctx, countQuery, args...).Scan(&total) if err != nil { return nil, 0, fmt.Errorf("failed to count policies: %w", err) } // Data query with pagination dataQuery := `SELECT id, version, name, description, bundesland, is_active, created_at, updated_at, approved_by, approved_at ` + baseQuery + ` ORDER BY created_at DESC` if filter.Limit > 0 { argCount++ dataQuery += fmt.Sprintf(" LIMIT $%d", argCount) args = append(args, filter.Limit) } if filter.Offset > 0 { argCount++ dataQuery += fmt.Sprintf(" OFFSET $%d", argCount) args = append(args, filter.Offset) } rows, err := s.pool.Query(ctx, dataQuery, args...) if err != nil { return nil, 0, fmt.Errorf("failed to list policies: %w", err) } defer rows.Close() policies := []SourcePolicy{} for rows.Next() { var p SourcePolicy err := rows.Scan( &p.ID, &p.Version, &p.Name, &p.Description, &p.Bundesland, &p.IsActive, &p.CreatedAt, &p.UpdatedAt, &p.ApprovedBy, &p.ApprovedAt, ) if err != nil { return nil, 0, fmt.Errorf("failed to scan policy: %w", err) } policies = append(policies, p) } return policies, total, nil } // UpdatePolicy updates an existing policy. func (s *Store) UpdatePolicy(ctx context.Context, id uuid.UUID, req *UpdateSourcePolicyRequest) (*SourcePolicy, error) { policy, err := s.GetPolicy(ctx, id) if err != nil { return nil, err } if policy == nil { return nil, fmt.Errorf("policy not found") } if req.Name != nil { policy.Name = *req.Name } if req.Description != nil { policy.Description = req.Description } if req.Bundesland != nil { policy.Bundesland = req.Bundesland } if req.IsActive != nil { policy.IsActive = *req.IsActive } policy.Version++ policy.UpdatedAt = time.Now() query := ` UPDATE source_policies SET version = $2, name = $3, description = $4, bundesland = $5, is_active = $6, updated_at = $7 WHERE id = $1 RETURNING id, version, name, description, bundesland, is_active, created_at, updated_at` err = s.pool.QueryRow(ctx, query, id, policy.Version, policy.Name, policy.Description, policy.Bundesland, policy.IsActive, policy.UpdatedAt, ).Scan( &policy.ID, &policy.Version, &policy.Name, &policy.Description, &policy.Bundesland, &policy.IsActive, &policy.CreatedAt, &policy.UpdatedAt, ) if err != nil { return nil, fmt.Errorf("failed to update policy: %w", err) } return policy, nil } // DeletePolicy deletes a policy by ID (soft delete via is_active = false). func (s *Store) DeletePolicy(ctx context.Context, id uuid.UUID) error { query := `UPDATE source_policies SET is_active = false, updated_at = $2 WHERE id = $1` _, err := s.pool.Exec(ctx, query, id, time.Now()) if err != nil { return fmt.Errorf("failed to delete policy: %w", err) } return nil } // ============================================================================= // PII RULES // ============================================================================= // CreatePIIRule creates a new PII rule. func (s *Store) CreatePIIRule(ctx context.Context, req *CreatePIIRuleRequest) (*PIIRule, error) { severity := PIISeverityBlock if req.Severity != "" { severity = req.Severity } rule := &PIIRule{ ID: uuid.New(), Name: req.Name, Description: req.Description, RuleType: req.RuleType, Pattern: req.Pattern, Severity: severity, IsActive: true, CreatedAt: time.Now(), UpdatedAt: time.Now(), } query := ` INSERT INTO pii_rules (id, name, description, rule_type, pattern, severity, is_active, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id` err := s.pool.QueryRow(ctx, query, rule.ID, rule.Name, rule.Description, rule.RuleType, rule.Pattern, rule.Severity, rule.IsActive, rule.CreatedAt, rule.UpdatedAt, ).Scan(&rule.ID) if err != nil { return nil, fmt.Errorf("failed to create PII rule: %w", err) } return rule, nil } // GetPIIRule retrieves a PII rule by ID. func (s *Store) GetPIIRule(ctx context.Context, id uuid.UUID) (*PIIRule, error) { query := ` SELECT id, name, description, rule_type, pattern, severity, is_active, created_at, updated_at FROM pii_rules WHERE id = $1` rule := &PIIRule{} err := s.pool.QueryRow(ctx, query, id).Scan( &rule.ID, &rule.Name, &rule.Description, &rule.RuleType, &rule.Pattern, &rule.Severity, &rule.IsActive, &rule.CreatedAt, &rule.UpdatedAt, ) if err == pgx.ErrNoRows { return nil, nil } if err != nil { return nil, fmt.Errorf("failed to get PII rule: %w", err) } return rule, nil } // ListPIIRules retrieves all PII rules. func (s *Store) ListPIIRules(ctx context.Context, activeOnly bool) ([]PIIRule, error) { query := ` SELECT id, name, description, rule_type, pattern, severity, is_active, created_at, updated_at FROM pii_rules` if activeOnly { query += ` WHERE is_active = true` } query += ` ORDER BY severity DESC, name` rows, err := s.pool.Query(ctx, query) if err != nil { return nil, fmt.Errorf("failed to list PII rules: %w", err) } defer rows.Close() rules := []PIIRule{} for rows.Next() { var r PIIRule err := rows.Scan( &r.ID, &r.Name, &r.Description, &r.RuleType, &r.Pattern, &r.Severity, &r.IsActive, &r.CreatedAt, &r.UpdatedAt, ) if err != nil { return nil, fmt.Errorf("failed to scan PII rule: %w", err) } rules = append(rules, r) } return rules, nil } // UpdatePIIRule updates an existing PII rule. func (s *Store) UpdatePIIRule(ctx context.Context, id uuid.UUID, req *UpdatePIIRuleRequest) (*PIIRule, error) { rule, err := s.GetPIIRule(ctx, id) if err != nil { return nil, err } if rule == nil { return nil, fmt.Errorf("PII rule not found") } if req.Name != nil { rule.Name = *req.Name } if req.Description != nil { rule.Description = req.Description } if req.RuleType != nil { rule.RuleType = *req.RuleType } if req.Pattern != nil { rule.Pattern = *req.Pattern } if req.Severity != nil { rule.Severity = *req.Severity } if req.IsActive != nil { rule.IsActive = *req.IsActive } rule.UpdatedAt = time.Now() query := ` UPDATE pii_rules SET name = $2, description = $3, rule_type = $4, pattern = $5, severity = $6, is_active = $7, updated_at = $8 WHERE id = $1` _, err = s.pool.Exec(ctx, query, id, rule.Name, rule.Description, rule.RuleType, rule.Pattern, rule.Severity, rule.IsActive, rule.UpdatedAt, ) if err != nil { return nil, fmt.Errorf("failed to update PII rule: %w", err) } return rule, nil } // DeletePIIRule deletes a PII rule by ID. func (s *Store) DeletePIIRule(ctx context.Context, id uuid.UUID) error { query := `DELETE FROM pii_rules WHERE id = $1` _, err := s.pool.Exec(ctx, query, id) if err != nil { return fmt.Errorf("failed to delete PII rule: %w", err) } return nil }