Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion core/src/main/java/org/apache/iceberg/rest/auth/OAuth2Util.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
import org.apache.iceberg.relocated.com.google.common.base.Joiner;
import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
Expand Down Expand Up @@ -243,7 +244,14 @@ public static OAuthTokenResponse fetchToken(
oauth2ServerUri,
request,
OAuthTokenResponse.class,
headers,
// RFC 6749 proposes two ways to send a credential: HTTP Basic authentication or
// request-body. Historically, the Iceberg library prefers the latter one for
// compatibility while the RFC recommends the former one. As sending both the
// Authorization header and the request-body parameters might confuse some authorization
// servers, the following line is excluding the Authorization header.
headers.entrySet().stream()
.filter(entry -> !AUTHORIZATION_HEADER.equals(entry.getKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)),
ErrorHandlers.oauthErrorHandler());
response.validate();

Expand Down
121 changes: 95 additions & 26 deletions core/src/test/java/org/apache/iceberg/rest/TestRESTCatalog.java
Original file line number Diff line number Diff line change
Expand Up @@ -415,15 +415,21 @@ public void testCatalogCredentialNoOauth2ServerUri() {

RESTCatalog catalog =
new RESTCatalog(SessionCatalog.SessionContext.createEmpty(), (config) -> adapter);
String catalogCredential = "catalog:secret";
catalog.initialize(
"prod", ImmutableMap.of(CatalogProperties.URI, "ignored", "credential", "catalog:secret"));
"prod", ImmutableMap.of(CatalogProperties.URI, "ignored", "credential", catalogCredential));

assertThat(catalog.tableExists(TBL)).isFalse();

