Skip to content

feat(rest_catalog::auth) Contextual Sessions - #3081

Merged
CTTY merged 11 commits into
apache:mainfrom
DerGut:contextual-session
Oct 9, 2026
Merged

CTTY merged 11 commits into
apache:mainfrom
DerGut:contextual-session

Conversation

@DerGut

@DerGut DerGut commented Aug 26, 2026 •

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Another piece that's working towards #2774.

This PR bridges the RestSessionCatalog's SessionContexts (introduced with #2920) with the AuthManager (introduced with #2838 thanks again @plusplusjiajia).
The RestSessionCatalog will be able to forward session context to an auth manager to generate "contextual" auth sessions. These auth sessions may contain credentials specific to a query in a multi-tenant system, i.e. a specific tenant's credentials.

What changes are included in this PR?

This PR adds a new pub trait method to the iceberg_catalog_rest::AuthManager with a default implementation. The new AuthManager::contextual_session allows implementors to convert per catalog operation SessionContext into HTTP-level authentication metadata.

async fn contextual_session(
        &self,
        _context: &SessionContext,
        catalog_session: Arc<dyn AuthSession>,
    ) -> Result<Arc<dyn AuthSession>> {
        Ok(catalog_session)
    }

The RestSessionCatalog receives updates to forward its SessionContext to its underlying RestClient which now holds an dyn AuthManager to authenticate any incoming session. Previously the AuthManager was only used once to retrieve a catalog session.

Are these changes tested?

Added a test test_contextual_session_authenticates_each_catalog_request to assert that the catalog session is passed as the parent, session context reaches the AuthManager, requests are modified with auth headers.

The OAuth2Manager::contextual_session implementation in #3170 verifies the trait with an actual implementation.

AI Disclosure

I've used Codex to help me with setting up this PR.

@DerGut

DerGut commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

This PR should now be ready for review, it also has it's API validated by #3170 but doesn't depend on it to be merged in any way 👍
cc @CTTY

@CTTY CTTY left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for having this! Left some questions

/// derive authentication for its [`SessionContext`].
auth_manager: Arc<dyn AuthManager>,
/// The catalog-wide session passed to [`AuthManager::contextual_session`].
catalog_session: Arc<dyn AuthSession>,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Storing catalog session here is fine but feels a bit excessive. Why not build contextual sessions in the catalog, and then pass them to RestClient::query_catalog?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Really only because the catalog is already doing quite a lot and the RestClient isn't.
I agree that conceptually it belongs closer to the catalog though.
I can make the change tomorrow 👌

@DerGut DerGut Sep 9, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

There's actually something more to it. The catalog session depends on the /v1/config handshake and on the init session. Because the RestClient is initialized lazily, it's not trivial to simply move the field to the RestSessionCatalog.

I see three options:

  1. keep them in the OnceCell<RestClient> and re-use the lazy intialization
  2. introduce new OnceCell<Arc<dyn AuthManager>> and OnceCell<Arc<dyn AuthSession>> and have their initialization depend on the rest client's initialization
  3. use a wrapper to encapsulate the rest client, auth manager and catalog session into a single lazy initialization

My thoughts on them:
2. is spreading lazy initialization logic around the code where it's really only one lazy chain that could be simplified
3. I could see this one work out but I haven't found a good name yet. I also am concerned that we're building a deep tree of encapsulating types: RestCatalog -> RestSessionCatalog -> <SomeNewType> -> RestClient -> HttpClient. The distinction between the RestClient and HttpClient isn't too well-defined yet (it's essentially an http client with rest-catalog specific config). Maybe this could be an opportunity to give it a more specific responsibility to save us from another layer -> that would get us to a hybrid of 1. and 3.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This makes sense, thanks for the detailed explanation!

I think keeping it as is for now is good. Maybe we need to rename RestClient to something else in the future since it represents more than just a client

@DerGut DerGut Sep 11, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'd rename it to RestCatalogClient for now to emphasize it's specificity to the catalog, will push a commit in a few minutes.
I have some refactors in mind that can go on top of this PR

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

  1. the auth manager has no dependency on sessions and shouldn't have to be loaded lazily -> it should be constructed by the builder
  2. going a step further, auth manager resolution only requires the RestConfig's properties, I'm exploring whether we shouldn't move the auth manager to the config and change it from a TypedBuilder pattern to a custom builder that returns a Result<RestConfig> with a resolved auth manager. This would avoid an Arc<dyn AuthManager> on the catalog just to pass it to RestCatalogClient initialization (and then not use it again)

@DerGut DerGut Sep 11, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Rename commit -> 8785ac8

Let me know what you think 🙏

/// Builder-supplied override retained so lazy client initialization can
/// clone it into the runtime state. When absent, a manager is resolved from
/// `rest.auth.type` during initialization.
auth_manager_override: Option<Arc<dyn AuthManager>>,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

naming is hard and I like the new name more!

