feat: wire in penguin-aaa (RBAC, tenant + audit middleware)#137
feat: wire in penguin-aaa (RBAC, tenant + audit middleware)#137PenguinzTech wants to merge 1 commit into
Conversation
Replaces the _check_user_permission / _check_org_permission stubs with real enforcement via penguin-aaa's RBACEnforcer. Adds ASGI middleware wrapping (TenantMiddleware + AuditMiddleware) around the Quart app, and a before_request bridge that populates g.claims from Elder's JWT so @require_scope / @require_role decorators work without an extra DB call. Key changes: - apps/api/auth/rbac.py (new): RBACEnforcer singleton with observer / editor / admin roles; PERMISSION_SCOPE_MAP bridging legacy names; check_permission() / check_org_permission() / role_scopes() - apps/api/auth/jwt_handler.py: generate_token() now embeds tenant, roles, and scope in the payload for penguin-aaa compatibility - apps/api/auth/decorators.py: stubs replaced with rbac.check_permission; adds @require_scope and @require_role Quart-native decorators - apps/api/main.py: create_asgi_app() wraps Quart with TenantMiddleware (required=False) and AuditMiddleware(StdoutSink); before_request bridge populates g.claims; __main__ updated to use create_asgi_app() - requirements.in / requirements.txt: add penguin-aaa==0.1.0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reviewer's GuideIntroduces penguin-aaa–based RBAC, tenant, and audit integration by wiring an RBACEnforcer singleton, enriching JWTs with role/scope/tenant claims, adding scope/role decorators that read JWT-derived claims from g, and wrapping the Quart app with TenantMiddleware and AuditMiddleware for ASGI deployment, while keeping existing blueprints and DB-based login/resource_role checks intact. Sequence diagram for JWT claims and RBAC-aware request handlingsequenceDiagram
actor User
participant Uvicorn
participant AuditMiddleware
participant TenantMiddleware
participant QuartApp
participant BeforeRequest
participant Decorators
User->>Uvicorn: HTTP request with Authorization header
Uvicorn->>AuditMiddleware: ASGI call
AuditMiddleware->>TenantMiddleware: ASGI call
TenantMiddleware->>QuartApp: ASGI call
QuartApp->>BeforeRequest: populate_claims()
BeforeRequest->>BeforeRequest: get_token_from_header()
BeforeRequest->>BeforeRequest: verify_token(token)
BeforeRequest->>BeforeRequest: g.claims = {sub, tenant, roles, scope}
BeforeRequest-->>QuartApp: continue
QuartApp->>Decorators: login_required()
Decorators->>Decorators: get_current_user()
QuartApp->>Decorators: require_scope("secrets:read")
Decorators->>Decorators: read g.claims["scope"]
Decorators-->>QuartApp: 403 if scope missing
QuartApp-->>User: HTTP response
File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 1 issue, and left some high level feedback:
- The new
require_scope/require_roledecorators useinspect.iscoroutinefunctionbutinspectis not imported inapps/api/auth/decorators.py, which will raise aNameErrorat runtime. - The
portal_rolehandling is now duplicated and slightly divergent betweenjwt_handler.generate_token,rbac.check_permission, andrbac.role_scopes; consider centralizing the defaulting/validation of role names in one helper to avoid future drift.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The new `require_scope`/`require_role` decorators use `inspect.iscoroutinefunction` but `inspect` is not imported in `apps/api/auth/decorators.py`, which will raise a `NameError` at runtime.
- The `portal_role` handling is now duplicated and slightly divergent between `jwt_handler.generate_token`, `rbac.check_permission`, and `rbac.role_scopes`; consider centralizing the defaulting/validation of role names in one helper to avoid future drift.
## Individual Comments
### Comment 1
<location path="apps/api/auth/rbac.py" line_range="75-84" />
<code_context>
+PERMISSION_SCOPE_MAP: dict[str, str] = {
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Unknown permission names silently returning False may make debugging harder.
In `check_permission`, unknown `permission_name` values (not in `PERMISSION_SCOPE_MAP`) cause a silent `False` result. While safe for security, this hides configuration or typo bugs and makes them hard to trace. Consider logging (e.g., debug/warning) when `scope is None`, or raising in non-prod, so misconfigured or missing permissions are surfaced early.
Suggested implementation:
```python
def check_permission(user: User, permission_name: str) -> bool:
"""Check an old-style permission name against the RBAC scopes.
Unknown permission names are treated as configuration/typo errors and logged
so they can be found early, while still returning False for safety.
"""
scope = PERMISSION_SCOPE_MAP.get(permission_name)
if scope is None:
# Unknown permission name: surface this as a misconfiguration instead
# of failing silently.
logger.warning(
"Unknown permission_name '%s' passed to check_permission; "
"no matching scope found in PERMISSION_SCOPE_MAP",
permission_name,
)
# In non-production environments, fail fast to catch configuration bugs.
if getattr(settings, "DEBUG", False):
raise ValueError(
f"Unknown permission_name '{permission_name}' passed to check_permission"
)
return False
return enforcer.check(user, scope)
```
I assumed the existence of a module-level `logger` and a `settings` object (e.g., Django-style). To wire this up fully you should:
1. At the top of `apps/api/auth/rbac.py`, ensure you have:
- `import logging`
- `from django.conf import settings` (or your project’s equivalent settings import)
- `logger = logging.getLogger(__name__)`
2. If your project uses a different logging/settings pattern, adjust `logger` and `settings.DEBUG` accordingly (e.g., `from apps.core import settings` or `from config import settings`).
3. Optionally, if you prefer a different non-prod toggle than `settings.DEBUG`, replace that condition with your environment check (e.g., `not settings.IS_PROD`).
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| PERMISSION_SCOPE_MAP: dict[str, str] = { | ||
| "manage_users": "users:write", | ||
| "view_users": "users:read", | ||
| "create_entity": "entities:write", | ||
| "edit_entity": "entities:write", | ||
| "delete_entity": "entities:delete", | ||
| "view_entity": "entities:read", | ||
| "create_organization": "organizations:write", | ||
| "edit_organization": "organizations:write", | ||
| "delete_organization": "organizations:delete", |
There was a problem hiding this comment.
suggestion (bug_risk): Unknown permission names silently returning False may make debugging harder.
In check_permission, unknown permission_name values (not in PERMISSION_SCOPE_MAP) cause a silent False result. While safe for security, this hides configuration or typo bugs and makes them hard to trace. Consider logging (e.g., debug/warning) when scope is None, or raising in non-prod, so misconfigured or missing permissions are surfaced early.
Suggested implementation:
def check_permission(user: User, permission_name: str) -> bool:
"""Check an old-style permission name against the RBAC scopes.
Unknown permission names are treated as configuration/typo errors and logged
so they can be found early, while still returning False for safety.
"""
scope = PERMISSION_SCOPE_MAP.get(permission_name)
if scope is None:
# Unknown permission name: surface this as a misconfiguration instead
# of failing silently.
logger.warning(
"Unknown permission_name '%s' passed to check_permission; "
"no matching scope found in PERMISSION_SCOPE_MAP",
permission_name,
)
# In non-production environments, fail fast to catch configuration bugs.
if getattr(settings, "DEBUG", False):
raise ValueError(
f"Unknown permission_name '{permission_name}' passed to check_permission"
)
return False
return enforcer.check(user, scope)I assumed the existence of a module-level logger and a settings object (e.g., Django-style). To wire this up fully you should:
- At the top of
apps/api/auth/rbac.py, ensure you have:import loggingfrom django.conf import settings(or your project’s equivalent settings import)logger = logging.getLogger(__name__)
- If your project uses a different logging/settings pattern, adjust
loggerandsettings.DEBUGaccordingly (e.g.,from apps.core import settingsorfrom config import settings). - Optionally, if you prefer a different non-prod toggle than
settings.DEBUG, replace that condition with your environment check (e.g.,not settings.IS_PROD).
CI SummaryJob Results
Branch: 137/merge | Commit: 48a3d5f | Run: #710 |
Summary
apps/api/auth/rbac.py(new):RBACEnforcersingleton withobserver/editor/adminroles defined inresource:actionscope format.PERMISSION_SCOPE_MAPbridges legacy permission strings (manage_users,view_users, etc.) to scopes.check_permission()replaces the_check_user_permissionstub that previously returnedTruefor everyone.apps/api/auth/jwt_handler.py:generate_token()now embedstenant,roles, andscopein the JWT payload — existing tokens without these fields still work (they just won't carry scope/role info until re-issued).apps/api/auth/decorators.py: stubs replaced with realenforcer.has_scope()calls; adds@require_scopeand@require_roleQuart-native decorators that readg.claims(no extra DB round-trip).apps/api/main.py:create_asgi_app()wraps Quart withTenantMiddleware(required=False)thenAuditMiddleware(StdoutSink). Abefore_requesthook populatesg.claimsfrom the decoded JWT so decorators work without touching the DB.requirements.in/requirements.txt: addspenguin-aaa==0.1.0.ASGI middleware stack
TenantMiddlewareruns in non-required mode because Elder's own JWTs now carrytenantonly after fresh login — older tokens without it still pass through. Switch torequired=Trueonce all tokens are re-issued.What's NOT changed
@login_requiredbehavior — still validates against the DB@resource_role_required— still queriesresource_rolestable (unchanged)Test plan
POST /api/v1/auth/login→ returned token now containstenant,roles,scopefieldsGET /api/v1/identitieswithobserverrole token → 403 (nousers:readscope... wait, observer hasusers:read— usemanage_usersendpoint) → test with non-admin identity hits@permission_required("manage_users")→ 403is_superuser=True→ all permissions pass regardless of role🤖 Generated with Claude Code
Summary by Sourcery
Integrate role- and scope-based authorization and tenant/audit middleware into the API, wiring JWT claims into request context for RBAC-aware access control.
New Features:
Enhancements: