Repository navigation
(Iceberg) Expose OAuth2 scope/audience for REST Catalog Auth - #1402
Merged
Merged
Conversation
Thread OAuth2 `scope` and `audience` through the public config surface (builders, CLI, HTTP API) so scope-gated Iceberg REST catalogs such as Snowflake Horizon / Apache Polaris can be configured via supported config. The engine already form-encodes `scope`; only the user-facing plumbing was missing, so every layer forced it to `None`. - api: add fluent `with_oauth2_scope`/`with_oauth2_audience` setters on IcebergCreateConfig (mutate the OAuth2 auth config in place) and delegating setters on R2rmlCreateConfig. The existing 3-arg `with_auth_oauth2` is unchanged for backward compatibility. - iceberg: omit `client_id` from the token-request form when empty, which Snowflake Horizon's `session:role:` exchange requires (a non-empty client_id is rejected with `invalid_scope`). - cli: add `--oauth2-scope`/`--oauth2-audience` to `fluree iceberg map`; relax activation so OAuth2 engages on token_url + client_secret with client_id defaulting to "" (Horizon/PAT callers omit it). - server: add `oauth2_scope`/`oauth2_audience` to the iceberg/map request with matching activation and build wiring. Tested with hermetic wiremock token-endpoint tests (scope sent, empty client_id omitted / present when non-empty, audience sent), builder + serde round-trip (no migration), and route/CLI deserialize tests. Refs #1394
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
Iceberg REST-catalog OAuth2 client-credentials auth already form-encodes a
scopeat the engine level (OAuth2Config.scope), butscopeandaudiencewere not exposed through any public config surface — the builders, the CLI,
and the HTTP API all hardcoded them to
None. As a result, connecting to ascope-gated Iceberg REST catalog was impossible via supported config. The
concrete blocked case is Snowflake Horizon (Snowflake's built-in
Apache-Polaris Iceberg REST endpoint), which requires
scope=session:role:<ROLE>on the/v1/oauth/tokensclient-credentialsexchange — and rejects a non-empty
client_idon that exchange with400 invalid_scope.This PR threads
scope/audiencethrough every layer and makes the emptyclient_idcase work.What changed (the middle-layer builder gap)
The engine already knew how to send
scope; the gap was purely the user-facingplumbing between the public config types and the engine
AuthConfig. Nothing inthe token-exchange or downstream read path changed.
fluree-db-api(builders): added fluentwith_oauth2_scope/with_oauth2_audiencesetters onIcebergCreateConfigthat mutate theexisting
AuthConfig::OAuth2ClientCredentialsin place (warn-and-no-op ifOAuth2 client-credentials auth has not been set first, or in Direct catalog
mode). Added delegating setters on
R2rmlCreateConfig. The existing 3-argwith_auth_oauth2(token_url, client_id, client_secret)is unchanged.fluree-db-iceberg(engine):fetch_tokennow omitsclient_idfromthe
client_credentialsform when it is empty (Snowflake Horizon'ssession:role:exchange requires an absent/emptyclient_id; a non-empty oneis interpreted as the principal flow and rejected).
fluree-db-cli: added--oauth2-scope/--oauth2-audiencetofluree iceberg map, wired into both the localwith_oauth2_*chaining andthe
args_to_jsonremote body. Relaxed activation: OAuth2 now engages ontoken_url + client_secret, withclient_iddefaulting to"".fluree-db-server: addedoauth2_scope/oauth2_audienceto theIcebergMapRequestbody with matching activation + build wiring.Files
fluree-db-api/src/graph_source/config.rsfluree-db-iceberg/src/auth/oauth2.rsfluree-db-cli/src/commands/iceberg.rsfluree-db-server/src/routes/iceberg.rsfluree-db-cli/src/cli.rs498 insertions, 11 deletions.
Tests + results
All tests are hermetic (no network — OAuth2 tests run against a
wiremockmocktoken server).
fluree-db-iceberg(--features aws): 146 lib tests pass, including thetwo new wiremock tests:
fetch_token_encodes_scope_and_omits_empty_client_id—scopeisform-encoded into the token request;
client_idis omitted when empty;audienceabsent whenNone.fetch_token_includes_client_id_when_non_empty_and_audience—client_idis present when non-empty;
audienceandscopeare sent.fluree-db-api(--features iceberg): 644 lib tests pass, includingbuilder, warns-without-oauth2 no-op, no-migration serde round-trip, and R2RML
delegation tests.
fluree-db-server(--features iceberg): route deserialize tests pass —oauth2_scopereaches the engineAuthConfig; a request withoutclient_secretdoes not activate OAuth2.fluree-db-cli(--features iceberg):args_to_jsonincludesscope/audience and omits
client_id;build_iceberg_configactivates OAuth2without
client_idand threads scope/audience; no secret -> no OAuth2.cargo clippyclean on all four crates;cargo fmt --checkclean.Risk / compatibility
AuthConfigJSON already supportedscope/audience; a round-trip test locksthe "no migration" claim. Existing 3-arg
with_auth_oauth2callers (incl. theit_graph_source_r2rmlintegration test) compile and pass unchanged.client_idomitted from the token form.Previously
client_idwas sent unconditionally (even as an empty string); nowan empty
client_idis omitted entirely. This is required by SnowflakeHorizon. For standard OAuth2 servers a non-empty
client_idis still sentexactly as before, so only the empty-string case is affected.
activation required
token_url + client_id + client_secretall present; now ittriggers on
token_url + client_secret, withclient_iddefaulting to"".A caller that supplies
token_url+client_secretbut noclient_idnowactivates OAuth2 (previously it silently did not). Supplying only
token_url(no secret) still does not activate OAuth2.
Follow-up (out of scope for this code PR)
Issue #1394 also asks for docs — a "Snowflake Horizon" section in
docs/graph-sources/iceberg.mdanddocs/cli/iceberg.mdshowing the happy path(
token_url=.../v1/oauth/tokens,client_id="",scope=session:role:<ROLE>,vended_credentials=true, UPPERCASE identifiers). Those are intentionally leftout of this code-only slice and can land as a separate docs change.
Fixes #1394