This is an automated email from the ASF dual-hosted git repository.
dweeks pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg.git
The following commit(s) were added to refs/heads/main by this push:
new cdf748e8e5 Auth Manager API part 2: AuthManager (#11809)
cdf748e8e5 is described below
commit cdf748e8e5537f13d861aa4c617a51f3e11dc97c
Author: Alexandre Dutra <[email protected]>
AuthorDate: Fri Dec 20 18:51:51 2024 +0100
Auth Manager API part 2: AuthManager (#11809)
* Auth Manager API part 2: AuthManager, AuthSession
* [review] close()
* [review] nits
* HTTPAuthSession close()
* mainSession -> catalogSession
* AuthManager constructor
---
.../java/org/apache/iceberg/rest/HTTPHeaders.java | 10 ++
.../org/apache/iceberg/rest/auth/AuthManager.java | 106 +++++++++++++++++++++
.../org/apache/iceberg/rest/auth/AuthManagers.java | 75 +++++++++++++++
.../apache/iceberg/rest/auth/AuthProperties.java | 37 +++++++
.../org/apache/iceberg/rest/auth/AuthSession.java | 57 +++++++++++
.../apache/iceberg/rest/auth/BasicAuthManager.java | 53 +++++++++++
.../iceberg/rest/auth/DefaultAuthSession.java | 57 +++++++++++
.../apache/iceberg/rest/auth/NoopAuthManager.java | 40 ++++++++
.../org/apache/iceberg/rest/TestHTTPHeaders.java | 12 +++
.../apache/iceberg/rest/auth/TestAuthManagers.java | 88 +++++++++++++++++
.../iceberg/rest/auth/TestBasicAuthManager.java | 62 ++++++++++++
.../iceberg/rest/auth/TestDefaultAuthSession.java | 72 ++++++++++++++
12 files changed, 669 insertions(+)
diff --git a/core/src/main/java/org/apache/iceberg/rest/HTTPHeaders.java
b/core/src/main/java/org/apache/iceberg/rest/HTTPHeaders.java
index 35710bd9a9..23263899b9 100644
--- a/core/src/main/java/org/apache/iceberg/rest/HTTPHeaders.java
+++ b/core/src/main/java/org/apache/iceberg/rest/HTTPHeaders.java
@@ -19,6 +19,7 @@
package org.apache.iceberg.rest;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
@@ -85,6 +86,15 @@ public interface HTTPHeaders {
return ImmutableHTTPHeaders.builder().addEntries(headers).build();
}
+ static HTTPHeaders of(Map<String, String> headers) {
+ return ImmutableHTTPHeaders.builder()
+ .entries(
+ headers.entrySet().stream()
+ .map(e -> HTTPHeader.of(e.getKey(), e.getValue()))
+ .collect(Collectors.toList()))
+ .build();
+ }
+
/** Represents an HTTP header as a name-value pair. */
@Value.Style(redactedMask = "****", depluralize = true)
@Value.Immutable
diff --git a/core/src/main/java/org/apache/iceberg/rest/auth/AuthManager.java
b/core/src/main/java/org/apache/iceberg/rest/auth/AuthManager.java
new file mode 100644
index 0000000000..8f6f16f925
--- /dev/null
+++ b/core/src/main/java/org/apache/iceberg/rest/auth/AuthManager.java
@@ -0,0 +1,106 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.rest.auth;
+
+import java.util.Map;
+import org.apache.iceberg.catalog.SessionCatalog;
+import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.rest.RESTClient;
+
+/**
+ * Manager for authentication sessions. This interface is used to create
sessions for the catalog,
+ * the tables/views, and any other context that requires authentication.
+ *
+ * <p>Managers are usually stateful and may require initialization and
cleanup. The manager is
+ * created by the catalog and is closed when the catalog is closed.
+ */
+public interface AuthManager extends AutoCloseable {
+
+ /**
+ * Returns a temporary session to use for contacting the configuration
endpoint only. Note that
+ * the returned session will be closed after the configuration endpoint is
contacted, and should
+ * not be cached.
+ *
+ * <p>The provided REST client is a short-lived client; it should only be
used to fetch initial
+ * credentials, if required, and must be discarded after that.
+ *
+ * <p>This method cannot return null. By default, it returns the catalog
session.
+ */
+ default AuthSession initSession(RESTClient initClient, Map<String, String>
properties) {
+ return catalogSession(initClient, properties);
+ }
+
+ /**
+ * Returns a long-lived session whose lifetime is tied to the owning
catalog. This session serves
+ * as the parent session for all other sessions (contextual and
table-specific). It is closed when
+ * the owning catalog is closed.
+ *
+ * <p>The provided REST client is a long-lived, shared client; if required,
implementors may store
+ * it and reuse it for all subsequent requests to the authorization server,
e.g. for renewing or
+ * refreshing credentials. It is not necessary to close it when {@link
#close()} is called.
+ *
+ * <p>This method cannot return null.
+ *
+ * <p>It is not required to cache the returned session internally, as the
catalog will keep it
+ * alive for the lifetime of the catalog.
+ */
+ AuthSession catalogSession(RESTClient sharedClient, Map<String, String>
properties);
+
+ /**
+ * Returns a session for a specific context.
+ *
+ * <p>If the context requires a specific {@link AuthSession}, this method
should return a new
+ * {@link AuthSession} instance, otherwise it should return the parent
session.
+ *
+ * <p>This method cannot return null. By default, it returns the parent
session.
+ *
+ * <p>Implementors should cache contextual sessions internally, as the
catalog will not cache
+ * them. Also, the owning catalog never closes contextual sessions;
implementations should manage
+ * their lifecycle themselves and close them when they are no longer needed.
+ */
+ default AuthSession contextualSession(SessionCatalog.SessionContext context,
AuthSession parent) {
+ return parent;
+ }
+
+ /**
+ * Returns a new session targeting a specific table or view. The properties
are the ones returned
+ * by the table/view endpoint.
+ *
+ * <p>If the table or view requires a specific {@link AuthSession}, this
method should return a
+ * new {@link AuthSession} instance, otherwise it should return the parent
session.
+ *
+ * <p>This method cannot return null. By default, it returns the parent
session.
+ *
+ * <p>Implementors should cache table sessions internally, as the catalog
will not cache them.
+ * Also, the owning catalog never closes table sessions; implementations
should manage their
+ * lifecycle themselves and close them when they are no longer needed.
+ */
+ default AuthSession tableSession(
+ TableIdentifier table, Map<String, String> properties, AuthSession
parent) {
+ return parent;
+ }
+
+ /**
+ * Closes the manager and releases any resources.
+ *
+ * <p>This method is called when the owning catalog is closed.
+ */
+ @Override
+ void close();
+}
diff --git a/core/src/main/java/org/apache/iceberg/rest/auth/AuthManagers.java
b/core/src/main/java/org/apache/iceberg/rest/auth/AuthManagers.java
new file mode 100644
index 0000000000..42c2b1eeba
--- /dev/null
+++ b/core/src/main/java/org/apache/iceberg/rest/auth/AuthManagers.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.rest.auth;
+
+import java.util.Locale;
+import java.util.Map;
+import org.apache.iceberg.common.DynConstructors;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class AuthManagers {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(AuthManagers.class);
+
+ private AuthManagers() {}
+
+ public static AuthManager loadAuthManager(String name, Map<String, String>
properties) {
+ String authType =
+ properties.getOrDefault(AuthProperties.AUTH_TYPE,
AuthProperties.AUTH_TYPE_NONE);
+
+ String impl;
+ switch (authType.toLowerCase(Locale.ROOT)) {
+ case AuthProperties.AUTH_TYPE_NONE:
+ impl = AuthProperties.AUTH_MANAGER_IMPL_NONE;
+ break;
+ case AuthProperties.AUTH_TYPE_BASIC:
+ impl = AuthProperties.AUTH_MANAGER_IMPL_BASIC;
+ break;
+ default:
+ impl = authType;
+ }
+
+ LOG.info("Loading AuthManager implementation: {}", impl);
+ DynConstructors.Ctor<AuthManager> ctor;
+ try {
+ ctor =
+ DynConstructors.builder(AuthManager.class)
+ .loader(AuthManagers.class.getClassLoader())
+ .impl(impl, String.class) // with name
+ .buildChecked();
+ } catch (NoSuchMethodException e) {
+ throw new IllegalArgumentException(
+ String.format(
+ "Cannot initialize AuthManager implementation %s: %s", impl,
e.getMessage()),
+ e);
+ }
+
+ AuthManager authManager;
+ try {
+ authManager = ctor.newInstance(name);
+ } catch (ClassCastException e) {
+ throw new IllegalArgumentException(
+ String.format("Cannot initialize AuthManager, %s does not implement
AuthManager", impl),
+ e);
+ }
+
+ return authManager;
+ }
+}
diff --git
a/core/src/main/java/org/apache/iceberg/rest/auth/AuthProperties.java
b/core/src/main/java/org/apache/iceberg/rest/auth/AuthProperties.java
new file mode 100644
index 0000000000..bf94311d55
--- /dev/null
+++ b/core/src/main/java/org/apache/iceberg/rest/auth/AuthProperties.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.rest.auth;
+
+public final class AuthProperties {
+
+ private AuthProperties() {}
+
+ public static final String AUTH_TYPE = "rest.auth.type";
+
+ public static final String AUTH_TYPE_NONE = "none";
+ public static final String AUTH_TYPE_BASIC = "basic";
+
+ public static final String AUTH_MANAGER_IMPL_NONE =
+ "org.apache.iceberg.rest.auth.NoopAuthManager";
+ public static final String AUTH_MANAGER_IMPL_BASIC =
+ "org.apache.iceberg.rest.auth.BasicAuthManager";
+
+ public static final String BASIC_USERNAME = "rest.auth.basic.username";
+ public static final String BASIC_PASSWORD = "rest.auth.basic.password";
+}
diff --git a/core/src/main/java/org/apache/iceberg/rest/auth/AuthSession.java
b/core/src/main/java/org/apache/iceberg/rest/auth/AuthSession.java
new file mode 100644
index 0000000000..eed7caf845
--- /dev/null
+++ b/core/src/main/java/org/apache/iceberg/rest/auth/AuthSession.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.rest.auth;
+
+import org.apache.iceberg.rest.HTTPRequest;
+
+/**
+ * An authentication session that can be used to authenticate outgoing HTTP
requests.
+ *
+ * <p>Authentication sessions are usually immutable, but may hold resources
that need to be released
+ * when the session is no longer needed. Implementations should override
{@link #close()} to release
+ * any resources.
+ */
+public interface AuthSession extends AutoCloseable {
+
+ /** An empty session that does nothing. */
+ AuthSession EMPTY =
+ new AuthSession() {
+ @Override
+ public HTTPRequest authenticate(HTTPRequest request) {
+ return request;
+ }
+
+ @Override
+ public void close() {}
+ };
+
+ /**
+ * Authenticates the given request and returns a new request with the
necessary authentication.
+ */
+ HTTPRequest authenticate(HTTPRequest request);
+
+ /**
+ * Closes the session and releases any resources. This method is called when
the session is no
+ * longer needed. Note that since sessions may be cached, this method may
not be called
+ * immediately after the session is no longer needed, but rather when the
session is evicted from
+ * the cache, or the cache itself is closed.
+ */
+ @Override
+ void close();
+}
diff --git
a/core/src/main/java/org/apache/iceberg/rest/auth/BasicAuthManager.java
b/core/src/main/java/org/apache/iceberg/rest/auth/BasicAuthManager.java
new file mode 100644
index 0000000000..d0d56d3d37
--- /dev/null
+++ b/core/src/main/java/org/apache/iceberg/rest/auth/BasicAuthManager.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.rest.auth;
+
+import java.util.Map;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.rest.HTTPHeaders;
+import org.apache.iceberg.rest.RESTClient;
+
+/** An auth manager that adds static BASIC authentication data to outgoing
HTTP requests. */
+public final class BasicAuthManager implements AuthManager {
+
+ public BasicAuthManager(String ignored) {
+ // no-op
+ }
+
+ @Override
+ public AuthSession catalogSession(RESTClient sharedClient, Map<String,
String> properties) {
+ Preconditions.checkArgument(
+ properties.containsKey(AuthProperties.BASIC_USERNAME),
+ "Invalid username: missing required property %s",
+ AuthProperties.BASIC_USERNAME);
+ Preconditions.checkArgument(
+ properties.containsKey(AuthProperties.BASIC_PASSWORD),
+ "Invalid password: missing required property %s",
+ AuthProperties.BASIC_PASSWORD);
+ String username = properties.get(AuthProperties.BASIC_USERNAME);
+ String password = properties.get(AuthProperties.BASIC_PASSWORD);
+ String credentials = username + ":" + password;
+ return
DefaultAuthSession.of(HTTPHeaders.of(OAuth2Util.basicAuthHeaders(credentials)));
+ }
+
+ @Override
+ public void close() {
+ // no resources to close
+ }
+}
diff --git
a/core/src/main/java/org/apache/iceberg/rest/auth/DefaultAuthSession.java
b/core/src/main/java/org/apache/iceberg/rest/auth/DefaultAuthSession.java
new file mode 100644
index 0000000000..002f47459d
--- /dev/null
+++ b/core/src/main/java/org/apache/iceberg/rest/auth/DefaultAuthSession.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.rest.auth;
+
+import org.apache.iceberg.rest.HTTPHeaders;
+import org.apache.iceberg.rest.HTTPRequest;
+import org.apache.iceberg.rest.ImmutableHTTPRequest;
+import org.immutables.value.Value;
+
+/**
+ * Default implementation of {@link AuthSession}. It authenticates requests by
setting the provided
+ * headers on the request.
+ *
+ * <p>Most {@link AuthManager} implementations should make use of this class,
unless they need to
+ * retain state when creating sessions, or if they need to modify the request
in a different way.
+ */
[email protected](redactedMask = "****")
[email protected]
+@SuppressWarnings({"ImmutablesStyle", "SafeLoggingPropagation"})
+public interface DefaultAuthSession extends AuthSession {
+
+ /** Headers containing authentication data to set on the request. */
+ HTTPHeaders headers();
+
+ @Override
+ default HTTPRequest authenticate(HTTPRequest request) {
+ HTTPHeaders headers = request.headers().putIfAbsent(headers());
+ return headers.equals(request.headers())
+ ? request
+ :
ImmutableHTTPRequest.builder().from(request).headers(headers).build();
+ }
+
+ @Override
+ default void close() {
+ // no resources to close
+ }
+
+ static DefaultAuthSession of(HTTPHeaders headers) {
+ return ImmutableDefaultAuthSession.builder().headers(headers).build();
+ }
+}
diff --git
a/core/src/main/java/org/apache/iceberg/rest/auth/NoopAuthManager.java
b/core/src/main/java/org/apache/iceberg/rest/auth/NoopAuthManager.java
new file mode 100644
index 0000000000..d706d78ef3
--- /dev/null
+++ b/core/src/main/java/org/apache/iceberg/rest/auth/NoopAuthManager.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.rest.auth;
+
+import java.util.Map;
+import org.apache.iceberg.rest.RESTClient;
+
+/** An auth manager that does not add any authentication data to outgoing HTTP
requests. */
+public class NoopAuthManager implements AuthManager {
+
+ public NoopAuthManager(String ignored) {
+ // no-op
+ }
+
+ @Override
+ public AuthSession catalogSession(RESTClient sharedClient, Map<String,
String> properties) {
+ return AuthSession.EMPTY;
+ }
+
+ @Override
+ public void close() {
+ // no resources to close
+ }
+}
diff --git a/core/src/test/java/org/apache/iceberg/rest/TestHTTPHeaders.java
b/core/src/test/java/org/apache/iceberg/rest/TestHTTPHeaders.java
index 9380073f76..a8531e6ff5 100644
--- a/core/src/test/java/org/apache/iceberg/rest/TestHTTPHeaders.java
+++ b/core/src/test/java/org/apache/iceberg/rest/TestHTTPHeaders.java
@@ -21,6 +21,7 @@ package org.apache.iceberg.rest;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
import org.apache.iceberg.rest.HTTPHeaders.HTTPHeader;
import org.junit.jupiter.api.Test;
@@ -119,6 +120,17 @@ class TestHTTPHeaders {
.hasMessage("headers");
}
+ @Test
+ void ofMap() {
+ HTTPHeaders actual =
+ HTTPHeaders.of(
+ ImmutableMap.of(
+ "header1", "value1a",
+ "HEADER1", "value1b",
+ "header2", "value2"));
+ assertThat(actual).isEqualTo(headers);
+ }
+
@Test
void invalidHeader() {
// invalid input (null name or value)
diff --git
a/core/src/test/java/org/apache/iceberg/rest/auth/TestAuthManagers.java
b/core/src/test/java/org/apache/iceberg/rest/auth/TestAuthManagers.java
new file mode 100644
index 0000000000..21bd8c1b29
--- /dev/null
+++ b/core/src/test/java/org/apache/iceberg/rest/auth/TestAuthManagers.java
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.rest.auth;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.util.Map;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class TestAuthManagers {
+
+ private final PrintStream standardErr = System.err;
+ private final ByteArrayOutputStream streamCaptor = new
ByteArrayOutputStream();
+
+ @BeforeEach
+ public void before() {
+ System.setErr(new PrintStream(streamCaptor));
+ }
+
+ @AfterEach
+ public void after() {
+ System.setErr(standardErr);
+ }
+
+ @Test
+ void noop() {
+ try (AuthManager manager = AuthManagers.loadAuthManager("test", Map.of()))
{
+ assertThat(manager).isInstanceOf(NoopAuthManager.class);
+ }
+ assertThat(streamCaptor.toString())
+ .contains(
+ "Loading AuthManager implementation:
org.apache.iceberg.rest.auth.NoopAuthManager");
+ }
+
+ @Test
+ void noopExplicit() {
+ try (AuthManager manager =
+ AuthManagers.loadAuthManager(
+ "test", Map.of(AuthProperties.AUTH_TYPE,
AuthProperties.AUTH_TYPE_NONE))) {
+ assertThat(manager).isInstanceOf(NoopAuthManager.class);
+ }
+ assertThat(streamCaptor.toString())
+ .contains(
+ "Loading AuthManager implementation:
org.apache.iceberg.rest.auth.NoopAuthManager");
+ }
+
+ @Test
+ void basicExplicit() {
+ try (AuthManager manager =
+ AuthManagers.loadAuthManager(
+ "test", Map.of(AuthProperties.AUTH_TYPE,
AuthProperties.AUTH_TYPE_BASIC))) {
+ assertThat(manager).isInstanceOf(BasicAuthManager.class);
+ }
+ assertThat(streamCaptor.toString())
+ .contains(
+ "Loading AuthManager implementation:
org.apache.iceberg.rest.auth.BasicAuthManager");
+ }
+
+ @Test
+ @SuppressWarnings("resource")
+ void nonExistentAuthManager() {
+ assertThatThrownBy(
+ () -> AuthManagers.loadAuthManager("test",
Map.of(AuthProperties.AUTH_TYPE, "unknown")))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("Cannot initialize AuthManager implementation
unknown");
+ }
+}
diff --git
a/core/src/test/java/org/apache/iceberg/rest/auth/TestBasicAuthManager.java
b/core/src/test/java/org/apache/iceberg/rest/auth/TestBasicAuthManager.java
new file mode 100644
index 0000000000..c34654cdef
--- /dev/null
+++ b/core/src/test/java/org/apache/iceberg/rest/auth/TestBasicAuthManager.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.rest.auth;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.util.Map;
+import org.apache.iceberg.rest.HTTPHeaders;
+import org.junit.jupiter.api.Test;
+
+class TestBasicAuthManager {
+
+ @Test
+ void missingUsername() {
+ try (AuthManager authManager = new BasicAuthManager("test")) {
+ assertThatThrownBy(() -> authManager.catalogSession(null, Map.of()))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage(
+ "Invalid username: missing required property %s",
AuthProperties.BASIC_USERNAME);
+ }
+ }
+
+ @Test
+ void missingPassword() {
+ try (AuthManager authManager = new BasicAuthManager("test")) {
+ Map<String, String> properties = Map.of(AuthProperties.BASIC_USERNAME,
"alice");
+ assertThatThrownBy(() -> authManager.catalogSession(null, properties))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage(
+ "Invalid password: missing required property %s",
AuthProperties.BASIC_PASSWORD);
+ }
+ }
+
+ @Test
+ void success() {
+ Map<String, String> properties =
+ Map.of(AuthProperties.BASIC_USERNAME, "alice",
AuthProperties.BASIC_PASSWORD, "secret");
+ try (AuthManager authManager = new BasicAuthManager("test");
+ AuthSession session = authManager.catalogSession(null, properties)) {
+ assertThat(session)
+ .isEqualTo(
+
DefaultAuthSession.of(HTTPHeaders.of(OAuth2Util.basicAuthHeaders("alice:secret"))));
+ }
+ }
+}
diff --git
a/core/src/test/java/org/apache/iceberg/rest/auth/TestDefaultAuthSession.java
b/core/src/test/java/org/apache/iceberg/rest/auth/TestDefaultAuthSession.java
new file mode 100644
index 0000000000..f6fee42e0d
--- /dev/null
+++
b/core/src/test/java/org/apache/iceberg/rest/auth/TestDefaultAuthSession.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.rest.auth;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.net.URI;
+import org.apache.iceberg.rest.HTTPHeaders;
+import org.apache.iceberg.rest.HTTPHeaders.HTTPHeader;
+import org.apache.iceberg.rest.HTTPRequest;
+import org.apache.iceberg.rest.HTTPRequest.HTTPMethod;
+import org.apache.iceberg.rest.ImmutableHTTPRequest;
+import org.junit.jupiter.api.Test;
+
+class TestDefaultAuthSession {
+
+ @Test
+ void authenticate() {
+ try (DefaultAuthSession session =
+ DefaultAuthSession.of(HTTPHeaders.of(HTTPHeader.of("Authorization",
"s3cr3t")))) {
+
+ HTTPRequest original =
+ ImmutableHTTPRequest.builder()
+ .method(HTTPMethod.GET)
+ .baseUri(URI.create("https://localhost"))
+ .path("path")
+ .build();
+
+ HTTPRequest authenticated = session.authenticate(original);
+
+ assertThat(authenticated.headers().entries())
+ .singleElement()
+ .extracting(HTTPHeader::name, HTTPHeader::value)
+ .containsExactly("Authorization", "s3cr3t");
+ }
+ }
+
+ @Test
+ void authenticateWithConflictingHeader() {
+ try (DefaultAuthSession session =
+ DefaultAuthSession.of(HTTPHeaders.of(HTTPHeader.of("Authorization",
"s3cr3t")))) {
+
+ HTTPRequest original =
+ ImmutableHTTPRequest.builder()
+ .method(HTTPMethod.GET)
+ .baseUri(URI.create("https://localhost"))
+ .path("path")
+ .headers(HTTPHeaders.of(HTTPHeader.of("Authorization", "other")))
+ .build();
+
+ HTTPRequest authenticated = session.authenticate(original);
+
+ assertThat(authenticated).isSameAs(original);
+ }
+ }
+}