This is an automated email from the ASF dual-hosted git repository.
sammichen 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 2cf4e918b8e HDDS-14364. [STS] Revoked permanent credential must render
all associated STS tokens useless (#9602)
2cf4e918b8e is described below
commit 2cf4e918b8e9a6812641e09a6f9a9a3d27b27243
Author: fmorg-git <[email protected]>
AuthorDate: Sun Jan 18 23:32:26 2026 -0800
HDDS-14364. [STS] Revoked permanent credential must render all associated
STS tokens useless (#9602)
---
.../hadoop/ozone/security/S3SecurityUtil.java | 26 ++++
.../hadoop/ozone/security/TestS3SecurityUtil.java | 154 ++++++++++++++++-----
2 files changed, 148 insertions(+), 32 deletions(-)
diff --git
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/S3SecurityUtil.java
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/S3SecurityUtil.java
index 792b64e8697..17b74bb7417 100644
---
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/S3SecurityUtil.java
+++
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/S3SecurityUtil.java
@@ -23,6 +23,7 @@
import static
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMTokenProto.Type.S3AUTHINFO;
import com.google.protobuf.ServiceException;
+import java.io.IOException;
import java.time.Clock;
import java.time.ZoneOffset;
import org.apache.hadoop.hdds.annotation.InterfaceAudience;
@@ -78,6 +79,13 @@ public static void validateS3Credential(OMRequest omRequest,
throw new OMException("STS token has been revoked", REVOKED_TOKEN);
}
+ // Ensure the principal that created the STS token
(originalAccessKeyId) has not been revoked
+ if (isOriginalAccessKeyIdRevoked(stsTokenIdentifier, ozoneManager)) {
+ LOG.info("OriginalAccessKeyId for session token has been revoked:
{}, {}",
+ stsTokenIdentifier.getOriginalAccessKeyId(),
stsTokenIdentifier.getTempAccessKeyId());
+ throw new OMException("STS token no longer valid:
OriginalAccessKeyId principal revoked", REVOKED_TOKEN);
+ }
+
// HMAC signature and expiration were validated above. Now validate
AWS signature.
validateSTSTokenAwsSignature(stsTokenIdentifier, omRequest);
OzoneManager.setStsTokenIdentifier(stsTokenIdentifier);
@@ -166,4 +174,22 @@ private static boolean isRevokedStsToken(String
sessionToken, OzoneManager ozone
throw new OMException(msg, e, INTERNAL_ERROR);
}
}
+
+ /**
+ * Returns true if the originalAccessKeyId of the STS token has been revoked.
+ */
+ private static boolean isOriginalAccessKeyIdRevoked(STSTokenIdentifier
stsTokenIdentifier, OzoneManager ozoneManager)
+ throws OMException {
+ // We already know originalAccessKeyId is not null from
STSSecurityUtil.ensureEssentialFieldsArePresentInToken()
+ // method called from
STSSecurityUtil.constructValidateAndDecryptSTSToken() method above
+ final String originalAccessKeyId =
stsTokenIdentifier.getOriginalAccessKeyId();
+ try {
+ // If the secret for the original principal is missing, it means it was
revoked
+ return
!ozoneManager.getS3SecretManager().hasS3Secret(originalAccessKeyId);
+ } catch (IOException e) {
+ final String msg = "Could not determine if original principal is
revoked: " + e.getMessage();
+ LOG.warn(msg, e);
+ throw new OMException(msg, e, INTERNAL_ERROR);
+ }
+ }
}
diff --git
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestS3SecurityUtil.java
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestS3SecurityUtil.java
index 20b3f3fb28b..d642c6e5ace 100644
---
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestS3SecurityUtil.java
+++
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestS3SecurityUtil.java
@@ -33,6 +33,7 @@
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
+import java.io.IOException;
import java.time.Clock;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
@@ -41,6 +42,7 @@
import org.apache.hadoop.hdds.utils.db.Table;
import org.apache.hadoop.ozone.om.OMMetadataManager;
import org.apache.hadoop.ozone.om.OzoneManager;
+import org.apache.hadoop.ozone.om.S3SecretManager;
import org.apache.hadoop.ozone.om.exceptions.OMException;
import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRequest;
import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.S3Authentication;
@@ -62,72 +64,107 @@ public class TestS3SecurityUtil {
@Test
public void testValidateS3CredentialFailsWhenTokenRevoked() throws Exception
{
- // If the revoked STS token table contains an entry for the temporary
access key id extracted from the session
- // token, validateS3Credential should reject the request with REVOKED_TOKEN
- final OMMetadataManager metadataManager = mock(OMMetadataManager.class);
- final Table<String, Long> revokedSTSTokenTable = new InMemoryTestTable<>();
+ // If the revoked STS token table contains an entry for the session token,
the request should be rejected with
+ // REVOKED_TOKEN
validateS3CredentialHelper(
- "session-token-a", metadataManager, revokedSTSTokenTable, true,
createSTSTokenIdentifier(),
- REVOKED_TOKEN, "STS token has been revoked");
+ new TestConfig()
+ .setTokenRevoked(true)
+ .setExpectedResult(REVOKED_TOKEN)
+ .setExpectedMessage("STS token has been revoked"));
}
@Test
public void testValidateS3CredentialWhenMetadataUnavailable() throws
Exception {
// If the metadata manager is not available, throws INTERNAL_ERROR
validateS3CredentialHelper(
- "session-token-b", null, null, false, createSTSTokenIdentifier(),
- INTERNAL_ERROR, "Could not determine STS revocation: metadataManager
is null");
+ new TestConfig()
+ .setMetadataManager(null)
+ .setExpectedResult(INTERNAL_ERROR)
+ .setExpectedMessage("Could not determine STS revocation:
metadataManager is null"));
}
@Test
public void testValidateS3CredentialSuccessWhenNotRevoked() throws Exception
{
// Normal case: token is NOT revoked and request is accepted
- final OMMetadataManager metadataManager = mock(OMMetadataManager.class);
- final Table<String, Long> revokedSTSTokenTable = new InMemoryTestTable<>();
- validateS3CredentialHelper(
- "session-token-c", metadataManager, revokedSTSTokenTable, false,
createSTSTokenIdentifier(),
- null, null);
+ validateS3CredentialHelper(new TestConfig());
}
@Test
public void
testValidateS3CredentialWhenMetadataManagerAvailableButRevokedTableNull()
throws Exception {
// If the revoked STS token table is not available, throws INTERNAL_ERROR
- final OMMetadataManager metadataManager = mock(OMMetadataManager.class);
validateS3CredentialHelper(
- "session-token-d", metadataManager, null, false,
createSTSTokenIdentifier(),
- INTERNAL_ERROR, "Could not determine STS revocation:
revokedStsTokenTable is null");
+ new TestConfig()
+ .setRevokedSTSTokenTable(null)
+ .setExpectedResult(INTERNAL_ERROR)
+ .setExpectedMessage("Could not determine STS revocation:
revokedStsTokenTable is null"));
}
@Test
public void testValidateS3CredentialWhenTableThrowsException() throws
Exception {
// If the revoked STS token table lookup throws, throws INTERNAL_ERROR
(wrapped)
- final OMMetadataManager metadataManager = mock(OMMetadataManager.class);
final Table<String, Long> revokedSTSTokenTable = spy(new
InMemoryTestTable<>());
doThrow(new RuntimeException("lookup
failed")).when(revokedSTSTokenTable).getIfExist(anyString());
+
validateS3CredentialHelper(
- "session-token-g", metadataManager, revokedSTSTokenTable, false,
createSTSTokenIdentifier(),
- INTERNAL_ERROR, "Could not determine STS revocation because of
Exception: lookup failed");
+ new TestConfig()
+ .setRevokedSTSTokenTable(revokedSTSTokenTable)
+ .setExpectedResult(INTERNAL_ERROR)
+ .setExpectedMessage("Could not determine STS revocation because of
Exception: lookup failed"));
}
- private void validateS3CredentialHelper(String sessionToken,
OMMetadataManager metadataManager,
- Table<String, Long> revokedSTSTokenTable, boolean isRevoked,
STSTokenIdentifier stsTokenIdentifier,
- OMException.ResultCodes expectedResult, String expectedMessageContents)
throws Exception {
+ @Test
+ public void
testValidateS3CredentialFailsWhenOriginalAccessKeyIdPrincipalRevoked() throws
Exception {
+ // If the originalAccessKeyId principal is revoked, throws REVOKED_TOKEN
+ validateS3CredentialHelper(
+ new TestConfig()
+ .setOriginalAccessKeyIdRevoked(true)
+ .setExpectedResult(REVOKED_TOKEN)
+ .setExpectedMessage("STS token no longer valid:
OriginalAccessKeyId principal revoked"));
+ }
+
+ @Test
+ public void
testValidateS3CredentialFailsWhenOriginalAccessKeyIdCheckThrows() throws
Exception {
+ // If checking originalAccessKeyId principal revocation fails, throws
INTERNAL_ERROR
+ validateS3CredentialHelper(
+ new TestConfig()
+ .setShouldOriginalAccessKeyIdCheckThrowError(true)
+ .setExpectedResult(INTERNAL_ERROR)
+ .setExpectedMessage("Could not determine if original principal is
revoked"));
+ }
+ private void validateS3CredentialHelper(TestConfig config) throws Exception {
try (OzoneManager ozoneManager = mock(OzoneManager.class)) {
when(ozoneManager.isSecurityEnabled()).thenReturn(true);
when(ozoneManager.getSecretKeyClient()).thenReturn(mock(SecretKeyClient.class));
+ final OMMetadataManager metadataManager = config.metadataManager;
when(ozoneManager.getMetadataManager()).thenReturn(metadataManager);
if (metadataManager != null) {
-
when(metadataManager.getS3RevokedStsTokenTable()).thenReturn(revokedSTSTokenTable);
+
when(metadataManager.getS3RevokedStsTokenTable()).thenReturn(config.revokedSTSTokenTable);
+ }
+
+ // Mock S3SecretManager to handle originalAccessKeyId checks
+ final S3SecretManager s3SecretManager = mock(S3SecretManager.class);
+ when(ozoneManager.getS3SecretManager()).thenReturn(s3SecretManager);
+ if (config.shouldOriginalAccessKeyIdCheckThrowError) {
+ when(s3SecretManager.hasS3Secret(anyString())).thenThrow(
+ new IOException("An error occurred while checking if s3Secret
exists"));
+ } else if (config.isOriginalAccessKeyIdRevoked) {
+ // Returning false means secret does NOT exist -> principal is revoked
+ when(s3SecretManager.hasS3Secret(anyString())).thenReturn(false);
+ } else {
+ // Returning true means secret exists -> principal is valid
+ when(s3SecretManager.hasS3Secret(anyString())).thenReturn(true);
}
- final String tempAccessKeyId = "temp-access-key-id";
- if (isRevoked) {
+ final String sessionToken = "session-token";
+ if (config.isTokenRevoked && config.revokedSTSTokenTable != null) {
final long insertionTimeMillis = CLOCK.millis();
- revokedSTSTokenTable.put(sessionToken, insertionTimeMillis);
+ config.revokedSTSTokenTable.put(sessionToken, insertionTimeMillis);
}
+ final STSTokenIdentifier stsTokenIdentifier = createSTSTokenIdentifier();
+
try (MockedStatic<STSSecurityUtil> stsSecurityUtilMock =
mockStatic(STSSecurityUtil.class, CALLS_REAL_METHODS);
MockedStatic<AWSV4AuthValidator> awsV4AuthValidatorMock =
mockStatic(
AWSV4AuthValidator.class, CALLS_REAL_METHODS)) {
@@ -143,15 +180,15 @@ private void validateS3CredentialHelper(String
sessionToken, OMMetadataManager m
final OMRequest omRequest =
createRequestWithSessionToken(sessionToken);
- if (expectedResult != null) {
+ if (config.expectedResult != null) {
final OMException omException = assertThrows(
OMException.class, () ->
S3SecurityUtil.validateS3Credential(omRequest, ozoneManager));
- assertEquals(expectedResult, omException.getResult());
- if (expectedMessageContents != null) {
+ assertEquals(config.expectedResult, omException.getResult());
+ if (config.expectedMessage != null) {
assertTrue(
- omException.getMessage().contains(expectedMessageContents),
- "Expected exception message to contain: '" +
expectedMessageContents + "' but was: '" +
- omException.getMessage() + "'");
+ omException.getMessage().contains(config.expectedMessage),
+ "Expected exception message to contain: '" +
config.expectedMessage + "' but was: '" +
+ omException.getMessage() + "'");
}
} else {
assertDoesNotThrow(() ->
S3SecurityUtil.validateS3Credential(omRequest, ozoneManager));
@@ -167,6 +204,7 @@ private STSTokenIdentifier createSTSTokenIdentifier() {
ENCRYPTION_KEY);
}
+ @SuppressWarnings("SameParameterValue")
private static OMRequest createRequestWithSessionToken(String sessionToken) {
final S3Authentication s3Authentication = S3Authentication.newBuilder()
.setAccessId("accessKeyId")
@@ -181,4 +219,56 @@ private static OMRequest
createRequestWithSessionToken(String sessionToken) {
.setS3Authentication(s3Authentication)
.build();
}
+
+ /**
+ * Helper class to create various scenarios for testing.
+ */
+ private static class TestConfig {
+ private OMMetadataManager metadataManager = mock(OMMetadataManager.class);
+ private Table<String, Long> revokedSTSTokenTable = new
InMemoryTestTable<>();
+ private boolean isTokenRevoked = false;
+ private boolean isOriginalAccessKeyIdRevoked = false;
+ private boolean shouldOriginalAccessKeyIdCheckThrowError = false;
+ private OMException.ResultCodes expectedResult = null;
+ private String expectedMessage = null;
+
+ @SuppressWarnings("SameParameterValue")
+ TestConfig setMetadataManager(OMMetadataManager metadataManager) {
+ this.metadataManager = metadataManager;
+ return this;
+ }
+
+ TestConfig setRevokedSTSTokenTable(Table<String, Long> table) {
+ this.revokedSTSTokenTable = table;
+ return this;
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ TestConfig setTokenRevoked(boolean isRevoked) {
+ this.isTokenRevoked = isRevoked;
+ return this;
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ TestConfig setOriginalAccessKeyIdRevoked(boolean isRevoked) {
+ this.isOriginalAccessKeyIdRevoked = isRevoked;
+ return this;
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ TestConfig setShouldOriginalAccessKeyIdCheckThrowError(boolean isError) {
+ this.shouldOriginalAccessKeyIdCheckThrowError = isError;
+ return this;
+ }
+
+ TestConfig setExpectedResult(OMException.ResultCodes result) {
+ this.expectedResult = result;
+ return this;
+ }
+
+ TestConfig setExpectedMessage(String message) {
+ this.expectedMessage = message;
+ return this;
+ }
+ }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]