Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auth Manager API part 4: RESTClient, HTTPClient #11992

Merged
merged 10 commits into from
Feb 6, 2025

Conversation

adutra
Copy link
Contributor

@adutra adutra commented Jan 17, 2025

4th PR for the Auth Manager API. Previous ones:

This PR introduces the required changes to RESTClient and HTTPClient. It also introduces a BaseHTTPClient abstract class to facilitate the creation and execution of HTTPRequests in a consistent way.

The biggest change in actually in TestRESTCatalog, because almost every Mockito.verify() call needs to be adapted.

The AuthManager API is still "unplugged" at this point.

\cc @nastra @danielcweeks

this.mapper = objectMapper;
this.authSession = AuthSession.EMPTY;
Copy link
Contributor

Choose a reason for hiding this comment

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

I think defaulting to empty opens up a risk that requests will be made without an explicit session. This somewhat hides the case where we missed a call that should have auth. I would think we would leave this a null and require that a session be used (e.g. validate the auth was set in the execute call). Thoughts @nastra?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think that makes sense to enforce/validate that a proper auth session is being used

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We are going to run into a chicken and egg problem: to create an auth session I need an HTTP client; but now, to create an HTTP client, I would need an auth session.

Copy link
Contributor

Choose a reason for hiding this comment

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

I understand that may seem like the case here, but I think we're just saying that you create it initially as set to null, but validate when execute is called that it is not null. You can still create the HTTPClient, but you can't make a request unless there is an active auth session. This just prevents unintentional use of an "empty" auth session (it needs to be intentional).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, now I get it. Let me try your suggestion.

@Fokko Fokko added this to the Iceberg 1.9.0 milestone Jan 25, 2025
.build())));
id -> {
RESTClient client =
httpClient().withAuthSession(org.apache.iceberg.rest.auth.AuthSession.EMPTY);
Copy link
Contributor

Choose a reason for hiding this comment

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

wouldn't this create a new http client every time this is called?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Indeed. The child client is lightweight though, but it's probably better to move this call to httpClient().

@@ -192,7 +192,8 @@ private RESTClient httpClient() {
HTTPClient.builder(properties())
.uri(baseSignerUri())
.withObjectMapper(S3ObjectMapper.mapper())
.build();
.build()
.withAuthSession(org.apache.iceberg.rest.auth.AuthSession.EMPTY);
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe worth having a withAuthSession on the builder class itself(similar to withObjectMapper)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Builder method added.

@@ -338,6 +339,7 @@ public SdkHttpFullRequest sign(
Consumer<Map<String, String>> responseHeadersConsumer = responseHeaders::putAll;
S3SignResponse s3SignResponse =
httpClient()
.withAuthSession(org.apache.iceberg.rest.auth.AuthSession.EMPTY)
Copy link
Contributor

Choose a reason for hiding this comment

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

I believe this change can be reverted now that the session is set in httpClient()

@@ -84,6 +85,7 @@ private RESTClient httpClient() {

private LoadCredentialsResponse fetchCredentials() {
return httpClient()
.withAuthSession(AuthSession.EMPTY)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I just took a quick look because I'm also working on the HttpClient code and was curious about this PR.

This might have been commented, but wouldn't it be possible to pass the AuthSession to the HttpClient during creation, maybe through a Builder? Then we could avoid adding these 'withAuthSession' calls when we use the httpClient.

Copy link
Contributor

Choose a reason for hiding this comment

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

Different sessions are used depending on the context, so it can't be restricted to a single one.

baseHeaders.forEach(allHeaders::putIfAbsent);
}

Preconditions.checkState(authSession != null, "no AuthSession available");
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
Preconditions.checkState(authSession != null, "no AuthSession available");
Preconditions.checkState(authSession != null, "Invalid auth session: null");

@adutra
Copy link
Contributor Author

adutra commented Jan 30, 2025

@danielcweeks @nastra is there anything else I need to change?

@jbonofre
Copy link
Member

jbonofre commented Feb 4, 2025

I see @nastra approved. I did a new pass and it looks good to me. @danielcweeks wdyt ?

@@ -192,6 +192,7 @@ private RESTClient httpClient() {
HTTPClient.builder(properties())
.uri(baseSignerUri())
.withObjectMapper(S3ObjectMapper.mapper())
.withAuthSession(org.apache.iceberg.rest.auth.AuthSession.EMPTY)
Copy link
Contributor

Choose a reason for hiding this comment

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

Why are we setting this to EMPTY? In order to sign we need an auth session. Wouldn't this result in no headers/sigs being produced if a call was made without explicitly setting the auth session? We want enforce that an auth session is used, but this results in falling back to nothing.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree with your suggestions for other components, but here I disagree: the HTTP client must be configured with an empty auth session because it will be used for token refreshes. If it contains, say, token t1, and token t1 gets refreshed and exchanged for t2, there would be a clash between the token from the HTTP client (t1) and the one from the refreshed auth session (t2).

Copy link
Contributor Author

@adutra adutra Feb 5, 2025

Choose a reason for hiding this comment

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

I will however move this call to the authSession() method and add a comment to explain why an EMPTY session is needed in this specific case.

client =
HTTPClient.builder(properties)
.uri(properties.get(URI))
.withAuthSession(AuthSession.EMPTY)
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here? The default should not be EMPTY, that just cases it to fall through.

try (RESTClient initClient =
clientBuilder
.apply(props)
.withAuthSession(org.apache.iceberg.rest.auth.AuthSession.EMPTY)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here, we shouldn't default to EMPTY

this.client =
clientBuilder
.apply(mergedProps)
.withAuthSession(org.apache.iceberg.rest.auth.AuthSession.EMPTY);
Copy link
Contributor

Choose a reason for hiding this comment

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

Here as well.

@danielcweeks
Copy link
Contributor

I feel like there may have been a small understanding with how we handle the initial authSession. We don't want to default it to empty otherwise if someone adds a request like client.get(...) it will proceed without an auth session as opposed to getting an error because they didn't use the proper client.withAuthSession(session).get(...).

It's better to fail early here so that we know the issue was a lack of auth info as part of the request because we'll only get a 401/403 back without knowing that we just failed to apply auth.

@@ -75,10 +76,13 @@ private RESTClient httpClient() {
if (null == client) {
synchronized (this) {
if (null == client) {
DefaultAuthSession authSession =
DefaultAuthSession.of(
HTTPHeaders.of(OAuth2Util.authHeaders(properties.get(OAuth2Properties.TOKEN))));
Copy link
Contributor

Choose a reason for hiding this comment

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

@adutra It's beyond the scope of this PR since this is an existing problem, but somehow we need the AuthManager/AuthSession to be able to refresh OAuth tokens here as well. The token here will expire at which point the credential provider will not be able to get new vended credentials.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes. I don't mind looking into it once we have the whole API merged 👍

@danielcweeks danielcweeks merged commit 9b1d18f into apache:main Feb 6, 2025
46 checks passed
@adutra adutra deleted the auth-manager-4 branch February 6, 2025 18:30
@adutra adutra mentioned this pull request Feb 7, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants