This is an automated email from the ASF dual-hosted git repository.
sodonnell pushed a commit to branch HDDS-13323-sts
in repository https://gitbox.apache.org/repos/asf/ozone.git
The following commit(s) were added to refs/heads/HDDS-13323-sts by this push:
new 3d177048952 HDDS-14067. [STS] Plumbing and CLI utility to revoke STS
token (#9435)
3d177048952 is described below
commit 3d1770489523381de629055f485cdf5af81fa2b9
Author: fmorg-git <[email protected]>
AuthorDate: Mon Dec 8 11:46:45 2025 -0800
HDDS-14067. [STS] Plumbing and CLI utility to revoke STS token (#9435)
---
.../java/org/apache/hadoop/ozone/OzoneConsts.java | 1 +
.../ozone/shell/s3/RevokeSTSTokenHandler.java | 79 +++++
.../org/apache/hadoop/ozone/shell/s3/S3Shell.java | 3 +-
.../apache/hadoop/ozone/client/ObjectStore.java | 10 +
.../ozone/client/protocol/ClientProtocol.java | 8 +
.../apache/hadoop/ozone/client/rpc/RpcClient.java | 5 +
.../main/java/org/apache/hadoop/ozone/OmUtils.java | 1 +
.../ozone/om/protocol/OzoneManagerProtocol.java | 10 +
...OzoneManagerProtocolClientSideTranslatorPB.java | 15 +
.../src/main/proto/OmClientProtocol.proto | 11 +
.../org/apache/hadoop/ozone/audit/OMAction.java | 2 +
.../om/ratis/utils/OzoneManagerRatisUtils.java | 3 +
.../s3/security/S3RevokeSTSTokenRequest.java | 122 +++++++
.../s3/security/S3RevokeSTSTokenResponse.java | 57 ++++
.../s3/security/TestS3RevokeSTSTokenRequest.java | 353 +++++++++++++++++++++
.../hadoop/ozone/client/ClientProtocolStub.java | 3 +
16 files changed, 682 insertions(+), 1 deletion(-)
diff --git
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/OzoneConsts.java
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/OzoneConsts.java
index 99d78f786fa..c7dacac989c 100644
--- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/OzoneConsts.java
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/OzoneConsts.java
@@ -301,6 +301,7 @@ public final class OzoneConsts {
public static final String S3_GETSECRET_USER = "S3GetSecretUser";
public static final String S3_SETSECRET_USER = "S3SetSecretUser";
public static final String S3_REVOKESECRET_USER = "S3RevokeSecretUser";
+ public static final String S3_REVOKESTSTOKEN_USER = "S3RevokeSTSTokenUser";
public static final String RENAMED_KEYS_MAP = "renamedKeysMap";
public static final String UNRENAMED_KEYS_MAP = "unRenamedKeysMap";
public static final String MULTIPART_UPLOAD_PART_NUMBER = "partNumber";
diff --git
a/hadoop-ozone/cli-shell/src/main/java/org/apache/hadoop/ozone/shell/s3/RevokeSTSTokenHandler.java
b/hadoop-ozone/cli-shell/src/main/java/org/apache/hadoop/ozone/shell/s3/RevokeSTSTokenHandler.java
new file mode 100644
index 00000000000..2f63d4f2a5a
--- /dev/null
+++
b/hadoop-ozone/cli-shell/src/main/java/org/apache/hadoop/ozone/shell/s3/RevokeSTSTokenHandler.java
@@ -0,0 +1,79 @@
+/*
+ * 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.shell.s3;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.Scanner;
+import org.apache.hadoop.ozone.client.OzoneClient;
+import org.apache.hadoop.ozone.shell.OzoneAddress;
+import picocli.CommandLine.Command;
+import picocli.CommandLine.Option;
+
+/**
+ * Executes revocation of STS tokens.
+ *
+ * <p>This command marks the specified STS temporary access key id as revoked
+ * by adding it to the OM's revoked STS token table. Subsequent S3 requests
+ * using the same temporary access key id will be rejected once the revocation
+ * state has propagated.</p>
+ */
+@Command(name = "revokeststoken",
+ description = "Revoke S3 STS token for the given access key id")
+public class RevokeSTSTokenHandler extends S3Handler {
+
+ @Option(names = "-k",
+ required = true,
+ description = "STS temporary access key id (for example, ASIA...)")
+ private String accessKeyId;
+
+ @Option(names = "-t",
+ required = true,
+ description = "STS session token")
+ private String sessionToken;
+
+ @Option(names = "-y",
+ description = "Continue without interactive user confirmation")
+ private boolean yes;
+
+ @Override
+ protected boolean isApplicable() {
+ return securityEnabled();
+ }
+
+ @Override
+ protected void execute(OzoneClient client, OzoneAddress address)
+ throws IOException {
+
+ if (!yes) {
+ out().print("Enter 'y' to confirm STS token revocation for accessKeyId
'" +
+ accessKeyId + "': ");
+ out().flush();
+ final Scanner scanner = new Scanner(new InputStreamReader(System.in,
StandardCharsets.UTF_8));
+ final String confirmation = scanner.next().trim().toLowerCase();
+ if (!"y".equals(confirmation)) {
+ out().println("Revoke STS token operation cancelled.");
+ return;
+ }
+ }
+
+ client.getObjectStore().revokeSTSToken(accessKeyId, sessionToken);
+ out().println("STS token revoked for accessKeyId '" + accessKeyId + "'.");
+ }
+}
diff --git
a/hadoop-ozone/cli-shell/src/main/java/org/apache/hadoop/ozone/shell/s3/S3Shell.java
b/hadoop-ozone/cli-shell/src/main/java/org/apache/hadoop/ozone/shell/s3/S3Shell.java
index 8c35a0c2e15..014ea4c83bd 100644
---
a/hadoop-ozone/cli-shell/src/main/java/org/apache/hadoop/ozone/shell/s3/S3Shell.java
+++
b/hadoop-ozone/cli-shell/src/main/java/org/apache/hadoop/ozone/shell/s3/S3Shell.java
@@ -28,7 +28,8 @@
subcommands = {
GetS3SecretHandler.class,
SetS3SecretHandler.class,
- RevokeS3SecretHandler.class
+ RevokeS3SecretHandler.class,
+ RevokeSTSTokenHandler.class
})
public class S3Shell extends Shell {
diff --git
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/ObjectStore.java
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/ObjectStore.java
index e783bafe227..226ebbfb034 100644
---
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/ObjectStore.java
+++
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/ObjectStore.java
@@ -766,6 +766,16 @@ public AssumeRoleResponseInfo assumeRole(String roleArn,
String roleSessionName,
return proxy.assumeRole(roleArn, roleSessionName, durationSeconds,
awsIamSessionPolicy);
}
+ /**
+ * Revokes an STS token.
+ * @param accessKeyId The STS accessKeyId (starting with ASIA...)
+ * @param sessionToken The STS session token
+ * @throws IOException if an error occurs while revoking the STS
token
+ */
+ public void revokeSTSToken(String accessKeyId, String sessionToken) throws
IOException {
+ proxy.revokeSTSToken(accessKeyId, sessionToken);
+ }
+
/**
* An Iterator to iterate over {@link SnapshotDiffJobIterator} list.
*/
diff --git
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java
index d4cc1d1fb51..0067407aff3 100644
---
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java
+++
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java
@@ -1372,4 +1372,12 @@ void deleteObjectTagging(String volumeName, String
bucketName, String keyName)
*/
AssumeRoleResponseInfo assumeRole(String roleArn, String roleSessionName,
int durationSeconds,
String awsIamSessionPolicy) throws IOException;
+
+ /**
+ * Revokes an STS token.
+ * @param accessKeyId The STS accessKeyId (starting with ASIA...)
+ * @param sessionToken The STS session token
+ * @throws IOException if an error occurs while revoking the STS
token
+ */
+ void revokeSTSToken(String accessKeyId, String sessionToken) throws
IOException;
}
diff --git
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java
index 5c3b8eb4793..791a159f01b 100644
---
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java
+++
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java
@@ -2797,6 +2797,11 @@ public AssumeRoleResponseInfo assumeRole(String roleArn,
String roleSessionName,
return ozoneManagerClient.assumeRole(roleArn, roleSessionName,
durationSeconds, awsIamSessionPolicy);
}
+ @Override
+ public void revokeSTSToken(String accessKeyId, String sessionToken) throws
IOException {
+ ozoneManagerClient.revokeSTSToken(accessKeyId, sessionToken);
+ }
+
private static ExecutorService createThreadPoolExecutor(
int corePoolSize, int maximumPoolSize, String threadNameFormat) {
return new ThreadPoolExecutor(corePoolSize, maximumPoolSize,
diff --git
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/OmUtils.java
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/OmUtils.java
index be1c422711a..dd70a9056f9 100644
--- a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/OmUtils.java
+++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/OmUtils.java
@@ -321,6 +321,7 @@ public static boolean isReadOnly(
case DeleteOpenKeys:
case SetS3Secret:
case RevokeS3Secret:
+ case RevokeSTSToken:
case PurgeDirectories:
case PurgePaths:
case CreateTenant:
diff --git
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/OzoneManagerProtocol.java
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/OzoneManagerProtocol.java
index 4261f71c4e5..f98196d7276 100644
---
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/OzoneManagerProtocol.java
+++
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/OzoneManagerProtocol.java
@@ -1191,4 +1191,14 @@ default AssumeRoleResponseInfo assumeRole(String
roleArn, String roleSessionName
String awsIamSessionPolicy) throws IOException {
throw new UnsupportedOperationException("OzoneManager does not require
this to be implemented");
}
+
+ /**
+ * Revokes an STS token.
+ * @param accessKeyId The STS accessKeyId (starting with ASIA...)
+ * @param sessionToken The STS session token
+ * @throws IOException if an error occurs while revoking the STS
token
+ */
+ default void revokeSTSToken(String accessKeyId, String sessionToken) throws
IOException {
+ throw new UnsupportedOperationException("OzoneManager does not require
this to be implemented");
+ }
}
diff --git
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
index 36adbe7b37f..105d353a463 100644
---
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
+++
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
@@ -2675,6 +2675,21 @@ public AssumeRoleResponseInfo assumeRole(String roleArn,
String roleSessionName,
handleError(submitRequest(omRequest)).getAssumeRoleResponse());
}
+ @Override
+ public void revokeSTSToken(String accessKeyId, String sessionToken) throws
IOException {
+ final OzoneManagerProtocolProtos.RevokeSTSTokenRequest request =
+ OzoneManagerProtocolProtos.RevokeSTSTokenRequest.newBuilder()
+ .setAccessKeyId(accessKeyId)
+ .setSessionToken(sessionToken)
+ .build();
+
+ final OMRequest omRequest = createOMRequest(Type.RevokeSTSToken)
+ .setRevokeSTSTokenRequest(request)
+ .build();
+
+ handleError(submitRequest(omRequest));
+ }
+
private SafeMode toProtoBuf(SafeModeAction action) {
switch (action) {
case ENTER:
diff --git
a/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
b/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
index 8e455e70342..6e36be5ca48 100644
--- a/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
+++ b/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
@@ -157,6 +157,7 @@ enum Type {
GetObjectTagging = 141;
DeleteObjectTagging = 142;
AssumeRole = 143;
+ RevokeSTSToken = 144;
}
enum SafeMode {
@@ -306,6 +307,7 @@ message OMRequest {
optional DeleteObjectTaggingRequest deleteObjectTaggingRequest =
142;
repeated SetSnapshotPropertyRequest SetSnapshotPropertyRequests =
143;
optional AssumeRoleRequest assumeRoleRequest =
144;
+ optional RevokeSTSTokenRequest revokeSTSTokenRequest =
145;
}
message OMResponse {
@@ -440,6 +442,7 @@ message OMResponse {
optional PutObjectTaggingResponse putObjectTaggingResponse =
141;
optional DeleteObjectTaggingResponse deleteObjectTaggingResponse =
142;
optional AssumeRoleResponse assumeRoleResponse =
143;
+ optional RevokeSTSTokenResponse revokeSTSTokenResponse =
144;
}
enum Status {
@@ -2381,6 +2384,14 @@ message AssumeRoleResponse {
required string assumedRoleId = 5;
}
+message RevokeSTSTokenRequest {
+ required string accessKeyId = 1;
+ required string sessionToken = 2;
+}
+
+message RevokeSTSTokenResponse {
+}
+
/**
The OM service that takes care of Ozone namespace.
*/
diff --git
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/audit/OMAction.java
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/audit/OMAction.java
index 9be2bdea709..f07c4494619 100644
---
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/audit/OMAction.java
+++
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/audit/OMAction.java
@@ -83,7 +83,9 @@ public enum OMAction implements AuditAction {
SET_S3_SECRET,
REVOKE_S3_SECRET,
+ // STS Actions
S3_ASSUME_ROLE,
+ REVOKE_STS_TOKEN,
CREATE_TENANT,
DELETE_TENANT,
diff --git
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ratis/utils/OzoneManagerRatisUtils.java
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ratis/utils/OzoneManagerRatisUtils.java
index 5548be7bd8b..706e00f9537 100644
---
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ratis/utils/OzoneManagerRatisUtils.java
+++
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ratis/utils/OzoneManagerRatisUtils.java
@@ -69,6 +69,7 @@
import
org.apache.hadoop.ozone.om.request.s3.multipart.S3ExpiredMultipartUploadsAbortRequest;
import org.apache.hadoop.ozone.om.request.s3.security.OMSetSecretRequest;
import org.apache.hadoop.ozone.om.request.s3.security.S3GetSecretRequest;
+import org.apache.hadoop.ozone.om.request.s3.security.S3RevokeSTSTokenRequest;
import org.apache.hadoop.ozone.om.request.s3.security.S3RevokeSecretRequest;
import
org.apache.hadoop.ozone.om.request.s3.tenant.OMSetRangerServiceVersionRequest;
import org.apache.hadoop.ozone.om.request.s3.tenant.OMTenantAssignAdminRequest;
@@ -196,6 +197,8 @@ public static OMClientRequest createClientRequest(OMRequest
omRequest,
return new OMSetSecretRequest(omRequest);
case RevokeS3Secret:
return new S3RevokeSecretRequest(omRequest);
+ case RevokeSTSToken:
+ return new S3RevokeSTSTokenRequest(omRequest);
case PurgeKeys:
return new OMKeyPurgeRequest(omRequest);
case PurgeDirectories:
diff --git
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3RevokeSTSTokenRequest.java
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3RevokeSTSTokenRequest.java
new file mode 100644
index 00000000000..ff7a3831d0d
--- /dev/null
+++
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3RevokeSTSTokenRequest.java
@@ -0,0 +1,122 @@
+/*
+ * 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.om.request.s3.security;
+
+import java.io.IOException;
+import java.time.Clock;
+import java.time.ZoneOffset;
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.hadoop.ozone.OzoneConsts;
+import org.apache.hadoop.ozone.audit.OMAction;
+import org.apache.hadoop.ozone.om.OzoneManager;
+import org.apache.hadoop.ozone.om.exceptions.OMException;
+import org.apache.hadoop.ozone.om.execution.flowcontrol.ExecutionContext;
+import org.apache.hadoop.ozone.om.request.OMClientRequest;
+import org.apache.hadoop.ozone.om.request.util.OmResponseUtil;
+import org.apache.hadoop.ozone.om.response.OMClientResponse;
+import
org.apache.hadoop.ozone.om.response.s3.security.S3RevokeSTSTokenResponse;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos;
+import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRequest;
+import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMResponse;
+import org.apache.hadoop.ozone.security.STSSecurityUtil;
+import org.apache.hadoop.ozone.security.STSTokenIdentifier;
+import org.apache.hadoop.security.UserGroupInformation;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Handles S3RevokeSTSTokenRequest request.
+ *
+ * <p>This request marks an STS temporary access key id as revoked by inserting
+ * it into the {@code s3RevokedStsTokenTable}. Subsequent S3 requests
+ * authenticated with the same STS access key id will be rejected when the
+ * revocation state has propagated.</p>
+ */
+public class S3RevokeSTSTokenRequest extends OMClientRequest {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(S3RevokeSTSTokenRequest.class);
+ private static final Clock CLOCK = Clock.system(ZoneOffset.UTC);
+
+ private String originalAccessKeyId;
+
+ public S3RevokeSTSTokenRequest(OMRequest omRequest) {
+ super(omRequest);
+ }
+
+ @Override
+ public OMRequest preExecute(OzoneManager ozoneManager) throws IOException {
+ final OzoneManagerProtocolProtos.RevokeSTSTokenRequest revokeReq =
+ getOmRequest().getRevokeSTSTokenRequest();
+
+ // Get the original (long-lived) access key id from the session token
+ // and enforce the same permission model that is used for S3 secret
+ // operations (get/set/revoke). Only the owner of the original access
+ // key (or an S3 / tenant admin) is allowed to revoke its temporary
+ // STS credentials.
+ final String sessionToken = revokeReq.getSessionToken();
+ final String tempAccessKeyId = revokeReq.getAccessKeyId();
+ final STSTokenIdentifier stsTokenIdentifier =
STSSecurityUtil.constructValidateAndDecryptSTSToken(
+ sessionToken, ozoneManager.getSecretKeyClient(), CLOCK);
+ originalAccessKeyId = stsTokenIdentifier.getOriginalAccessKeyId();
+
+ // Validate that the Access Key ID in the request matches the one in the
token
+ // to prevent users from revoking arbitrary keys using a valid token.
+ if (!stsTokenIdentifier.getTempAccessKeyId().equals(tempAccessKeyId)) {
+ throw new OMException("Access Key ID in request does not match the
session token",
+ OMException.ResultCodes.INVALID_REQUEST);
+ }
+
+ final UserGroupInformation ugi =
S3SecretRequestHelper.getOrCreateUgi(originalAccessKeyId);
+ S3SecretRequestHelper.checkAccessIdSecretOpPermission(ozoneManager, ugi,
originalAccessKeyId);
+
+ final OMRequest.Builder omRequest = OMRequest.newBuilder()
+ .setRevokeSTSTokenRequest(revokeReq)
+ .setCmdType(getOmRequest().getCmdType())
+ .setClientId(getOmRequest().getClientId())
+ .setUserInfo(getUserInfo());
+
+ if (getOmRequest().hasTraceID()) {
+ omRequest.setTraceID(getOmRequest().getTraceID());
+ }
+
+ return omRequest.build();
+ }
+
+ @Override
+ public OMClientResponse validateAndUpdateCache(OzoneManager ozoneManager,
ExecutionContext context) {
+ final OMResponse.Builder omResponse =
OmResponseUtil.getOMResponseBuilder(getOmRequest());
+
+ final OzoneManagerProtocolProtos.RevokeSTSTokenRequest revokeReq =
getOmRequest().getRevokeSTSTokenRequest();
+ final String accessKeyId = revokeReq.getAccessKeyId();
+ final String sessionToken = revokeReq.getSessionToken();
+
+ // All actual DB mutations are done in the response's addToDBBatch().
+ final OMClientResponse omClientResponse = new S3RevokeSTSTokenResponse(
+ accessKeyId, sessionToken, omResponse.build());
+
+ // Audit log
+ final Map<String, String> auditMap = new HashMap<>();
+ auditMap.put(OzoneConsts.S3_REVOKESTSTOKEN_USER, originalAccessKeyId);
+ markForAudit(ozoneManager.getAuditLogger(), buildAuditMessage(
+ OMAction.REVOKE_STS_TOKEN, auditMap, null,
getOmRequest().getUserInfo()));
+
+ LOG.info("Marked STS temporary access key '{}' as revoked.", accessKeyId);
+ return omClientResponse;
+ }
+}
diff --git
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/s3/security/S3RevokeSTSTokenResponse.java
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/s3/security/S3RevokeSTSTokenResponse.java
new file mode 100644
index 00000000000..523311bbadb
--- /dev/null
+++
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/s3/security/S3RevokeSTSTokenResponse.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.hadoop.ozone.om.response.s3.security;
+
+import static
org.apache.hadoop.ozone.om.codec.OMDBDefinition.S3_REVOKED_STS_TOKEN_TABLE;
+import static
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.Status.OK;
+
+import jakarta.annotation.Nonnull;
+import java.io.IOException;
+import org.apache.hadoop.hdds.utils.db.BatchOperation;
+import org.apache.hadoop.hdds.utils.db.Table;
+import org.apache.hadoop.ozone.om.OMMetadataManager;
+import org.apache.hadoop.ozone.om.response.CleanupTableInfo;
+import org.apache.hadoop.ozone.om.response.OMClientResponse;
+import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMResponse;
+
+/**
+ * Response for RevokeSTSToken request.
+ */
+@CleanupTableInfo(cleanupTables = {S3_REVOKED_STS_TOKEN_TABLE})
+public class S3RevokeSTSTokenResponse extends OMClientResponse {
+
+ private final String accessKeyId;
+ private final String sessionToken;
+
+ public S3RevokeSTSTokenResponse(String accessKeyId, String sessionToken,
@Nonnull OMResponse omResponse) {
+ super(omResponse);
+ this.accessKeyId = accessKeyId;
+ this.sessionToken = sessionToken;
+ }
+
+ @Override
+ public void addToDBBatch(OMMetadataManager omMetadataManager, BatchOperation
batchOperation) throws IOException {
+ if (accessKeyId != null && getOMResponse().hasStatus() &&
getOMResponse().getStatus() == OK) {
+ final Table<String, String> table =
omMetadataManager.getS3RevokedStsTokenTable();
+ if (table != null) {
+ // Store sessionToken as value
+ table.putWithBatch(batchOperation, accessKeyId, sessionToken);
+ }
+ }
+ }
+}
diff --git
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/security/TestS3RevokeSTSTokenRequest.java
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/security/TestS3RevokeSTSTokenRequest.java
new file mode 100644
index 00000000000..9a68c047f00
--- /dev/null
+++
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/security/TestS3RevokeSTSTokenRequest.java
@@ -0,0 +1,353 @@
+/*
+ * 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.om.request.s3.security;
+
+import static
org.apache.hadoop.security.authentication.util.KerberosName.DEFAULT_MECHANISM;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.util.Optional;
+import java.util.UUID;
+import org.apache.hadoop.hdds.security.symmetric.SecretKeyClient;
+import org.apache.hadoop.ipc.ExternalCall;
+import org.apache.hadoop.ipc.Server;
+import org.apache.hadoop.ozone.om.OMMultiTenantManager;
+import org.apache.hadoop.ozone.om.OzoneManager;
+import org.apache.hadoop.ozone.om.exceptions.OMException;
+import org.apache.hadoop.ozone.om.request.OMClientRequest;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos;
+import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRequest;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.Type;
+import org.apache.hadoop.ozone.security.STSTokenSecretManager;
+import org.apache.hadoop.ozone.security.SecretKeyTestClient;
+import org.apache.hadoop.security.UserGroupInformation;
+import org.apache.hadoop.security.authentication.util.KerberosName;
+import org.apache.ozone.test.TestClock;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for {@link S3RevokeSTSTokenRequest}.
+ */
+public class TestS3RevokeSTSTokenRequest {
+
+ private static final TestClock CLOCK = TestClock.newInstance();
+
+ private STSTokenSecretManager stsTokenSecretManager;
+ private SecretKeyClient secretKeyClient;
+ private OMMultiTenantManager omMultiTenantManager;
+
+ @BeforeEach
+ public void setUp() throws Exception {
+ // Initialize KerberosName rules so that UGI short names derived from
+ // principals like "[email protected]" are computed correctly.
+ KerberosName.setRuleMechanism(DEFAULT_MECHANISM);
+ KerberosName.setRules(
+ "RULE:[2:$1@$0](.*@EXAMPLE.COM)s/@.*//\n" +
"RULE:[1:$1@$0](.*@EXAMPLE.COM)s/@.*//\n" + "DEFAULT");
+
+ secretKeyClient = new SecretKeyTestClient();
+ stsTokenSecretManager = new STSTokenSecretManager(secretKeyClient);
+ // Multi-tenant manager mock used for tests that exercise the S3
multi-tenancy permission branch.
+ omMultiTenantManager = mock(OMMultiTenantManager.class);
+ }
+
+ @AfterEach
+ public void tearDown() {
+ Server.getCurCall().remove();
+ }
+
+ @Test
+ public void testPreExecuteFailsForNonOwnerOfOriginalAccessKey() throws
Exception {
+ // Verify that preExecute enforces permissions based on the original
access key id encoded in the STS token
+ // and rejects revocation attempts from non-owners.
+ final String tempAccessKeyId = "ASIA12345678";
+ final String originalAccessKeyId = "original-access-key-id";
+ final String sessionToken = createSessionToken(tempAccessKeyId,
originalAccessKeyId);
+
+ // An RPC call running another Kerberos identity should NOT be allowed to
revoke the token whose original
+ // access key id is different.
+ final UserGroupInformation tempUgi =
UserGroupInformation.createRemoteUser("another-kerberos-identity");
+ Server.getCurCall().set(new StubCall(tempUgi));
+
+ OMException ex;
+ try (OzoneManager ozoneManager = mock(OzoneManager.class)) {
+ when(ozoneManager.isS3MultiTenancyEnabled()).thenReturn(false);
+ when(ozoneManager.isS3Admin(any(UserGroupInformation.class)))
+ .thenReturn(false);
+ when(ozoneManager.getSecretKeyClient()).thenReturn(secretKeyClient);
+
+ final OzoneManagerProtocolProtos.RevokeSTSTokenRequest revokeRequest =
+ OzoneManagerProtocolProtos.RevokeSTSTokenRequest.newBuilder()
+ .setAccessKeyId(tempAccessKeyId)
+ .setSessionToken(sessionToken)
+ .build();
+
+ final OMRequest omRequest = OMRequest.newBuilder()
+ .setClientId(UUID.randomUUID().toString())
+ .setCmdType(Type.RevokeSTSToken)
+ .setRevokeSTSTokenRequest(revokeRequest)
+ .build();
+
+ final OMClientRequest omClientRequest = new
S3RevokeSTSTokenRequest(omRequest);
+
+ ex = assertThrows(OMException.class, () ->
omClientRequest.preExecute(ozoneManager));
+ }
+ assertEquals(OMException.ResultCodes.USER_MISMATCH, ex.getResult());
+ }
+
+ @Test
+ public void testPreExecuteSucceedsForOriginalAccessKeyOwner() throws
Exception {
+ // Verify that preExecute allows the owner of the original access key id
(as encoded in the STS token)
+ // to revoke the temporary credentials.
+ final String tempAccessKeyId = "ASIA4567891230";
+ final String originalAccessKeyId = "original-access-key-id";
+ final String sessionToken = createSessionToken(tempAccessKeyId,
originalAccessKeyId);
+
+ // Simulate RPC call running as originalAccessKeyId
+ final UserGroupInformation originalUgi =
UserGroupInformation.createRemoteUser(originalAccessKeyId);
+ Server.getCurCall().set(new StubCall(originalUgi));
+
+ final OzoneManager ozoneManager = mock(OzoneManager.class);
+ when(ozoneManager.isS3MultiTenancyEnabled()).thenReturn(false);
+ when(ozoneManager.isS3Admin(any(UserGroupInformation.class)))
+ .thenReturn(false);
+ when(ozoneManager.getSecretKeyClient()).thenReturn(secretKeyClient);
+
+ final OzoneManagerProtocolProtos.RevokeSTSTokenRequest revokeRequest =
+ OzoneManagerProtocolProtos.RevokeSTSTokenRequest.newBuilder()
+ .setAccessKeyId(tempAccessKeyId)
+ .setSessionToken(sessionToken)
+ .build();
+
+ final OMRequest omRequest = OMRequest.newBuilder()
+ .setClientId(UUID.randomUUID().toString())
+ .setCmdType(Type.RevokeSTSToken)
+ .setRevokeSTSTokenRequest(revokeRequest)
+ .build();
+
+ final OMClientRequest omClientRequest = new
S3RevokeSTSTokenRequest(omRequest);
+ final OMRequest result = omClientRequest.preExecute(ozoneManager);
+ assertEquals(Type.RevokeSTSToken, result.getCmdType());
+ }
+
+ @Test
+ public void testPreExecuteSucceedsForTenantAccessIdOwner() throws Exception {
+ // When S3 multi-tenancy is enabled and the original access key id is
assigned to a tenant, verify that
+ // the tenant access ID owner is allowed to revoke the temporary
credentials.
+ final String tenantId = "finance";
+ final String originalAccessKeyId = "[email protected]";
+ final String tempAccessKeyId = "ASIA123456789";
+ final String sessionToken = createSessionToken(tempAccessKeyId,
originalAccessKeyId);
+
+ // Caller short name "alice" should match the owner username returned from
the multi-tenant manager.
+ final UserGroupInformation callerUgi =
UserGroupInformation.createRemoteUser(originalAccessKeyId);
+ Server.getCurCall().set(new StubCall(callerUgi));
+
+ final OzoneManager ozoneManager = mock(OzoneManager.class);
+ when(ozoneManager.isS3MultiTenancyEnabled()).thenReturn(true);
+
when(ozoneManager.getMultiTenantManager()).thenReturn(omMultiTenantManager);
+ when(ozoneManager.getSecretKeyClient()).thenReturn(secretKeyClient);
+
+ // Original access key id is assigned to a tenant and owned by "alice".
+ when(omMultiTenantManager.getTenantForAccessID(originalAccessKeyId))
+ .thenReturn(Optional.of(tenantId));
+ when(omMultiTenantManager.getUserNameGivenAccessId(originalAccessKeyId))
+ .thenReturn("alice");
+ // Not a tenant admin; ownership should be sufficient.
+ when(omMultiTenantManager.isTenantAdmin(callerUgi, tenantId, false))
+ .thenReturn(false);
+
+ final OzoneManagerProtocolProtos.RevokeSTSTokenRequest revokeRequest =
+ OzoneManagerProtocolProtos.RevokeSTSTokenRequest.newBuilder()
+ .setAccessKeyId(tempAccessKeyId)
+ .setSessionToken(sessionToken)
+ .build();
+
+ final OMRequest omRequest = OMRequest.newBuilder()
+ .setClientId(UUID.randomUUID().toString())
+ .setCmdType(Type.RevokeSTSToken)
+ .setRevokeSTSTokenRequest(revokeRequest)
+ .build();
+
+ final OMClientRequest omClientRequest = new
S3RevokeSTSTokenRequest(omRequest);
+
+ final OMRequest result = omClientRequest.preExecute(ozoneManager);
+ assertEquals(Type.RevokeSTSToken, result.getCmdType());
+ }
+
+ @Test
+ public void testPreExecuteSucceedsForTenantAdmin() throws Exception {
+ // When S3 multi-tenancy is enabled and the original access key id is
assigned to a tenant, verify that a
+ // tenant admin (who is not the owner) is allowed to revoke the temporary
credentials.
+ final String tenantId = "finance";
+ final String originalAccessKeyId = "[email protected]";
+ final String tempAccessKeyId = "ASIA4567890123";
+ final String sessionToken = createSessionToken(tempAccessKeyId,
originalAccessKeyId);
+
+ // Caller short name "bob" does not own the access ID but will be
configured as tenant admin.
+ final UserGroupInformation callerUgi =
UserGroupInformation.createRemoteUser("[email protected]");
+ Server.getCurCall().set(new StubCall(callerUgi));
+
+ final OzoneManager ozoneManager = mock(OzoneManager.class);
+ when(ozoneManager.isS3MultiTenancyEnabled()).thenReturn(true);
+
when(ozoneManager.getMultiTenantManager()).thenReturn(omMultiTenantManager);
+ when(ozoneManager.getSecretKeyClient()).thenReturn(secretKeyClient);
+
+ // Original access key id is assigned to a tenant and owned by "alice".
+ when(omMultiTenantManager.getTenantForAccessID(originalAccessKeyId))
+ .thenReturn(Optional.of(tenantId));
+ when(omMultiTenantManager.getUserNameGivenAccessId(originalAccessKeyId))
+ .thenReturn("alice");
+ // Caller is configured as tenant admin so the check should pass.
+ when(omMultiTenantManager.isTenantAdmin(callerUgi, tenantId, false))
+ .thenReturn(true);
+
+ final OzoneManagerProtocolProtos.RevokeSTSTokenRequest revokeRequest =
+ OzoneManagerProtocolProtos.RevokeSTSTokenRequest.newBuilder()
+ .setAccessKeyId(tempAccessKeyId)
+ .setSessionToken(sessionToken)
+ .build();
+
+ final OMRequest omRequest = OMRequest.newBuilder()
+ .setClientId(UUID.randomUUID().toString())
+ .setCmdType(Type.RevokeSTSToken)
+ .setRevokeSTSTokenRequest(revokeRequest)
+ .build();
+
+ final OMClientRequest omClientRequest = new
S3RevokeSTSTokenRequest(omRequest);
+
+ final OMRequest result = omClientRequest.preExecute(ozoneManager);
+ assertEquals(Type.RevokeSTSToken, result.getCmdType());
+ }
+
+ @Test
+ public void testPreExecuteFailsForNonOwnerNonAdminInTenant() throws
Exception {
+ // When S3 multi-tenancy is enabled and the original access key id is
assigned to a tenant, verify that a
+ // non-owner, non-admin caller is rejected.
+ final String tenantId = "finance";
+ final String originalAccessKeyId = "[email protected]";
+ final String tempAccessKeyId = "ASIA123456789";
+ final String sessionToken = createSessionToken(tempAccessKeyId,
originalAccessKeyId);
+
+ // Caller short name "carol" does not own the access ID and is not
+ // configured as tenant admin.
+ final UserGroupInformation callerUgi =
UserGroupInformation.createRemoteUser("[email protected]");
+ Server.getCurCall().set(new StubCall(callerUgi));
+
+ final OMException ex;
+ try (OzoneManager ozoneManager = mock(OzoneManager.class)) {
+ when(ozoneManager.isS3MultiTenancyEnabled()).thenReturn(true);
+
when(ozoneManager.getMultiTenantManager()).thenReturn(omMultiTenantManager);
+ when(ozoneManager.getSecretKeyClient()).thenReturn(secretKeyClient);
+
+ // Original access key id is assigned to a tenant and owned by "alice".
+ when(omMultiTenantManager.getTenantForAccessID(originalAccessKeyId))
+ .thenReturn(Optional.of(tenantId));
+ when(omMultiTenantManager.getUserNameGivenAccessId(originalAccessKeyId))
+ .thenReturn("alice");
+ // Caller is not a tenant admin.
+ when(omMultiTenantManager.isTenantAdmin(callerUgi, tenantId, false))
+ .thenReturn(false);
+
+ final OzoneManagerProtocolProtos.RevokeSTSTokenRequest revokeRequest =
+ OzoneManagerProtocolProtos.RevokeSTSTokenRequest.newBuilder()
+ .setAccessKeyId(tempAccessKeyId)
+ .setSessionToken(sessionToken)
+ .build();
+
+ final OMRequest omRequest = OMRequest.newBuilder()
+ .setClientId(UUID.randomUUID().toString())
+ .setCmdType(Type.RevokeSTSToken)
+ .setRevokeSTSTokenRequest(revokeRequest)
+ .build();
+
+ final OMClientRequest omClientRequest = new
S3RevokeSTSTokenRequest(omRequest);
+
+ ex = assertThrows(OMException.class, () ->
omClientRequest.preExecute(ozoneManager));
+ }
+ assertEquals(OMException.ResultCodes.USER_MISMATCH, ex.getResult());
+ }
+
+ @Test
+ public void testPreExecuteFailsForMismatchedAccessKeyId() throws Exception {
+ // Verify that if the request access key id does not match the one inside
the session token, the request is
+ // rejected. This prevents a user with a valid session token from revoking
arbitrary STS credentials.
+ final String tempAccessKeyId = "ASIA123456789";
+ final String otherAccessKeyId = "ASI987654321";
+ final String originalAccessKeyId = "original-access-key-id";
+ final String sessionToken = createSessionToken(tempAccessKeyId,
originalAccessKeyId);
+
+ // Caller is the owner of the session token, so permissions should pass
+ final UserGroupInformation originalUgi =
UserGroupInformation.createRemoteUser(originalAccessKeyId);
+ Server.getCurCall().set(new StubCall(originalUgi));
+
+ final OMException ex;
+ try (OzoneManager ozoneManager = mock(OzoneManager.class)) {
+ when(ozoneManager.isS3MultiTenancyEnabled()).thenReturn(false);
+ when(ozoneManager.isS3Admin(any(UserGroupInformation.class)))
+ .thenReturn(false);
+ when(ozoneManager.getSecretKeyClient()).thenReturn(secretKeyClient);
+
+ // Request tries to revoke otherAccessKeyId using a token for
tempAccessKeyId
+ final OzoneManagerProtocolProtos.RevokeSTSTokenRequest revokeRequest =
+ OzoneManagerProtocolProtos.RevokeSTSTokenRequest.newBuilder()
+ .setAccessKeyId(otherAccessKeyId)
+ .setSessionToken(sessionToken)
+ .build();
+
+ final OMRequest omRequest = OMRequest.newBuilder()
+ .setClientId(UUID.randomUUID().toString())
+ .setCmdType(Type.RevokeSTSToken)
+ .setRevokeSTSTokenRequest(revokeRequest)
+ .build();
+
+ final OMClientRequest omClientRequest = new
S3RevokeSTSTokenRequest(omRequest);
+
+ ex = assertThrows(OMException.class, () ->
omClientRequest.preExecute(ozoneManager));
+ }
+ assertEquals(OMException.ResultCodes.INVALID_REQUEST, ex.getResult());
+ }
+
+ /**
+ * Stub used to inject a remote user into the
ProtobufRpcEngine.Server.getRemoteUser() thread-local.
+ */
+ private static final class StubCall extends ExternalCall<String> {
+ private final UserGroupInformation ugi;
+
+ StubCall(UserGroupInformation ugi) {
+ super(null);
+ this.ugi = ugi;
+ }
+
+ @Override
+ public UserGroupInformation getRemoteUser() {
+ return ugi;
+ }
+ }
+
+ private String createSessionToken(String tempAccessKeyId, String
originalAccessKeyId) throws IOException {
+ return stsTokenSecretManager.createSTSTokenString(
+ tempAccessKeyId, originalAccessKeyId,
"arn:aws:iam::123456789012:role/test-role", 3600,
+ "test-secret-access-key", "test-session-policy", CLOCK);
+ }
+}
diff --git
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/ClientProtocolStub.java
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/ClientProtocolStub.java
index ef0d32e2387..b56c6ca3fe8 100644
---
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/ClientProtocolStub.java
+++
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/ClientProtocolStub.java
@@ -814,4 +814,7 @@ public AssumeRoleResponseInfo assumeRole(
return null;
}
+ @Override
+ public void revokeSTSToken(String accessKeyId, String sessionToken) throws
IOException {
+ }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]