Skip to content

Commit ae4bdb3

Browse files
authored
HDDS-14091. [STS] Deny access if STS token is found in revoked table (#9445)
1 parent 2062a06 commit ae4bdb3

9 files changed

Lines changed: 343 additions & 12 deletions

File tree

hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/exceptions/OMException.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,5 +275,7 @@ public enum ResultCodes {
275275
KEY_UNDER_LEASE_SOFT_LIMIT_PERIOD,
276276

277277
TOO_MANY_SNAPSHOTS,
278+
279+
REVOKED_TOKEN,
278280
}
279281
}

hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,8 @@ enum Status {
572572
KEY_UNDER_LEASE_SOFT_LIMIT_PERIOD = 97;
573573

574574
TOO_MANY_SNAPSHOTS = 98;
575+
576+
REVOKED_TOKEN = 99;
575577
}
576578

577579
/**

hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataManagerImpl.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -490,7 +490,9 @@ protected void initializeOmTables(CacheType cacheType,
490490
compactionLogTable = initializer.get(OMDBDefinition.COMPACTION_LOG_TABLE_DEF);
491491

492492
// temporaryAccessKeyId -> sessionToken
493-
s3RevokedStsTokenTable = initializer.get(OMDBDefinition.S3_REVOKED_STS_TOKEN_TABLE_DEF);
493+
// FULL_CACHE keeps revocations in memory as there are not expected to be many revoked tokens
494+
s3RevokedStsTokenTable = initializer.get(
495+
OMDBDefinition.S3_REVOKED_STS_TOKEN_TABLE_DEF, CacheType.FULL_CACHE);
494496
}
495497

496498
/**

hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/S3SecurityUtil.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,19 @@
1717

1818
package org.apache.hadoop.ozone.security;
1919

20+
import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.INTERNAL_ERROR;
2021
import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.INVALID_TOKEN;
22+
import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.REVOKED_TOKEN;
2123
import static org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMTokenProto.Type.S3AUTHINFO;
2224

2325
import com.google.protobuf.ServiceException;
2426
import java.time.Clock;
2527
import java.time.ZoneOffset;
2628
import org.apache.hadoop.hdds.annotation.InterfaceAudience;
2729
import org.apache.hadoop.hdds.annotation.InterfaceStability;
30+
import org.apache.hadoop.hdds.utils.db.Table;
2831
import org.apache.hadoop.io.Text;
32+
import org.apache.hadoop.ozone.om.OMMetadataManager;
2933
import org.apache.hadoop.ozone.om.OzoneManager;
3034
import org.apache.hadoop.ozone.om.exceptions.OMException;
3135
import org.apache.hadoop.ozone.om.exceptions.OMLeaderNotReadyException;
@@ -34,6 +38,8 @@
3438
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.S3Authentication;
3539
import org.apache.hadoop.ozone.protocolPB.OzoneManagerProtocolServerSideTranslatorPB;
3640
import org.apache.hadoop.security.token.SecretManager;
41+
import org.slf4j.Logger;
42+
import org.slf4j.LoggerFactory;
3743

3844
/**
3945
* Utility class which holds methods required for parse/validation of
@@ -44,6 +50,7 @@
4450
public final class S3SecurityUtil {
4551

4652
private static final Clock CLOCK = Clock.system(ZoneOffset.UTC);
53+
private static final Logger LOG = LoggerFactory.getLogger(S3SecurityUtil.class);
4754

4855
private S3SecurityUtil() {
4956
}
@@ -64,6 +71,13 @@ public static void validateS3Credential(OMRequest omRequest,
6471
if (!token.isEmpty()) {
6572
final STSTokenIdentifier stsTokenIdentifier = STSSecurityUtil.constructValidateAndDecryptSTSToken(
6673
token, ozoneManager.getSecretKeyClient(), CLOCK);
74+
75+
// Ensure the token is not revoked
76+
if (isRevokedStsTempAccessKeyId(stsTokenIdentifier, ozoneManager)) {
77+
LOG.info("Session token has been revoked: {}, {}", stsTokenIdentifier.getTempAccessKeyId(), token);
78+
throw new OMException("STS token has been revoked", REVOKED_TOKEN);
79+
}
80+
6781
// HMAC signature and expiration were validated above. Now validate AWS signature.
6882
validateSTSTokenAwsSignature(stsTokenIdentifier, omRequest);
6983
OzoneManager.setStsTokenIdentifier(stsTokenIdentifier);
@@ -124,4 +138,35 @@ private static void validateSTSTokenAwsSignature(STSTokenIdentifier stsTokenIden
124138
throw new OMException(
125139
"STS token validation failed for token: " + omRequest.getS3Authentication().getSessionToken(), INVALID_TOKEN);
126140
}
141+
142+
/**
143+
* Returns true if the STS token's temporary access key ID is present in the revoked STS token table.
144+
*/
145+
private static boolean isRevokedStsTempAccessKeyId(STSTokenIdentifier stsTokenIdentifier, OzoneManager ozoneManager)
146+
throws OMException {
147+
try {
148+
final OMMetadataManager metadataManager = ozoneManager.getMetadataManager();
149+
if (metadataManager == null) {
150+
final String msg = "Could not determine STS revocation: metadataManager is null";
151+
LOG.warn(msg);
152+
throw new OMException(msg, INTERNAL_ERROR);
153+
}
154+
155+
final Table<String, String> revokedStsTokenTable = metadataManager.getS3RevokedStsTokenTable();
156+
if (revokedStsTokenTable == null) {
157+
final String msg = "Could not determine STS revocation: revokedStsTokenTable is null";
158+
LOG.warn(msg);
159+
throw new OMException(msg, INTERNAL_ERROR);
160+
}
161+
162+
// When the STSTokenIdentifier is validated, it ensures the temp access key id is not null/empty
163+
final String tempAccessKeyId = stsTokenIdentifier.getTempAccessKeyId();
164+
165+
return revokedStsTokenTable.getIfExist(tempAccessKeyId) != null;
166+
} catch (Exception e) {
167+
final String msg = "Could not determine STS revocation because of Exception: " + e.getMessage();
168+
LOG.warn(msg, e);
169+
throw new OMException(msg, e, INTERNAL_ERROR);
170+
}
171+
}
127172
}

hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSSecurityUtil.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@
1919

2020
import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.INVALID_TOKEN;
2121

22+
import com.google.common.annotations.VisibleForTesting;
2223
import com.google.protobuf.InvalidProtocolBufferException;
2324
import java.io.IOException;
2425
import java.time.Clock;
2526
import java.util.UUID;
27+
import org.apache.commons.lang3.StringUtils;
2628
import org.apache.hadoop.hdds.annotation.InterfaceAudience;
2729
import org.apache.hadoop.hdds.annotation.InterfaceStability;
2830
import org.apache.hadoop.hdds.security.symmetric.ManagedSecretKey;
@@ -102,6 +104,9 @@ private static STSTokenIdentifier verifyAndDecryptToken(Token<STSTokenIdentifier
102104
throw new SecretManager.InvalidToken("Invalid STS token - could not readFromByteArray: " + e.getMessage());
103105
}
104106

107+
// Ensure essential fields are present in the token
108+
ensureEssentialFieldsArePresentInToken(tokenId);
109+
105110
// Check expiration
106111
if (tokenId.isExpired(clock.instant())) {
107112
throw new SecretManager.InvalidToken("Invalid STS token - token expired at " + tokenId.getExpiry());
@@ -149,5 +154,25 @@ private static Token<STSTokenIdentifier> decodeTokenFromString(String encodedTok
149154
throw new SecretManager.InvalidToken("Failed to decode STS token string: " + e);
150155
}
151156
}
157+
158+
@VisibleForTesting
159+
static void ensureEssentialFieldsArePresentInToken(STSTokenIdentifier stsTokenIdentifier)
160+
throws SecretManager.InvalidToken {
161+
if (StringUtils.isEmpty(stsTokenIdentifier.getTempAccessKeyId())) {
162+
throw new SecretManager.InvalidToken("Invalid STS token - tempAccessKeyId is null/empty");
163+
}
164+
if (stsTokenIdentifier.getExpiry() == null) {
165+
throw new SecretManager.InvalidToken("Invalid STS token - expiry is null");
166+
}
167+
if (StringUtils.isEmpty(stsTokenIdentifier.getRoleArn())) {
168+
throw new SecretManager.InvalidToken("Invalid STS token - roleArn is null/empty");
169+
}
170+
if (StringUtils.isEmpty(stsTokenIdentifier.getOriginalAccessKeyId())) {
171+
throw new SecretManager.InvalidToken("Invalid STS token - originalAccessKeyId is null/empty");
172+
}
173+
if (StringUtils.isEmpty(stsTokenIdentifier.getSecretAccessKey())) {
174+
throw new SecretManager.InvalidToken("Invalid STS token - secretAccessKey is null/empty");
175+
}
176+
}
152177
}
153178

hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestOmMetadataManager.java

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
import org.apache.hadoop.hdds.protocol.StorageType;
8080
import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
8181
import org.apache.hadoop.hdds.utils.TransactionInfo;
82+
import org.apache.hadoop.hdds.utils.db.Table;
8283
import org.apache.hadoop.hdds.utils.db.cache.CacheKey;
8384
import org.apache.hadoop.hdds.utils.db.cache.CacheValue;
8485
import org.apache.hadoop.ozone.om.codec.OMDBDefinition;
@@ -1303,18 +1304,29 @@ public void testS3RevokedStsTokenTablePutAndGet() throws Exception {
13031304
final String tempAccessKeyId2 = "ASIA904E65QIGL9ON305";
13041305
final String sessionToken2 = "test-session-token-2";
13051306

1306-
omMetadataManager.getS3RevokedStsTokenTable()
1307-
.put(tempAccessKeyId1, sessionToken1);
1308-
omMetadataManager.getS3RevokedStsTokenTable()
1309-
.put(tempAccessKeyId2, sessionToken2);
1307+
final Table<String, String> table = omMetadataManager.getS3RevokedStsTokenTable();
1308+
1309+
// This table is configured as FULL_CACHE in OmMetadataManagerImpl.
1310+
// A put() writes to RocksDB but does not update the table cache, so get() and getIfExist() will return null unless
1311+
// the cache is updated with addCacheEntry(). getSkipCache() will read the DB instead of the cache.
1312+
table.put(tempAccessKeyId1, sessionToken1);
1313+
table.put(tempAccessKeyId2, sessionToken2);
1314+
1315+
// Verify the values are persisted in RocksDB.
1316+
assertEquals(sessionToken1, table.getSkipCache(tempAccessKeyId1));
1317+
assertEquals(sessionToken2, table.getSkipCache(tempAccessKeyId2));
1318+
1319+
// Update cache to make get/getIfExist reflect the write for FULL_CACHE tables.
1320+
table.addCacheEntry(tempAccessKeyId1, sessionToken1, 1L);
1321+
table.addCacheEntry(tempAccessKeyId2, sessionToken2, 1L);
13101322

13111323
// Verify get and getIfExist return the stored value
1312-
assertEquals(sessionToken1, omMetadataManager.getS3RevokedStsTokenTable().get(tempAccessKeyId1));
1313-
assertEquals(sessionToken1, omMetadataManager.getS3RevokedStsTokenTable().getIfExist(tempAccessKeyId1));
1314-
assertEquals(sessionToken2, omMetadataManager.getS3RevokedStsTokenTable().get(tempAccessKeyId2));
1315-
assertEquals(sessionToken2, omMetadataManager.getS3RevokedStsTokenTable().getIfExist(tempAccessKeyId2));
1324+
assertEquals(sessionToken1, table.get(tempAccessKeyId1));
1325+
assertEquals(sessionToken1, table.getIfExist(tempAccessKeyId1));
1326+
assertEquals(sessionToken2, table.get(tempAccessKeyId2));
1327+
assertEquals(sessionToken2, table.getIfExist(tempAccessKeyId2));
13161328

13171329
// Unknown key should return null for getIfExist
1318-
assertNull(omMetadataManager.getS3RevokedStsTokenTable().getIfExist("ASIA_UNKNOWN_ACCESS_KEY"));
1330+
assertNull(table.getIfExist("ASIA_UNKNOWN_ACCESS_KEY"));
13191331
}
13201332
}
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.hadoop.ozone.security;
19+
20+
import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.INTERNAL_ERROR;
21+
import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.REVOKED_TOKEN;
22+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
23+
import static org.junit.jupiter.api.Assertions.assertEquals;
24+
import static org.junit.jupiter.api.Assertions.assertThrows;
25+
import static org.junit.jupiter.api.Assertions.assertTrue;
26+
import static org.mockito.ArgumentMatchers.any;
27+
import static org.mockito.ArgumentMatchers.anyString;
28+
import static org.mockito.ArgumentMatchers.eq;
29+
import static org.mockito.Mockito.CALLS_REAL_METHODS;
30+
import static org.mockito.Mockito.doThrow;
31+
import static org.mockito.Mockito.mock;
32+
import static org.mockito.Mockito.mockStatic;
33+
import static org.mockito.Mockito.spy;
34+
import static org.mockito.Mockito.when;
35+
36+
import java.time.Clock;
37+
import java.util.UUID;
38+
import java.util.concurrent.ThreadLocalRandom;
39+
import org.apache.hadoop.hdds.security.symmetric.SecretKeyClient;
40+
import org.apache.hadoop.hdds.utils.db.InMemoryTestTable;
41+
import org.apache.hadoop.hdds.utils.db.Table;
42+
import org.apache.hadoop.ozone.om.OMMetadataManager;
43+
import org.apache.hadoop.ozone.om.OzoneManager;
44+
import org.apache.hadoop.ozone.om.exceptions.OMException;
45+
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRequest;
46+
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.S3Authentication;
47+
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.Type;
48+
import org.apache.ozone.test.TestClock;
49+
import org.junit.jupiter.api.Test;
50+
import org.mockito.MockedStatic;
51+
52+
/**
53+
* Tests for STS revocation handling in {@link S3SecurityUtil}.
54+
*/
55+
public class TestS3SecurityUtil {
56+
private static final byte[] ENCRYPTION_KEY = new byte[5];
57+
private static final TestClock CLOCK = TestClock.newInstance();
58+
59+
{
60+
ThreadLocalRandom.current().nextBytes(ENCRYPTION_KEY);
61+
}
62+
63+
@Test
64+
public void testValidateS3CredentialFailsWhenTokenRevoked() throws Exception {
65+
// If the revoked STS token table contains an entry for the temporary access key id extracted from the session
66+
// token, validateS3Credential should reject the request with REVOKED_TOKEN
67+
final OMMetadataManager metadataManager = mock(OMMetadataManager.class);
68+
final Table<String, String> revokedSTSTokenTable = new InMemoryTestTable<>();
69+
validateS3CredentialHelper(
70+
"session-token-a", metadataManager, revokedSTSTokenTable, true, createSTSTokenIdentifier(),
71+
REVOKED_TOKEN, "STS token has been revoked");
72+
}
73+
74+
@Test
75+
public void testValidateS3CredentialWhenMetadataUnavailable() throws Exception {
76+
// If the metadata manager is not available, throws INTERNAL_ERROR
77+
validateS3CredentialHelper(
78+
"session-token-b", null, null, false, createSTSTokenIdentifier(),
79+
INTERNAL_ERROR, "Could not determine STS revocation: metadataManager is null");
80+
}
81+
82+
@Test
83+
public void testValidateS3CredentialSuccessWhenNotRevoked() throws Exception {
84+
// Normal case: token is NOT revoked and request is accepted
85+
final OMMetadataManager metadataManager = mock(OMMetadataManager.class);
86+
final Table<String, String> revokedSTSTokenTable = new InMemoryTestTable<>();
87+
validateS3CredentialHelper(
88+
"session-token-c", metadataManager, revokedSTSTokenTable, false, createSTSTokenIdentifier(),
89+
null, null);
90+
}
91+
92+
@Test
93+
public void testValidateS3CredentialWhenMetadataManagerAvailableButRevokedTableNull() throws Exception {
94+
// If the revoked STS token table is not available, throws INTERNAL_ERROR
95+
final OMMetadataManager metadataManager = mock(OMMetadataManager.class);
96+
validateS3CredentialHelper(
97+
"session-token-d", metadataManager, null, false, createSTSTokenIdentifier(),
98+
INTERNAL_ERROR, "Could not determine STS revocation: revokedStsTokenTable is null");
99+
}
100+
101+
@Test
102+
public void testValidateS3CredentialWhenTableThrowsException() throws Exception {
103+
// If the revoked STS token table lookup throws, throws INTERNAL_ERROR (wrapped)
104+
final OMMetadataManager metadataManager = mock(OMMetadataManager.class);
105+
final Table<String, String> revokedSTSTokenTable = spy(new InMemoryTestTable<>());
106+
doThrow(new RuntimeException("lookup failed")).when(revokedSTSTokenTable).getIfExist(anyString());
107+
validateS3CredentialHelper(
108+
"session-token-g", metadataManager, revokedSTSTokenTable, false, createSTSTokenIdentifier(),
109+
INTERNAL_ERROR, "Could not determine STS revocation because of Exception: lookup failed");
110+
}
111+
112+
private void validateS3CredentialHelper(String sessionToken, OMMetadataManager metadataManager,
113+
Table<String, String> revokedSTSTokenTable, boolean isRevoked, STSTokenIdentifier stsTokenIdentifier,
114+
OMException.ResultCodes expectedResult, String expectedMessageContents) throws Exception {
115+
116+
try (OzoneManager ozoneManager = mock(OzoneManager.class)) {
117+
when(ozoneManager.isSecurityEnabled()).thenReturn(true);
118+
when(ozoneManager.getSecretKeyClient()).thenReturn(mock(SecretKeyClient.class));
119+
120+
when(ozoneManager.getMetadataManager()).thenReturn(metadataManager);
121+
if (metadataManager != null) {
122+
when(metadataManager.getS3RevokedStsTokenTable()).thenReturn(revokedSTSTokenTable);
123+
}
124+
125+
final String tempAccessKeyId = "temp-access-key-id";
126+
if (isRevoked) {
127+
if (revokedSTSTokenTable == null) {
128+
throw new IllegalArgumentException("revokedSTSTokenTable must not be null when isRevoked=true");
129+
}
130+
revokedSTSTokenTable.put(tempAccessKeyId, sessionToken);
131+
}
132+
133+
try (MockedStatic<STSSecurityUtil> stsSecurityUtilMock = mockStatic(STSSecurityUtil.class, CALLS_REAL_METHODS);
134+
MockedStatic<AWSV4AuthValidator> awsV4AuthValidatorMock = mockStatic(
135+
AWSV4AuthValidator.class, CALLS_REAL_METHODS)) {
136+
137+
stsSecurityUtilMock.when(
138+
() -> STSSecurityUtil.constructValidateAndDecryptSTSToken(
139+
eq(sessionToken), any(SecretKeyClient.class), any(Clock.class)))
140+
.thenReturn(stsTokenIdentifier);
141+
142+
// Mock AWS V4 signature validation
143+
awsV4AuthValidatorMock.when(() -> AWSV4AuthValidator.validateRequest(anyString(), anyString(), anyString()))
144+
.thenReturn(true);
145+
146+
final OMRequest omRequest = createRequestWithSessionToken(sessionToken);
147+
148+
if (expectedResult != null) {
149+
final OMException omException = assertThrows(
150+
OMException.class, () -> S3SecurityUtil.validateS3Credential(omRequest, ozoneManager));
151+
assertEquals(expectedResult, omException.getResult());
152+
if (expectedMessageContents != null) {
153+
assertTrue(
154+
omException.getMessage().contains(expectedMessageContents),
155+
"Expected exception message to contain: '" + expectedMessageContents + "' but was: '" +
156+
omException.getMessage() + "'");
157+
}
158+
} else {
159+
assertDoesNotThrow(() -> S3SecurityUtil.validateS3Credential(omRequest, ozoneManager));
160+
}
161+
}
162+
}
163+
}
164+
165+
private STSTokenIdentifier createSTSTokenIdentifier() {
166+
return new STSTokenIdentifier(
167+
"temp-access-key-id", "original-access-key-id", "arn:aws:iam::123456789012:role/test-role",
168+
CLOCK.instant().plusSeconds(3600), "secret-access-key", "session-policy",
169+
ENCRYPTION_KEY);
170+
}
171+
172+
private static OMRequest createRequestWithSessionToken(String sessionToken) {
173+
final S3Authentication s3Authentication = S3Authentication.newBuilder()
174+
.setAccessId("accessKeyId")
175+
.setStringToSign("string-to-sign")
176+
.setSignature("signature")
177+
.setSessionToken(sessionToken)
178+
.build();
179+
180+
return OMRequest.newBuilder()
181+
.setClientId(UUID.randomUUID().toString())
182+
.setCmdType(Type.CreateVolume)
183+
.setS3Authentication(s3Authentication)
184+
.build();
185+
}
186+
}

0 commit comments

Comments
 (0)