Skip to content

feat(oauth2): Map OAuth2 claims to Visible Server Groups - #10234

Open
beasteers wants to merge 2 commits into
pgadmin-org:masterfrom
beasteers:oauth-group-claims
Open

feat(oauth2): Map OAuth2 claims to Visible Server Groups#10234
beasteers wants to merge 2 commits into
pgadmin-org:masterfrom
beasteers:oauth-group-claims

Conversation

@beasteers

@beasteers beasteers commented Aug 5, 2026

Copy link
Copy Markdown

Summary

Control access to server groups using OAuth2 token claims, letting you create different levels of access tied to centralized SSO roles.

Fixes #9704

What changed

Two new per-provider config keys were added to OAUTH2_CONFIG in web/config.py:

  • OAUTH2_SERVER_GROUP_CLAIM — the claim (from the ID token or userinfo profile) whose values are treated as server group names.
  • OAUTH2_SERVER_GROUP_CLAIM_MAPPING — optional dict mapping a claim value → one server group name or a list of server group names, useful when the claim carries opaque codes (e.g. readonly) that don't match group names directly.

How it works

In web/pgadmin/authenticate/oauth2.py, the new _extract_server_group_claims() resolves the allowed groups during login and stores them in session['oauth2_server_group_claims']:

  • If the claim is not configured, the session key is cleared.
  • Claim values may be a single value or a list.
  • Mapping values may be a string or a list.

In web/pgadmin/utils/server_access.py, get_server_groups_for_user_query() now appends an additional OR condition matching ServerGroup.name against the resolved claim-based groups — for OAuth2 users only. This applies alongside the existing ownership and shared-server (Server.shared) conditions.

Tests

web/pgadmin/browser/tests/test_oauth2_with_mocking.py:

  • Two new providers: oidc-server-groups-direct (no mapping) and oidc-server-groups-mapped (with mapping).
  • Two new login-success cases asserting session['oauth2_server_group_claims'] is populated correctly.
  • The existing default assertions confirm the key is absent when the claim is not configured.

Backwards compatibility

Fully opt-in. When OAUTH2_SERVER_GROUP_CLAIM is not set, behavior is unchanged.

Summary by CodeRabbit

  • New Features

    • Added OAuth2/OIDC server-group visibility controls based on identity-token or profile claims.
    • Added optional mappings from claim values to server-group names.
    • OAuth2 users now see permitted owned, shared, and claim-based server groups.
    • Outdated group visibility data is cleared when filtering is disabled.
  • Documentation

    • Documented session storage, visibility behavior, and handling of unmapped claim values.
  • Tests

    • Added coverage for direct and mapped server-group claims during successful login.

Add OAUTH2_SERVER_GROUP_CLAIM and OAUTH2_SERVER_GROUP_CLAIM_MAPPING
provider config keys. When configured, claim values determine which
server groups a user can see, applied as an additional OR condition
alongside ownership and shared-server visibility.
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 78644436-df66-41e9-818e-afd94187f3ac

📥 Commits

Reviewing files that changed from the base of the PR and between c6088c5 and 19c0599.

📒 Files selected for processing (2)
  • web/pgadmin/authenticate/oauth2.py
  • web/pgadmin/browser/tests/test_oauth2_with_mocking.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • web/pgadmin/browser/tests/test_oauth2_with_mocking.py
  • web/pgadmin/authenticate/oauth2.py

Walkthrough

OAuth2 provider settings now support server-group claims and optional mappings. Login resolves groups from ID-token or userinfo claims and stores them in the session. Server-group queries include matching OAuth2 groups. Mocked OIDC tests cover direct and mapped claims.

Changes

OAuth2 Server-Group Visibility

