Repository navigation
feat(rest_catalog::auth) Contextual Sessions - #3081
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.
| 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.
There was a problem hiding this comment.
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.
8785ac8 to
8d13731
Compare
…ple Http" This reverts commit 32a9dc5.
This prevents cloning the HeaderMap of the client because a new session is passed for every HTTP request anyway
bb87e36 to
9267df8
Compare
|
Thanks for the review @laskoviymishka 🙇 I should have addressed all your comments! |
|
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 |
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).
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.