feat: automatic resource pool viewer access via OAuth2 provider linkage#1315
Merged
SalimKayal merged 3 commits intoJun 11, 2026
Merged
Conversation
This was referenced May 19, 2026
Merged
1141074 to
52fa3bd
Compare
Coverage Report for CI Build 27291758994Warning Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes. Coverage at 86.336% (no base build to compare)Details
Uncovered Changes
Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
52fa3bd to
2ac8da5
Compare
c98413c to
79b9eb6
Compare
79b9eb6 to
48540be
Compare
Collaborator
Author
|
Behavior is verifiable on staging. |
…ct (#1317) * feat: sync RP access on OAuth connect/disconnect 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. * feat: trigger RP sync in OAuth flows 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: wire member_repo and fix types - 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 * chore: logging * fix: ensure session deletion, remove repetition * chore: public naming for on_oauth2* * refactor: remove need for `| None` annotation * refactor: apply changes in rp memberships in one go * feat: transmit sessions * fix: one session per authz op * fix: remove useless session
…OAuth2 provider linkage (#1314) * chore: feature branch * feat: inject MemberRepository into ResourcePoolRepository + add failing tests for RP remote provider sync - Add member_repo parameter to ResourcePoolRepository constructor - Wire member_repo in dependencies.py and test/utils.py - Add TDD tests for auto-grant/revoke on RP CRUD operations * feat: add helper to query connected users by provider_id Adds _get_connected_user_ids to ResourcePoolRepository for fetching users with a connected OAuth2 link to a given provider. * feat: auto-sync viewers on RP remote provider update When updating a resource pool's remote provider: - Capture old provider_id before the transaction - After the transaction, revoke connected users of the old provider - Grant connected users of the new provider - Log warnings on soft failure * test: fix RP update tests by adding required default class * test: fix sentinel value error by inserting connections in separate sessions * style: format and sort imports in test_sqlalchemy_pool_repo.py * fix: validate provider on RP update and handle stale users in sync - Add OAuth2ClientORM existence check when updating resource pool remote provider_id, matching the validation already done on insert. - Remove pre-transaction query for old_provider_id to eliminate race condition; capture it from the already-loaded ORM inside the tx. - Grant/revoke auto-synced memberships individually instead of bulk, so one missing Keycloak user does not abort the whole operation. * test: add regression tests for RP remote provider edge cases - Test that updating a resource pool to a non-existent provider_id fails. - Test that insert/update auto-grants skip users missing from Keycloak instead of failing the entire batch. - Test that swapping remote providers skips stale connected users. * squashme: formatting * fix: use authz in tests after rebase * refactor: batch authz ops * refactor: remove duplicate session * fix: allow missing for grant members on resource pools * feat: sync resource pool viewer access on OAuth2 connect and disconnect (#1317) * feat: sync RP access on OAuth connect/disconnect 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. * feat: trigger RP sync in OAuth flows 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: wire member_repo and fix types - 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 * chore: logging * fix: ensure session deletion, remove repetition * chore: public naming for on_oauth2* * refactor: remove need for `| None` annotation * refactor: apply changes in rp memberships in one go * feat: transmit sessions * fix: one session per authz op * fix: remove useless session * chore: feature branch * refactor: use update_membership for clarity * chore: raise ValidationError instead of MissingResourceError * fix: reuse sessions when possible * chore: add TODO for session reuse in AUTHZ
* test: add RP remote provider end-to-end tests * test: add OAuth callback RP access end-to-end tests * chore: remove useless test
48540be to
3efd2a7
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
Automatically authorize individual users as
vieweron resource pools when they connect to a matching OAuth2 provider, without manual admin intervention and without hidden groups.This closes the loop between
connected_services(OAuth2 lifecycle) andcrc(resource pools) by making the OAuth2 connection the source of truth for resource pool access.Motivation & Context
Resource pools already support polymorphic members (users, groups, projects) via Authzed/SpiceDB, and
connected_servicesalready supports HPC integrations (FirecREST, Run:AI) through OAuth2. However, granting access to a resource pool tied to an OAuth provider previously required manual admin intervention.This PR implements automatic authorization in both directions:
remote.provider_id, all currently-connected users are grantedvieweraccess. When the provider is changed or removed, old access is revoked.vieweron all existing resource pools linked to that provider. When they disconnect, that access is revoked.Key Design Decisions
No Database Migrations
We rely entirely on existing schema:
ResourcePoolORM.remote_provider_idalready links a resource pool to an OAuth2 client.OAuth2ConnectionORMalready tracksuser_id,client_id, andstatus(connected|pending).relation viewer: useronresource_pool.Direct User Authorization (No Hidden Groups)
Rather than creating synthetic/hidden groups, we authorize users directly via the
viewerrelation in SpiceDB. This is simpler and more transparent.A known consequence: manual admin
viewergrants and auto-grants are indistinguishable in SpiceDB. Disconnecting a provider unconditionally revokesviewerfor its connected users — this is accepted because the OAuth connection is the source of truth.Validation Before Mutation
Before inserting or updating a resource pool with a
provider_id, we validate that the referenced OAuth2 client exists. If it doesn't, we raiseMissingResourceError. Previously, arbitrary provider IDs were silently accepted.Best-Effort Membership Sync with Soft Failure
Membership sync happens outside the main DB transaction (because
MemberRepositoryoperations write to SpiceDB via their own session). If a bulk grant fails (e.g., a connected user is missing from Keycloak), the resource pool DB row has already been committed. We handle this by syncing users individually withtry/exceptand warning logs, ensuring one bad user record does not block access for everyone else.Event-Driven Sync at OAuth Lifecycle Boundaries
We hook into 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)Both calls are wrapped in
try/exceptso that the primary OAuth operation never fails because of an Authz issue.Changes
components/renku_data_services/crc/db.pyResourcePoolRepository.__init__now accepts an optionalmember_repo: MemberRepository._get_connected_user_ids(provider_id)to queryOAuth2ConnectionORMfor users withstatus == connected.insert_resource_pool: validates provider existence, then auto-grants connected users after successful insert.update_resource_pool: captures old provider, validates new provider, then syncs memberships (revoke old → grant new) after the transaction.delete_resource_pool: no changes needed — Authz cascade already cleans up all relations.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.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.py&orm.pyOAuth2Connectiondataclass and ORMdump()now exposeuser_idandclient_id. Previously onlyprovider_idwas exposed.bases/renku_data_services/data_api/dependencies.py&test/utils.pymember_repointo bothResourcePoolRepositoryandConnectedServicesRepositoryconstruction.Tests
test/components/renku_data_services/db/test_sqlalchemy_pool_repo.pytest/components/renku_data_services/connected_services/test_db.pytest/bases/renku_data_services/data_api/test_resource_pools.pytest_post_resource_pool_with_remote_grants_connected_users,test_delete_resource_pool_removes_access,test_patch_resource_pool_remote_change_swaps_access.test/bases/renku_data_services/data_api/test_connected_services.pytest_oauth_callback_adds_user_to_rp,test_delete_oauth_connection_removes_rp_access.Behavioral Changes (Notable)
Stricter validation of
remote.provider_idPOST /api/data/resource_poolsandPATCH /api/data/resource_pools/{id}now validate that the referencedprovider_idexists inoauth2_clients. Non-existent IDs now return a 404-style error instead of silent acceptance.OAuth2 callback now triggers resource pool access grants
After a user completes the OAuth2 callback (
/api/data/oauth2/callback), they automatically receivevieweraccess to all resource pools linked to that provider.OAuth2 disconnect now triggers resource pool access revocation
Deleting an OAuth2 connection (
DELETE /api/data/oauth2/connections/{id}) unconditionally revokesvieweraccess from all resource pools linked to that provider.OAuth2Connectionnow exposesuser_idandclient_idThis is an additive change — existing consumers are unaffected.
Risks & Mitigations
insert_resource_poolandupdate_resource_poolreturn from their DB transaction before callingmember_repobulk operations. If the bulk operation fails, the RP has already been committed. This is acceptable for MVP because SpiceDB is the secondary store; failing to sync memberships is a soft failure that can be retried or fixed by re-updating the RP.GET /resource_poolsdepend on Authz + DB consistency. The@Authz.authz_changedecorator writes to SpiceDB synchronously, so tests should be deterministic as long as they use the same fixture instances.PR Stack