feat(backend): add local auth + group-based RBAC matching F11 (B0.6)
- Permission enum + GroupName enum + GROUP_PERMISSIONS mapping mirror the F11 matrix in code (verifiable against the spec table in tests). - @require_perm decorator: 401 on anonymous, 403 on missing permission, passes through otherwise. Pure-function user_has() for unit-testing. - AuthUser (Flask-Login wrapper) resolves the permission set from a User's groups; load_user is the Flask-Login user_loader. - bcrypt password hashing helpers (12 rounds by default, configurable). - SOC opaque token (D-006): secrets.token_urlsafe(32), bcrypt-hashed at rest, plain value returned once at creation and never re-displayable. - Group-based RBAC from day one (D-003) — Keycloak OIDC in v2 maps onto the same group model.
This commit is contained in:
95
backend/src/mimic/rbac/matrix.py
Normal file
95
backend/src/mimic/rbac/matrix.py
Normal file
@@ -0,0 +1,95 @@
|
||||
"""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"
|
||||
|
||||
|
||||
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)
|
||||
Reference in New Issue
Block a user