// no token or credential for catalog token exchange
Mockito.verify(adapter)
.execute(
reqMatcher(HTTPMethod.POST, ResourcePaths.tokens(), emptyHeaders),
reqMatcher(
HTTPMethod.POST,
ResourcePaths.tokens(),
emptyHeaders,
ImmutableMap.of(),
createClientCredentialsRequest(catalogCredential)),
eq(OAuthTokenResponse.class),
any(),
any());
Expand Down Expand Up @@ -454,13 +460,14 @@ public void testCatalogCredential(String oauth2ServerUri) {

RESTCatalog catalog =
new RESTCatalog(SessionCatalog.SessionContext.createEmpty(), (config) -> adapter);
String catalogCredential = "catalog:secret";
catalog.initialize(
"prod",
ImmutableMap.of(
CatalogProperties.URI,
"ignored",
"credential",
"catalog:secret",
catalogCredential,
OAuth2Properties.OAUTH2_SERVER_URI,
oauth2ServerUri));

Expand All @@ -469,7 +476,12 @@ public void testCatalogCredential(String oauth2ServerUri) {
// no token or credential for catalog token exchange
Mockito.verify(adapter)
.execute(
reqMatcher(HTTPMethod.POST, oauth2ServerUri, emptyHeaders),
reqMatcher(
HTTPMethod.POST,
oauth2ServerUri,
emptyHeaders,
ImmutableMap.of(),
createClientCredentialsRequest(catalogCredential)),
eq(OAuthTokenResponse.class),
any(),
any());
Expand Down Expand Up @@ -498,11 +510,12 @@ public void testCatalogBearerTokenWithClientCredential(String oauth2ServerUri) {

RESTCatalogAdapter adapter = Mockito.spy(new RESTCatalogAdapter(backendCatalog));

String contextCredential = "user:secret";
SessionCatalog.SessionContext context =
new SessionCatalog.SessionContext(
UUID.randomUUID().toString(),
"user",
ImmutableMap.of("credential", "user:secret"),
ImmutableMap.of("credential", contextCredential),
ImmutableMap.of());

RESTCatalog catalog = new RESTCatalog(context, (config) -> adapter);
Expand All @@ -525,10 +538,15 @@ public void testCatalogBearerTokenWithClientCredential(String oauth2ServerUri) {
eq(ConfigResponse.class),
any(),
any());
// use the bearer token to fetch the context token
// use the request-body to fetch the context token
Mockito.verify(adapter)
.execute(
reqMatcher(HTTPMethod.POST, oauth2ServerUri, catalogHeaders),
reqMatcher(
HTTPMethod.POST,
oauth2ServerUri,
ImmutableMap.of(),
ImmutableMap.of(),
createClientCredentialsRequest(contextCredential)),
eq(OAuthTokenResponse.class),
any(),
any());
Expand All @@ -552,21 +570,23 @@ public void testCatalogCredentialWithClientCredential(String oauth2ServerUri) {

RESTCatalogAdapter adapter = Mockito.spy(new RESTCatalogAdapter(backendCatalog));

String contextCredential = "user:secret";
SessionCatalog.SessionContext context =
new SessionCatalog.SessionContext(
UUID.randomUUID().toString(),
"user",
ImmutableMap.of("credential", "user:secret"),
ImmutableMap.of("credential", contextCredential),
ImmutableMap.of());

RESTCatalog catalog = new RESTCatalog(context, (config) -> adapter);
String catalogCredential = "catalog:secret";
catalog.initialize(
"prod",
ImmutableMap.of(
CatalogProperties.URI,
"ignored",
"credential",
"catalog:secret",
catalogCredential,
OAuth2Properties.OAUTH2_SERVER_URI,
oauth2ServerUri));

Expand All @@ -575,7 +595,12 @@ public void testCatalogCredentialWithClientCredential(String oauth2ServerUri) {
// call client credentials with no initial auth
Mockito.verify(adapter)
.execute(
reqMatcher(HTTPMethod.POST, oauth2ServerUri, emptyHeaders),
reqMatcher(
HTTPMethod.POST,
oauth2ServerUri,
emptyHeaders,
ImmutableMap.of(),
createClientCredentialsRequest(catalogCredential)),
eq(OAuthTokenResponse.class),
any(),
any());
Expand All @@ -589,7 +614,12 @@ public void testCatalogCredentialWithClientCredential(String oauth2ServerUri) {
// use the client credential to fetch the context token
Mockito.verify(adapter)
.execute(
reqMatcher(HTTPMethod.POST, oauth2ServerUri, catalogHeaders),
reqMatcher(
HTTPMethod.POST,
oauth2ServerUri,
emptyHeaders,
ImmutableMap.of(),
createClientCredentialsRequest(contextCredential)),
eq(OAuthTokenResponse.class),
any(),
any());
Expand All @@ -613,11 +643,12 @@ public void testCatalogBearerTokenAndCredentialWithClientCredential(String oauth

RESTCatalogAdapter adapter = Mockito.spy(new RESTCatalogAdapter(backendCatalog));

String contextCredential = "user:secret";
SessionCatalog.SessionContext context =
new SessionCatalog.SessionContext(
UUID.randomUUID().toString(),
"user",
ImmutableMap.of("credential", "user:secret"),
ImmutableMap.of("credential", contextCredential),
ImmutableMap.of());

RESTCatalog catalog = new RESTCatalog(context, (config) -> adapter);
Expand Down Expand Up @@ -652,7 +683,12 @@ public void testCatalogBearerTokenAndCredentialWithClientCredential(String oauth
// use the client credential to fetch the context token
Mockito.verify(adapter)
.execute(
reqMatcher(HTTPMethod.POST, oauth2ServerUri, catalogHeaders),
reqMatcher(
HTTPMethod.POST,
oauth2ServerUri,
ImmutableMap.of(),
ImmutableMap.of(),
createClientCredentialsRequest(contextCredential)),
eq(OAuthTokenResponse.class),
any(),
any());
Expand Down Expand Up @@ -828,7 +864,19 @@ private void testClientAuth(

// token passes a static token. otherwise, validate a client credentials or token exchange
// request
if (!credentials.containsKey("token")) {
if (!credentials.containsKey("token") && credentials.containsKey("credential")) {
Mockito.verify(adapter)
.execute(
reqMatcher(
HTTPMethod.POST,
oauth2ServerUri,
ImmutableMap.of(),
ImmutableMap.of(),
createClientCredentialsRequest(credentials.get("credential"))),
eq(OAuthTokenResponse.class),
any(),
any());
} else if (!credentials.containsKey("token")) {
Mockito.verify(adapter)
.execute(
reqMatcher(HTTPMethod.POST, oauth2ServerUri, catalogHeaders),
Expand Down Expand Up @@ -1294,13 +1342,14 @@ public void testCatalogTokenRefresh(String oauth2ServerUri) {
UUID.randomUUID().toString(), "user", contextCredentials, ImmutableMap.of());

RESTCatalog catalog = new RESTCatalog(context, (config) -> adapter);
String catalogCredential = "catalog:secret";
catalog.initialize(
"prod",
ImmutableMap.of(
CatalogProperties.URI,
"ignored",
"credential",
"catalog:secret",
catalogCredential,
OAuth2Properties.OAUTH2_SERVER_URI,
oauth2ServerUri));

Expand All @@ -1311,7 +1360,12 @@ public void testCatalogTokenRefresh(String oauth2ServerUri) {
// call client credentials with no initial auth
Mockito.verify(adapter)
.execute(
reqMatcher(HTTPMethod.POST, oauth2ServerUri, emptyHeaders),
reqMatcher(
HTTPMethod.POST,
oauth2ServerUri,
emptyHeaders,
ImmutableMap.of(),
createClientCredentialsRequest(catalogCredential)),
eq(OAuthTokenResponse.class),
any(),
any());
Expand Down Expand Up @@ -1400,13 +1454,14 @@ public void testCatalogRefreshedTokenIsUsed(String oauth2ServerUri) {
UUID.randomUUID().toString(), "user", contextCredentials, ImmutableMap.of());

RESTCatalog catalog = new RESTCatalog(context, (config) -> adapter);
String catalogCredential = "catalog:secret";
catalog.initialize(
"prod",
ImmutableMap.of(
CatalogProperties.URI,
"ignored",
"credential",
"catalog:secret",
catalogCredential,
OAuth2Properties.OAUTH2_SERVER_URI,
oauth2ServerUri));

Expand All @@ -1418,20 +1473,14 @@ public void testCatalogRefreshedTokenIsUsed(String oauth2ServerUri) {
assertThat(catalog.tableExists(TBL)).isFalse();

// call client credentials with no initial auth
Map<String, String> clientCredentialsRequest =
ImmutableMap.of(
"grant_type", "client_credentials",
"client_id", "catalog",
"client_secret", "secret",
"scope", "catalog");
Mockito.verify(adapter)
.execute(
reqMatcher(
HTTPMethod.POST,
oauth2ServerUri,
emptyHeaders,
Map.of(),
clientCredentialsRequest),
createClientCredentialsRequest(catalogCredential)),
eq(OAuthTokenResponse.class),
any(),
any());
Expand Down Expand Up @@ -1516,13 +1565,14 @@ public void testCatalogTokenRefreshExchangeDisabled(String oauth2ServerUri) {
UUID.randomUUID().toString(), "user", contextCredentials, ImmutableMap.of());

RESTCatalog catalog = new RESTCatalog(context, (config) -> adapter);
String catalogCredential = "catalog:secret";
catalog.initialize(
"prod",
ImmutableMap.of(
CatalogProperties.URI,
"ignored",
OAuth2Properties.CREDENTIAL,
"catalog:secret",
catalogCredential,
OAuth2Properties.TOKEN_EXCHANGE_ENABLED,
"false",
OAuth2Properties.OAUTH2_SERVER_URI,
Expand All @@ -1535,7 +1585,12 @@ public void testCatalogTokenRefreshExchangeDisabled(String oauth2ServerUri) {
// call client credentials with no initial auth
Mockito.verify(adapter)
.execute(
reqMatcher(HTTPMethod.POST, oauth2ServerUri, emptyHeaders),
reqMatcher(
HTTPMethod.POST,
oauth2ServerUri,
emptyHeaders,
ImmutableMap.of(),
createClientCredentialsRequest(catalogCredential)),
eq(OAuthTokenResponse.class),
any(),
any());
Expand Down Expand Up @@ -3163,4 +3218,18 @@ private static List<HTTPRequest> allRequests(RESTCatalogAdapter adapter) {
verify(adapter, atLeastOnce()).execute(captor.capture(), any(), any(), any());
return captor.getAllValues();
}

private static Map<String, String> createClientCredentialsRequest(String credential) {
String[] parsed = credential.split(":");
assertThat(parsed.length).isEqualTo(2);
return ImmutableMap.of(
"grant_type",
"client_credentials",
"client_id",
parsed[0],
"client_secret",
parsed[1],
"scope",
"catalog");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ void contextualSessionCredentialsProvided() {
SessionCatalog.SessionContext context =
new SessionCatalog.SessionContext(
"test", "test", Map.of(OAuth2Properties.CREDENTIAL, "client:secret"), Map.of());
Map<String, String> properties = Map.of();
Map<String, String> properties = Map.of(OAuth2Properties.CREDENTIAL, "client:secret");
try (OAuth2Manager manager = new OAuth2Manager("test");
OAuth2Util.AuthSession catalogSession = manager.catalogSession(client, properties);
OAuth2Util.AuthSession contextualSession =
Expand All @@ -243,7 +243,7 @@ void contextualSessionCredentialsProvided() {
.as("should create session cache for context with credentials")
.satisfies(cache -> assertThat(cache.sessionCache().asMap()).hasSize(1));
}
Mockito.verify(client)
Mockito.verify(client, times(2))
.postForm(
any(),
eq(
Expand All @@ -255,7 +255,7 @@ void contextualSessionCredentialsProvided() {
eq(OAuthTokenResponse.class),
eq(Map.of()),
any());
Mockito.verify(client).withAuthSession(any());
Mockito.verify(client, times(2)).withAuthSession(any());
Mockito.verifyNoMoreInteractions(client);
}

Expand Down