shinkan-jinkendo/backend/migrations/079_capabilities.sql
Lars 30dc30c7aa
Some checks failed
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Failing after 0s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Failing after 4m0s
Test Suite / playwright-tests (push) Failing after 3m41s
Enhance Tenant Context and Access Control Features
- Introduced `email_verified` and `account_state` attributes in the `TenantContext` to improve user state management.
- Updated the `resolve_tenant_context` function to dynamically fetch `email_verified` status from the database and determine `account_state` based on user roles and memberships.
- Implemented `assert_min_account_state` checks across various endpoints to enforce access control based on user account status.
- Incremented version to 1.1.0 in version.py to reflect these enhancements in tenant context management and access control.
2026-06-06 21:10:52 +02:00

212 lines
12 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).
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 REFERENCES features(id) ON DELETE SET NULL,
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.cap_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;