async fn check_exists_via_head(&self, client: &RestClient, url: String) -> Result<bool> {
async fn check_exists_via_head(
&self,
context: &SessionContext,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Same here, seems like we can pass the pre-built contextual session to this function directly

Comment thread crates/catalog/rest/src/catalog.rs Outdated

let request = HttpRequest::build(request_builder)?;
let http_response = client.query_catalog(request).await?;
let session = client.contextual_session(context).await?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We can build the contextual session using the auth manager here

@CTTY CTTY left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM!

/// derive authentication for its [`SessionContext`].
auth_manager: Arc<dyn AuthManager>,
/// The catalog-wide session passed to [`AuthManager::contextual_session`].
catalog_session: Arc<dyn AuthSession>,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This makes sense, thanks for the detailed explanation!

I think keeping it as is for now is good. Maybe we need to rename RestClient to something else in the future since it represents more than just a client

@CTTY
CTTY enabled auto-merge September 11, 2026 23:15
auto-merge was automatically disabled September 11, 2026 23:55

Head branch was pushed to by a user without write access

@alexanderbianchi

Copy link
Copy Markdown
Contributor

@CTTY Looks like auto-merge was disabled

@laskoviymishka laskoviymishka 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.

Looks good to me too — coming in late after CTTY's approval, so mostly a fresh set of eyes on the public API. The core move is right: keeping HttpClient session-free and deriving auth per operation through contextual_session, and it nicely fixes the two existence-check paths that were dropping the context before.

Nothing blocks the merge for me. Left a few inline notes — a couple worth tidying while it's open (the double HeaderMap clone, the _context underscore) and a nudge to exercise more than list_namespaces in the test, since check_exists_via_head is the path that was silently broken and the one where the wrong tenant's session leaks existence. The rest are just forward-looking notes for the auth series.

Comment thread crates/catalog/rest/src/catalog.rs Resolved
Comment thread crates/catalog/rest/src/catalog.rs Outdated
request: HttpRequest,
) -> Result<HttpResponse> {
self.http_client
.with_auth_session(session)

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 think this quietly doubles the per-request HeaderMap clone. with_auth_session does Self { auth_session, ..self.clone() }, which clones extra_headers, and then HttpClient::query_catalog clones extra_headers again when it extends the request. Before this PR the session was baked in at init and we paid one clone per request.

An internal method that authenticates in place keeps it to a single clone:

pub(crate) async fn query_catalog_with_session(
    &self,
    session: &dyn AuthSession,
    mut request: HttpRequest,
) -> Result<HttpResponse> {
    session.authenticate(&mut request).await?;
    let mut inner = request.into_inner();
    inner.headers_mut().extend(self.extra_headers.clone());
    HttpResponse::read(self.client.execute(inner).await?).await
}

Not a blocker, but it's a regression on the hot path for single-context callers, so I'd fix it here.

@DerGut DerGut Oct 7, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The benefit was negligible in my benchmarks but the clone still shows a design issue: a contextual auth session is now passed with every catalog request, but the old client that was designed around catalog sessions assumed they were static for the lifetime of the client.

d33821a and 085bf79 get this straight. A session is now passed on every request to query_catalog and the client doesn't hold any sessions anymore.
The catalog's use of the HttpClient now also diverges from the AuthManager's use. The HttpClient::post_form method is public for implementors of auth managers. It's now not using any catalog sessions anymore because it's meant to derive them. If implementors need to authenticate their requests, they can still do so with client-level headers.

Comment thread crates/catalog/rest/src/auth/mod.rs Resolved
Comment thread crates/catalog/rest/src/auth/mod.rs Resolved
Comment thread crates/catalog/rest/src/auth/mod.rs Resolved
Comment thread crates/catalog/rest/src/catalog.rs Resolved
@DerGut
DerGut force-pushed the contextual-session branch from 8785ac8 to 8d13731 Compare October 7, 2026 12:00
DerGut added 5 commits October 7, 2026 14:19
This prevents cloning the HeaderMap of the client because a new session
is passed for every HTTP request anyway
@DerGut
DerGut force-pushed the contextual-session branch from bb87e36 to 9267df8 Compare October 7, 2026 16:39
@DerGut

DerGut commented Oct 7, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review @laskoviymishka 🙇 I should have addressed all your comments!

@CTTY

CTTY commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator

Oops, I thought this was merged in a while ago. Thanks Andrei for the additional review! Now with all comments addressed I'm adding this to the merge queue now

@CTTY
CTTY added this pull request to the merge queue Oct 8, 2026
Merged via the queue into apache:main with commit 7f9691f Oct 9, 2026
24 checks passed
zakariya-s added a commit to zakariya-s/iceberg-rust that referenced this pull request Oct 26, 2026
Port the vended credential provider to contextual sessions (apache#3081):
credential requests are authenticated by a table session derived from
the contextual session of the operation that loaded the table, and the
table-scoped client no longer carries a session. Resolve conflicts with
HDFS support (apache#3111) and ADLS relative paths (apache#3311).
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.

4 participants