This is an automated email from the ASF dual-hosted git repository.
smengcl pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ozone.git
The following commit(s) were added to refs/heads/master by this push:
new 1911e03d11e Enable OM S3 gRPC auth.
1911e03d11e is described below
commit 1911e03d11e0ccdfb0588ae0a508db5b6f8a2d7a
Author: Siyao Meng <[email protected]>
AuthorDate: Thu Aug 20 14:33:07 2026 -0700
Enable OM S3 gRPC auth.
---
.../org/apache/hadoop/ozone/om/OMConfigKeys.java | 16 +++
.../hadoop/ozone/TestOzoneConfigurationFields.java | 2 +
.../hadoop/ozone/om/GrpcOzoneManagerServer.java | 40 +++++-
.../hadoop/ozone/om/OzoneManagerServiceGrpc.java | 34 ++++-
.../hadoop/ozone/om/request/OMClientRequest.java | 7 +-
.../ozone/om/TestGrpcOzoneManagerServer.java | 152 +++++++++++++++++++++
.../request/TestOMClientRequestWithUserInfo.java | 67 +++++----
.../ozone/s3secret/S3SecretEndpointBase.java | 32 +++++
.../ozone/s3secret/TestS3SecretEndpointBase.java | 69 ++++++++++
9 files changed, 380 insertions(+), 39 deletions(-)
diff --git
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/OMConfigKeys.java
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/OMConfigKeys.java
index 2990410fbae..ab0c792cc90 100644
---
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/OMConfigKeys.java
+++
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/OMConfigKeys.java
@@ -525,6 +525,22 @@ public final class OMConfigKeys {
public static final boolean OZONE_OM_S3_GRPC_SERVER_ENABLED_DEFAULT =
true;
+ /**
+ * When {@code ozone.security.enabled} is true, reject OMRequests received on
+ * the OM S3 gateway gRPC endpoint that do not carry S3 authentication. The
S3
+ * Gateway attaches S3 authentication to the requests it forwards on behalf
of
+ * its clients; requests without it would otherwise be processed with a
+ * client-asserted identity. Identity-free bootstrap reads the S3 Gateway
must
+ * issue before any per-request S3 authentication exists (ServiceList) are
+ * exempt. Set to false only for compatibility with deployments that rely on
+ * unauthenticated (anonymous) access through this endpoint. Has no effect
when
+ * {@code ozone.security.enabled} is false.
+ */
+ public static final String OZONE_OM_S3_GRPC_AUTH_REQUIRED =
+ "ozone.om.s3.grpc.auth.required";
+ public static final boolean OZONE_OM_S3_GRPC_AUTH_REQUIRED_DEFAULT =
+ true;
+
public static final String OZONE_OM_NAMESPACE_STRICT_S3 =
"ozone.om.namespace.s3.strict";
public static final boolean OZONE_OM_NAMESPACE_STRICT_S3_DEFAULT =
diff --git
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/TestOzoneConfigurationFields.java
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/TestOzoneConfigurationFields.java
index a26a5b7ecc8..dacaa417564 100644
---
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/TestOzoneConfigurationFields.java
+++
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/TestOzoneConfigurationFields.java
@@ -168,6 +168,8 @@ private void addPropertiesNotInXml() {
OMConfigKeys.OZONE_OM_RATIS_SNAPSHOT_AUTO_TRIGGER_THRESHOLD_KEY,
OMConfigKeys.OZONE_OM_HA_PREFIX,
OMConfigKeys.OZONE_OM_GRPC_PORT_KEY,
+ // Security hardening toggle; documented on the OMConfigKeys constant.
+ OMConfigKeys.OZONE_OM_S3_GRPC_AUTH_REQUIRED,
// TODO HDDS-2856
OMConfigKeys.OZONE_RANGER_OM_IGNORE_SERVER_CERT,
OMConfigKeys.OZONE_RANGER_OM_CONNECTION_TIMEOUT,
diff --git
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/GrpcOzoneManagerServer.java
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/GrpcOzoneManagerServer.java
index a05dc47c9b0..b6f5452c600 100644
---
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/GrpcOzoneManagerServer.java
+++
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/GrpcOzoneManagerServer.java
@@ -28,6 +28,7 @@
import static
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_GRPC_WORKERGROUP_SIZE_DEFAULT;
import static
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_GRPC_WORKERGROUP_SIZE_KEY;
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import io.grpc.Server;
import io.grpc.ServerInterceptors;
@@ -103,10 +104,30 @@ public GrpcOzoneManagerServer(OzoneConfiguration config,
this.threadNamePrefix = threadPrefix;
this.omS3gGrpcMetrics = GrpcMetrics.create(config);
- init(omTranslator,
- delegationTokenMgr,
- config,
- caClient);
+ try {
+ init(omTranslator,
+ delegationTokenMgr,
+ config,
+ caClient);
+ } catch (RuntimeException e) {
+ // GrpcMetrics was registered above; if init() fails the server (for
+ // example failing closed on TLS setup) stop() never runs, so drop the
+ // registration here to avoid leaking the metrics source.
+ omS3gGrpcMetrics.unRegister();
+ throw e;
+ }
+ }
+
+ /**
+ * Whether the OM S3 gateway gRPC endpoint must reject OMRequests that carry
+ * no S3 authentication. Only enforced when security is enabled; controlled
by
+ * {@link OMConfigKeys#OZONE_OM_S3_GRPC_AUTH_REQUIRED} (default true).
+ */
+ @VisibleForTesting
+ static boolean isS3AuthRequired(OzoneConfiguration conf) {
+ return new SecurityConfig(conf).isSecurityEnabled()
+ && conf.getBoolean(OMConfigKeys.OZONE_OM_S3_GRPC_AUTH_REQUIRED,
+ OMConfigKeys.OZONE_OM_S3_GRPC_AUTH_REQUIRED_DEFAULT);
}
public void init(OzoneManagerProtocolServerSideTranslatorPB omTranslator,
@@ -142,6 +163,9 @@ public void init(OzoneManagerProtocolServerSideTranslatorPB
omTranslator,
workerEventLoopGroup =
new NioEventLoopGroup(workerGroupSize, workerFactory);
+ SecurityConfig secConf = new SecurityConfig(omServerConfig);
+ boolean s3AuthRequired = isS3AuthRequired(omServerConfig);
+
NettyServerBuilder nettyServerBuilder = NettyServerBuilder.forPort(port)
.maxInboundMessageSize(maxSize)
.bossEventLoopGroup(bossEventLoopGroup)
@@ -149,14 +173,13 @@ public void
init(OzoneManagerProtocolServerSideTranslatorPB omTranslator,
.channelType(NioServerSocketChannel.class)
.executor(readExecutors)
.addService(ServerInterceptors.intercept(
- new OzoneManagerServiceGrpc(omTranslator),
+ new OzoneManagerServiceGrpc(omTranslator, s3AuthRequired),
new ClientAddressServerInterceptor(),
new GrpcMetricsServerResponseInterceptor(omS3gGrpcMetrics),
new GrpcMetricsServerRequestInterceptor(omS3gGrpcMetrics)))
.addTransportFilter(
new GrpcMetricsServerTransportFilter(omS3gGrpcMetrics));
- SecurityConfig secConf = new SecurityConfig(omServerConfig);
if (secConf.isSecurityEnabled() && secConf.isGrpcTlsEnabled()) {
try {
SslContextBuilder sslClientContextBuilder =
@@ -171,7 +194,10 @@ public void
init(OzoneManagerProtocolServerSideTranslatorPB omTranslator,
SupportedCipherSuiteFilter.INSTANCE);
nettyServerBuilder.sslContext(sslContextBuilder.build());
} catch (Exception ex) {
- LOG.error("Unable to setup TLS for secure Om S3g GRPC channel.", ex);
+ // Fail closed: silently starting this server in plaintext when TLS
+ // was requested would expose an unauthenticated OM endpoint.
+ throw new IllegalStateException(
+ "Unable to setup TLS for secure Om S3g GRPC channel.", ex);
}
}
diff --git
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManagerServiceGrpc.java
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManagerServiceGrpc.java
index 05abb25c376..99d376e7549 100644
---
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManagerServiceGrpc.java
+++
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManagerServiceGrpc.java
@@ -20,11 +20,14 @@
import com.google.protobuf.RpcController;
import io.grpc.Status;
import java.io.IOException;
+import java.util.EnumSet;
+import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.hadoop.ipc_.RPC;
import org.apache.hadoop.ipc_.Server;
import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRequest;
import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMResponse;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.Type;
import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerServiceGrpc.OzoneManagerServiceImplBase;
import
org.apache.hadoop.ozone.protocolPB.OzoneManagerProtocolServerSideTranslatorPB;
import org.apache.hadoop.ozone.util.UUIDUtil;
@@ -41,11 +44,24 @@ public class OzoneManagerServiceGrpc extends
OzoneManagerServiceImplBase {
* RpcController is not used and hence is set to null.
*/
private static final RpcController NULL_RPC_CONTROLLER = null;
+ /**
+ * OMRequests the S3 gateway client must issue before any per-request S3
+ * identity exists: RpcClient bootstrap (OzoneClientCache.initialize ->
+ * getServiceInfo), the background CA-cert refresher, and server-defaults
+ * lookups all fetch the ServiceList before EndpointBase has set any
+ * thread-local S3Auth. These identity-free, read-only cmdTypes stay
+ * callable without S3 authentication in secure mode.
+ */
+ private static final Set<Type> S3_AUTH_EXEMPT_CMD_TYPES =
+ EnumSet.of(Type.ServiceList);
private final OzoneManagerProtocolServerSideTranslatorPB omTranslator;
+ private final boolean s3AuthRequired;
OzoneManagerServiceGrpc(
- OzoneManagerProtocolServerSideTranslatorPB omTranslator) {
+ OzoneManagerProtocolServerSideTranslatorPB omTranslator,
+ boolean s3AuthRequired) {
this.omTranslator = omTranslator;
+ this.s3AuthRequired = s3AuthRequired;
}
@Override
@@ -55,6 +71,22 @@ public void submitRequest(OMRequest request,
LOG.debug("OzoneManagerServiceGrpc: OzoneManagerServiceImplBase " +
"processing s3g client submit request - for command {}",
request.getCmdType().name());
+
+ // This endpoint carries no channel-level client authentication, so in
+ // secure mode a request acting on behalf of a user must present S3
+ // authentication (validated downstream against the caller's S3 secret);
+ // without it the request identity would be client-asserted. Only the
+ // identity-free bootstrap reads in S3_AUTH_EXEMPT_CMD_TYPES are exempt.
+ if (s3AuthRequired && !request.hasS3Authentication()
+ && !S3_AUTH_EXEMPT_CMD_TYPES.contains(request.getCmdType())) {
+ LOG.warn("Rejecting {} OMRequest without S3 authentication received" +
+ " on the S3G gRPC endpoint", request.getCmdType().name());
+ responseObserver.onError(Status.UNAUTHENTICATED
+ .withDescription("S3 authentication is required for requests to" +
+ " this endpoint in secure mode (ozone.om.s3.grpc.auth.required)")
+ .asRuntimeException());
+ return;
+ }
AtomicInteger callCount = new AtomicInteger(0);
org.apache.hadoop.ipc_.Server.getCurCall().set(new Server.Call(1,
diff --git
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/OMClientRequest.java
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/OMClientRequest.java
index 884c5fa310b..a916475f7c4 100644
---
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/OMClientRequest.java
+++
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/OMClientRequest.java
@@ -177,8 +177,11 @@ public OzoneManagerProtocolProtos.UserInfo getUserInfo()
throws IOException {
userInfo.setUserName(user.getUserName());
}
- // for gRPC s3g omRequests that contain user name
- if (user == null && omRequest.hasUserInfo()) {
+ // for gRPC s3g omRequests that contain user name. Only adopt the
+ // client-supplied user name when no identity was established above:
+ // it is unauthenticated data and must never override the identity
+ // derived from S3 authentication or from the RPC user.
+ if (user == null && !userInfo.hasUserName() && omRequest.hasUserInfo()) {
userInfo.setUserName(omRequest.getUserInfo().getUserName());
}
diff --git
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestGrpcOzoneManagerServer.java
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestGrpcOzoneManagerServer.java
index 106efd73d68..d23da421fb1 100644
---
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestGrpcOzoneManagerServer.java
+++
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestGrpcOzoneManagerServer.java
@@ -17,9 +17,31 @@
package org.apache.hadoop.ozone.om;
+import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_GRPC_TLS_ENABLED;
+import static
org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_SECURITY_ENABLED_KEY;
+import static
org.apache.hadoop.ozone.om.request.OMRequestTestUtils.createRequestWithS3Credentials;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+import com.google.protobuf.ServiceException;
+import io.grpc.stub.StreamObserver;
import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import
org.apache.hadoop.hdds.security.x509.certificate.client.CertificateClient;
+import org.apache.hadoop.hdds.security.x509.exception.CertificateException;
+import org.apache.hadoop.metrics2.lib.DefaultMetricsSystem;
+import org.apache.hadoop.ozone.grpc.metrics.GrpcMetrics;
+import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRequest;
+import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMResponse;
+import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.Status;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.Type;
import
org.apache.hadoop.ozone.protocolPB.OzoneManagerProtocolServerSideTranslatorPB;
import org.junit.jupiter.api.Test;
@@ -47,4 +69,134 @@ public void testStartStop() throws Exception {
}
}
+ @Test
+ public void testRequestWithoutS3AuthRejectedWhenAuthRequired() {
+ OzoneManagerProtocolServerSideTranslatorPB omTranslator =
+ mock(OzoneManagerProtocolServerSideTranslatorPB.class);
+ OzoneManagerServiceGrpc service =
+ new OzoneManagerServiceGrpc(omTranslator, true);
+ OMRequest request = OMRequest.newBuilder()
+ .setCmdType(Type.GetS3VolumeContext)
+ .setClientId("test-client")
+ .build();
+ @SuppressWarnings("unchecked")
+ StreamObserver<OMResponse> observer = mock(StreamObserver.class);
+
+ service.submitRequest(request, observer);
+
+ verify(observer).onError(any());
+ verify(observer, never()).onNext(any());
+ verifyNoInteractions(omTranslator);
+ }
+
+ @Test
+ public void testServiceListWithoutS3AuthAllowedForS3gBootstrap()
+ throws ServiceException {
+ // ServiceList is the identity-free bootstrap read the S3 gateway issues
+ // before any S3Auth exists (see S3_AUTH_EXEMPT_CMD_TYPES); it must pass
+ // even when enforcement is on, or the gateway cannot create its OM client.
+ OMRequest request = OMRequest.newBuilder()
+ .setCmdType(Type.ServiceList)
+ .setClientId("test-client")
+ .build();
+
+ assertRequestPassedThrough(request, true);
+ }
+
+ @Test
+ public void testRequestWithS3AuthAllowedWhenAuthRequired()
+ throws ServiceException {
+ // A request that carries S3 authentication is passed through to the
+ // translator, which validates the credential; the gate only rejects
+ // requests that carry no S3 authentication at all.
+ OMRequest request = createRequestWithS3Credentials("accessId", "signature",
+ "stringToSign").toBuilder()
+ .setCmdType(Type.GetS3VolumeContext)
+ .build();
+
+ assertRequestPassedThrough(request, true);
+ }
+
+ @Test
+ public void testRequestWithoutS3AuthAllowedWhenAuthNotRequired()
+ throws ServiceException {
+ // With enforcement off (non-secure mode, or the knob disabled) a request
+ // without S3 authentication is passed through unchanged.
+ OMRequest request = OMRequest.newBuilder()
+ .setCmdType(Type.GetS3VolumeContext)
+ .setClientId("test-client")
+ .build();
+
+ assertRequestPassedThrough(request, false);
+ }
+
+ /**
+ * Asserts the request is forwarded to the translator and its response is
+ * relayed back to the client (onNext + onCompleted, no onError).
+ */
+ private static void assertRequestPassedThrough(OMRequest request,
+ boolean s3AuthRequired) throws ServiceException {
+ OzoneManagerProtocolServerSideTranslatorPB omTranslator =
+ mock(OzoneManagerProtocolServerSideTranslatorPB.class);
+ OMResponse response = OMResponse.newBuilder()
+ .setCmdType(request.getCmdType())
+ .setStatus(Status.OK)
+ .build();
+ when(omTranslator.submitRequest(any(), any())).thenReturn(response);
+ OzoneManagerServiceGrpc service =
+ new OzoneManagerServiceGrpc(omTranslator, s3AuthRequired);
+ @SuppressWarnings("unchecked")
+ StreamObserver<OMResponse> observer = mock(StreamObserver.class);
+
+ service.submitRequest(request, observer);
+
+ verify(omTranslator).submitRequest(any(), any());
+ verify(observer).onNext(response);
+ verify(observer, never()).onError(any());
+ verify(observer).onCompleted();
+ }
+
+ @Test
+ public void testTlsSetupFailureFailsClosed() throws Exception {
+ // In secure mode with gRPC TLS enabled, a failure setting up TLS must fail
+ // OM startup rather than silently exposing a plaintext, unauthenticated
+ // endpoint, and must not leak the GrpcMetrics registration on the way out.
+ OzoneConfiguration conf = new OzoneConfiguration();
+ conf.setBoolean(OZONE_SECURITY_ENABLED_KEY, true);
+ conf.setBoolean(HDDS_GRPC_TLS_ENABLED, true);
+ OzoneManager ozoneManager = mock(OzoneManager.class);
+ CertificateClient caClient = mock(CertificateClient.class);
+ when(caClient.getKeyManager())
+ .thenThrow(new CertificateException("injected TLS setup failure"));
+
+ assertThrows(IllegalStateException.class, () ->
+ new GrpcOzoneManagerServer(conf, ozoneManager.getOmServerProtocol(),
+ ozoneManager.getDelegationTokenMgr(), caClient, ""));
+ assertNull(DefaultMetricsSystem.instance()
+ .getSource(GrpcMetrics.class.getSimpleName()));
+ }
+
+ @Test
+ public void testS3AuthNotRequiredWhenSecurityDisabled() {
+ // The gate is off in non-secure mode even if the knob is left on.
+ OzoneConfiguration conf = new OzoneConfiguration();
+ conf.setBoolean(OMConfigKeys.OZONE_OM_S3_GRPC_AUTH_REQUIRED, true);
+ assertFalse(GrpcOzoneManagerServer.isS3AuthRequired(conf));
+ }
+
+ @Test
+ public void testS3AuthRequiredByDefaultInSecureMode() {
+ OzoneConfiguration conf = new OzoneConfiguration();
+ conf.setBoolean(OZONE_SECURITY_ENABLED_KEY, true);
+ assertTrue(GrpcOzoneManagerServer.isS3AuthRequired(conf));
+ }
+
+ @Test
+ public void testS3AuthCanBeDisabledInSecureMode() {
+ OzoneConfiguration conf = new OzoneConfiguration();
+ conf.setBoolean(OZONE_SECURITY_ENABLED_KEY, true);
+ conf.setBoolean(OMConfigKeys.OZONE_OM_S3_GRPC_AUTH_REQUIRED, false);
+ assertFalse(GrpcOzoneManagerServer.isS3AuthRequired(conf));
+ }
+
}
diff --git
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/TestOMClientRequestWithUserInfo.java
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/TestOMClientRequestWithUserInfo.java
index 38036912b96..2dc4e6b5d67 100644
---
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/TestOMClientRequestWithUserInfo.java
+++
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/TestOMClientRequestWithUserInfo.java
@@ -42,6 +42,7 @@
import org.apache.hadoop.ozone.om.OmMetadataManagerImpl;
import org.apache.hadoop.ozone.om.OzoneManager;
import org.apache.hadoop.ozone.om.helpers.BucketLayout;
+import org.apache.hadoop.ozone.om.protocolPB.grpc.GrpcClientConstants;
import org.apache.hadoop.ozone.om.request.bucket.OMBucketCreateRequest;
import org.apache.hadoop.ozone.om.request.key.OMKeyCommitRequest;
import org.apache.hadoop.ozone.om.upgrade.OMLayoutVersionManager;
@@ -138,35 +139,43 @@ public void testUserInfoInCaseOfHadoopTransport() throws
Exception {
}
@Test
- public void testUserInfoInCaseOfGrpcTransport() throws IOException {
- try (MockedStatic<Context> mockedGrpcRequestContextKey =
- mockStatic(Context.class)) {
- // given
- Context.Key<String> hostnameKey = mock(Context.Key.class);
- when(hostnameKey.get()).thenReturn("hostname");
-
- Context.Key<String> ipAddress = mock(Context.Key.class);
- when(ipAddress.get()).thenReturn("172.5.3.5");
-
- mockedGrpcRequestContextKey.when(() -> Context.key("CLIENT_HOSTNAME"))
- .thenReturn(hostnameKey);
- mockedGrpcRequestContextKey.when(() -> Context.key("CLIENT_IP_ADDRESS"))
- .thenReturn(ipAddress);
-
- OMRequest s3SignedOMRequest = createRequestWithS3Credentials("AccessId",
- "Signature", "StringToSign");
- OMClientRequest omClientRequest =
- new OMKeyCommitRequest(s3SignedOMRequest, mock(BucketLayout.class));
-
- // when
- OzoneManagerProtocolProtos.UserInfo userInfo =
- omClientRequest.getUserInfo();
-
- // then
- assertEquals("hostname", userInfo.getHostName());
- assertEquals("172.5.3.5", userInfo.getRemoteAddress());
- assertEquals("AccessId", userInfo.getUserName());
- }
+ public void testUserInfoInCaseOfGrpcTransport() throws Exception {
+ OMRequest s3SignedOMRequest = createRequestWithS3Credentials("AccessId",
+ "Signature", "StringToSign");
+ OMClientRequest omClientRequest =
+ new OMKeyCommitRequest(s3SignedOMRequest, mock(BucketLayout.class));
+
+ // The gRPC transport propagates the client host/IP through the request
+ // Context rather than the Hadoop RPC Server thread-local, so attach a real
+ // Context carrying those values and resolve the UserInfo within it.
+ Context grpcContext = Context.current()
+ .withValue(GrpcClientConstants.CLIENT_HOSTNAME_CTX_KEY, "hostname")
+ .withValue(GrpcClientConstants.CLIENT_IP_ADDRESS_CTX_KEY, "172.5.3.5");
+ OzoneManagerProtocolProtos.UserInfo userInfo =
+ grpcContext.call(omClientRequest::getUserInfo);
+
+ assertEquals("hostname", userInfo.getHostName());
+ assertEquals("172.5.3.5", userInfo.getRemoteAddress());
+ assertEquals("AccessId", userInfo.getUserName());
+ }
+
+ @Test
+ public void testClientSuppliedUserNameDoesNotOverrideS3AuthIdentity()
+ throws IOException {
+ // A gRPC S3G request carries validated S3 authentication (accessId
+ // "AccessId") but also a client-supplied userInfo.userName. The
+ // client-supplied name is unauthenticated and must never override the
+ // identity derived from S3 authentication; getUserInfo() must resolve the
+ // acting user from the accessId, not the forged name.
+ OMRequest request = createRequestWithS3Credentials("AccessId", "Signature",
+ "StringToSign").toBuilder()
+ .setUserInfo(OzoneManagerProtocolProtos.UserInfo.newBuilder()
+ .setUserName("forged-admin"))
+ .build();
+ OMClientRequest omClientRequest =
+ new OMKeyCommitRequest(request, mock(BucketLayout.class));
+
+ assertEquals("AccessId", omClientRequest.getUserInfo().getUserName());
}
}
diff --git
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3secret/S3SecretEndpointBase.java
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3secret/S3SecretEndpointBase.java
index 9a1a6af21ea..c63751c8308 100644
---
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3secret/S3SecretEndpointBase.java
+++
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3secret/S3SecretEndpointBase.java
@@ -17,6 +17,9 @@
package org.apache.hadoop.ozone.s3secret;
+import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_TRANSPORT_CLASS;
+import static
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_TRANSPORT_CLASS_DEFAULT;
+
import com.google.common.annotations.VisibleForTesting;
import java.io.IOException;
import java.util.Map;
@@ -25,6 +28,7 @@
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.core.Context;
import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.ozone.OzoneSecurityUtil;
import org.apache.hadoop.ozone.audit.AuditAction;
import org.apache.hadoop.ozone.audit.AuditEventStatus;
import org.apache.hadoop.ozone.audit.AuditLogger;
@@ -35,12 +39,17 @@
import org.apache.hadoop.ozone.om.protocol.S3Auth;
import org.apache.hadoop.ozone.s3.OzoneClientCache;
import org.apache.hadoop.ozone.s3.util.AuditUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/**
* Base implementation of endpoint for working with S3 secret.
*/
public class S3SecretEndpointBase implements Auditor {
+ private static final Logger LOG =
+ LoggerFactory.getLogger(S3SecretEndpointBase.class);
+
private final OzoneConfiguration conf;
private OzoneClient client;
@@ -54,6 +63,24 @@ public class S3SecretEndpointBase implements Auditor {
S3SecretEndpointBase(OzoneConfiguration conf) {
this.conf = new OzoneConfiguration(conf);
this.conf.setBoolean(S3Auth.S3_AUTH_CHECK, false);
+ // S3 secret generate/revoke carry no per-request S3 signature, and the
+ // S3G -> OM gRPC endpoint has no client authentication. In secure mode,
+ // route these operations over the Kerberos-authenticated OM RPC transport
+ // so OM authorizes the real caller (the S3 Gateway principal, which must
+ // be an S3 administrator) instead of a client-asserted identity.
+ if (OzoneSecurityUtil.isSecurityEnabled(this.conf)) {
+ String configured = this.conf.get(OZONE_OM_TRANSPORT_CLASS);
+ if (configured != null
+ && !configured.equals(OZONE_OM_TRANSPORT_CLASS_DEFAULT)) {
+ // The gateway data path commonly runs on gRPC, which is never safe for
+ // secret ops; override it for this endpoint only and leave the rest of
+ // the S3 Gateway on the configured transport.
+ LOG.warn("Overriding OM transport from {} to {} for S3 secret "
+ + "operations in secure mode; other S3 Gateway clients are "
+ + "unaffected.", configured, OZONE_OM_TRANSPORT_CLASS_DEFAULT);
+ }
+ this.conf.set(OZONE_OM_TRANSPORT_CLASS,
OZONE_OM_TRANSPORT_CLASS_DEFAULT);
+ }
}
@PostConstruct
@@ -107,6 +134,11 @@ public void setContext(ContainerRequestContext context) {
this.context = context;
}
+ @VisibleForTesting
+ OzoneConfiguration getConf() {
+ return conf;
+ }
+
protected Map<String, String> getAuditParameters() {
return AuditUtils.getAuditParameters(context);
}
diff --git
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3secret/TestS3SecretEndpointBase.java
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3secret/TestS3SecretEndpointBase.java
new file mode 100644
index 00000000000..4a1628a0cb6
--- /dev/null
+++
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3secret/TestS3SecretEndpointBase.java
@@ -0,0 +1,69 @@
+/*
+ * 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.hadoop.ozone.s3secret;
+
+import static
org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_SECURITY_ENABLED_KEY;
+import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_TRANSPORT_CLASS;
+import static
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_TRANSPORT_CLASS_DEFAULT;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.ozone.om.protocolPB.GrpcOmTransportFactory;
+import org.apache.ozone.test.GenericTestUtils.LogCapturer;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests OM transport selection for the S3 secret endpoints.
+ */
+class TestS3SecretEndpointBase {
+
+ @Test
+ void testSecretOpsUseRpcTransportInSecureMode() {
+ OzoneConfiguration conf = new OzoneConfiguration();
+ conf.setBoolean(OZONE_SECURITY_ENABLED_KEY, true);
+ // Simulate the S3 Gateway pinning the gRPC transport for its data client.
+ conf.set(OZONE_OM_TRANSPORT_CLASS, GrpcOmTransportFactory.class.getName());
+
+ LogCapturer logs = LogCapturer.captureLogs(S3SecretEndpointBase.class);
+ S3SecretManagementEndpoint endpoint =
+ new S3SecretManagementEndpoint(conf);
+
+ // The gRPC S3 endpoint has no client authentication, so in secure mode
+ // secret ops must fall back to the Kerberos-authenticated RPC transport
+ // where OM can authorize the real caller.
+ assertEquals(OZONE_OM_TRANSPORT_CLASS_DEFAULT,
+ endpoint.getConf().get(OZONE_OM_TRANSPORT_CLASS));
+ // The override is not silent: it warns that only this endpoint changed.
+ assertThat(logs.getOutput()).contains("Overriding OM transport");
+ }
+
+ @Test
+ void testTransportUnchangedWhenSecurityDisabled() {
+ OzoneConfiguration conf = new OzoneConfiguration();
+ String grpcTransport = GrpcOmTransportFactory.class.getName();
+ conf.set(OZONE_OM_TRANSPORT_CLASS, grpcTransport);
+
+ S3SecretManagementEndpoint endpoint =
+ new S3SecretManagementEndpoint(conf);
+
+ // Non-secure mode is outside the security model; leave the transport
as-is.
+ assertEquals(grpcTransport,
+ endpoint.getConf().get(OZONE_OM_TRANSPORT_CLASS));
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]