Skip to content

[SSO] Add foundation for JWT group-to-role sync#36292

Open
mtabebe wants to merge 1 commit intoMaterializeInc:mainfrom
mtabebe:ma/sso/scim-foundation
Open

[SSO] Add foundation for JWT group-to-role sync#36292
mtabebe wants to merge 1 commit intoMaterializeInc:mainfrom
mtabebe:ma/sso/scim-foundation

Conversation

@mtabebe
Copy link
Copy Markdown
Contributor

@mtabebe mtabebe commented Apr 27, 2026

Add the building blocks for OIDC JWT group-to-role synchronization:

mz_jwt_sync builtin sentinel role that marks role memberships managed by JWT sync, distinguishing them from manual grants.

Three dyncfg system variables:

  • oidc_group_role_sync_enabled (bool, default false): feature gate

  • oidc_group_claim (string, default "groups"): JWT claim name

  • oidc_group_role_sync_strict (bool, default false): fail-closed mode

  • OidcClaims::groups() extraction method that normalizes group names (lowercase, deduplicate, sort) from JWT claims. Handles array-of-strings, single-string, and filters non-string values. Returns None for missing claims (skip sync) vs Some(vec![]) for empty claims (revoke all).

Addresses: SQL-176, SQL-177, SQL-178

@mtabebe mtabebe force-pushed the ma/sso/scim-foundation branch 2 times, most recently from a51f9fe to 89c42e8 Compare April 27, 2026 15:03
Add the building blocks for OIDC JWT group-to-role synchronization:

mz_jwt_sync builtin sentinel role that marks role memberships managed
by JWT sync, distinguishing them from manual grants.

Three dyncfg system variables:
- oidc_group_role_sync_enabled (bool, default false): feature gate
- oidc_group_claim (string, default "groups"): JWT claim name
- oidc_group_role_sync_strict (bool, default false): fail-closed mode

- OidcClaims::groups() extraction method that normalizes group names
  (lowercase, deduplicate, sort) from JWT claims. Handles array-of-strings,
  single-string, and filters non-string values. Returns None for missing
  claims (skip sync) vs Some(vec![]) for empty claims (revoke all).

No behavioral change
@mtabebe mtabebe force-pushed the ma/sso/scim-foundation branch from 89c42e8 to 5434073 Compare April 27, 2026 15:29
@mtabebe mtabebe requested a review from SangJunBak April 27, 2026 15:40
@mtabebe mtabebe marked this pull request as ready for review April 27, 2026 15:41
@mtabebe mtabebe requested review from a team as code owners April 27, 2026 15:41
Copy link
Copy Markdown
Contributor

@SangJunBak SangJunBak left a comment

Choose a reason for hiding this comment

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

All just nits but otw looks good!

Comment on lines +180 to +216
simple conn=mz_system,user=mz_system
ALTER SYSTEM SET oidc_group_claim = 'roles'
----
COMPLETE 0

simple conn=mz_system,user=mz_system
SHOW oidc_group_claim
----
roles
COMPLETE 1

simple conn=mz_system,user=mz_system
ALTER SYSTEM SET oidc_group_role_sync_strict = true
----
COMPLETE 0

simple conn=mz_system,user=mz_system
SHOW oidc_group_role_sync_strict
----
on
COMPLETE 1

# Reset to defaults
simple conn=mz_system,user=mz_system
ALTER SYSTEM RESET oidc_group_role_sync_enabled
----
COMPLETE 0

simple conn=mz_system,user=mz_system
ALTER SYSTEM RESET oidc_group_claim
----
COMPLETE 0

simple conn=mz_system,user=mz_system
ALTER SYSTEM RESET oidc_group_role_sync_strict
----
COMPLETE 0
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.

I wonder how valuable these tests are and whether they belong in this test file?

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.

I'd imagine us extending these tests in src/environmentd/tests/auth.rs in which we're going to be testing this code path anyways

pub fn groups(&self, claim_name: &str) -> Option<Vec<String>> {
let value = self.unknown_claims.get(claim_name)?;

let raw_groups: Vec<String> = match value {
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.

Nit: I wonder if we're being too verbose with the comments? We say the same thing more concisely in the top level comment for each of these comments and the code already reads well

Comment on lines +285 to +291
// Normalize: lowercase for case-insensitive role matching,
// filter out empty strings (not valid role names), deduplicate
// via BTreeSet (which also sorts), then collect into a Vec for
// the caller. Sorted order makes downstream diff computation
// deterministic.
let normalized: Vec<String> = raw_groups
.into_iter()
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.

If we're already lowercasing, I wonder if it's worth trimming the whitespace from each end too?

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.

Nevermind, I just reailzed SQL accepts whitespace in role names

// Any other JSON type can't represent group names — treat as
// if the claim were absent so we don't accidentally revoke all
// roles based on garbage data.
_ => return None,
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.

Wonder if we should log warns each time we expect a group claim to exist but it doesn't?

Comment on lines +861 to +862
// Single-string claim that is whitespace-only: not lowercased into
// anything meaningful, not filtered (only empty strings are dropped).
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.

Re: the trim comment, maybe it makes sense to trim then filter for empty strings?

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.

Nvm

}

#[mz_ore::test]
fn test_groups_case_insensitive_dedup() {
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.

nit: Feels like test_groups_mixed_case already handles this

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.

2 participants