Initial commit: breakpilot-core - Shared Infrastructure

Docker Compose with 24+ services:
- PostgreSQL (PostGIS), Valkey, MinIO, Qdrant
- Vault (PKI/TLS), Nginx (Reverse Proxy)
- Backend Core API, Consent Service, Billing Service
- RAG Service, Embedding Service
- Gitea, Woodpecker CI/CD
- Night Scheduler, Health Aggregator
- Jitsi (Web/XMPP/JVB/Jicofo), Mailpit

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Boenisch
2026-02-11 23:47:13 +01:00
commit ad111d5e69
244 changed files with 84288 additions and 0 deletions

View File

@@ -0,0 +1,321 @@
-- AI Compliance SDK - RBAC Schema
-- Migration 001: Multi-Tenant RBAC with Namespace Isolation
-- Enable UUID extension
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
-- ============================================================================
-- Tenants (Mandanten)
-- ============================================================================
CREATE TABLE IF NOT EXISTS compliance_tenants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
slug VARCHAR(100) NOT NULL UNIQUE,
settings JSONB DEFAULT '{}',
max_users INT DEFAULT 100,
llm_quota_monthly INT DEFAULT 10000,
status VARCHAR(50) DEFAULT 'active',
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_compliance_tenants_slug ON compliance_tenants(slug);
CREATE INDEX idx_compliance_tenants_status ON compliance_tenants(status);
-- ============================================================================
-- Namespaces (Abteilungen - z.B. CFO Use-Case)
-- ============================================================================
CREATE TABLE IF NOT EXISTS compliance_namespaces (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES compliance_tenants(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
slug VARCHAR(100) NOT NULL,
parent_namespace_id UUID REFERENCES compliance_namespaces(id) ON DELETE SET NULL,
isolation_level VARCHAR(50) DEFAULT 'strict', -- 'strict', 'shared', 'public'
data_classification VARCHAR(50) DEFAULT 'internal', -- 'public', 'internal', 'confidential', 'restricted'
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(tenant_id, slug)
);
CREATE INDEX idx_compliance_namespaces_tenant ON compliance_namespaces(tenant_id);
CREATE INDEX idx_compliance_namespaces_parent ON compliance_namespaces(parent_namespace_id);
-- ============================================================================
-- Roles with Permissions
-- ============================================================================
CREATE TABLE IF NOT EXISTS compliance_roles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES compliance_tenants(id) ON DELETE CASCADE,
name VARCHAR(100) NOT NULL,
description TEXT,
permissions TEXT[] NOT NULL DEFAULT '{}',
is_system_role BOOLEAN DEFAULT FALSE,
hierarchy_level INT DEFAULT 100,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(tenant_id, name)
);
CREATE INDEX idx_compliance_roles_tenant ON compliance_roles(tenant_id);
CREATE INDEX idx_compliance_roles_system ON compliance_roles(is_system_role);
-- ============================================================================
-- System Roles (Pre-defined)
-- ============================================================================
INSERT INTO compliance_roles (name, description, permissions, is_system_role, hierarchy_level) VALUES
('compliance_executive', 'Executive mit Lesezugriff auf Compliance-Daten und LLM-Queries',
ARRAY['compliance:*:read', 'llm:query:execute', 'audit:own:read'], TRUE, 10),
('compliance_officer', 'Compliance-Verantwortlicher mit vollem Zugriff',
ARRAY['compliance:*', 'audit:*', 'llm:*', 'namespace:read'], TRUE, 20),
('data_protection_officer', 'Datenschutzbeauftragter',
ARRAY['compliance:privacy:*', 'consent:*', 'dsr:*', 'audit:read', 'llm:query:execute'], TRUE, 25),
('namespace_admin', 'Administrator fuer einen Namespace',
ARRAY['namespace:own:admin', 'compliance:own:*', 'llm:own:query', 'audit:own:read'], TRUE, 50),
('auditor', 'Auditor mit Lesezugriff',
ARRAY['compliance:read', 'audit:log:read', 'evidence:read'], TRUE, 60),
('compliance_user', 'Standardbenutzer mit eingeschraenktem Zugriff',
ARRAY['compliance:own:read', 'llm:own:query'], TRUE, 100)
ON CONFLICT (tenant_id, name) DO NOTHING;
-- ============================================================================
-- User-Role Assignments with Namespace Scope
-- ============================================================================
CREATE TABLE IF NOT EXISTS compliance_user_roles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL,
role_id UUID NOT NULL REFERENCES compliance_roles(id) ON DELETE CASCADE,
tenant_id UUID NOT NULL REFERENCES compliance_tenants(id) ON DELETE CASCADE,
namespace_id UUID REFERENCES compliance_namespaces(id) ON DELETE CASCADE,
granted_by UUID NOT NULL,
expires_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(user_id, role_id, tenant_id, namespace_id)
);
CREATE INDEX idx_compliance_user_roles_user ON compliance_user_roles(user_id);
CREATE INDEX idx_compliance_user_roles_tenant ON compliance_user_roles(tenant_id);
CREATE INDEX idx_compliance_user_roles_namespace ON compliance_user_roles(namespace_id);
CREATE INDEX idx_compliance_user_roles_expires ON compliance_user_roles(expires_at) WHERE expires_at IS NOT NULL;
-- ============================================================================
-- LLM Access Policies
-- ============================================================================
CREATE TABLE IF NOT EXISTS compliance_llm_policies (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES compliance_tenants(id) ON DELETE CASCADE,
namespace_id UUID REFERENCES compliance_namespaces(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
description TEXT,
allowed_data_categories TEXT[] DEFAULT '{}', -- 'salary', 'health', 'personal', 'financial'
blocked_data_categories TEXT[] DEFAULT '{}',
require_pii_redaction BOOLEAN DEFAULT TRUE,
pii_redaction_level VARCHAR(50) DEFAULT 'strict', -- 'strict', 'moderate', 'minimal', 'none'
allowed_models TEXT[] DEFAULT '{}', -- 'qwen2.5:7b', 'claude-3-sonnet'
max_tokens_per_request INT DEFAULT 4000,
max_requests_per_day INT DEFAULT 1000,
max_requests_per_hour INT DEFAULT 100,
is_active BOOLEAN DEFAULT TRUE,
priority INT DEFAULT 100, -- Lower = higher priority
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_compliance_llm_policies_tenant ON compliance_llm_policies(tenant_id);
CREATE INDEX idx_compliance_llm_policies_namespace ON compliance_llm_policies(namespace_id);
CREATE INDEX idx_compliance_llm_policies_active ON compliance_llm_policies(is_active, priority);
-- ============================================================================
-- LLM Audit Log (Immutable)
-- ============================================================================
CREATE TABLE IF NOT EXISTS compliance_llm_audit_log (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES compliance_tenants(id),
namespace_id UUID REFERENCES compliance_namespaces(id),
user_id UUID NOT NULL,
session_id VARCHAR(100),
operation VARCHAR(100) NOT NULL, -- 'query', 'completion', 'embedding', 'analysis'
model_used VARCHAR(100) NOT NULL,
provider VARCHAR(50) NOT NULL, -- 'ollama', 'anthropic', 'openai'
prompt_hash VARCHAR(64) NOT NULL, -- SHA-256 of prompt (no raw PII stored)
prompt_length INT NOT NULL,
response_length INT,
tokens_used INT NOT NULL,
duration_ms INT NOT NULL,
pii_detected BOOLEAN DEFAULT FALSE,
pii_types_detected TEXT[] DEFAULT '{}',
pii_redacted BOOLEAN DEFAULT FALSE,
policy_id UUID REFERENCES compliance_llm_policies(id),
policy_violations TEXT[] DEFAULT '{}',
data_categories_accessed TEXT[] DEFAULT '{}',
error_message TEXT,
request_metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Partitioning-ready indexes for large audit tables
CREATE INDEX idx_llm_audit_tenant_date ON compliance_llm_audit_log(tenant_id, created_at DESC);
CREATE INDEX idx_llm_audit_user ON compliance_llm_audit_log(user_id, created_at DESC);
CREATE INDEX idx_llm_audit_namespace ON compliance_llm_audit_log(namespace_id, created_at DESC);
CREATE INDEX idx_llm_audit_operation ON compliance_llm_audit_log(operation, created_at DESC);
CREATE INDEX idx_llm_audit_pii ON compliance_llm_audit_log(pii_detected, created_at DESC) WHERE pii_detected = TRUE;
CREATE INDEX idx_llm_audit_violations ON compliance_llm_audit_log(created_at DESC) WHERE array_length(policy_violations, 1) > 0;
-- ============================================================================
-- General Audit Trail
-- ============================================================================
CREATE TABLE IF NOT EXISTS compliance_audit_trail (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES compliance_tenants(id),
namespace_id UUID REFERENCES compliance_namespaces(id),
user_id UUID NOT NULL,
action VARCHAR(100) NOT NULL, -- 'create', 'update', 'delete', 'access', 'export'
resource_type VARCHAR(100) NOT NULL, -- 'role', 'namespace', 'policy', 'evidence'
resource_id UUID,
old_values JSONB,
new_values JSONB,
ip_address INET,
user_agent TEXT,
reason TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_audit_trail_tenant_date ON compliance_audit_trail(tenant_id, created_at DESC);
CREATE INDEX idx_audit_trail_user ON compliance_audit_trail(user_id, created_at DESC);
CREATE INDEX idx_audit_trail_resource ON compliance_audit_trail(resource_type, resource_id);
-- ============================================================================
-- API Keys for SDK Access
-- ============================================================================
CREATE TABLE IF NOT EXISTS compliance_api_keys (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES compliance_tenants(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
key_hash VARCHAR(64) NOT NULL UNIQUE, -- SHA-256 of API key
key_prefix VARCHAR(8) NOT NULL, -- First 8 chars for identification
permissions TEXT[] DEFAULT '{}',
namespace_restrictions UUID[] DEFAULT '{}', -- Empty = all namespaces
rate_limit_per_hour INT DEFAULT 1000,
expires_at TIMESTAMPTZ,
last_used_at TIMESTAMPTZ,
is_active BOOLEAN DEFAULT TRUE,
created_by UUID NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_api_keys_tenant ON compliance_api_keys(tenant_id);
CREATE INDEX idx_api_keys_prefix ON compliance_api_keys(key_prefix);
CREATE INDEX idx_api_keys_active ON compliance_api_keys(is_active, expires_at);
-- ============================================================================
-- LLM Usage Statistics (Aggregated)
-- ============================================================================
CREATE TABLE IF NOT EXISTS compliance_llm_usage_stats (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES compliance_tenants(id),
namespace_id UUID REFERENCES compliance_namespaces(id),
user_id UUID,
period_start DATE NOT NULL,
period_type VARCHAR(20) NOT NULL, -- 'daily', 'weekly', 'monthly'
total_requests INT DEFAULT 0,
total_tokens INT DEFAULT 0,
total_duration_ms BIGINT DEFAULT 0,
requests_with_pii INT DEFAULT 0,
policy_violations INT DEFAULT 0,
models_used JSONB DEFAULT '{}', -- {"qwen2.5:7b": 100, "claude-3-sonnet": 50}
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(tenant_id, namespace_id, user_id, period_start, period_type)
);
CREATE INDEX idx_llm_usage_tenant_period ON compliance_llm_usage_stats(tenant_id, period_start DESC);
-- ============================================================================
-- Helper Functions
-- ============================================================================
-- Function to check if user has permission in namespace
CREATE OR REPLACE FUNCTION check_namespace_permission(
p_user_id UUID,
p_tenant_id UUID,
p_namespace_id UUID,
p_permission TEXT
) RETURNS BOOLEAN AS $$
DECLARE
has_permission BOOLEAN := FALSE;
BEGIN
SELECT EXISTS (
SELECT 1
FROM compliance_user_roles ur
JOIN compliance_roles r ON ur.role_id = r.id
WHERE ur.user_id = p_user_id
AND ur.tenant_id = p_tenant_id
AND (ur.namespace_id = p_namespace_id OR ur.namespace_id IS NULL)
AND (ur.expires_at IS NULL OR ur.expires_at > NOW())
AND (
p_permission = ANY(r.permissions)
OR EXISTS (
SELECT 1 FROM unnest(r.permissions) perm
WHERE perm LIKE '%:*' AND p_permission LIKE replace(perm, ':*', '') || ':%'
)
)
) INTO has_permission;
RETURN has_permission;
END;
$$ LANGUAGE plpgsql;
-- Function to get effective permissions for user in namespace
CREATE OR REPLACE FUNCTION get_effective_permissions(
p_user_id UUID,
p_tenant_id UUID,
p_namespace_id UUID
) RETURNS TEXT[] AS $$
DECLARE
permissions TEXT[];
BEGIN
SELECT array_agg(DISTINCT perm)
INTO permissions
FROM (
SELECT unnest(r.permissions) as perm
FROM compliance_user_roles ur
JOIN compliance_roles r ON ur.role_id = r.id
WHERE ur.user_id = p_user_id
AND ur.tenant_id = p_tenant_id
AND (ur.namespace_id = p_namespace_id OR ur.namespace_id IS NULL)
AND (ur.expires_at IS NULL OR ur.expires_at > NOW())
) sub;
RETURN COALESCE(permissions, '{}');
END;
$$ LANGUAGE plpgsql;
-- ============================================================================
-- Default Tenant for Breakpilot (Self-Hosting)
-- ============================================================================
INSERT INTO compliance_tenants (name, slug, settings, max_users, llm_quota_monthly)
VALUES (
'Breakpilot',
'breakpilot',
'{"deployment": "self-hosted", "hybrid_mode": true}',
1000,
100000
) ON CONFLICT (slug) DO NOTHING;
-- Default namespaces
INSERT INTO compliance_namespaces (tenant_id, name, slug, data_classification)
SELECT
t.id,
ns.name,
ns.slug,
ns.classification
FROM compliance_tenants t
CROSS JOIN (VALUES
('Allgemein', 'general', 'internal'),
('Finanzen', 'finance', 'restricted'),
('Personal', 'hr', 'confidential'),
('IT', 'it', 'internal'),
('Compliance', 'compliance', 'confidential')
) AS ns(name, slug, classification)
WHERE t.slug = 'breakpilot'
ON CONFLICT (tenant_id, slug) DO NOTHING;

View File

@@ -0,0 +1,215 @@
-- DSGVO Schema Migration
-- AI Compliance SDK - Phase 4: DSGVO Integration
-- ============================================================================
-- VVT - Verarbeitungsverzeichnis (Art. 30 DSGVO)
-- ============================================================================
CREATE TABLE IF NOT EXISTS dsgvo_processing_activities (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES compliance_tenants(id) ON DELETE CASCADE,
namespace_id UUID REFERENCES compliance_namespaces(id) ON DELETE SET NULL,
name VARCHAR(255) NOT NULL,
description TEXT,
purpose TEXT NOT NULL,
legal_basis VARCHAR(50) NOT NULL, -- consent, contract, legal_obligation, vital_interests, public_interest, legitimate_interests
legal_basis_details TEXT,
data_categories JSONB DEFAULT '[]',
data_subject_categories JSONB DEFAULT '[]',
recipients JSONB DEFAULT '[]',
third_country_transfer BOOLEAN DEFAULT FALSE,
transfer_safeguards TEXT,
retention_period VARCHAR(255),
retention_policy_id UUID,
tom_reference JSONB DEFAULT '[]',
dsfa_required BOOLEAN DEFAULT FALSE,
dsfa_id UUID,
responsible_person VARCHAR(255),
responsible_department VARCHAR(255),
systems JSONB DEFAULT '[]',
status VARCHAR(50) DEFAULT 'draft', -- draft, active, under_review, archived
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
created_by UUID NOT NULL,
last_reviewed_at TIMESTAMPTZ,
next_review_at TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS idx_dsgvo_pa_tenant ON dsgvo_processing_activities(tenant_id);
CREATE INDEX IF NOT EXISTS idx_dsgvo_pa_status ON dsgvo_processing_activities(tenant_id, status);
CREATE INDEX IF NOT EXISTS idx_dsgvo_pa_namespace ON dsgvo_processing_activities(namespace_id) WHERE namespace_id IS NOT NULL;
-- ============================================================================
-- DSFA - Datenschutz-Folgenabschätzung (Art. 35 DSGVO)
-- ============================================================================
CREATE TABLE IF NOT EXISTS dsgvo_dsfa (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES compliance_tenants(id) ON DELETE CASCADE,
namespace_id UUID REFERENCES compliance_namespaces(id) ON DELETE SET NULL,
processing_activity_id UUID REFERENCES dsgvo_processing_activities(id) ON DELETE SET NULL,
name VARCHAR(255) NOT NULL,
description TEXT,
processing_description TEXT,
necessity_assessment TEXT,
proportionality_assessment TEXT,
risks JSONB DEFAULT '[]',
mitigations JSONB DEFAULT '[]',
dpo_consulted BOOLEAN DEFAULT FALSE,
dpo_opinion TEXT,
authority_consulted BOOLEAN DEFAULT FALSE,
authority_reference VARCHAR(255),
status VARCHAR(50) DEFAULT 'draft', -- draft, in_progress, completed, approved, rejected
overall_risk_level VARCHAR(20), -- low, medium, high, very_high
conclusion TEXT,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
created_by UUID NOT NULL,
approved_by UUID,
approved_at TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS idx_dsgvo_dsfa_tenant ON dsgvo_dsfa(tenant_id);
CREATE INDEX IF NOT EXISTS idx_dsgvo_dsfa_status ON dsgvo_dsfa(tenant_id, status);
CREATE INDEX IF NOT EXISTS idx_dsgvo_dsfa_pa ON dsgvo_dsfa(processing_activity_id) WHERE processing_activity_id IS NOT NULL;
-- ============================================================================
-- TOM - Technische und Organisatorische Maßnahmen (Art. 32 DSGVO)
-- ============================================================================
CREATE TABLE IF NOT EXISTS dsgvo_tom (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES compliance_tenants(id) ON DELETE CASCADE,
namespace_id UUID REFERENCES compliance_namespaces(id) ON DELETE SET NULL,
category VARCHAR(50) NOT NULL, -- access_control, encryption, pseudonymization, etc.
subcategory VARCHAR(100),
name VARCHAR(255) NOT NULL,
description TEXT,
type VARCHAR(20) NOT NULL, -- technical, organizational
implementation_status VARCHAR(50) DEFAULT 'planned', -- planned, in_progress, implemented, verified, not_applicable
implemented_at TIMESTAMPTZ,
verified_at TIMESTAMPTZ,
verified_by UUID,
effectiveness_rating VARCHAR(20), -- low, medium, high
documentation TEXT,
responsible_person VARCHAR(255),
responsible_department VARCHAR(255),
review_frequency VARCHAR(50), -- monthly, quarterly, annually
last_review_at TIMESTAMPTZ,
next_review_at TIMESTAMPTZ,
related_controls JSONB DEFAULT '[]',
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
created_by UUID NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_dsgvo_tom_tenant ON dsgvo_tom(tenant_id);
CREATE INDEX IF NOT EXISTS idx_dsgvo_tom_category ON dsgvo_tom(tenant_id, category);
CREATE INDEX IF NOT EXISTS idx_dsgvo_tom_status ON dsgvo_tom(tenant_id, implementation_status);
-- ============================================================================
-- DSR - Data Subject Requests / Betroffenenrechte (Art. 15-22 DSGVO)
-- ============================================================================
CREATE TABLE IF NOT EXISTS dsgvo_dsr (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES compliance_tenants(id) ON DELETE CASCADE,
namespace_id UUID REFERENCES compliance_namespaces(id) ON DELETE SET NULL,
request_type VARCHAR(50) NOT NULL, -- access, rectification, erasure, restriction, portability, objection
status VARCHAR(50) DEFAULT 'received', -- received, verified, in_progress, completed, rejected, extended
subject_name VARCHAR(255) NOT NULL,
subject_email VARCHAR(255) NOT NULL,
subject_identifier VARCHAR(255),
request_description TEXT,
request_channel VARCHAR(50), -- email, form, phone, letter
received_at TIMESTAMPTZ NOT NULL,
verified_at TIMESTAMPTZ,
verification_method VARCHAR(100),
deadline_at TIMESTAMPTZ NOT NULL,
extended_deadline_at TIMESTAMPTZ,
extension_reason TEXT,
completed_at TIMESTAMPTZ,
response_sent BOOLEAN DEFAULT FALSE,
response_sent_at TIMESTAMPTZ,
response_method VARCHAR(50),
rejection_reason TEXT,
notes TEXT,
affected_systems JSONB DEFAULT '[]',
assigned_to UUID,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
created_by UUID NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_dsgvo_dsr_tenant ON dsgvo_dsr(tenant_id);
CREATE INDEX IF NOT EXISTS idx_dsgvo_dsr_status ON dsgvo_dsr(tenant_id, status);
CREATE INDEX IF NOT EXISTS idx_dsgvo_dsr_deadline ON dsgvo_dsr(tenant_id, deadline_at) WHERE status NOT IN ('completed', 'rejected');
CREATE INDEX IF NOT EXISTS idx_dsgvo_dsr_type ON dsgvo_dsr(tenant_id, request_type);
-- ============================================================================
-- Retention Policies - Löschfristen (Art. 17 DSGVO)
-- ============================================================================
CREATE TABLE IF NOT EXISTS dsgvo_retention_policies (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES compliance_tenants(id) ON DELETE CASCADE,
namespace_id UUID REFERENCES compliance_namespaces(id) ON DELETE SET NULL,
name VARCHAR(255) NOT NULL,
description TEXT,
data_category VARCHAR(100) NOT NULL,
retention_period_days INT NOT NULL,
retention_period_text VARCHAR(255), -- Human readable
legal_basis VARCHAR(100),
legal_reference VARCHAR(255), -- § 147 AO, § 257 HGB, etc.
deletion_method VARCHAR(50), -- automatic, manual, anonymization
deletion_procedure TEXT,
exception_criteria TEXT,
applicable_systems JSONB DEFAULT '[]',
responsible_person VARCHAR(255),
responsible_department VARCHAR(255),
status VARCHAR(50) DEFAULT 'draft', -- draft, active, archived
last_review_at TIMESTAMPTZ,
next_review_at TIMESTAMPTZ,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
created_by UUID NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_dsgvo_retention_tenant ON dsgvo_retention_policies(tenant_id);
CREATE INDEX IF NOT EXISTS idx_dsgvo_retention_status ON dsgvo_retention_policies(tenant_id, status);
CREATE INDEX IF NOT EXISTS idx_dsgvo_retention_category ON dsgvo_retention_policies(tenant_id, data_category);
-- ============================================================================
-- Insert default TOM categories as reference data
-- ============================================================================
-- This is optional - the categories are also defined in code
-- But having them in the database allows for easier UI population
CREATE TABLE IF NOT EXISTS dsgvo_tom_categories (
id VARCHAR(50) PRIMARY KEY,
name_de VARCHAR(255) NOT NULL,
name_en VARCHAR(255) NOT NULL,
description_de TEXT,
article_reference VARCHAR(50)
);
INSERT INTO dsgvo_tom_categories (id, name_de, name_en, description_de, article_reference) VALUES
('access_control', 'Zutrittskontrolle', 'Physical Access Control', 'Maßnahmen zur Verhinderung des unbefugten Zutritts zu Datenverarbeitungsanlagen', 'Art. 32 Abs. 1 lit. b'),
('admission_control', 'Zugangskontrolle', 'Logical Access Control', 'Maßnahmen zur Verhinderung der unbefugten Nutzung von DV-Systemen', 'Art. 32 Abs. 1 lit. b'),
('access_management', 'Zugriffskontrolle', 'Access Management', 'Maßnahmen zur Gewährleistung, dass nur befugte Personen Zugriff auf Daten haben', 'Art. 32 Abs. 1 lit. b'),
('transfer_control', 'Weitergabekontrolle', 'Transfer Control', 'Maßnahmen zur Verhinderung des unbefugten Lesens, Kopierens oder Entfernens bei der Übertragung', 'Art. 32 Abs. 1 lit. b'),
('input_control', 'Eingabekontrolle', 'Input Control', 'Maßnahmen zur Nachvollziehbarkeit von Eingabe, Änderung und Löschung von Daten', 'Art. 32 Abs. 1 lit. b'),
('availability_control', 'Verfügbarkeitskontrolle', 'Availability Control', 'Maßnahmen zum Schutz gegen zufällige oder mutwillige Zerstörung oder Verlust', 'Art. 32 Abs. 1 lit. b, c'),
('separation_control', 'Trennungskontrolle', 'Separation Control', 'Maßnahmen zur getrennten Verarbeitung von Daten, die zu unterschiedlichen Zwecken erhoben wurden', 'Art. 32 Abs. 1 lit. b'),
('encryption', 'Verschlüsselung', 'Encryption', 'Verschlüsselung personenbezogener Daten', 'Art. 32 Abs. 1 lit. a'),
('pseudonymization', 'Pseudonymisierung', 'Pseudonymization', 'Verarbeitung in einer Weise, dass die Daten ohne zusätzliche Informationen nicht mehr zugeordnet werden können', 'Art. 32 Abs. 1 lit. a'),
('resilience', 'Belastbarkeit', 'Resilience', 'Fähigkeit, die Verfügbarkeit und den Zugang bei einem Zwischenfall rasch wiederherzustellen', 'Art. 32 Abs. 1 lit. b, c'),
('recovery', 'Wiederherstellung', 'Recovery', 'Verfahren zur Wiederherstellung der Verfügbarkeit und des Zugangs', 'Art. 32 Abs. 1 lit. c'),
('testing', 'Regelmäßige Überprüfung', 'Regular Testing', 'Verfahren zur regelmäßigen Überprüfung, Bewertung und Evaluierung der Wirksamkeit', 'Art. 32 Abs. 1 lit. d')
ON CONFLICT (id) DO NOTHING;

View File

@@ -0,0 +1,96 @@
-- Migration 003: UCCA (Use-Case Compliance & Feasibility Advisor) Schema
-- Creates table for storing AI use-case assessments
-- ============================================================================
-- UCCA Assessments Table
-- ============================================================================
CREATE TABLE IF NOT EXISTS ucca_assessments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES compliance_tenants(id) ON DELETE CASCADE,
namespace_id UUID REFERENCES compliance_namespaces(id) ON DELETE SET NULL,
-- Metadata
title VARCHAR(500),
policy_version VARCHAR(50) NOT NULL DEFAULT '1.0.0',
status VARCHAR(50) DEFAULT 'completed',
-- Input
intake JSONB NOT NULL, -- Full UseCaseIntake
use_case_text_stored BOOLEAN DEFAULT FALSE, -- Opt-in for raw text storage
use_case_text_hash VARCHAR(64), -- SHA-256 hash (always stored)
-- Results - Main verdict
feasibility VARCHAR(20) NOT NULL, -- YES/CONDITIONAL/NO
risk_level VARCHAR(20) NOT NULL, -- MINIMAL/LOW/MEDIUM/HIGH/UNACCEPTABLE
complexity VARCHAR(10) NOT NULL, -- LOW/MEDIUM/HIGH
risk_score INT NOT NULL DEFAULT 0, -- 0-100
-- Results - Details (JSONB for flexibility)
triggered_rules JSONB DEFAULT '[]', -- Array of TriggeredRule
required_controls JSONB DEFAULT '[]', -- Array of RequiredControl
recommended_architecture JSONB DEFAULT '[]', -- Array of PatternRecommendation
forbidden_patterns JSONB DEFAULT '[]', -- Array of ForbiddenPattern
example_matches JSONB DEFAULT '[]', -- Array of ExampleMatch
-- Results - Flags
dsfa_recommended BOOLEAN DEFAULT FALSE,
art22_risk BOOLEAN DEFAULT FALSE, -- Art. 22 GDPR automated decision risk
training_allowed VARCHAR(50), -- YES/CONDITIONAL/NO
-- LLM Explanation (optional)
explanation_text TEXT,
explanation_generated_at TIMESTAMPTZ,
explanation_model VARCHAR(100),
-- Domain classification
domain VARCHAR(50),
-- Audit trail
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
created_by UUID NOT NULL
);
-- ============================================================================
-- Indexes for Performance
-- ============================================================================
-- Primary lookup by tenant
CREATE INDEX idx_ucca_tenant ON ucca_assessments(tenant_id);
-- List view with sorting by date
CREATE INDEX idx_ucca_tenant_created ON ucca_assessments(tenant_id, created_at DESC);
-- Filter by feasibility
CREATE INDEX idx_ucca_tenant_feasibility ON ucca_assessments(tenant_id, feasibility);
-- Filter by domain
CREATE INDEX idx_ucca_tenant_domain ON ucca_assessments(tenant_id, domain);
-- Filter by risk level
CREATE INDEX idx_ucca_tenant_risk ON ucca_assessments(tenant_id, risk_level);
-- JSONB index for searching within triggered_rules
CREATE INDEX idx_ucca_triggered_rules ON ucca_assessments USING GIN (triggered_rules);
-- ============================================================================
-- Comments for Documentation
-- ============================================================================
COMMENT ON TABLE ucca_assessments IS 'UCCA (Use-Case Compliance & Feasibility Advisor) assessments - stores evaluated AI use cases with GDPR compliance verdicts';
COMMENT ON COLUMN ucca_assessments.intake IS 'Full UseCaseIntake JSON including data types, purpose, automation level, hosting, etc.';
COMMENT ON COLUMN ucca_assessments.use_case_text_stored IS 'Whether the raw use case description text is stored (opt-in)';
COMMENT ON COLUMN ucca_assessments.use_case_text_hash IS 'SHA-256 hash of use case text for deduplication without storing raw text';
COMMENT ON COLUMN ucca_assessments.feasibility IS 'Overall verdict: YES (low risk), CONDITIONAL (needs controls), NO (not allowed)';
COMMENT ON COLUMN ucca_assessments.risk_score IS 'Numeric risk score 0-100 calculated from triggered rules';
COMMENT ON COLUMN ucca_assessments.triggered_rules IS 'Array of rules that were triggered during evaluation';
COMMENT ON COLUMN ucca_assessments.required_controls IS 'Array of controls/mitigations that must be implemented';
COMMENT ON COLUMN ucca_assessments.recommended_architecture IS 'Array of recommended architecture patterns';
COMMENT ON COLUMN ucca_assessments.forbidden_patterns IS 'Array of patterns that must NOT be used';
COMMENT ON COLUMN ucca_assessments.example_matches IS 'Array of matching didactic examples';
COMMENT ON COLUMN ucca_assessments.dsfa_recommended IS 'Whether a Data Protection Impact Assessment is recommended';
COMMENT ON COLUMN ucca_assessments.art22_risk IS 'Whether there is risk under Art. 22 GDPR (automated individual decisions)';
COMMENT ON COLUMN ucca_assessments.training_allowed IS 'Whether model training with the data is allowed';
COMMENT ON COLUMN ucca_assessments.explanation_text IS 'LLM-generated explanation in German (optional)';

View File

@@ -0,0 +1,168 @@
-- Migration 004: UCCA Escalation Workflow
-- Implements E0-E3 escalation levels with DSB routing
-- ============================================================================
-- Escalation Levels (Reference)
-- ============================================================================
-- E0: Auto-Approve - Only INFO rules triggered, Risk < 20
-- E1: Team-Lead Review - WARN rules OR Risk 20-40
-- E2: DSB Consultation - Art. 9 data OR Risk 40-60 OR DSFA recommended
-- E3: DSB + Legal - BLOCK rules OR Risk > 60 OR Art. 22 risk
-- ============================================================================
-- Escalation Queue Table
-- ============================================================================
CREATE TABLE IF NOT EXISTS ucca_escalations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES compliance_tenants(id) ON DELETE CASCADE,
assessment_id UUID NOT NULL REFERENCES ucca_assessments(id) ON DELETE CASCADE,
-- Escalation Level
escalation_level VARCHAR(10) NOT NULL CHECK (escalation_level IN ('E0', 'E1', 'E2', 'E3')),
escalation_reason TEXT NOT NULL,
-- Routing
assigned_to UUID, -- User ID of assignee (DSB, Team Lead, etc.)
assigned_role VARCHAR(50), -- Role for assignment (dsb, team_lead, legal)
assigned_at TIMESTAMPTZ,
-- Status
status VARCHAR(30) NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending', 'assigned', 'in_review', 'approved', 'rejected', 'returned')),
-- Review
reviewer_id UUID,
reviewer_notes TEXT,
reviewed_at TIMESTAMPTZ,
-- Decision
decision VARCHAR(20) CHECK (decision IN ('approve', 'reject', 'modify', 'escalate')),
decision_notes TEXT,
decision_at TIMESTAMPTZ,
-- Conditions for approval
conditions JSONB DEFAULT '[]', -- Array of conditions that must be met
-- Timestamps
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
due_date TIMESTAMPTZ, -- SLA deadline
-- Notifications sent
notification_sent BOOLEAN DEFAULT FALSE,
notification_sent_at TIMESTAMPTZ
);
-- ============================================================================
-- Escalation History (Audit Trail)
-- ============================================================================
CREATE TABLE IF NOT EXISTS ucca_escalation_history (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
escalation_id UUID NOT NULL REFERENCES ucca_escalations(id) ON DELETE CASCADE,
-- What changed
action VARCHAR(50) NOT NULL, -- created, assigned, reviewed, decided, escalated, etc.
old_status VARCHAR(30),
new_status VARCHAR(30),
old_level VARCHAR(10),
new_level VARCHAR(10),
-- Who and when
actor_id UUID NOT NULL,
actor_role VARCHAR(50),
notes TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- ============================================================================
-- DSB Assignment Pool
-- ============================================================================
CREATE TABLE IF NOT EXISTS ucca_dsb_pool (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES compliance_tenants(id) ON DELETE CASCADE,
user_id UUID NOT NULL,
user_name VARCHAR(255) NOT NULL,
user_email VARCHAR(255) NOT NULL,
role VARCHAR(50) NOT NULL DEFAULT 'dsb', -- dsb, deputy_dsb, legal
is_active BOOLEAN DEFAULT TRUE,
max_concurrent_reviews INT DEFAULT 10,
current_reviews INT DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(tenant_id, user_id)
);
-- ============================================================================
-- SLA Configuration per Escalation Level
-- ============================================================================
CREATE TABLE IF NOT EXISTS ucca_escalation_sla (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES compliance_tenants(id) ON DELETE CASCADE,
escalation_level VARCHAR(10) NOT NULL CHECK (escalation_level IN ('E0', 'E1', 'E2', 'E3')),
-- SLA settings
response_hours INT NOT NULL DEFAULT 24, -- Hours to first response
resolution_hours INT NOT NULL DEFAULT 72, -- Hours to resolution
-- Notification settings
notify_on_creation BOOLEAN DEFAULT TRUE,
notify_on_approaching_sla BOOLEAN DEFAULT TRUE,
notify_on_sla_breach BOOLEAN DEFAULT TRUE,
approaching_sla_hours INT DEFAULT 8, -- Notify X hours before SLA breach
-- Auto-escalation
auto_escalate_on_breach BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(tenant_id, escalation_level)
);
-- ============================================================================
-- Indexes
-- ============================================================================
-- Fast lookup by tenant and status
CREATE INDEX idx_ucca_escalations_tenant_status ON ucca_escalations(tenant_id, status);
-- Fast lookup by assignee
CREATE INDEX idx_ucca_escalations_assigned ON ucca_escalations(assigned_to, status);
-- Fast lookup by assessment
CREATE INDEX idx_ucca_escalations_assessment ON ucca_escalations(assessment_id);
-- SLA monitoring (find escalations approaching or past due date)
CREATE INDEX idx_ucca_escalations_due ON ucca_escalations(due_date) WHERE status NOT IN ('approved', 'rejected');
-- History lookup
CREATE INDEX idx_ucca_escalation_history_escalation ON ucca_escalation_history(escalation_id);
-- DSB pool lookup
CREATE INDEX idx_ucca_dsb_pool_tenant ON ucca_dsb_pool(tenant_id, is_active);
-- ============================================================================
-- Default SLA Values (inserted on first use)
-- ============================================================================
-- Note: These will be inserted per-tenant when needed via application logic
-- E0: Auto-approve, no SLA
-- E1: 24h response, 72h resolution
-- E2: 8h response, 48h resolution
-- E3: 4h response, 24h resolution (urgent)
-- ============================================================================
-- Comments
-- ============================================================================
COMMENT ON TABLE ucca_escalations IS 'UCCA escalation queue for assessments requiring review';
COMMENT ON COLUMN ucca_escalations.escalation_level IS 'E0=Auto, E1=Team, E2=DSB, E3=DSB+Legal';
COMMENT ON COLUMN ucca_escalations.conditions IS 'JSON array of conditions required for approval';
COMMENT ON TABLE ucca_escalation_history IS 'Audit trail of all escalation state changes';
COMMENT ON TABLE ucca_dsb_pool IS 'Pool of DSB/Legal reviewers for assignment';
COMMENT ON TABLE ucca_escalation_sla IS 'SLA configuration per escalation level per tenant';

View File

@@ -0,0 +1,193 @@
-- ============================================================================
-- Migration 005: Roadmap Schema
-- Compliance Roadmap Management with Import Support
-- ============================================================================
-- Roadmaps table
CREATE TABLE IF NOT EXISTS roadmaps (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
namespace_id UUID REFERENCES namespaces(id) ON DELETE SET NULL,
title VARCHAR(255) NOT NULL,
description TEXT,
version VARCHAR(50) DEFAULT '1.0',
-- Links to other entities
assessment_id UUID REFERENCES ucca_assessments(id) ON DELETE SET NULL,
portfolio_id UUID, -- Will reference portfolio table when created
-- Status tracking
status VARCHAR(50) DEFAULT 'draft', -- draft, active, completed, archived
total_items INT DEFAULT 0,
completed_items INT DEFAULT 0,
progress INT DEFAULT 0, -- Percentage 0-100
-- Timeline
start_date DATE,
target_date DATE,
-- Audit
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_by UUID NOT NULL
);
-- Roadmap items table
CREATE TABLE IF NOT EXISTS roadmap_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
roadmap_id UUID NOT NULL REFERENCES roadmaps(id) ON DELETE CASCADE,
-- Core fields
title VARCHAR(500) NOT NULL,
description TEXT,
category VARCHAR(50) DEFAULT 'TECHNICAL', -- TECHNICAL, ORGANIZATIONAL, PROCESSUAL, DOCUMENTATION, TRAINING
priority VARCHAR(50) DEFAULT 'MEDIUM', -- CRITICAL, HIGH, MEDIUM, LOW
status VARCHAR(50) DEFAULT 'PLANNED', -- PLANNED, IN_PROGRESS, BLOCKED, COMPLETED, DEFERRED
-- Compliance mapping
control_id VARCHAR(100), -- e.g., "CTRL-AVV"
regulation_ref VARCHAR(255), -- e.g., "DSGVO Art. 28"
gap_id VARCHAR(100), -- e.g., "GAP_AVV_MISSING"
-- Effort estimation
effort_days INT,
effort_hours INT,
estimated_cost INT, -- EUR
-- Assignment
assignee_id UUID,
assignee_name VARCHAR(255),
department VARCHAR(255),
-- Timeline
planned_start DATE,
planned_end DATE,
actual_start DATE,
actual_end DATE,
-- Dependencies (JSONB arrays of UUIDs)
depends_on JSONB DEFAULT '[]',
blocked_by JSONB DEFAULT '[]',
-- Evidence
evidence_required JSONB DEFAULT '[]', -- Array of strings
evidence_provided JSONB DEFAULT '[]', -- Array of strings
-- Notes
notes TEXT,
risk_notes TEXT,
-- Import metadata
source_row INT,
source_file VARCHAR(500),
-- Ordering
sort_order INT DEFAULT 0,
-- Audit
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Import jobs table
CREATE TABLE IF NOT EXISTS roadmap_import_jobs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
roadmap_id UUID REFERENCES roadmaps(id) ON DELETE SET NULL,
-- File info
filename VARCHAR(500) NOT NULL,
format VARCHAR(50) NOT NULL, -- EXCEL, CSV, JSON
file_size BIGINT,
content_type VARCHAR(255),
-- Status
status VARCHAR(50) DEFAULT 'pending', -- pending, parsing, parsed, validating, completed, failed
error_message TEXT,
-- Parsing results
total_rows INT DEFAULT 0,
valid_rows INT DEFAULT 0,
invalid_rows INT DEFAULT 0,
imported_items INT DEFAULT 0,
-- Parsed items (before confirmation)
parsed_items JSONB DEFAULT '[]',
-- Audit
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
completed_at TIMESTAMPTZ,
created_by UUID NOT NULL
);
-- ============================================================================
-- Indexes
-- ============================================================================
-- Roadmaps indexes
CREATE INDEX IF NOT EXISTS idx_roadmaps_tenant ON roadmaps(tenant_id);
CREATE INDEX IF NOT EXISTS idx_roadmaps_status ON roadmaps(tenant_id, status);
CREATE INDEX IF NOT EXISTS idx_roadmaps_assessment ON roadmaps(assessment_id) WHERE assessment_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_roadmaps_portfolio ON roadmaps(portfolio_id) WHERE portfolio_id IS NOT NULL;
-- Roadmap items indexes
CREATE INDEX IF NOT EXISTS idx_roadmap_items_roadmap ON roadmap_items(roadmap_id);
CREATE INDEX IF NOT EXISTS idx_roadmap_items_status ON roadmap_items(roadmap_id, status);
CREATE INDEX IF NOT EXISTS idx_roadmap_items_priority ON roadmap_items(roadmap_id, priority);
CREATE INDEX IF NOT EXISTS idx_roadmap_items_category ON roadmap_items(roadmap_id, category);
CREATE INDEX IF NOT EXISTS idx_roadmap_items_assignee ON roadmap_items(assignee_id) WHERE assignee_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_roadmap_items_control ON roadmap_items(control_id) WHERE control_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_roadmap_items_deadline ON roadmap_items(planned_end) WHERE planned_end IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_roadmap_items_sort ON roadmap_items(roadmap_id, sort_order);
-- Import jobs indexes
CREATE INDEX IF NOT EXISTS idx_import_jobs_tenant ON roadmap_import_jobs(tenant_id);
CREATE INDEX IF NOT EXISTS idx_import_jobs_status ON roadmap_import_jobs(tenant_id, status);
CREATE INDEX IF NOT EXISTS idx_import_jobs_roadmap ON roadmap_import_jobs(roadmap_id) WHERE roadmap_id IS NOT NULL;
-- ============================================================================
-- Triggers for updated_at
-- ============================================================================
-- Trigger function (reuse if exists)
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ language 'plpgsql';
-- Roadmaps trigger
DROP TRIGGER IF EXISTS update_roadmaps_updated_at ON roadmaps;
CREATE TRIGGER update_roadmaps_updated_at
BEFORE UPDATE ON roadmaps
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- Roadmap items trigger
DROP TRIGGER IF EXISTS update_roadmap_items_updated_at ON roadmap_items;
CREATE TRIGGER update_roadmap_items_updated_at
BEFORE UPDATE ON roadmap_items
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- Import jobs trigger
DROP TRIGGER IF EXISTS update_roadmap_import_jobs_updated_at ON roadmap_import_jobs;
CREATE TRIGGER update_roadmap_import_jobs_updated_at
BEFORE UPDATE ON roadmap_import_jobs
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- ============================================================================
-- Comments
-- ============================================================================
COMMENT ON TABLE roadmaps IS 'Compliance implementation roadmaps';
COMMENT ON TABLE roadmap_items IS 'Individual items/tasks in a compliance roadmap';
COMMENT ON TABLE roadmap_import_jobs IS 'Track file imports for roadmap items';
COMMENT ON COLUMN roadmap_items.control_id IS 'Reference to controls catalog (e.g., CTRL-AVV)';
COMMENT ON COLUMN roadmap_items.regulation_ref IS 'Reference to regulation article (e.g., DSGVO Art. 28)';
COMMENT ON COLUMN roadmap_items.gap_id IS 'Reference to gap mapping (e.g., GAP_AVV_MISSING)';
COMMENT ON COLUMN roadmap_items.depends_on IS 'Array of item IDs this item depends on';
COMMENT ON COLUMN roadmap_items.blocked_by IS 'Array of item IDs currently blocking this item';

View File

@@ -0,0 +1,207 @@
-- ============================================================================
-- Migration 006: Workshop Session Schema
-- Collaborative Compliance Workshop Sessions
-- ============================================================================
-- Workshop sessions table
CREATE TABLE IF NOT EXISTS workshop_sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
namespace_id UUID REFERENCES namespaces(id) ON DELETE SET NULL,
-- Session info
title VARCHAR(255) NOT NULL,
description TEXT,
session_type VARCHAR(50) NOT NULL, -- 'ucca', 'dsfa', 'custom'
status VARCHAR(50) DEFAULT 'DRAFT', -- DRAFT, SCHEDULED, ACTIVE, PAUSED, COMPLETED, CANCELLED
-- Wizard configuration
wizard_schema VARCHAR(100), -- Reference to wizard schema version
current_step INT DEFAULT 1,
total_steps INT DEFAULT 10,
-- Links to other entities
assessment_id UUID REFERENCES ucca_assessments(id) ON DELETE SET NULL,
roadmap_id UUID REFERENCES roadmaps(id) ON DELETE SET NULL,
portfolio_id UUID, -- Will reference portfolio table when created
-- Scheduling
scheduled_start TIMESTAMPTZ,
scheduled_end TIMESTAMPTZ,
actual_start TIMESTAMPTZ,
actual_end TIMESTAMPTZ,
-- Access control
join_code VARCHAR(10) NOT NULL UNIQUE,
require_auth BOOLEAN DEFAULT FALSE,
allow_anonymous BOOLEAN DEFAULT TRUE,
-- Settings (JSONB)
settings JSONB DEFAULT '{}',
-- Audit
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_by UUID NOT NULL
);
-- Workshop participants table
CREATE TABLE IF NOT EXISTS workshop_participants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
session_id UUID NOT NULL REFERENCES workshop_sessions(id) ON DELETE CASCADE,
user_id UUID, -- Null for anonymous participants
-- Info
name VARCHAR(255) NOT NULL,
email VARCHAR(255),
role VARCHAR(50) DEFAULT 'STAKEHOLDER', -- FACILITATOR, EXPERT, STAKEHOLDER, OBSERVER
department VARCHAR(255),
-- Status
is_active BOOLEAN DEFAULT TRUE,
last_active_at TIMESTAMPTZ,
joined_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
left_at TIMESTAMPTZ,
-- Permissions
can_edit BOOLEAN DEFAULT TRUE,
can_comment BOOLEAN DEFAULT TRUE,
can_approve BOOLEAN DEFAULT FALSE
);
-- Workshop step progress table
CREATE TABLE IF NOT EXISTS workshop_step_progress (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
session_id UUID NOT NULL REFERENCES workshop_sessions(id) ON DELETE CASCADE,
step_number INT NOT NULL,
-- Status
status VARCHAR(50) DEFAULT 'pending', -- pending, in_progress, completed, skipped
progress INT DEFAULT 0, -- 0-100
-- Timestamps
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
-- Notes
notes TEXT,
UNIQUE(session_id, step_number)
);
-- Workshop responses table
CREATE TABLE IF NOT EXISTS workshop_responses (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
session_id UUID NOT NULL REFERENCES workshop_sessions(id) ON DELETE CASCADE,
participant_id UUID NOT NULL REFERENCES workshop_participants(id) ON DELETE CASCADE,
-- Question reference
step_number INT NOT NULL,
field_id VARCHAR(100) NOT NULL,
-- Response data
value JSONB, -- Can be any JSON type
value_type VARCHAR(50), -- string, boolean, array, number, object
-- Status
status VARCHAR(50) DEFAULT 'SUBMITTED', -- PENDING, DRAFT, SUBMITTED, REVIEWED
-- Review
reviewed_by UUID,
reviewed_at TIMESTAMPTZ,
review_notes TEXT,
-- Audit
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- Unique constraint per participant per field
UNIQUE(session_id, participant_id, field_id)
);
-- Workshop comments table
CREATE TABLE IF NOT EXISTS workshop_comments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
session_id UUID NOT NULL REFERENCES workshop_sessions(id) ON DELETE CASCADE,
participant_id UUID NOT NULL REFERENCES workshop_participants(id) ON DELETE CASCADE,
-- Target (one of these should be set)
step_number INT,
field_id VARCHAR(100),
response_id UUID REFERENCES workshop_responses(id) ON DELETE CASCADE,
-- Content
text TEXT NOT NULL,
is_resolved BOOLEAN DEFAULT FALSE,
-- Audit
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- ============================================================================
-- Indexes
-- ============================================================================
-- Session indexes
CREATE INDEX IF NOT EXISTS idx_workshop_sessions_tenant ON workshop_sessions(tenant_id);
CREATE INDEX IF NOT EXISTS idx_workshop_sessions_status ON workshop_sessions(tenant_id, status);
CREATE INDEX IF NOT EXISTS idx_workshop_sessions_join_code ON workshop_sessions(join_code);
CREATE INDEX IF NOT EXISTS idx_workshop_sessions_assessment ON workshop_sessions(assessment_id) WHERE assessment_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_workshop_sessions_created_by ON workshop_sessions(created_by);
-- Participant indexes
CREATE INDEX IF NOT EXISTS idx_workshop_participants_session ON workshop_participants(session_id);
CREATE INDEX IF NOT EXISTS idx_workshop_participants_user ON workshop_participants(user_id) WHERE user_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_workshop_participants_active ON workshop_participants(session_id, is_active);
-- Step progress indexes
CREATE INDEX IF NOT EXISTS idx_workshop_step_progress_session ON workshop_step_progress(session_id);
-- Response indexes
CREATE INDEX IF NOT EXISTS idx_workshop_responses_session ON workshop_responses(session_id);
CREATE INDEX IF NOT EXISTS idx_workshop_responses_participant ON workshop_responses(participant_id);
CREATE INDEX IF NOT EXISTS idx_workshop_responses_step ON workshop_responses(session_id, step_number);
CREATE INDEX IF NOT EXISTS idx_workshop_responses_field ON workshop_responses(session_id, field_id);
-- Comment indexes
CREATE INDEX IF NOT EXISTS idx_workshop_comments_session ON workshop_comments(session_id);
CREATE INDEX IF NOT EXISTS idx_workshop_comments_response ON workshop_comments(response_id) WHERE response_id IS NOT NULL;
-- ============================================================================
-- Triggers
-- ============================================================================
-- Reuse existing update_updated_at_column function
-- Sessions trigger
DROP TRIGGER IF EXISTS update_workshop_sessions_updated_at ON workshop_sessions;
CREATE TRIGGER update_workshop_sessions_updated_at
BEFORE UPDATE ON workshop_sessions
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- Responses trigger
DROP TRIGGER IF EXISTS update_workshop_responses_updated_at ON workshop_responses;
CREATE TRIGGER update_workshop_responses_updated_at
BEFORE UPDATE ON workshop_responses
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- Comments trigger
DROP TRIGGER IF EXISTS update_workshop_comments_updated_at ON workshop_comments;
CREATE TRIGGER update_workshop_comments_updated_at
BEFORE UPDATE ON workshop_comments
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- ============================================================================
-- Comments
-- ============================================================================
COMMENT ON TABLE workshop_sessions IS 'Collaborative compliance workshop sessions';
COMMENT ON TABLE workshop_participants IS 'Participants in workshop sessions';
COMMENT ON TABLE workshop_step_progress IS 'Progress tracking for each wizard step';
COMMENT ON TABLE workshop_responses IS 'Participant responses to wizard questions';
COMMENT ON TABLE workshop_comments IS 'Comments and discussions on responses';
COMMENT ON COLUMN workshop_sessions.join_code IS 'Code for participants to join the session';
COMMENT ON COLUMN workshop_sessions.settings IS 'JSON settings (allow_back_navigation, require_all_responses, etc.)';
COMMENT ON COLUMN workshop_responses.value IS 'JSON response value (can be any type)';

View File

@@ -0,0 +1,267 @@
-- ============================================================================
-- Migration 007: Portfolio Schema
-- AI Use Case Portfolio Management with Merge Support
-- ============================================================================
-- Portfolios table
CREATE TABLE IF NOT EXISTS portfolios (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
namespace_id UUID REFERENCES namespaces(id) ON DELETE SET NULL,
-- Info
name VARCHAR(255) NOT NULL,
description TEXT,
status VARCHAR(50) DEFAULT 'DRAFT', -- DRAFT, ACTIVE, REVIEW, APPROVED, ARCHIVED
-- Organization
department VARCHAR(255),
business_unit VARCHAR(255),
owner VARCHAR(255),
owner_email VARCHAR(255),
-- Aggregated metrics (computed)
total_assessments INT DEFAULT 0,
total_roadmaps INT DEFAULT 0,
total_workshops INT DEFAULT 0,
avg_risk_score DECIMAL(5,2) DEFAULT 0,
high_risk_count INT DEFAULT 0,
conditional_count INT DEFAULT 0,
approved_count INT DEFAULT 0,
compliance_score DECIMAL(5,2) DEFAULT 0, -- 0-100
-- Settings (JSONB)
settings JSONB DEFAULT '{}',
-- Audit
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_by UUID NOT NULL,
approved_at TIMESTAMPTZ,
approved_by UUID
);
-- Portfolio items table (links portfolios to assessments, roadmaps, workshops)
CREATE TABLE IF NOT EXISTS portfolio_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
portfolio_id UUID NOT NULL REFERENCES portfolios(id) ON DELETE CASCADE,
item_type VARCHAR(50) NOT NULL, -- ASSESSMENT, ROADMAP, WORKSHOP, DOCUMENT
item_id UUID NOT NULL,
-- Cached info from the linked item
title VARCHAR(500),
status VARCHAR(50),
risk_level VARCHAR(20),
risk_score INT DEFAULT 0,
feasibility VARCHAR(20),
-- Ordering and categorization
sort_order INT DEFAULT 0,
tags JSONB DEFAULT '[]',
notes TEXT,
-- Audit
added_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
added_by UUID NOT NULL,
-- Unique constraint: item can only be in portfolio once
UNIQUE(portfolio_id, item_id)
);
-- Portfolio activity log table
CREATE TABLE IF NOT EXISTS portfolio_activity (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
portfolio_id UUID NOT NULL REFERENCES portfolios(id) ON DELETE CASCADE,
-- Activity info
timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
action VARCHAR(50) NOT NULL, -- added, removed, updated, merged, approved, submitted
item_type VARCHAR(50),
item_id UUID,
item_title VARCHAR(500),
user_id UUID NOT NULL,
-- Additional details
details JSONB
);
-- ============================================================================
-- Indexes
-- ============================================================================
-- Portfolio indexes
CREATE INDEX IF NOT EXISTS idx_portfolios_tenant ON portfolios(tenant_id);
CREATE INDEX IF NOT EXISTS idx_portfolios_tenant_status ON portfolios(tenant_id, status);
CREATE INDEX IF NOT EXISTS idx_portfolios_department ON portfolios(tenant_id, department) WHERE department IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_portfolios_business_unit ON portfolios(tenant_id, business_unit) WHERE business_unit IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_portfolios_owner ON portfolios(owner) WHERE owner IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_portfolios_created_by ON portfolios(created_by);
CREATE INDEX IF NOT EXISTS idx_portfolios_risk_score ON portfolios(avg_risk_score);
-- Portfolio item indexes
CREATE INDEX IF NOT EXISTS idx_portfolio_items_portfolio ON portfolio_items(portfolio_id);
CREATE INDEX IF NOT EXISTS idx_portfolio_items_type ON portfolio_items(portfolio_id, item_type);
CREATE INDEX IF NOT EXISTS idx_portfolio_items_item ON portfolio_items(item_id);
CREATE INDEX IF NOT EXISTS idx_portfolio_items_risk ON portfolio_items(portfolio_id, risk_level);
CREATE INDEX IF NOT EXISTS idx_portfolio_items_feasibility ON portfolio_items(portfolio_id, feasibility);
CREATE INDEX IF NOT EXISTS idx_portfolio_items_sort ON portfolio_items(portfolio_id, sort_order);
-- Activity indexes
CREATE INDEX IF NOT EXISTS idx_portfolio_activity_portfolio ON portfolio_activity(portfolio_id);
CREATE INDEX IF NOT EXISTS idx_portfolio_activity_timestamp ON portfolio_activity(portfolio_id, timestamp DESC);
CREATE INDEX IF NOT EXISTS idx_portfolio_activity_user ON portfolio_activity(user_id);
-- ============================================================================
-- Triggers
-- ============================================================================
-- Reuse existing update_updated_at_column function
-- Portfolios trigger
DROP TRIGGER IF EXISTS update_portfolios_updated_at ON portfolios;
CREATE TRIGGER update_portfolios_updated_at
BEFORE UPDATE ON portfolios
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- ============================================================================
-- Functions for Metrics Calculation
-- ============================================================================
-- Function to recalculate portfolio metrics
CREATE OR REPLACE FUNCTION recalculate_portfolio_metrics(p_portfolio_id UUID)
RETURNS VOID AS $$
DECLARE
v_total_assessments INT;
v_total_roadmaps INT;
v_total_workshops INT;
v_avg_risk DECIMAL(5,2);
v_high_risk INT;
v_conditional INT;
v_approved INT;
v_compliance DECIMAL(5,2);
BEGIN
-- Count by type
SELECT COUNT(*) INTO v_total_assessments
FROM portfolio_items
WHERE portfolio_id = p_portfolio_id AND item_type = 'ASSESSMENT';
SELECT COUNT(*) INTO v_total_roadmaps
FROM portfolio_items
WHERE portfolio_id = p_portfolio_id AND item_type = 'ROADMAP';
SELECT COUNT(*) INTO v_total_workshops
FROM portfolio_items
WHERE portfolio_id = p_portfolio_id AND item_type = 'WORKSHOP';
-- Calculate risk metrics
SELECT COALESCE(AVG(risk_score), 0) INTO v_avg_risk
FROM portfolio_items
WHERE portfolio_id = p_portfolio_id AND item_type = 'ASSESSMENT';
SELECT COUNT(*) INTO v_high_risk
FROM portfolio_items
WHERE portfolio_id = p_portfolio_id
AND item_type = 'ASSESSMENT'
AND risk_level IN ('HIGH', 'UNACCEPTABLE');
SELECT COUNT(*) INTO v_conditional
FROM portfolio_items
WHERE portfolio_id = p_portfolio_id
AND item_type = 'ASSESSMENT'
AND feasibility = 'CONDITIONAL';
SELECT COUNT(*) INTO v_approved
FROM portfolio_items
WHERE portfolio_id = p_portfolio_id
AND item_type = 'ASSESSMENT'
AND feasibility = 'YES';
-- Calculate compliance score
IF v_total_assessments > 0 THEN
v_compliance := (v_approved::DECIMAL / v_total_assessments) * 100;
ELSE
v_compliance := 0;
END IF;
-- Update portfolio
UPDATE portfolios SET
total_assessments = v_total_assessments,
total_roadmaps = v_total_roadmaps,
total_workshops = v_total_workshops,
avg_risk_score = v_avg_risk,
high_risk_count = v_high_risk,
conditional_count = v_conditional,
approved_count = v_approved,
compliance_score = v_compliance,
updated_at = NOW()
WHERE id = p_portfolio_id;
END;
$$ LANGUAGE plpgsql;
-- Trigger function to auto-update metrics on item changes
CREATE OR REPLACE FUNCTION portfolio_items_metrics_trigger()
RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'DELETE' THEN
PERFORM recalculate_portfolio_metrics(OLD.portfolio_id);
RETURN OLD;
ELSE
PERFORM recalculate_portfolio_metrics(NEW.portfolio_id);
RETURN NEW;
END IF;
END;
$$ LANGUAGE plpgsql;
-- Trigger for auto metrics update
DROP TRIGGER IF EXISTS trg_portfolio_items_metrics ON portfolio_items;
CREATE TRIGGER trg_portfolio_items_metrics
AFTER INSERT OR UPDATE OR DELETE ON portfolio_items
FOR EACH ROW EXECUTE FUNCTION portfolio_items_metrics_trigger();
-- ============================================================================
-- Views
-- ============================================================================
-- View for portfolio summary with counts
CREATE OR REPLACE VIEW portfolio_summary_view AS
SELECT
p.id,
p.tenant_id,
p.name,
p.description,
p.status,
p.department,
p.business_unit,
p.owner,
p.total_assessments,
p.total_roadmaps,
p.total_workshops,
p.avg_risk_score,
p.high_risk_count,
p.conditional_count,
p.approved_count,
p.compliance_score,
p.created_at,
p.updated_at,
(p.total_assessments + p.total_roadmaps + p.total_workshops) as total_items,
CASE
WHEN p.high_risk_count > 0 THEN 'CRITICAL'
WHEN p.conditional_count > p.approved_count THEN 'WARNING'
ELSE 'GOOD'
END as health_status
FROM portfolios p;
-- ============================================================================
-- Comments
-- ============================================================================
COMMENT ON TABLE portfolios IS 'AI use case portfolios for grouping and managing multiple assessments';
COMMENT ON TABLE portfolio_items IS 'Items linked to portfolios (assessments, roadmaps, workshops)';
COMMENT ON TABLE portfolio_activity IS 'Activity log for portfolio changes';
COMMENT ON COLUMN portfolios.compliance_score IS 'Percentage of assessments with YES feasibility (0-100)';
COMMENT ON COLUMN portfolios.avg_risk_score IS 'Average risk score across all assessments in portfolio';
COMMENT ON COLUMN portfolio_items.item_type IS 'Type of linked item: ASSESSMENT, ROADMAP, WORKSHOP, DOCUMENT';
COMMENT ON COLUMN portfolio_items.sort_order IS 'Custom ordering within the portfolio';
COMMENT ON FUNCTION recalculate_portfolio_metrics(UUID) IS 'Recalculates aggregated metrics for a portfolio';