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:
42
backend/src/mimic/rbac/decorators.py
Normal file
42
backend/src/mimic/rbac/decorators.py
Normal file
@@ -0,0 +1,42 @@
|
||||
"""`@require_perm` Flask decorator (group-based RBAC)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from functools import wraps
|
||||
from typing import TYPE_CHECKING, ParamSpec, TypeVar
|
||||
|
||||
from flask import abort
|
||||
from flask_login import current_user
|
||||
|
||||
from mimic.rbac.matrix import Permission
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
P = ParamSpec("P")
|
||||
R = TypeVar("R")
|
||||
|
||||
|
||||
def require_perm(perm: Permission) -> Callable[[Callable[P, R]], Callable[P, R]]:
|
||||
"""Reject the request with 401/403 unless the user holds `perm`."""
|
||||
|
||||
def _decorate(view: Callable[P, R]) -> Callable[P, R]:
|
||||
@wraps(view)
|
||||
def _wrapped(*args: P.args, **kwargs: P.kwargs) -> R:
|
||||
user = current_user
|
||||
if not getattr(user, "is_authenticated", False):
|
||||
abort(401)
|
||||
permissions: frozenset[Permission] = getattr(user, "permissions", frozenset())
|
||||
if perm not in permissions:
|
||||
abort(403)
|
||||
return view(*args, **kwargs)
|
||||
|
||||
return _wrapped # type: ignore[return-value]
|
||||
|
||||
return _decorate
|
||||
|
||||
|
||||
def user_has(perm: Permission, permissions: frozenset[Permission]) -> bool:
|
||||
"""Pure helper, easier to unit-test than the decorator."""
|
||||
return perm in permissions
|
||||
Reference in New Issue
Block a user