"""F11 permission matrix as code. This mirrors the spec table 1:1 — keeping it as a single source in code lets us hash-check it against the spec in CI (spec-analyst task S0.2). Three default groups are seeded by Alembic and align with the three user types. """ from __future__ import annotations import enum class Permission(enum.StrEnum): """Application permissions. Codes are kebab-case + dotted scope.""" # Engagement ENGAGEMENT_CREATE = "engagement.create" ENGAGEMENT_READ = "engagement.read" ENGAGEMENT_READ_OWN = "engagement.read_own" # scoped to soc_session ENGAGEMENT_UPDATE = "engagement.update" ENGAGEMENT_DELETE = "engagement.delete" ENGAGEMENT_MEMBER_MANAGE = "engagement.member.manage" ENGAGEMENT_SOC_TOKEN_ISSUE = "engagement.soc_token.issue" # noqa: S105 # Hosts HOST_CRUD = "host.crud" # TTPs TTP_READ = "ttp.read" TTP_DRAFT = "ttp.draft" TTP_PROMOTE = "ttp.promote" # Imports IMPORT_JOURNAL = "import.journal" # Scenarios SCENARIO_CRUD = "scenario.crud" # Runs RUN_START = "run.start" RUN_CONTROL = "run.control" # Detection / evidence EVIDENCE_ADD = "evidence.add" DETECTION_ADD = "detection.add" # Cleanup CLEANUP_TRIGGER = "cleanup.trigger" # Reports REPORT_GENERATE = "report.generate" REPORT_READ = "report.read" # Audit AUDIT_READ = "audit.read" # User management (D-015): gates all /api/v1/users CRUD. rt_lead only. USER_MANAGE = "user.manage" ALL_PERMISSIONS: tuple[Permission, ...] = tuple(Permission) class GroupName(enum.StrEnum): RT_OPERATOR = "rt_operator" RT_LEAD = "rt_lead" SOC_ANALYST = "soc_analyst" # Source-of-truth mapping derived from spec §F11. Verified by tests against # the spec table. GROUP_PERMISSIONS: dict[GroupName, frozenset[Permission]] = { GroupName.RT_OPERATOR: frozenset( { Permission.ENGAGEMENT_CREATE, Permission.ENGAGEMENT_READ, Permission.HOST_CRUD, Permission.TTP_READ, Permission.TTP_DRAFT, Permission.IMPORT_JOURNAL, Permission.SCENARIO_CRUD, Permission.EVIDENCE_ADD, Permission.CLEANUP_TRIGGER, Permission.REPORT_READ, } ), GroupName.RT_LEAD: frozenset(ALL_PERMISSIONS), GroupName.SOC_ANALYST: frozenset( { Permission.ENGAGEMENT_READ_OWN, Permission.DETECTION_ADD, Permission.REPORT_READ, } ), } DEFAULT_GROUPS: tuple[GroupName, ...] = tuple(GroupName)