Files
Benjamin Boenisch 4435e7ea0a Initial commit: breakpilot-compliance - Compliance SDK Platform
Services: Admin-Compliance, Backend-Compliance,
AI-Compliance-SDK, Consent-SDK, Developer-Portal,
PCA-Platform, DSMS

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:28 +01:00

322 lines
14 KiB
PL/PgSQL

-- 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;