package policy import ( "context" "encoding/json" "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 } // ============================================================================= // ALLOWED SOURCES // ============================================================================= // CreateSource creates a new allowed source. func (s *Store) CreateSource(ctx context.Context, req *CreateAllowedSourceRequest) (*AllowedSource, error) { trustBoost := 0.5 if req.TrustBoost != nil { trustBoost = *req.TrustBoost } source := &AllowedSource{ ID: uuid.New(), PolicyID: req.PolicyID, Domain: req.Domain, Name: req.Name, Description: req.Description, License: req.License, LegalBasis: req.LegalBasis, CitationTemplate: req.CitationTemplate, TrustBoost: trustBoost, IsActive: true, CreatedAt: time.Now(), UpdatedAt: time.Now(), } query := ` INSERT INTO allowed_sources (id, policy_id, domain, name, description, license, legal_basis, citation_template, trust_boost, is_active, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING id` err := s.pool.QueryRow(ctx, query, source.ID, source.PolicyID, source.Domain, source.Name, source.Description, source.License, source.LegalBasis, source.CitationTemplate, source.TrustBoost, source.IsActive, source.CreatedAt, source.UpdatedAt, ).Scan(&source.ID) if err != nil { return nil, fmt.Errorf("failed to create source: %w", err) } // Create default operation permissions err = s.createDefaultOperations(ctx, source.ID) if err != nil { return nil, fmt.Errorf("failed to create default operations: %w", err) } return source, nil } // createDefaultOperations creates default operation permissions for a source. func (s *Store) createDefaultOperations(ctx context.Context, sourceID uuid.UUID) error { defaults := []struct { op Operation allowed bool citation bool }{ {OperationLookup, true, true}, {OperationRAG, true, true}, {OperationTraining, false, false}, // VERBOTEN by default {OperationExport, true, true}, } for _, d := range defaults { query := ` INSERT INTO operation_permissions (id, source_id, operation, is_allowed, requires_citation, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7)` _, err := s.pool.Exec(ctx, query, uuid.New(), sourceID, d.op, d.allowed, d.citation, time.Now(), time.Now()) if err != nil { return err } } return nil } // GetSource retrieves a source by ID. func (s *Store) GetSource(ctx context.Context, id uuid.UUID) (*AllowedSource, error) { query := ` SELECT als.id, als.policy_id, als.domain, als.name, als.description, als.license, als.legal_basis, als.citation_template, als.trust_boost, als.is_active, als.created_at, als.updated_at, sp.name as policy_name FROM allowed_sources als JOIN source_policies sp ON als.policy_id = sp.id WHERE als.id = $1` source := &AllowedSource{} err := s.pool.QueryRow(ctx, query, id).Scan( &source.ID, &source.PolicyID, &source.Domain, &source.Name, &source.Description, &source.License, &source.LegalBasis, &source.CitationTemplate, &source.TrustBoost, &source.IsActive, &source.CreatedAt, &source.UpdatedAt, &source.PolicyName, ) if err == pgx.ErrNoRows { return nil, nil } if err != nil { return nil, fmt.Errorf("failed to get source: %w", err) } // Load operations ops, err := s.GetOperationsBySourceID(ctx, source.ID) if err != nil { return nil, err } source.Operations = ops return source, nil } // GetSourceByDomain retrieves a source by domain with optional bundesland filter. func (s *Store) GetSourceByDomain(ctx context.Context, domain string, bundesland *Bundesland) (*AllowedSource, error) { query := ` SELECT als.id, als.policy_id, als.domain, als.name, als.description, als.license, als.legal_basis, als.citation_template, als.trust_boost, als.is_active, als.created_at, als.updated_at FROM allowed_sources als JOIN source_policies sp ON als.policy_id = sp.id WHERE als.is_active = true AND sp.is_active = true AND (als.domain = $1 OR $1 LIKE '%.' || als.domain) AND (sp.bundesland IS NULL OR sp.bundesland = $2) LIMIT 1` source := &AllowedSource{} err := s.pool.QueryRow(ctx, query, domain, bundesland).Scan( &source.ID, &source.PolicyID, &source.Domain, &source.Name, &source.Description, &source.License, &source.LegalBasis, &source.CitationTemplate, &source.TrustBoost, &source.IsActive, &source.CreatedAt, &source.UpdatedAt, ) if err == pgx.ErrNoRows { return nil, nil } if err != nil { return nil, fmt.Errorf("failed to get source by domain: %w", err) } // Load operations ops, err := s.GetOperationsBySourceID(ctx, source.ID) if err != nil { return nil, err } source.Operations = ops return source, nil } // ListSources retrieves sources with optional filters. func (s *Store) ListSources(ctx context.Context, filter *SourceListFilter) ([]AllowedSource, int, error) { baseQuery := `FROM allowed_sources als JOIN source_policies sp ON als.policy_id = sp.id WHERE 1=1` args := []interface{}{} argCount := 0 if filter.PolicyID != nil { argCount++ baseQuery += fmt.Sprintf(" AND als.policy_id = $%d", argCount) args = append(args, *filter.PolicyID) } if filter.Domain != nil { argCount++ baseQuery += fmt.Sprintf(" AND als.domain ILIKE $%d", argCount) args = append(args, "%"+*filter.Domain+"%") } if filter.License != nil { argCount++ baseQuery += fmt.Sprintf(" AND als.license = $%d", argCount) args = append(args, *filter.License) } if filter.IsActive != nil { argCount++ baseQuery += fmt.Sprintf(" AND als.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 sources: %w", err) } // Data query dataQuery := `SELECT als.id, als.policy_id, als.domain, als.name, als.description, als.license, als.legal_basis, als.citation_template, als.trust_boost, als.is_active, als.created_at, als.updated_at, sp.name as policy_name ` + baseQuery + ` ORDER BY als.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 sources: %w", err) } defer rows.Close() sources := []AllowedSource{} for rows.Next() { var src AllowedSource err := rows.Scan( &src.ID, &src.PolicyID, &src.Domain, &src.Name, &src.Description, &src.License, &src.LegalBasis, &src.CitationTemplate, &src.TrustBoost, &src.IsActive, &src.CreatedAt, &src.UpdatedAt, &src.PolicyName, ) if err != nil { return nil, 0, fmt.Errorf("failed to scan source: %w", err) } sources = append(sources, src) } return sources, total, nil } // UpdateSource updates an existing source. func (s *Store) UpdateSource(ctx context.Context, id uuid.UUID, req *UpdateAllowedSourceRequest) (*AllowedSource, error) { source, err := s.GetSource(ctx, id) if err != nil { return nil, err } if source == nil { return nil, fmt.Errorf("source not found") } if req.Domain != nil { source.Domain = *req.Domain } if req.Name != nil { source.Name = *req.Name } if req.Description != nil { source.Description = req.Description } if req.License != nil { source.License = *req.License } if req.LegalBasis != nil { source.LegalBasis = req.LegalBasis } if req.CitationTemplate != nil { source.CitationTemplate = req.CitationTemplate } if req.TrustBoost != nil { source.TrustBoost = *req.TrustBoost } if req.IsActive != nil { source.IsActive = *req.IsActive } source.UpdatedAt = time.Now() query := ` UPDATE allowed_sources SET domain = $2, name = $3, description = $4, license = $5, legal_basis = $6, citation_template = $7, trust_boost = $8, is_active = $9, updated_at = $10 WHERE id = $1` _, err = s.pool.Exec(ctx, query, id, source.Domain, source.Name, source.Description, source.License, source.LegalBasis, source.CitationTemplate, source.TrustBoost, source.IsActive, source.UpdatedAt, ) if err != nil { return nil, fmt.Errorf("failed to update source: %w", err) } return source, nil } // DeleteSource deletes a source by ID. func (s *Store) DeleteSource(ctx context.Context, id uuid.UUID) error { query := `DELETE FROM allowed_sources WHERE id = $1` _, err := s.pool.Exec(ctx, query, id) if err != nil { return fmt.Errorf("failed to delete source: %w", err) } return nil } // ============================================================================= // OPERATION PERMISSIONS // ============================================================================= // GetOperationsBySourceID retrieves all operation permissions for a source. func (s *Store) GetOperationsBySourceID(ctx context.Context, sourceID uuid.UUID) ([]OperationPermission, error) { query := ` SELECT id, source_id, operation, is_allowed, requires_citation, notes, created_at, updated_at FROM operation_permissions WHERE source_id = $1 ORDER BY operation` rows, err := s.pool.Query(ctx, query, sourceID) if err != nil { return nil, fmt.Errorf("failed to get operations: %w", err) } defer rows.Close() ops := []OperationPermission{} for rows.Next() { var op OperationPermission err := rows.Scan( &op.ID, &op.SourceID, &op.Operation, &op.IsAllowed, &op.RequiresCitation, &op.Notes, &op.CreatedAt, &op.UpdatedAt, ) if err != nil { return nil, fmt.Errorf("failed to scan operation: %w", err) } ops = append(ops, op) } return ops, nil } // UpdateOperationPermission updates an operation permission. func (s *Store) UpdateOperationPermission(ctx context.Context, id uuid.UUID, req *UpdateOperationPermissionRequest) (*OperationPermission, error) { query := `SELECT id, source_id, operation, is_allowed, requires_citation, notes, created_at, updated_at FROM operation_permissions WHERE id = $1` op := &OperationPermission{} err := s.pool.QueryRow(ctx, query, id).Scan( &op.ID, &op.SourceID, &op.Operation, &op.IsAllowed, &op.RequiresCitation, &op.Notes, &op.CreatedAt, &op.UpdatedAt, ) if err == pgx.ErrNoRows { return nil, fmt.Errorf("operation permission not found") } if err != nil { return nil, fmt.Errorf("failed to get operation: %w", err) } if req.IsAllowed != nil { op.IsAllowed = *req.IsAllowed } if req.RequiresCitation != nil { op.RequiresCitation = *req.RequiresCitation } if req.Notes != nil { op.Notes = req.Notes } op.UpdatedAt = time.Now() updateQuery := ` UPDATE operation_permissions SET is_allowed = $2, requires_citation = $3, notes = $4, updated_at = $5 WHERE id = $1` _, err = s.pool.Exec(ctx, updateQuery, id, op.IsAllowed, op.RequiresCitation, op.Notes, op.UpdatedAt) if err != nil { return nil, fmt.Errorf("failed to update operation: %w", err) } return op, nil } // GetOperationsMatrix retrieves all operation permissions grouped by source. func (s *Store) GetOperationsMatrix(ctx context.Context) ([]AllowedSource, error) { query := ` SELECT als.id, als.domain, als.name, als.license, als.is_active, sp.name as policy_name, sp.bundesland FROM allowed_sources als JOIN source_policies sp ON als.policy_id = sp.id WHERE als.is_active = true AND sp.is_active = true ORDER BY sp.bundesland NULLS FIRST, als.name` rows, err := s.pool.Query(ctx, query) if err != nil { return nil, fmt.Errorf("failed to get operations matrix: %w", err) } defer rows.Close() sources := []AllowedSource{} for rows.Next() { var src AllowedSource var bundesland *Bundesland err := rows.Scan( &src.ID, &src.Domain, &src.Name, &src.License, &src.IsActive, &src.PolicyName, &bundesland, ) if err != nil { return nil, fmt.Errorf("failed to scan source: %w", err) } // Load operations for each source ops, err := s.GetOperationsBySourceID(ctx, src.ID) if err != nil { return nil, err } src.Operations = ops sources = append(sources, src) } return sources, 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 } // ============================================================================= // AUDIT LOG // ============================================================================= // CreateAuditLog creates a new audit log entry. func (s *Store) CreateAuditLog(ctx context.Context, entry *PolicyAuditLog) error { entry.ID = uuid.New() entry.CreatedAt = time.Now() query := ` INSERT INTO policy_audit_log (id, action, entity_type, entity_id, old_value, new_value, user_id, user_email, ip_address, user_agent, created_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)` _, err := s.pool.Exec(ctx, query, entry.ID, entry.Action, entry.EntityType, entry.EntityID, entry.OldValue, entry.NewValue, entry.UserID, entry.UserEmail, entry.IPAddress, entry.UserAgent, entry.CreatedAt, ) if err != nil { return fmt.Errorf("failed to create audit log: %w", err) } return nil } // ListAuditLogs retrieves audit logs with filters. func (s *Store) ListAuditLogs(ctx context.Context, filter *AuditLogFilter) ([]PolicyAuditLog, int, error) { baseQuery := `FROM policy_audit_log WHERE 1=1` args := []interface{}{} argCount := 0 if filter.EntityType != nil { argCount++ baseQuery += fmt.Sprintf(" AND entity_type = $%d", argCount) args = append(args, *filter.EntityType) } if filter.EntityID != nil { argCount++ baseQuery += fmt.Sprintf(" AND entity_id = $%d", argCount) args = append(args, *filter.EntityID) } if filter.Action != nil { argCount++ baseQuery += fmt.Sprintf(" AND action = $%d", argCount) args = append(args, *filter.Action) } if filter.UserEmail != nil { argCount++ baseQuery += fmt.Sprintf(" AND user_email ILIKE $%d", argCount) args = append(args, "%"+*filter.UserEmail+"%") } if filter.FromDate != nil { argCount++ baseQuery += fmt.Sprintf(" AND created_at >= $%d", argCount) args = append(args, *filter.FromDate) } if filter.ToDate != nil { argCount++ baseQuery += fmt.Sprintf(" AND created_at <= $%d", argCount) args = append(args, *filter.ToDate) } // 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 audit logs: %w", err) } // Data query dataQuery := `SELECT id, action, entity_type, entity_id, old_value, new_value, user_id, user_email, ip_address, user_agent, created_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 audit logs: %w", err) } defer rows.Close() logs := []PolicyAuditLog{} for rows.Next() { var l PolicyAuditLog err := rows.Scan( &l.ID, &l.Action, &l.EntityType, &l.EntityID, &l.OldValue, &l.NewValue, &l.UserID, &l.UserEmail, &l.IPAddress, &l.UserAgent, &l.CreatedAt, ) if err != nil { return nil, 0, fmt.Errorf("failed to scan audit log: %w", err) } logs = append(logs, l) } return logs, total, nil } // ============================================================================= // BLOCKED CONTENT LOG // ============================================================================= // CreateBlockedContentLog creates a new blocked content log entry. func (s *Store) CreateBlockedContentLog(ctx context.Context, entry *BlockedContentLog) error { entry.ID = uuid.New() entry.CreatedAt = time.Now() query := ` INSERT INTO blocked_content_log (id, url, domain, block_reason, matched_rule_id, details, created_at) VALUES ($1, $2, $3, $4, $5, $6, $7)` _, err := s.pool.Exec(ctx, query, entry.ID, entry.URL, entry.Domain, entry.BlockReason, entry.MatchedRuleID, entry.Details, entry.CreatedAt, ) if err != nil { return fmt.Errorf("failed to create blocked content log: %w", err) } return nil } // ListBlockedContent retrieves blocked content logs with filters. func (s *Store) ListBlockedContent(ctx context.Context, filter *BlockedContentFilter) ([]BlockedContentLog, int, error) { baseQuery := `FROM blocked_content_log WHERE 1=1` args := []interface{}{} argCount := 0 if filter.Domain != nil { argCount++ baseQuery += fmt.Sprintf(" AND domain ILIKE $%d", argCount) args = append(args, "%"+*filter.Domain+"%") } if filter.BlockReason != nil { argCount++ baseQuery += fmt.Sprintf(" AND block_reason = $%d", argCount) args = append(args, *filter.BlockReason) } if filter.FromDate != nil { argCount++ baseQuery += fmt.Sprintf(" AND created_at >= $%d", argCount) args = append(args, *filter.FromDate) } if filter.ToDate != nil { argCount++ baseQuery += fmt.Sprintf(" AND created_at <= $%d", argCount) args = append(args, *filter.ToDate) } // 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 blocked content: %w", err) } // Data query dataQuery := `SELECT id, url, domain, block_reason, matched_rule_id, details, created_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 blocked content: %w", err) } defer rows.Close() logs := []BlockedContentLog{} for rows.Next() { var l BlockedContentLog err := rows.Scan( &l.ID, &l.URL, &l.Domain, &l.BlockReason, &l.MatchedRuleID, &l.Details, &l.CreatedAt, ) if err != nil { return nil, 0, fmt.Errorf("failed to scan blocked content: %w", err) } logs = append(logs, l) } return logs, total, nil } // ============================================================================= // STATISTICS // ============================================================================= // GetStats retrieves aggregated statistics for the policy system. func (s *Store) GetStats(ctx context.Context) (*PolicyStats, error) { stats := &PolicyStats{ SourcesByLicense: make(map[string]int), BlocksByReason: make(map[string]int), } // Active policies err := s.pool.QueryRow(ctx, `SELECT COUNT(*) FROM source_policies WHERE is_active = true`).Scan(&stats.ActivePolicies) if err != nil { return nil, fmt.Errorf("failed to count active policies: %w", err) } // Total sources err = s.pool.QueryRow(ctx, `SELECT COUNT(*) FROM allowed_sources`).Scan(&stats.TotalSources) if err != nil { return nil, fmt.Errorf("failed to count total sources: %w", err) } // Active sources err = s.pool.QueryRow(ctx, `SELECT COUNT(*) FROM allowed_sources WHERE is_active = true`).Scan(&stats.ActiveSources) if err != nil { return nil, fmt.Errorf("failed to count active sources: %w", err) } // Blocked today err = s.pool.QueryRow(ctx, `SELECT COUNT(*) FROM blocked_content_log WHERE created_at >= CURRENT_DATE`).Scan(&stats.BlockedToday) if err != nil { return nil, fmt.Errorf("failed to count blocked today: %w", err) } // Blocked total err = s.pool.QueryRow(ctx, `SELECT COUNT(*) FROM blocked_content_log`).Scan(&stats.BlockedTotal) if err != nil { return nil, fmt.Errorf("failed to count blocked total: %w", err) } // Active PII rules err = s.pool.QueryRow(ctx, `SELECT COUNT(*) FROM pii_rules WHERE is_active = true`).Scan(&stats.PIIRulesActive) if err != nil { return nil, fmt.Errorf("failed to count active PII rules: %w", err) } // Sources by license rows, err := s.pool.Query(ctx, `SELECT license, COUNT(*) FROM allowed_sources GROUP BY license`) if err != nil { return nil, fmt.Errorf("failed to count sources by license: %w", err) } defer rows.Close() for rows.Next() { var license string var count int if err := rows.Scan(&license, &count); err != nil { return nil, err } stats.SourcesByLicense[license] = count } // Blocks by reason rows, err = s.pool.Query(ctx, `SELECT block_reason, COUNT(*) FROM blocked_content_log GROUP BY block_reason`) if err != nil { return nil, fmt.Errorf("failed to count blocks by reason: %w", err) } defer rows.Close() for rows.Next() { var reason string var count int if err := rows.Scan(&reason, &count); err != nil { return nil, err } stats.BlocksByReason[reason] = count } // Compliance score (simplified: active sources / total sources) if stats.TotalSources > 0 { stats.ComplianceScore = float64(stats.ActiveSources) / float64(stats.TotalSources) * 100 } return stats, nil } // ============================================================================= // YAML LOADER // ============================================================================= // LoadFromYAML loads initial policy data from YAML configuration. func (s *Store) LoadFromYAML(ctx context.Context, config *BundeslaenderConfig) error { // Load federal policy if config.Federal.Name != "" { err := s.loadPolicy(ctx, nil, &config.Federal, &config.DefaultOperations) if err != nil { return fmt.Errorf("failed to load federal policy: %w", err) } } // Load Bundesland policies for code, policyConfig := range config.Bundeslaender { if code == "federal" || code == "default_operations" || code == "pii_rules" { continue } bl := Bundesland(code) err := s.loadPolicy(ctx, &bl, &policyConfig, &config.DefaultOperations) if err != nil { return fmt.Errorf("failed to load policy for %s: %w", code, err) } } // Load PII rules for _, ruleConfig := range config.PIIRules { err := s.loadPIIRule(ctx, &ruleConfig) if err != nil { return fmt.Errorf("failed to load PII rule %s: %w", ruleConfig.Name, err) } } return nil } func (s *Store) loadPolicy(ctx context.Context, bundesland *Bundesland, config *PolicyConfig, ops *OperationsConfig) error { // Create policy policy, err := s.CreatePolicy(ctx, &CreateSourcePolicyRequest{ Name: config.Name, Bundesland: bundesland, }) if err != nil { return err } // Create sources for _, srcConfig := range config.Sources { trustBoost := 0.5 if srcConfig.TrustBoost > 0 { trustBoost = srcConfig.TrustBoost } var legalBasis, citation *string if srcConfig.LegalBasis != "" { legalBasis = &srcConfig.LegalBasis } if srcConfig.CitationTemplate != "" { citation = &srcConfig.CitationTemplate } _, err := s.CreateSource(ctx, &CreateAllowedSourceRequest{ PolicyID: policy.ID, Domain: srcConfig.Domain, Name: srcConfig.Name, License: License(srcConfig.License), LegalBasis: legalBasis, CitationTemplate: citation, TrustBoost: &trustBoost, }) if err != nil { return fmt.Errorf("failed to create source %s: %w", srcConfig.Domain, err) } } return nil } func (s *Store) loadPIIRule(ctx context.Context, config *PIIRuleConfig) error { severity := PIISeverityBlock if config.Severity != "" { severity = PIISeverity(config.Severity) } _, err := s.CreatePIIRule(ctx, &CreatePIIRuleRequest{ Name: config.Name, RuleType: PIIRuleType(config.Type), Pattern: config.Pattern, Severity: severity, }) return err } // ToJSON converts an entity to JSON for audit logging. func ToJSON(v interface{}) json.RawMessage { data, _ := json.Marshal(v) return data }