Skip to content

Commit 65b57e9

Browse files
author
Fabian Morgan
committed
deny access if STS token is found in revoked table
1 parent 8f7ec10 commit 65b57e9

5 files changed

Lines changed: 237 additions & 1 deletion

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
@@ -569,6 +569,8 @@ enum Status {
569569
KEY_UNDER_LEASE_SOFT_LIMIT_PERIOD = 97;
570570

571571
TOO_MANY_SNAPSHOTS = 98;
572+
573+
REVOKED_TOKEN = 99;
572574
}
573575

574576
/**

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

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,17 @@
1818
package org.apache.hadoop.ozone.security;
1919

2020
import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.INVALID_TOKEN;
21+
import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.REVOKED_TOKEN;
2122
import static org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMTokenProto.Type.S3AUTHINFO;
2223

2324
import com.google.protobuf.ServiceException;
2425
import java.time.Clock;
2526
import java.time.ZoneOffset;
2627
import org.apache.hadoop.hdds.annotation.InterfaceAudience;
2728
import org.apache.hadoop.hdds.annotation.InterfaceStability;
29+
import org.apache.hadoop.hdds.utils.db.Table;
2830
import org.apache.hadoop.io.Text;
31+
import org.apache.hadoop.ozone.om.OMMetadataManager;
2932
import org.apache.hadoop.ozone.om.OzoneManager;
3033
import org.apache.hadoop.ozone.om.exceptions.OMException;
3134
import org.apache.hadoop.ozone.om.exceptions.OMLeaderNotReadyException;
@@ -34,6 +37,8 @@
3437
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.S3Authentication;
3538
import org.apache.hadoop.ozone.protocolPB.OzoneManagerProtocolServerSideTranslatorPB;
3639
import org.apache.hadoop.security.token.SecretManager;
40+
import org.slf4j.Logger;
41+
import org.slf4j.LoggerFactory;
3742

3843
/**
3944
* Utility class which holds methods required for parse/validation of
@@ -44,6 +49,7 @@
4449
public final class S3SecurityUtil {
4550

4651
private static final Clock CLOCK = Clock.system(ZoneOffset.UTC);
52+
private static final Logger LOG = LoggerFactory.getLogger(S3SecurityUtil.class);
4753

4854
private S3SecurityUtil() {
4955
}
@@ -64,6 +70,13 @@ public static void validateS3Credential(OMRequest omRequest,
6470
if (!token.isEmpty()) {
6571
final STSTokenIdentifier stsTokenIdentifier = STSSecurityUtil.constructValidateAndDecryptSTSToken(
6672
token, ozoneManager.getSecretKeyClient(), CLOCK);
73+
74+
// Ensure the token is not revoked
75+
if (isRevokedStsTempAccessKeyId(stsTokenIdentifier, ozoneManager)) {
76+
LOG.info("Session token has been revoked: {}, {}", stsTokenIdentifier.getTempAccessKeyId(), token);
77+
throw new OMException("STS token has been revoked", REVOKED_TOKEN);
78+
}
79+
6780
// HMAC signature and expiration were validated above. Now validate AWS signature.
6881
validateSTSTokenAwsSignature(stsTokenIdentifier, omRequest);
6982
OzoneManager.setStsTokenIdentifier(stsTokenIdentifier);
@@ -124,4 +137,32 @@ private static void validateSTSTokenAwsSignature(STSTokenIdentifier stsTokenIden
124137
throw new OMException(
125138
"STS token validation failed for token: " + omRequest.getS3Authentication().getSessionToken(), INVALID_TOKEN);
126139
}
140+
141+
/**
142+
* Returns true if the STS token's temporary access key ID is present in the revoked STS token table.
143+
*/
144+
private static boolean isRevokedStsTempAccessKeyId(STSTokenIdentifier stsTokenIdentifier, OzoneManager ozoneManager) {
145+
try {
146+
final OMMetadataManager metadataManager = ozoneManager.getMetadataManager();
147+
if (metadataManager == null) {
148+
return false;
149+
}
150+
151+
final Table<String, String> revokedStsTokenTable = metadataManager.getS3RevokedStsTokenTable();
152+
if (revokedStsTokenTable == null) {
153+
return false;
154+
}
155+
156+
final String tempAccessKeyId = stsTokenIdentifier.getTempAccessKeyId();
157+
if (tempAccessKeyId == null || tempAccessKeyId.isEmpty()) {
158+
return false;
159+
}
160+
161+
return revokedStsTokenTable.getIfExist(tempAccessKeyId) != null;
162+
} catch (Exception e) {
163+
// Any DB or codec problem is treated as best-effort failure.
164+
LOG.warn("Failed to check STS token revocation state: {}", e.getMessage());
165+
return false;
166+
}
167+
}
127168
}
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
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.REVOKED_TOKEN;
21+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
22+
import static org.junit.jupiter.api.Assertions.assertEquals;
23+
import static org.junit.jupiter.api.Assertions.assertThrows;
24+
import static org.mockito.ArgumentMatchers.any;
25+
import static org.mockito.ArgumentMatchers.anyString;
26+
import static org.mockito.ArgumentMatchers.eq;
27+
import static org.mockito.Mockito.CALLS_REAL_METHODS;
28+
import static org.mockito.Mockito.mock;
29+
import static org.mockito.Mockito.mockStatic;
30+
import static org.mockito.Mockito.when;
31+
32+
import java.io.IOException;
33+
import java.time.Clock;
34+
import java.time.Instant;
35+
import java.util.UUID;
36+
import java.util.concurrent.ThreadLocalRandom;
37+
import org.apache.hadoop.hdds.security.symmetric.SecretKeyClient;
38+
import org.apache.hadoop.hdds.utils.db.InMemoryTestTable;
39+
import org.apache.hadoop.hdds.utils.db.Table;
40+
import org.apache.hadoop.ozone.om.OMMetadataManager;
41+
import org.apache.hadoop.ozone.om.OzoneManager;
42+
import org.apache.hadoop.ozone.om.exceptions.OMException;
43+
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRequest;
44+
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.S3Authentication;
45+
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.Type;
46+
import org.junit.jupiter.api.Test;
47+
import org.mockito.MockedStatic;
48+
49+
/**
50+
* Tests for STS revocation handling in {@link S3SecurityUtil}.
51+
*/
52+
public class TestS3SecurityUtil {
53+
private static final byte[] ENCRYPTION_KEY = new byte[5];
54+
55+
{
56+
ThreadLocalRandom.current().nextBytes(ENCRYPTION_KEY);
57+
}
58+
59+
@Test
60+
public void testValidateS3CredentialFailsWhenTokenRevoked() throws Exception {
61+
// If the revoked STS token table contains an entry for the temporary access key id extracted from the session
62+
// token, validateS3Credential should reject the request with REVOKED_TOKEN
63+
final String sessionToken = "session-token-a";
64+
final String tempAccessKeyId = "ASIA123456789";
65+
66+
try (OzoneManager ozoneManager = mock(OzoneManager.class)) {
67+
when(ozoneManager.isSecurityEnabled()).thenReturn(true);
68+
when(ozoneManager.getSecretKeyClient()).thenReturn(mock(SecretKeyClient.class));
69+
70+
final OMMetadataManager metadataManager = mock(OMMetadataManager.class);
71+
when(ozoneManager.getMetadataManager()).thenReturn(metadataManager);
72+
73+
final Table<String, String> revokedSTSTokenTable = new InMemoryTestTable<>();
74+
when(metadataManager.getS3RevokedStsTokenTable()).thenReturn(revokedSTSTokenTable);
75+
76+
// Mock STSSecurityUtil to return a token whose tempAccessKeyId matches the one that's revoked.
77+
final STSTokenIdentifier stsTokenIdentifier = new STSTokenIdentifier(
78+
tempAccessKeyId, "original-access-key-id", "arn:aws:iam::123456789012:role/test-role",
79+
Instant.now().plusSeconds(3600), "secret-access-key", "session-policy",
80+
ENCRYPTION_KEY);
81+
82+
try (MockedStatic<STSSecurityUtil> stsSecurityUtilMock = mockStatic(STSSecurityUtil.class, CALLS_REAL_METHODS)) {
83+
stsSecurityUtilMock.when(
84+
() -> STSSecurityUtil.constructValidateAndDecryptSTSToken(
85+
eq(sessionToken), any(SecretKeyClient.class), any(Clock.class)))
86+
.thenReturn(stsTokenIdentifier);
87+
88+
// Revoke the tempAccessKeyId
89+
revokedSTSTokenTable.put(tempAccessKeyId, sessionToken);
90+
91+
final OMRequest omRequest = createRequestWithSessionToken(sessionToken);
92+
final OMException ex = assertThrows(
93+
OMException.class, () -> S3SecurityUtil.validateS3Credential(omRequest, ozoneManager));
94+
assertEquals(REVOKED_TOKEN, ex.getResult());
95+
}
96+
}
97+
}
98+
99+
@Test
100+
public void testValidateS3CredentialWhenMetadataUnavailable() {
101+
// If the metadata manager is not available, the revocation check should not cause the request to be rejected.
102+
final String sessionToken = "session-token-b";
103+
104+
try (OzoneManager ozoneManager = mock(OzoneManager.class)) {
105+
when(ozoneManager.isSecurityEnabled()).thenReturn(true);
106+
when(ozoneManager.getMetadataManager()).thenReturn(null);
107+
when(ozoneManager.getSecretKeyClient()).thenReturn(mock(SecretKeyClient.class));
108+
109+
final OMRequest omRequest = createRequestWithSessionToken(sessionToken);
110+
111+
try (MockedStatic<STSSecurityUtil> stsSecurityUtilMock = mockStatic(STSSecurityUtil.class, CALLS_REAL_METHODS);
112+
MockedStatic<AWSV4AuthValidator> awsV4AuthValidatorMock = mockStatic(
113+
AWSV4AuthValidator.class, CALLS_REAL_METHODS)) {
114+
115+
final STSTokenIdentifier stsTokenIdentifier = new STSTokenIdentifier(
116+
"temp-access-key-id", "original-access-key-id", "arn:aws:iam::123456789012:role/test-role",
117+
Instant.now().plusSeconds(3600), "secret-access-key", "session-policy",
118+
ENCRYPTION_KEY);
119+
120+
stsSecurityUtilMock.when(
121+
() -> STSSecurityUtil.constructValidateAndDecryptSTSToken(
122+
eq(sessionToken), any(SecretKeyClient.class), any(Clock.class)))
123+
.thenReturn(stsTokenIdentifier);
124+
125+
// Mock AWS V4 signature validation
126+
awsV4AuthValidatorMock.when(() -> AWSV4AuthValidator.validateRequest(anyString(), anyString(), anyString()))
127+
.thenReturn(true);
128+
129+
assertDoesNotThrow(() -> S3SecurityUtil.validateS3Credential(omRequest, ozoneManager));
130+
}
131+
} catch (IOException e) {
132+
throw new RuntimeException(e);
133+
}
134+
}
135+
136+
@Test
137+
public void testValidateS3CredentialSuccessWhenNotRevoked() {
138+
// Normal case: token is NOT revoked and request is accepted
139+
final String sessionToken = "session-token-c";
140+
141+
try (OzoneManager ozoneManager = mock(OzoneManager.class)) {
142+
when(ozoneManager.isSecurityEnabled()).thenReturn(true);
143+
when(ozoneManager.getSecretKeyClient()).thenReturn(mock(SecretKeyClient.class));
144+
145+
final OMMetadataManager metadataManager = mock(OMMetadataManager.class);
146+
when(ozoneManager.getMetadataManager()).thenReturn(metadataManager);
147+
148+
final Table<String, String> revokedSTSTokenTable = new InMemoryTestTable<>();
149+
when(metadataManager.getS3RevokedStsTokenTable()).thenReturn(revokedSTSTokenTable);
150+
151+
// Not revoked -> getIfExist returns null by default in InMemoryTestTable
152+
final OMRequest omRequest = createRequestWithSessionToken(sessionToken);
153+
final STSTokenIdentifier stsTokenIdentifier = new STSTokenIdentifier(
154+
"temp-access-key-id", "original-access-key-id", "arn:aws:iam::123456789012:role/test-role",
155+
Instant.now().plusSeconds(3600), "secret-access-key", "session-policy",
156+
ENCRYPTION_KEY);
157+
158+
try (MockedStatic<STSSecurityUtil> stsSecurityUtilMock = mockStatic(STSSecurityUtil.class, CALLS_REAL_METHODS);
159+
MockedStatic<AWSV4AuthValidator> awsV4AuthValidatorMock = mockStatic(
160+
AWSV4AuthValidator.class, CALLS_REAL_METHODS)) {
161+
162+
stsSecurityUtilMock.when(
163+
() -> STSSecurityUtil.constructValidateAndDecryptSTSToken(
164+
eq(sessionToken), any(SecretKeyClient.class), any(Clock.class)))
165+
.thenReturn(stsTokenIdentifier);
166+
awsV4AuthValidatorMock.when(() -> AWSV4AuthValidator.validateRequest(anyString(), anyString(), anyString()))
167+
.thenReturn(true);
168+
169+
assertDoesNotThrow(() -> S3SecurityUtil.validateS3Credential(omRequest, ozoneManager));
170+
}
171+
} catch (IOException e) {
172+
throw new RuntimeException(e);
173+
}
174+
}
175+
176+
private static OMRequest createRequestWithSessionToken(String sessionToken) {
177+
final S3Authentication s3Authentication = S3Authentication.newBuilder()
178+
.setAccessId("accessKeyId")
179+
.setStringToSign("string-to-sign")
180+
.setSignature("signature")
181+
.setSessionToken(sessionToken)
182+
.build();
183+
184+
return OMRequest.newBuilder()
185+
.setClientId(UUID.randomUUID().toString())
186+
.setCmdType(Type.CreateVolume)
187+
.setS3Authentication(s3Authentication)
188+
.build();
189+
}
190+
}

hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenIdentifier.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import java.time.Instant;
2929
import java.time.temporal.ChronoUnit;
3030
import java.util.UUID;
31+
import java.util.concurrent.ThreadLocalRandom;
3132
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMTokenProto;
3233
import org.junit.jupiter.api.Test;
3334

@@ -39,7 +40,7 @@ public class TestSTSTokenIdentifier {
3940
private static final byte[] ENCRYPTION_KEY = new byte[5];
4041

4142
{
42-
new SecureRandom().nextBytes(ENCRYPTION_KEY);
43+
ThreadLocalRandom.current().nextBytes(ENCRYPTION_KEY);
4344
}
4445

4546
@Test

0 commit comments

Comments
 (0)