shinkan-jinkendo/backend/migrations/079_capabilities.sql
Lars 20927a5969
All checks were successful
Deploy Development / deploy (push) Successful in 46s
Test Suite / pytest-backend (push) Successful in 41s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m15s
Fix capability ID reference in migration script
- Updated the SQL migration script to select the correct capability ID from the capabilities table, ensuring accurate role capability grants during database migrations.
2026-06-07 06:43:01 +02:00

226 lines
13 KiB
SQL

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