Conversation
be83e3d to
d539f65
Compare
CTTY
left a comment
There was a problem hiding this comment.
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>, |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 👌
There was a problem hiding this comment.
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:
- keep them in the
OnceCell<RestClient>and re-use the lazy intialization - introduce new
OnceCell<Arc<dyn AuthManager>>andOnceCell<Arc<dyn AuthSession>>and have their initialization depend on the rest client's initialization - 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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
- the auth manager has no dependency on sessions and shouldn't have to be loaded lazily -> it should be constructed by the builder
- 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 aTypedBuilderpattern to a custom builder that returns aResult<RestConfig>with a resolved auth manager. This would avoid anArc<dyn AuthManager>on the catalog just to pass it toRestCatalogClientinitialization (and then not use it again)
There was a problem hiding this comment.
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>>, |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
Same here, seems like we can pass the pre-built contextual session to this function directly
|
|
||
| let request = HttpRequest::build(request_builder)?; | ||
| let http_response = client.query_catalog(request).await?; | ||
| let session = client.contextual_session(context).await?; |
There was a problem hiding this comment.
We can build the contextual session using the auth manager here
| /// derive authentication for its [`SessionContext`]. | ||
| auth_manager: Arc<dyn AuthManager>, | ||
| /// The catalog-wide session passed to [`AuthManager::contextual_session`]. | ||
| catalog_session: Arc<dyn AuthSession>, |
There was a problem hiding this comment.
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
Head branch was pushed to by a user without write access
dd6aaaa to
8785ac8
Compare
|
@CTTY Looks like auto-merge was disabled |
laskoviymishka
left a comment
There was a problem hiding this comment.
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.
| } | ||
|
|
||
| #[tokio::test] | ||
| async fn test_contextual_session_authenticates_catalog_operation() { |
There was a problem hiding this comment.
This is the one thing I'd want before merge: the new wiring touches all twelve operations, but only list_namespaces is exercised here.
The gap I care about most is check_exists_via_head — both table_exists and the HEAD branch of namespace_exists route through it, and that path silently ignored the context before this PR. An existence check sent with the wrong tenant's session is how "does table X exist" leaks across tenants, so I'd really like a HEAD test with match_header on the contextual session for both.
While we're in there, a test where contextual_session returns Err (asserting the op fails before any HTTP request goes out) plus one write-op test would cover the parts most likely to break silently on a mechanical slip. The existing ContextManager harness makes each ~15 lines. wdyt?
| request: HttpRequest, | ||
| ) -> Result<HttpResponse> { | ||
| self.http_client | ||
| .with_auth_session(session) |
There was a problem hiding this comment.
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.
| /// context may therefore return the previously cached session. | ||
| async fn contextual_session( | ||
| &self, | ||
| _context: &SessionContext, |
There was a problem hiding this comment.
Small one, but the underscore here fights the doc. The docstring tells implementors to key their cache off SessionContext::session_id, yet _context renders in the generated public API and in rust-analyzer as "this argument is unused" — the opposite message.
I'd drop the underscore from the trait signature and silence the default body instead:
async fn contextual_session(
&self,
context: &SessionContext,
catalog_session: Arc<dyn AuthSession>,
) -> Result<Arc<dyn AuthSession>> {
let _ = context;
Ok(catalog_session)
}| /// [`SessionContext::session_id`] and are responsible for eviction and | ||
| /// releasing any associated resources. Reusing a session ID with different | ||
| /// context may therefore return the previously cached session. | ||
| async fn contextual_session( |
There was a problem hiding this comment.
Not blocking — more a design question for the series. The doc asks implementors to cache sessions and "release any associated resources," but the trait gives them no signal for when to do that. Java's AuthManager extends AutoCloseable and OAuth2Manager.close() calls sessionCache.invalidateAll() for exactly this.
In a long-lived multi-tenant catalog, a per-context cache grows unbounded with no teardown hook. Worth a default async fn close(&self) -> Result<()> { Ok(()) } now, or at least a doc note that Drop is the intended cleanup point? Happy to leave it for a follow-up if that's the plan.
| /// The catalog calls this method only after [`Self::catalog_session`] has | ||
| /// succeeded. `catalog_session` is the catalog session returned by this | ||
| /// manager. If the context does not require different authentication, | ||
| /// implementations should return `catalog_session` unchanged. |
There was a problem hiding this comment.
Two things the implementor guidance leaves unsaid that I think are worth a sentence each, since the OAuth2 impl in #3170 will hit both.
First, contextual_session gets no HttpClient — an implementation that needs one for a token exchange has to stash a clone of the client passed to catalog_session. That's the intended pattern (Java holds the client internally too), but nothing here says so.
Second, the catalog doesn't serialize concurrent calls with the same session_id, so a caching impl without its own synchronization can race and build several sessions for one context. A note that impls must guard concurrent creation would save someone that bug.
| let endpoint = client.config.namespaces_endpoint(); | ||
| let mut namespaces = Vec::new(); | ||
| let mut next_token = None; | ||
| let session = client.contextual_session(context).await?; |
There was a problem hiding this comment.
One subtle thing — we grab the contextual session once here and Arc::clone it across every page. If an implementation hands back a short-lived token and a listing spans more pages than the token's TTL, the later pages go out with stale auth and surface as a surprise 401 deep in the loop rather than at the call.
Probably fine for now since no built-in manager does short-lived contextual tokens yet, but worth either re-deriving per page or a doc line telling impls a session may be reused across an operation's requests. wdyt? (Same applies to list_tables.)
Which issue does this PR close?
Another piece that's working towards #2774.
This PR bridges the
RestSessionCatalog'sSessionContexts (introduced with #2920) with theAuthManager(introduced with #2838 thanks again @plusplusjiajia).The
RestSessionCatalogwill 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
pubtrait method to theiceberg_catalog_rest::AuthManagerwith a default implementation. The newAuthManager::contextual_sessionallows implementors to convert per catalog operationSessionContextinto HTTP-level authentication metadata.The
RestSessionCatalogreceives updates to forward itsSessionContextto its underlyingRestClientwhich now holds andyn AuthManagerto authenticate any incoming session. Previously theAuthManagerwas only used once to retrieve a catalog session.Are these changes tested?
Added a test
test_contextual_session_authenticates_each_catalog_requestto assert that the catalog session is passed as the parent, session context reaches theAuthManager, requests are modified with auth headers.The
OAuth2Manager::contextual_sessionimplementation in #3170 verifies the trait with an actual implementation.AI Disclosure
I've used Codex to help me with setting up this PR.