Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 42s
CI / test-go-edu-search (push) Successful in 34s
CI / test-python-klausur (push) Failing after 2m51s
CI / test-python-agent-core (push) Successful in 21s
CI / test-nodejs-website (push) Successful in 29s
sed replacement left orphaned hostname references in story page and empty lines in getApiBase functions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
360 lines
9.6 KiB
Go
360 lines
9.6 KiB
Go
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
|
|
}
|