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]


Reply via email to