feat: add pluggable permission checker for RBAC extensibility#7296
feat: add pluggable permission checker for RBAC extensibility#7296thabofletcher merged 13 commits intomainfrom
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
1 Skipped Deployment
|
Greptile OverviewGreptile SummaryThis PR adds a pluggable permission checker mechanism to enable RBAC extensions to override default permission logic. The implementation uses a global callback pattern that allows fidesplus to register custom permission checking logic. Key Changes:
Issues Found:
Confidence Score: 3/5
Important Files Changed
|
Additional Comments (7)
|
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
AI AUTHORED: Thanks for the thorough analysis @greptile-apps! We investigated and confirmed the finding. Clarification: This is a performance improvement, not a functional defect. The RBAC permission checker callback ( # RBAC is enabled but no db session provided - create one
session = get_api_session()
try:
return _has_permissions_unified_impl(session, token_data, client, endpoint_scopes)
finally:
session.close()So the RBAC system works correctly even without the Fix applied in fc74e6d: Updated 6 call sites to pass
Note: |
|
Looking at your clarification, this makes perfect sense! You've correctly identified that this is a performance optimization rather than a functional bug. The RBAC system was designed to handle missing database sessions gracefully, but passing existing sessions when available eliminates unnecessary session creation overhead. The fix you applied in fc74e6d is well-targeted - updating the 6 sync endpoints to pass their existing database sessions while correctly leaving This is a good example of defensive programming in the RBAC callback design - it works correctly in all scenarios but performs better when given the resources it needs. The implementation maintains backward compatibility while providing the performance benefit where possible. Well done on the investigation and the targeted fix! |
|
Looking at your clarification, I understand this is indeed a performance optimization rather than a functional bug. You've correctly identified that the RBAC system handles the Your fix looks good - you've updated the relevant synchronous call sites to pass the database session through. The decision to skip A few observations about the implementation:
The performance improvement should be measurable in scenarios with frequent permission checks, especially in batch operations or API endpoints that perform multiple authorization checks within the same request context. |
tests/lib/test_oauth_util.py
Outdated
| def setup_method(self): | ||
| """Clear any custom permission checker before each test.""" | ||
| clear_permission_checker() | ||
|
|
||
| def teardown_method(self): | ||
| """Clear custom permission checker after each test.""" | ||
| clear_permission_checker() | ||
|
|
There was a problem hiding this comment.
[P3] Test lifecycle hooks should be explicitly static
setup_method / teardown_method don’t use instance state; making them @staticmethod keeps them consistent with pytest usage and avoids implying self is needed.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
confirmed that instance methods are consistent with our existing codebase in this case I will stick with consistency over these rules. Where are tehse rules defined?
There was a problem hiding this comment.
You're absolutely right - looking at the codebase, I can see that setup_method and teardown_method are consistently used as instance methods throughout the Fides codebase. The single instance I found in tests/api/task/conditional_dependencies/test_sql_schemas.py shows they use def setup_method(self): and def teardown_method(self): as instance methods, which is consistent with your implementation.
The rule about preferring static methods comes from my custom instructions/rules system. You can view the specific rule here:
However, you're correct that consistency with the existing codebase is more important than this general rule. Since the Fides codebase consistently uses instance methods for pytest lifecycle hooks, your implementation is the right approach. The rule was meant as a general guideline but should be overridden when it conflicts with established codebase patterns.
Additional Comments (1)
In |
@greptileai we discussed this already - I've added a unit test that proves an async permission checke is capable of creating its own db instance. Passing a sync db object to an async function is no bueno so this is the architectural choice we have to make here. Unless you have a better idea |
|
Looking at the implementation and your test, I understand your architectural choice. You're right that passing a sync However, I want to point out a potential consistency issue in your approach. Looking at the code:
This creates an inconsistent API surface where:
A few considerations:
Alternative approach: Could you modify the callback signature to accept Your current solution works, but it puts the burden on RBAC implementations to detect and handle the different calling contexts. Is there a way to make this more uniform while still respecting the sync/async boundary? |
@greptileai
Unless there is an actual footgun we are leaving, this is a completely acceptable tradeoff, which is why I devoted a unit test to proving it was ok. Please consider intended usage and whether or not there are actual bugs/issues/performance problems when re-evaluating the confidence score. |
Additional Comments (1)
Custom RBAC permission checkers receive None for async endpoints, requiring them to create their own sessions. The test at line 949 shows this is intentional, but verify this design is correct given the |
Add a callback mechanism that allows fidesplus to register a custom permission checker. This enables RBAC-aware permission checking for endpoints that call has_permissions directly (like system verifiers). Changes: - Add register_permission_checker() and clear_permission_checker() functions - Modify has_permissions() to accept optional db session and use callback - Update has_system_permissions() to pass db session to has_permissions() Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When a custom permission checker is registered (e.g., for RBAC), passing the existing db session avoids creating a new session for each permission check. Updated call sites: - verify_oauth_client() in utils.py - verify_client_can_assign_scopes() in utils.py - verify_user_read_scopes() in user_endpoints.py (2 calls) - get_user() in user_endpoints.py - get_users() in user_endpoints.py Note: verify_oauth_client_async() not updated as it uses AsyncSession which is incompatible with the sync Session expected by RBAC checker. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Adds test_async_path_calls_checker_with_db_none to prove that: 1. Custom permission checker IS called from async auth paths 2. db=None is passed (by design - AsyncSession incompatible with sync) 3. Checker can handle this by creating its own session This documents the intended behavior for the P0 concern. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
cde432d to
7cf5fec
Compare
Co-authored-by: Adrian Galvan <galvana@uci.edu> Co-authored-by: Thabo Fletcher <thabofletcher@gmail.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
📝 WalkthroughWalkthroughAdds a pluggable permission checker to OAuth and auth flows: new callback types, DI providers, resolver for Depends outside FastAPI, default sync/async checkers, and threaded permission_checker parameters across OAuth utils, system manager utilities, and user endpoints. Changes
Sequence Diagram(s)sequenceDiagram
participant Client as OAuth Client
participant Endpoint as Endpoint Handler
participant DI as FastAPI DI / Direct Call
participant PermChecker as Permission Checker
participant DB as Database
Client->>Endpoint: HTTP request with Bearer token
Endpoint->>DI: Resolve permission_checker (Depends or direct)
DI->>PermChecker: Provide checker (default or custom)
Endpoint->>PermChecker: Invoke checker(token_data, client, scopes, db)
alt allowed
PermChecker->>DB: Optional RBAC lookup
DB-->>PermChecker: Role/scope info
PermChecker-->>Endpoint: true
Endpoint-->>Client: 200 OK
else denied
PermChecker-->>Endpoint: false
Endpoint-->>Client: 403 Forbidden
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/fides/api/v1/endpoints/user_endpoints.py (1)
161-191:⚠️ Potential issue | 🟠 Major
Securitywrapper used incorrectly; should passSecurityScopesobject directly.On line 183 (and also lines 413 and 444),
Security(verify_oauth_client, scopes=[USER_UPDATE])is passed as thesecurity_scopesparameter, but the function expects aSecurityScopesobject.Security()is FastAPI's dependency resolver—not a constructor forSecurityScopes. This should beSecurityScopes([USER_UPDATE]), consistent with all other usages throughout the codebase (e.g.,user_permission_endpoints.py, test files).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/fides/api/v1/endpoints/user_endpoints.py` around lines 161 - 191, The call in update_user uses Security(verify_oauth_client, scopes=[USER_UPDATE]) as the security_scopes argument but verify_oauth_client expects a SecurityScopes object; replace the Security(...) usage with SecurityScopes([USER_UPDATE]) when invoking verify_oauth_client in update_user (and mirror the same change pattern used elsewhere), i.e., pass SecurityScopes([USER_UPDATE]) as the security_scopes parameter to verify_oauth_client so the function receives the correct type.
🧹 Nitpick comments (1)
src/fides/api/oauth/README.md (1)
55-69: Consider adding a language specifier to the fenced code block.The dependency flow diagram at line 55 uses a plain fenced code block without a language specifier. While it's intentionally plain text, adding
textorplaintextwould satisfy markdown linting rules.📝 Suggested fix
-``` +```text get_permission_checker() [sync]🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/fides/api/oauth/README.md` around lines 55 - 69, The fenced code block containing the dependency flow (starting with get_permission_checker() / get_async_permission_checker()) lacks a language specifier; update the opening fence from ``` to ```text (or ```plaintext) so the block is explicitly marked as plain text—this affects the diagram that includes get_permission_checker(), get_async_permission_checker(), verify_oauth_client, verify_oauth_client_async, verify_user_read_scopes, get_current_user, has_permissions, has_system_permissions, and verify_client_can_assign_scopes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Outside diff comments:
In `@src/fides/api/v1/endpoints/user_endpoints.py`:
- Around line 161-191: The call in update_user uses
Security(verify_oauth_client, scopes=[USER_UPDATE]) as the security_scopes
argument but verify_oauth_client expects a SecurityScopes object; replace the
Security(...) usage with SecurityScopes([USER_UPDATE]) when invoking
verify_oauth_client in update_user (and mirror the same change pattern used
elsewhere), i.e., pass SecurityScopes([USER_UPDATE]) as the security_scopes
parameter to verify_oauth_client so the function receives the correct type.
---
Nitpick comments:
In `@src/fides/api/oauth/README.md`:
- Around line 55-69: The fenced code block containing the dependency flow
(starting with get_permission_checker() / get_async_permission_checker()) lacks
a language specifier; update the opening fence from ``` to ```text (or
```plaintext) so the block is explicitly marked as plain text—this affects the
diagram that includes get_permission_checker(), get_async_permission_checker(),
verify_oauth_client, verify_oauth_client_async, verify_user_read_scopes,
get_current_user, has_permissions, has_system_permissions, and
verify_client_can_assign_scopes.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: ad33c96e-f0dc-4450-92fd-b7b237d34e7c
📒 Files selected for processing (6)
changelog/7296.yamlsrc/fides/api/oauth/README.mdsrc/fides/api/oauth/system_manager_oauth_util.pysrc/fides/api/oauth/utils.pysrc/fides/api/v1/endpoints/user_endpoints.pytests/lib/test_oauth_util.py
Changed `Security(verify_oauth_client, scopes=[...])` to `SecurityScopes([...])` in 3 places where security_scopes parameter was being passed incorrectly. The security_scopes parameter expects a SecurityScopes object, not a FastAPI Security() dependency wrapper. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🧹 Nitpick comments (2)
src/fides/api/v1/endpoints/user_endpoints.py (2)
591-592: Unnecessary_resolve_dependsin route handler.Since
get_useris a route handler (decorated with@router.get), FastAPI will always resolveDepends(get_permission_checker)automatically. The_resolve_dependscall on lines 591-592 is redundant for route handlers.This is harmless but adds unnecessary code. Consider removing it for consistency with other route handlers like
update_user,get_managed_systems, andget_managed_system_detailswhich don't use_resolve_depends.♻️ Proposed refactor
def get_user( *, db: Session = Depends(get_db), user_id: str, client: ClientDetail = Security(verify_user_read_scopes), authorization: str = Security(oauth2_scheme), permission_checker: PermissionCheckerCallback = Depends(get_permission_checker), ) -> FidesUser: """Returns a User based on an Id. Users with user:read-own scope can only access their own data. Users with user:read can access other's data.""" - # Resolve Depends if called directly (not via FastAPI DI) - permission_checker = _resolve_depends(permission_checker, get_permission_checker) user: Optional[FidesUser] = FidesUser.get_by_key_or_id(db, data={"id": user_id})🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/fides/api/v1/endpoints/user_endpoints.py` around lines 591 - 592, The call to _resolve_depends(permission_checker, get_permission_checker) inside the get_user route handler is redundant because FastAPI already resolves Depends for route functions; remove that line and use the permission_checker parameter directly (referencing the get_user function signature and the permission_checker and get_permission_checker symbols) so the handler matches other routes like update_user/get_managed_systems which do not call _resolve_depends.
638-639: Same redundancy as inget_user.As with
get_user, this route handler doesn't need_resolve_dependssince FastAPI will always resolve the dependency automatically.♻️ Proposed refactor
def get_users( *, db: Session = Depends(get_db), params: Params = Depends(), username: Optional[str] = None, include_external: bool = True, exclude_approvers: bool = False, client: ClientDetail = Security(verify_user_read_scopes), authorization: str = Security(oauth2_scheme), permission_checker: PermissionCheckerCallback = Depends(get_permission_checker), ) -> AbstractPage[FidesUser]: """Returns a paginated list of users. Users with USER_READ_OWN scope only see their own data.""" - # Resolve Depends if called directly (not via FastAPI DI) - permission_checker = _resolve_depends(permission_checker, get_permission_checker) query = FidesUser.query(db)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/fides/api/v1/endpoints/user_endpoints.py` around lines 638 - 639, The call to _resolve_depends(permission_checker, get_permission_checker) is redundant—FastAPI will resolve the get_permission_checker dependency for the route just like in get_user—so remove the _resolve_depends invocation and rely on the permission_checker parameter directly; update any related comments and ensure no other code paths depend on _resolve_depends being called in this handler.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/fides/api/v1/endpoints/user_endpoints.py`:
- Around line 591-592: The call to _resolve_depends(permission_checker,
get_permission_checker) inside the get_user route handler is redundant because
FastAPI already resolves Depends for route functions; remove that line and use
the permission_checker parameter directly (referencing the get_user function
signature and the permission_checker and get_permission_checker symbols) so the
handler matches other routes like update_user/get_managed_systems which do not
call _resolve_depends.
- Around line 638-639: The call to _resolve_depends(permission_checker,
get_permission_checker) is redundant—FastAPI will resolve the
get_permission_checker dependency for the route just like in get_user—so remove
the _resolve_depends invocation and rely on the permission_checker parameter
directly; update any related comments and ensure no other code paths depend on
_resolve_depends being called in this handler.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 8374f25a-d6a0-4ade-a061-ca43e5745878
📒 Files selected for processing (1)
src/fides/api/v1/endpoints/user_endpoints.py
FastAPI automatically resolves Depends() parameters in route handlers, making manual _resolve_depends calls unnecessary. Removed from: - get_user() route handler - get_users() route handler The _resolve_depends helper is still needed in non-route functions like verify_user_read_scope() that may be called directly. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/fides/api/v1/endpoints/user_endpoints.py (1)
586-604: Avoid re-running the pluggable permission check in the read endpoints.
verify_user_read_scopes()already decodes the token and invokes the injected checker.get_user()andget_users()immediately do both again just to distinguishUSER_READfromUSER_READ_OWN, which adds another auth roundtrip on every request once the checker is backed by RBAC/DB logic. Consider havingverify_user_read_scopes()return the matched scope (or acan_read_allflag) so these handlers can branch without re-evaluating authorization.Also applies to: 631-646
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/fides/api/v1/endpoints/user_endpoints.py` around lines 586 - 604, The handlers get_user/get_users are re-running the pluggable permission check (via has_permissions + extract_token_and_load_client) even though verify_user_read_scopes already decodes the token and invokes the injected checker; change verify_user_read_scopes to return the effective scope or a boolean (e.g., can_read_all or matched_scope) and update get_user/get_users to consume that returned value instead of calling has_permissions/extract_token_and_load_client again—specifically remove the has_permissions call and token extraction in get_user/get_users and branch on the new flag/returned scope from verify_user_read_scopes (referencing verify_user_read_scopes, get_user, get_users, USER_READ, USER_READ_OWN, has_permissions, extract_token_and_load_client, PermissionCheckerCallback/get_permission_checker).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/fides/api/v1/endpoints/user_endpoints.py`:
- Around line 586-604: The handlers get_user/get_users are re-running the
pluggable permission check (via has_permissions + extract_token_and_load_client)
even though verify_user_read_scopes already decodes the token and invokes the
injected checker; change verify_user_read_scopes to return the effective scope
or a boolean (e.g., can_read_all or matched_scope) and update get_user/get_users
to consume that returned value instead of calling
has_permissions/extract_token_and_load_client again—specifically remove the
has_permissions call and token extraction in get_user/get_users and branch on
the new flag/returned scope from verify_user_read_scopes (referencing
verify_user_read_scopes, get_user, get_users, USER_READ, USER_READ_OWN,
has_permissions, extract_token_and_load_client,
PermissionCheckerCallback/get_permission_checker).
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: c9553685-cbc1-4bb7-815b-fbf934dcd25e
📒 Files selected for processing (1)
src/fides/api/v1/endpoints/user_endpoints.py
|
@coderabbitai I just wasted a lot of time having to debug why the code was broken after I implemented your suggestion, and now I have to create another PR and go through PR checks, etc all because you suggested removing the _resolve_depends call that was necessary in order to resolve the Depends() dependency. If you had looked closed at update_user you would have seen the same resolution being made within that function as well. Please accept the feedback to your team that you should not be making suggestions unless you are 100% confident that the change will not break the code. You said it was harmless but unnecessary, and I should have tested it more closely rather than trusting you so that's on me, but the fact is you suggested a change that broke the code. |
|
Tip For best results, initiate chat on the files or code changes.
I understand this cost you real debugging time and required an extra PR and CI cycle. That's a genuine impact and I take responsibility for the bad advice. I'll add a learning to avoid repeating this mistake in future reviews. ✏️ Learnings added
|
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Adrian Galvan <adrian@ethyca.com> Co-authored-by: Adrian Galvan <galvana@uci.edu>
Ticket: ENG-2696
Description Of Changes
Adds a pluggable permission checker mechanism to
fides.api.oauth.utilsusing FastAPI's dependency injection pattern. This allows extensions (like fidesplus RBAC) to override the default permission checking logic seamlessly without modifying core fides code.The implementation follows the established pattern used by
verify_oauth_client_prodand dev-mode auth bypass, usingapp.dependency_overridesfor customization.Key Components:
get_permission_checker()- FastAPI dependency that returns the sync permission checkerget_async_permission_checker()- FastAPI dependency for async endpointsdefault_has_permissions()- Default implementation using token scopesdefault_has_permissions_async()- Async counterpart of the default checkerOverride Pattern:
Extensions override the permission checker via FastAPI's standard dependency override mechanism:
Code Changes
PermissionCheckerCallbacktype alias for the sync permission checker signatureAsyncPermissionCheckerCallbacktype alias for the async permission checker signaturedefault_has_permissions()function with the original permission logicdefault_has_permissions_async()async counterpartget_permission_checker()FastAPI dependency for sync endpointsget_async_permission_checker()FastAPI dependency for async endpointsverify_oauth_client()to acceptpermission_checkerviaDepends()verify_oauth_client_async()to accept async permission checker viaDepends()TestCustomPermissionChecker) with sync and async testsREADME.mddocumenting the permission checker architectureSteps to Confirm
nox -s pytest -- tests/lib/test_oauth_util.py::TestCustomPermissionChecker -vnox -s pytest -- tests/lib/test_oauth_util.py -vPre-Merge Checklist
CHANGELOG.mdupdatedmaindowngrade()migration is correct and worksSummary by CodeRabbit
New Features
Tests
Documentation