-- Migration 079: Capability-Registry + Rollen-Grants (M3 / CAPABILITY_CATALOG.v1.md C1) -- Account-Gates und Enforcement in Python (account_lifecycle.py, capabilities.py). -- Voraussetzung: Migration 078 (features.id TEXT). Kein FK auf features — vermeidet -- Startup-Abbruch wenn 078 noch aussteht oder features-Schema driftet (001 vs v9c). DO $migration$ BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'features' AND column_name = 'limit_type' ) THEN RAISE EXCEPTION 'Migration 079: features-Tabelle nicht v9c (limit_type fehlt). Zuerst 078_club_features_and_plans anwenden.'; END IF; END $migration$; CREATE TABLE IF NOT EXISTS capabilities ( id TEXT PRIMARY KEY, name TEXT NOT NULL, description TEXT, domain TEXT NOT NULL, min_account_state TEXT NOT NULL DEFAULT 'active_member' CHECK (min_account_state IN ( 'unverified', 'verified_pending_club', 'active_member', 'platform_admin' )), linked_feature_id TEXT, active BOOLEAN NOT NULL DEFAULT true, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_capabilities_domain ON capabilities(domain) WHERE active = true; CREATE TABLE IF NOT EXISTS club_role_capability_grants ( role_code TEXT NOT NULL, capability_id TEXT NOT NULL REFERENCES capabilities(id) ON DELETE CASCADE, PRIMARY KEY (role_code, capability_id) ); CREATE INDEX IF NOT EXISTS idx_club_role_cap_grants_cap ON club_role_capability_grants(capability_id); CREATE TABLE IF NOT EXISTS portal_role_capability_grants ( portal_role TEXT NOT NULL, capability_id TEXT NOT NULL REFERENCES capabilities(id) ON DELETE CASCADE, PRIMARY KEY (portal_role, capability_id) ); -- ── Seed: Capabilities (v1 Katalog §5) ─────────────────────────────────────── INSERT INTO capabilities (id, name, domain, min_account_state, linked_feature_id) VALUES ('account.settings.read', 'Einstellungen lesen', 'account', 'unverified', NULL), ('account.settings.update', 'Einstellungen ändern', 'account', 'unverified', NULL), ('account.password.change', 'Passwort ändern', 'account', 'unverified', NULL), ('account.resend_verification', 'Verifizierung erneut senden', 'account', 'unverified', NULL), ('club.directory.read', 'Vereinsverzeichnis', 'club', 'verified_pending_club', NULL), ('club.join_request.create', 'Vereinsbeitritt beantragen', 'club', 'verified_pending_club', NULL), ('club.join_request.withdraw', 'Beitrittsantrag zurückziehen', 'club', 'verified_pending_club', NULL), ('club.join_request.read_own', 'Eigene Beitrittsanträge', 'club', 'verified_pending_club', NULL), ('org.club.read', 'Vereine lesen', 'org', 'active_member', NULL), ('org.club.create', 'Verein anlegen', 'org', 'platform_admin', NULL), ('org.club.update', 'Verein bearbeiten', 'org', 'active_member', NULL), ('org.club.delete', 'Verein löschen', 'org', 'platform_admin', NULL), ('org.structure.manage', 'Vereinsstruktur verwalten', 'org', 'active_member', 'training_groups'), ('org.members.read', 'Mitgliederliste', 'org', 'active_member', NULL), ('org.members.manage', 'Mitglieder verwalten', 'org', 'active_member', 'active_members'), ('org.members.directory', 'Mitglieder-Verzeichnis', 'org', 'active_member', NULL), ('org.join_request.review', 'Beitrittsanträge prüfen', 'org', 'active_member', NULL), ('org.inbox.read', 'Posteingang', 'org', 'active_member', NULL), ('exercises.read', 'Übungen lesen', 'exercises', 'active_member', NULL), ('exercises.create', 'Übung anlegen', 'exercises', 'active_member', 'exercises'), ('exercises.update', 'Übung bearbeiten', 'exercises', 'active_member', NULL), ('exercises.delete', 'Übung löschen', 'exercises', 'active_member', NULL), ('exercises.bulk_metadata', 'Übungen Stapel-Metadaten', 'exercises', 'active_member', NULL), ('exercises.ai.suggest', 'KI-Vorschlag Übung', 'exercises', 'active_member', 'ai_calls'), ('exercises.ai.regenerate', 'KI neu generieren', 'exercises', 'active_member', 'ai_calls'), ('exercises.media.read', 'Übungsmedien lesen', 'exercises', 'active_member', NULL), ('exercises.media.upload', 'Übungsmedien hochladen', 'exercises', 'active_member', 'exercise_media'), ('exercises.variants.manage', 'Übungsvarianten', 'exercises', 'active_member', NULL), ('media.library.read', 'Medienbibliothek lesen', 'media', 'active_member', NULL), ('media.library.upload', 'Medienbibliothek Upload', 'media', 'active_member', 'exercise_media'), ('media.library.update', 'Medienbibliothek bearbeiten', 'media', 'active_member', NULL), ('media.library.lifecycle', 'Medien-Lifecycle', 'media', 'active_member', NULL), ('media.rights.declare', 'Medienrechte erklären', 'media', 'active_member', NULL), ('media.admin.rights_review', 'Medienrechte Review (Plattform)', 'media', 'platform_admin', NULL), ('modules.read', 'Trainingsmodule lesen', 'modules', 'active_member', NULL), ('modules.create', 'Trainingsmodul anlegen', 'modules', 'active_member', 'training_programs'), ('modules.update', 'Trainingsmodul bearbeiten', 'modules', 'active_member', NULL), ('modules.delete', 'Trainingsmodul löschen', 'modules', 'active_member', NULL), ('framework.read', 'Rahmenprogramme lesen', 'framework', 'active_member', NULL), ('framework.create', 'Rahmenprogramm anlegen', 'framework', 'active_member', 'training_programs'), ('framework.update', 'Rahmenprogramm bearbeiten', 'framework', 'active_member', NULL), ('framework.delete', 'Rahmenprogramm löschen', 'framework', 'active_member', NULL), ('plan_templates.read', 'Planungsvorlagen lesen', 'planning', 'active_member', NULL), ('plan_templates.manage', 'Planungsvorlagen verwalten', 'planning', 'active_member', NULL), ('progression.read', 'Progressionspfade lesen', 'progression', 'active_member', NULL), ('progression.manage', 'Progressionspfade verwalten', 'progression', 'active_member', NULL), ('planning.calendar.read', 'Planungskalender lesen', 'planning', 'active_member', NULL), ('planning.units.create', 'Trainingseinheit anlegen', 'planning', 'active_member', 'training_units'), ('planning.units.update', 'Trainingseinheit bearbeiten', 'planning', 'active_member', NULL), ('planning.units.delete', 'Trainingseinheit löschen', 'planning', 'active_member', NULL), ('planning.units.run', 'Training durchführen', 'planning', 'active_member', NULL), ('planning.coach.execute', 'Coach ausführen', 'planning', 'active_member', NULL), ('planning.ai.suggest', 'Planungs-KI Suggest', 'planning', 'active_member', 'ai_calls'), ('planning.ai.progression_path', 'Planungs-KI Progressionspfad', 'planning', 'active_member', 'ai_calls'), ('skills.catalog.read', 'Fähigkeitenkatalog', 'skills', 'active_member', NULL), ('skills.discovery.read', 'Fähigkeiten-Discovery', 'skills', 'active_member', NULL), ('skill_profiles.read', 'Skill-Profile lesen', 'skills', 'active_member', NULL), ('governance.content_report.create', 'Inhalt melden', 'governance', 'active_member', NULL), ('governance.content_report.review', 'Meldungen prüfen', 'governance', 'active_member', NULL), ('platform.admin.access', 'Plattform-Admin-Bereich', 'platform', 'platform_admin', NULL), ('platform.users.manage', 'Nutzer verwalten', 'platform', 'platform_admin', NULL), ('platform.catalogs.manage', 'Kataloge verwalten', 'platform', 'platform_admin', NULL), ('platform.maturity_models.manage', 'Reifegradmodelle', 'platform', 'platform_admin', NULL), ('platform.wiki_import.execute', 'Wiki-Import', 'platform', 'platform_admin', 'wiki_import'), ('platform.ai_prompts.manage', 'KI-Prompts verwalten', 'platform', 'platform_admin', NULL), ('platform.exercise_enrichment.execute', 'Übungs-Anreicherung KI', 'platform', 'platform_admin', 'ai_calls'), ('platform.user_content.moderate', 'Nutzer-Inhalte moderieren', 'platform', 'platform_admin', NULL), ('platform.legal_documents.manage', 'Rechtstexte verwalten', 'platform', 'platform_admin', NULL), ('platform.media_storage.manage', 'Medienspeicher verwalten', 'platform', 'platform_admin', NULL), ('platform.club_creation.approve', 'Vereinsgründung freigeben', 'platform', 'platform_admin', NULL) ON CONFLICT (id) DO NOTHING; -- ── Vereinsrollen-Grants (§6 — nur eingeschränkte Capabilities) ───────────── -- Konvention: keine Grant-Zeile = alle aktiven Mitglieder (min_account_state reicht). INSERT INTO club_role_capability_grants (role_code, capability_id) SELECT r.role_code, c.id FROM (VALUES ('club_admin', 'org.structure.manage'), ('division_lead', 'org.structure.manage'), ('club_admin', 'org.members.manage'), ('club_admin', 'org.join_request.review'), ('club_admin', 'org.inbox.read'), ('club_admin', 'exercises.create'), ('trainer', 'exercises.create'), ('content_editor', 'exercises.create'), ('division_lead', 'exercises.create'), ('club_admin', 'exercises.update'), ('trainer', 'exercises.update'), ('content_editor', 'exercises.update'), ('division_lead', 'exercises.update'), ('club_admin', 'exercises.delete'), ('club_admin', 'exercises.bulk_metadata'), ('content_editor', 'exercises.bulk_metadata'), ('club_admin', 'exercises.ai.suggest'), ('trainer', 'exercises.ai.suggest'), ('content_editor', 'exercises.ai.suggest'), ('division_lead', 'exercises.ai.suggest'), ('club_admin', 'exercises.ai.regenerate'), ('trainer', 'exercises.ai.regenerate'), ('content_editor', 'exercises.ai.regenerate'), ('division_lead', 'exercises.ai.regenerate'), ('club_admin', 'exercises.media.upload'), ('trainer', 'exercises.media.upload'), ('content_editor', 'exercises.media.upload'), ('club_admin', 'exercises.variants.manage'), ('trainer', 'exercises.variants.manage'), ('content_editor', 'exercises.variants.manage'), ('club_admin', 'media.library.upload'), ('trainer', 'media.library.upload'), ('content_editor', 'media.library.upload'), ('club_admin', 'media.library.update'), ('trainer', 'media.library.update'), ('content_editor', 'media.library.update'), ('club_admin', 'media.library.lifecycle'), ('trainer', 'media.library.lifecycle'), ('club_admin', 'media.rights.declare'), ('trainer', 'media.rights.declare'), ('club_admin', 'modules.create'), ('trainer', 'modules.create'), ('content_editor', 'modules.create'), ('club_admin', 'modules.update'), ('trainer', 'modules.update'), ('content_editor', 'modules.update'), ('club_admin', 'modules.delete'), ('club_admin', 'framework.create'), ('trainer', 'framework.create'), ('club_admin', 'framework.update'), ('trainer', 'framework.update'), ('club_admin', 'framework.delete'), ('club_admin', 'plan_templates.manage'), ('trainer', 'plan_templates.manage'), ('club_admin', 'progression.manage'), ('trainer', 'progression.manage'), ('content_editor', 'progression.manage'), ('club_admin', 'planning.units.create'), ('trainer', 'planning.units.create'), ('division_lead', 'planning.units.create'), ('club_admin', 'planning.units.update'), ('trainer', 'planning.units.update'), ('division_lead', 'planning.units.update'), ('club_admin', 'planning.units.delete'), ('trainer', 'planning.units.delete'), ('club_admin', 'planning.units.run'), ('trainer', 'planning.units.run'), ('division_lead', 'planning.units.run'), ('club_admin', 'planning.coach.execute'), ('trainer', 'planning.coach.execute'), ('club_admin', 'planning.ai.suggest'), ('trainer', 'planning.ai.suggest'), ('division_lead', 'planning.ai.suggest'), ('club_admin', 'planning.ai.progression_path'), ('trainer', 'planning.ai.progression_path'), ('division_lead', 'planning.ai.progression_path'), ('club_admin', 'skills.discovery.read'), ('trainer', 'skills.discovery.read'), ('content_editor', 'skills.discovery.read'), ('club_admin', 'governance.content_report.review') ) AS r(role_code, cap_id) JOIN capabilities c ON c.id = r.cap_id ON CONFLICT DO NOTHING; -- org.club.update: club_admin (zusätzlich zu platform_admin via Bypass) INSERT INTO club_role_capability_grants (role_code, capability_id) VALUES ('club_admin', 'org.club.update') ON CONFLICT DO NOTHING; -- ── Portal-Rollen ─────────────────────────────────────────────────────────── INSERT INTO portal_role_capability_grants (portal_role, capability_id) SELECT 'admin', id FROM capabilities WHERE id = 'platform.admin.access' ON CONFLICT DO NOTHING; INSERT INTO portal_role_capability_grants (portal_role, capability_id) SELECT 'superadmin', id FROM capabilities WHERE domain = 'platform' ON CONFLICT DO NOTHING;