feat: sync resource pool viewer access on OAuth2 connect and disconnect#1317
Open
SalimKayal wants to merge 5 commits into
Conversation
This was referenced May 20, 2026
Open
b50f189 to
7438590
Compare
Coverage Report for CI Build 26224451094Warning No base build found for commit Coverage: 86.41%Details
Uncovered Changes
Coverage RegressionsRequires a base build to compare against. How to fix this → Coverage Stats
💛 - Coveralls |
aac2112 to
ad7c1f8
Compare
7438590 to
3ffeb53
Compare
Add _on_oauth2_connected and _on_oauth2_disconnected helpers that auto-grant/revoke viewer access to resource pools linked to the provider. Wire member_repo through constructors. 5 new tests cover: connect grant, disconnect revoke, no-match no-op, multi-user grant, isolated disconnect.
Call _on_oauth2_connected after successful fetch_token in authorize callback. Call _on_oauth2_disconnected before deleting connection row. Add test verifying delete_oauth2_connection revokes RP access.
- Fix forward reference in dependencies.py by initializing connected_services_repo without member_repo and setting it later - Add user_id/client_id to OAuth2Connection model for type safety - Guard blueprint callback against None values - Remove debug logging from _on_oauth2_connected
3ffeb53 to
94b8d9f
Compare
ad7c1f8 to
0bb0524
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR implements the second half of automatic resource pool authorization: when a user successfully completes an OAuth2 authorization flow (connects to a provider), they are automatically granted
vieweraccess to all resource pools whoseremote.provider_idmatches that OAuth client. Conversely, when a user deletes their OAuth2 connection, that access is revoked.This complements #1314 to make the OAuth2 connection the source of truth for resource pool access.
Motivation & Context
Resource pools can be linked to OAuth2 providers via
remote.provider_id. In Chunk 1, we ensured that when an admin creates or updates such a resource pool, all currently-connected users are automatically granted access. However, if a user connects to the provider after the resource pool already exists, they would not receive access until the resource pool is modified.This PR closes that gap by hooking into the OAuth2 lifecycle events:
GET /api/data/oauth2/callback) — triggered after the token exchange succeeds.DELETE /api/data/oauth2/connections/{id}) — triggered when the user deletes their connection.Design Decisions & Rationale
1. Event-Driven Sync at OAuth Lifecycle Boundaries
We hook into two existing flows rather than adding new endpoints:
connected_services/blueprints.py::authorize_callback_on_oauth2_connected(user_id, provider_id)connected_services/db.py::delete_oauth2_connection_on_oauth2_disconnected(user_id, provider_id)This keeps the change localized and avoids polling or background jobs.
2. Best-Effort Sync — OAuth Flow Must Not Fail
Both the connect callback and disconnect operation are wrapped in
try/exceptwith warning logs:Rationale: The OAuth2 connection itself is the primary operation. If SpiceDB is temporarily unavailable or a user record is inconsistent, we still want the user to successfully connect to the provider. The RP access can be retried or fixed later by reconnecting.
3. Disconnect Double-Call Pattern (Transactional + Best-Effort)
In
delete_oauth2_connection,_on_oauth2_disconnectedis called twice:session.delete(conn)— inside the transaction, so that if revocation fails due to a transient error, the transaction rolls back and the connection is preserved (allowing retry).session.flush()— in a separatetry/except, as a best-effort cleanup in case the first call succeeded but we want to ensure the connection is deleted even if revocation fails.This ensures we do not leave orphaned connections when revocation fails, while still attempting to keep DB and Authz state consistent.
4. Cross-Component Dependency Injection
ConnectedServicesRepositorynow accepts an optionalmember_repo: MemberRepository | None. This is a cross-component import (connected_services→crc), which is already practiced elsewhere (e.g.,data_api/dependencies.pyconstructs both repositories). UsingTYPE_CHECKINGavoids circular import issues at runtime.5. Exposing
user_idandclient_idonOAuth2ConnectionThe
OAuth2Connectionmodel andOAuth2ConnectionORM.dump()now includeuser_idandclient_idfields. Previously, onlyprovider_id(which is the same asclient_id) was exposed. This allows theauthorize_callbackblueprint to access the connecting user's ID and the provider ID directly from thefetch_tokenresult without re-querying the database.Changes
components/renku_data_services/connected_services/db.pyConnectedServicesRepository.__init__now accepts an optionalmember_repo: MemberRepository._on_oauth2_connected(user_id, client_id): queries all RPs linked to the provider and grants the user asvieweron each._on_oauth2_disconnected(user_id, client_id): queries all RPs linked to the provider and revokes the user'svieweraccess from each.InternalServiceAdmin()for authz and loop through matching RPs with per-RPtry/except.delete_oauth2_connection: calls_on_oauth2_disconnectedbefore deletion and again after flush as best-effort.components/renku_data_services/connected_services/blueprints.pyauthorize_callback: after successfulfetch_token, extractsuser_idandprovider_idfromclient.connectionand calls_on_oauth2_connectedwith failure isolation.components/renku_data_services/connected_services/models.pyOAuth2Connectiondataclass addeduser_id: str | None = Noneandclient_id: str | None = None.components/renku_data_services/connected_services/orm.pyOAuth2ConnectionORM.dump()now includesuser_idandclient_idin the returned model.bases/renku_data_services/data_api/dependencies.py&test/utils.pymember_repointoConnectedServicesRepositoryconstruction.test/components/renku_data_services/connected_services/test_db.pytest_oauth_connect_adds_user_to_rptest_oauth_disconnect_removes_user_from_rptest_oauth_connect_with_no_matching_rp_does_nothingtest_two_users_connect_same_integration_both_get_accesstest_user_disconnect_only_affects_their_rp_accesstest_delete_connection_revokes_rp_accessBehavioral Changes (Notable)
OAuth2 Callback Now Triggers Resource Pool Access Grants
Previously: After a user completed the OAuth2 callback (
/api/data/oauth2/callback), they were connected to the provider but received no automatic resource pool access. Admins had to manually grantvieweron each relevant resource pool.Now: The callback automatically grants
vieweraccess to all resource pools linked to that provider. The user will see these resource pools immediately on their nextGET /api/data/resource_poolsrequest.OAuth2 Disconnect Now Triggers Resource Pool Access Revocation
Previously: Deleting an OAuth2 connection only removed the connection row. Any resource pool
viewergrants previously received via that connection remained in SpiceDB.Now: Disconnecting unconditionally revokes
vieweraccess from all resource pools linked to that provider. This is accepted because the OAuth connection is the source of truth (see Chunk 1 rationale).OAuth2ConnectionModel Now Exposesuser_idandclient_idPreviously:
OAuth2Connectiononly exposedid,provider_id,status, andnext_url.Now: It also exposes
user_idandclient_id. This is a additive change — existing consumers are unaffected. It enables the callback blueprint to pass the correct IDs to_on_oauth2_connectedwithout additional DB queries.Resilience: OAuth Flow Succeeds Even If RP Sync Fails
If SpiceDB or the membership repository is unavailable during the OAuth callback, the user will still successfully connect to the provider. A warning is logged and the RP access can be re-synced by reconnecting. This is a deliberate soft-failure strategy to avoid making the OAuth flow dependent on the authz subsystem.
Everything else is backward-compatible:
ConnectedServicesRepository.__init__addsmember_repo: MemberRepository | None = None— optional, defaults toNone.member_repois provided.PR Stack