Adds `Permission.USER_MANAGE = "user.manage"` to the F11 matrix. rt_lead already holds ALL_PERMISSIONS so GROUP_PERMISSIONS is unchanged — rt_lead gets the new permission automatically, rt_operator and soc_analyst get 403. Alembic migration `202605230001_add_user_manage_permission`: - inserts the `user.manage` row into `permission`, - inserts the `(rt_lead, user.manage)` link into `group_permission`, - exposes `_DELTA_PERMISSIONS` / `_DELTA_GROUP_PERMISSIONS` for parity tests. The previous `test_frozen_*_matches_runtime` invariant (MA3) is generalised to "runtime = initial frozen ∪ deltas of every migration in `_DELTAS`". New migrations register themselves there without editing the historical one. Verbatim wording from spec-analyst is recorded as D-015 in `tasks/spec-decisions.md` (separate commit).
99 lines
2.5 KiB
Python
99 lines
2.5 KiB
Python
"""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)
|