Layer / File(s) Summary
Claim configuration and session resolution
web/config.py, web/pgadmin/authenticate/oauth2.py
Provider configuration defines the claim name and optional mapping. Login resolves unique groups from ID-token or userinfo claims and stores or clears the session value.
Server-group access filtering
web/pgadmin/utils/server_access.py
Server-group queries include owned groups, eligible shared-server groups, and groups listed in OAuth2 session claims.
OAuth2 server-group validation
web/pgadmin/browser/tests/test_oauth2_with_mocking.py
Mocked OIDC scenarios validate direct claims, mapped claims, provider configuration, and session state.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant OAuth2Provider
  participant OAuth2Login
  participant FlaskSession
  participant ServerGroupQuery
  OAuth2Provider->>OAuth2Login: Provide ID-token or userinfo claim
  OAuth2Login->>OAuth2Login: Resolve and map server groups
  OAuth2Login->>FlaskSession: Store oauth2_server_group_claims
  ServerGroupQuery->>FlaskSession: Read OAuth2 server groups
  ServerGroupQuery-->>OAuth2Login: Return accessible server groups
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the primary change: mapping OAuth2 claims to visible server groups.
Linked Issues check ✅ Passed The changes satisfy #9704 by mapping OAuth2/OIDC claims to server-group access with configuration, session handling, filtering, and tests.
Out of Scope Changes check ✅ Passed All changes support the linked issue by implementing configuration, claim resolution, access filtering, documentation, and focused tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web/pgadmin/authenticate/oauth2.py`:
- Around line 559-563: Update the needs_userinfo decision in get_user_profile()
to also fetch userinfo when OAUTH2_SERVER_GROUP_CLAIM is configured but absent
from the ID-token claims, even when the username claim is present. Ensure the
existing server-group resolution can use the fetched profile, and add a
regression test covering a server-group claim available only in userinfo.

In `@web/pgadmin/browser/tests/test_oauth2_with_mocking.py`:
- Around line 471-478: Update the session assertion in the OAuth2 test around
expected_server_groups to compare server-group contents without requiring
ordering, using assertCountEqual for the non-None case while preserving the
existing absence assertion for None.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: fc568543-fd85-472e-b433-0aa726934cd3

📥 Commits

Reviewing files that changed from the base of the PR and between 067f7af and c6088c5.

📒 Files selected for processing (4)
  • web/config.py
  • web/pgadmin/authenticate/oauth2.py
  • web/pgadmin/browser/tests/test_oauth2_with_mocking.py
  • web/pgadmin/utils/server_access.py

Comment thread web/pgadmin/authenticate/oauth2.py
Comment thread web/pgadmin/browser/tests/test_oauth2_with_mocking.py

@dpage dpage left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this, and for the linked issue: claim-driven server group access is something we get asked about fairly regularly, so the direction is welcome. The claim plumbing in oauth2.py reads well, the needs_userinfo handling is right, and the session round-trip is covered by tests. My concerns are almost entirely about the single consumer in server_access.py, where I think there are two problems that need resolving before this can go in, plus a design question that is worth agreeing first.

Must fix

1. An OAuth2 user can rename another user's server group.

In web/pgadmin/utils/server_access.py, the hide_shared branch previously applied ServerGroup.user_id == current_user.id as a standalone filter, whereas it is now folded into or_(*conditions) alongside the claim condition. ServerGroupView.update() in web/pgadmin/browser/server_groups/__init__.py:226 passes hide_shared=True specifically to obtain an owned-only lookup, as its comment says, and it then assigns servergroup.name and commits without any further ownership check. So an OAuth2 user whose claim list happens to contain a name such as Production can PUT the id of somebody else's group of that name and rename it. The claim condition needs to stay out of the hide_shared=True branch; ideally the write paths would not share this query at all, since it is now doing double duty as both a visibility helper and an ownership check.

2. As written, the claim grants visibility but not access, so the feature is close to a no-op.

Server visibility is decided entirely by get_user_server_query() (server_access.py), which is still own-or-shared and is untouched here, and ServerModule.get_nodes() (browser/server_groups/servers/__init__.py:292) goes through it. That leaves three cases and none of them do what the issue asks for: a claim-granted group owned by another user shows up in the tree but is empty, because that owner's private servers are still filtered out; a group that does contain shared=True servers is already visible to every authenticated user via the existing has_shared_servers condition, so the new OR adds nothing; and with the hide_shared_server preference enabled, get_servers() skips non-owned servers anyway, so the group is empty again. To actually deliver #9704 the claim has to feed server access as well as group listing, not just the group query.

Design question

The description frames this as controlling access and offering different levels of access, but the implementation can only ever widen visibility, so an administrator cannot use it to confine a user to a subset of groups. The only existing mechanism that hides shared groups is the per-user hide_shared_server preference, which the user can simply switch off, so that is not an access control either. I think the semantics we want are "when the claim is configured, visibility becomes owned groups plus claim-granted groups", which is restrictive rather than additive, but it would be worth agreeing that before you spend more time on the implementation.

Should fix

Matching ServerGroup.name across all owners. ServerGroup rows are per-user, so ServerGroup.name.in_(...) matches every user's group with a given name. Two users each owning a group called "Production" will present the claim holder with two indistinguishable tree nodes, and ServerGroupView.properties() (server_groups/__init__.py:280) returns the owner's user_id, so it also discloses which accounts exist. The lookup should be scoped to a defined owner rather than matched globally by name.

Ordering of the session write. In web/pgadmin/authenticate/oauth2.py, session['oauth2_server_group_claims'] is written before the OAUTH2_ADDITIONAL_CLAIMS validation that can still reject the login. It is not exploitable, since the consumer is gated on current_user.auth_source == OAUTH2, but moving it below the valid_combined check is cleaner and costs nothing.

Test coverage for the behaviour that changed. The new cases assert what lands in the session, which is useful, but nothing exercises get_server_groups_for_user_query() with claims present, and in particular nothing covers the hide_shared=True path where the rename problem above lives. CodeRabbit also asked for a regression test covering a server group claim that exists only in the userinfo response; the needs_userinfo fix was made but that test has not been added.

Documentation. docs/en_US/oauth2.rst documents every other OAUTH2_* key in its configuration table, with a worked example section for OAUTH2_ADDITIONAL_CLAIMS, so the two new keys need equivalent rows and a short example covering what happens to unmapped claim values.

Minor

list(set(server_groups)) gives non-deterministic ordering, which is why the test needed assertCountEqual; list(dict.fromkeys(server_groups)) dedupes whilst preserving order. mapping.get(value) will raise TypeError: unhashable type if a claim value is a list or a dict, which some providers emit when groups are objects rather than strings, and since the call is not wrapped that would surface as a 500 during login rather than a clear message, so skipping non-scalar values would be safer. Finally, claim values are matched to group names exactly, with no trimming or case folding, which is a defensible choice but worth stating in the docs given how often provider group names arrive /-prefixed or differently cased.

Notes

Claims are resolved only at login, so a role change at the identity provider takes effect on the user's next login. That is reasonable, but it deserves a line in the documentation. The module docstring at the top of server_access.py also still says these helpers restrict users to servers they own or that have been explicitly shared, and only the function docstring was updated.

Verification

I ran browser.tests.test_oauth2_with_mocking against the branch and all 23 cases pass, including both new scenarios, and pycodestyle is clean on all four changed files. One unrelated failure appeared in ReverseEngineeredSQLTestCases, but it reproduces on an unmodified master in the same working tree, so it is environmental rather than anything to do with this change. Both CodeRabbit findings were valid and look correctly addressed in 19c0599.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Map OAuth claims to Server Groups

2 participants