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
- Updated the SQL migration script to select the correct capability ID from the capabilities table, ensuring accurate role capability grants during database migrations.
226 lines
13 KiB
SQL
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;
|