Skip to content

Commit 8f9e629

Browse files
author
Fabian Morgan
committed
handle latent inconsistencies in s3 api acl checks
1 parent c463a67 commit 8f9e629

7 files changed

Lines changed: 265 additions & 7 deletions

File tree

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2311,6 +2311,14 @@ message S3Authentication {
23112311
// If present, indicates this request uses STS temporary credentials
23122312
// and carries the base64-encoded session token for validation.
23132313
optional string sessionToken = 4;
2314+
// The following fields are resolved from the STS session token by OM.
2315+
// They are used to enforce STS session policies during Ratis apply.
2316+
// They must be written or cleared by the OM leader when the token is validated.
2317+
optional string resolvedStsSessionPolicy = 5;
2318+
optional string resolvedStsRoleArn = 6;
2319+
optional string resolvedStsOriginalAccessKeyId = 7;
2320+
optional string resolvedStsTempAccessKeyId = 8;
2321+
optional string resolvedStsSecretKeyId = 9;
23142322
}
23152323

23162324
message RecoverLeaseRequest {

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@
5555
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMResponse;
5656
import org.apache.hadoop.ozone.protocolPB.OzoneManagerRequestHandler;
5757
import org.apache.hadoop.ozone.protocolPB.RequestHandler;
58+
import org.apache.hadoop.ozone.security.STSSecurityUtil;
59+
import org.apache.hadoop.ozone.security.STSTokenIdentifier;
5860
import org.apache.hadoop.security.UserGroupInformation;
5961
import org.apache.hadoop.util.Time;
6062
import org.apache.hadoop.util.concurrent.HadoopExecutors;
@@ -617,7 +619,29 @@ public void close() {
617619
*/
618620
@VisibleForTesting
619621
OMResponse runCommand(OMRequest request, TermIndex termIndex) {
622+
boolean isStsThreadLocalSet = false;
620623
try {
624+
if (ozoneManager.isSecurityEnabled() && request.hasS3Authentication()) {
625+
// STS token verification runs on the leader RPC path so we don't need to recheck here on the apply
626+
// after the log is committed
627+
STSSecurityUtil.ensureResolvedStsFieldsInvariants(request);
628+
629+
final OzoneManagerProtocolProtos.S3Authentication s3Auth = request.getS3Authentication();
630+
if (s3Auth.hasSessionToken() && !s3Auth.getSessionToken().isEmpty()) {
631+
// ThreadLocal carries session policy for OmMetadataReader
632+
final STSTokenIdentifier rehydratedTokenIdentifier = new STSTokenIdentifier(
633+
s3Auth.hasResolvedStsTempAccessKeyId() ? s3Auth.getResolvedStsTempAccessKeyId() : "",
634+
s3Auth.hasResolvedStsOriginalAccessKeyId() ? s3Auth.getResolvedStsOriginalAccessKeyId() : "",
635+
s3Auth.hasResolvedStsRoleArn() ? s3Auth.getResolvedStsRoleArn() : "",
636+
java.time.Instant.MAX, // ensure it deterministically is not expired
637+
"", // no secretAccessKey needed
638+
s3Auth.hasResolvedStsSessionPolicy() ? s3Auth.getResolvedStsSessionPolicy() : "",
639+
null // no encryption key needed
640+
);
641+
OzoneManager.setStsTokenIdentifier(rehydratedTokenIdentifier);
642+
isStsThreadLocalSet = true;
643+
}
644+
}
621645
ExecutionContext context = ExecutionContext.of(termIndex.getIndex(), termIndex);
622646
final OMClientResponse omClientResponse = handler.handleWriteRequest(
623647
request, context, ozoneManagerDoubleBuffer);
@@ -636,6 +660,10 @@ OMResponse runCommand(OMRequest request, TermIndex termIndex) {
636660
// For any Runtime exceptions, terminate OM.
637661
String errorMessage = "Request " + request + " failed with exception";
638662
ExitUtils.terminate(1, errorMessage, e, LOG);
663+
} finally {
664+
if (isStsThreadLocalSet) {
665+
OzoneManager.setStsTokenIdentifier(null);
666+
}
639667
}
640668
return null;
641669
}

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

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import java.util.LinkedHashMap;
2828
import java.util.Map;
2929
import java.util.Objects;
30+
import java.util.UUID;
3031
import org.apache.commons.lang3.StringUtils;
3132
import org.apache.hadoop.hdds.utils.TransactionInfo;
3233
import org.apache.hadoop.ipc_.ProtobufRpcEngine;
@@ -54,6 +55,7 @@
5455
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.LayoutVersion;
5556
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRequest;
5657
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMResponse;
58+
import org.apache.hadoop.ozone.security.STSTokenIdentifier;
5759
import org.apache.hadoop.ozone.security.acl.IAccessAuthorizer;
5860
import org.apache.hadoop.ozone.security.acl.OzoneObj;
5961
import org.apache.hadoop.ozone.security.acl.OzoneObjInfo;
@@ -112,15 +114,50 @@ public OMClientRequest(OMRequest omRequest) {
112114
*/
113115
public OMRequest preExecute(OzoneManager ozoneManager)
114116
throws IOException {
115-
LayoutVersion layoutVersion = LayoutVersion.newBuilder()
117+
final LayoutVersion layoutVersion = LayoutVersion.newBuilder()
116118
.setVersion(ozoneManager.getVersionManager().getMetadataLayoutVersion())
117119
.build();
118-
omRequest = getOmRequest().toBuilder()
120+
121+
final OMRequest.Builder requestBuilder = getOmRequest().toBuilder()
119122
.setUserInfo(getUserIfNotExists(ozoneManager))
120-
.setLayoutVersion(layoutVersion).build();
123+
.setLayoutVersion(layoutVersion);
124+
125+
if (requestBuilder.hasS3Authentication()) {
126+
requestBuilder.setS3Authentication(
127+
resolveS3Authentication(requestBuilder.getS3Authentication(), OzoneManager.getStsTokenIdentifier()));
128+
}
129+
130+
omRequest = requestBuilder.build();
121131
return omRequest;
122132
}
123133

134+
private static OzoneManagerProtocolProtos.S3Authentication resolveS3Authentication(
135+
OzoneManagerProtocolProtos.S3Authentication s3Auth, STSTokenIdentifier stsTokenIdentifier) {
136+
final OzoneManagerProtocolProtos.S3Authentication.Builder s3AuthBuilder = s3Auth.toBuilder();
137+
138+
if (s3Auth.hasSessionToken() && !s3Auth.getSessionToken().isEmpty() && stsTokenIdentifier != null) {
139+
s3AuthBuilder.setResolvedStsSessionPolicy(
140+
StringUtils.defaultString(stsTokenIdentifier.getSessionPolicy()));
141+
s3AuthBuilder.setResolvedStsRoleArn(
142+
StringUtils.defaultString(stsTokenIdentifier.getRoleArn()));
143+
s3AuthBuilder.setResolvedStsOriginalAccessKeyId(
144+
StringUtils.defaultString(stsTokenIdentifier.getOriginalAccessKeyId()));
145+
s3AuthBuilder.setResolvedStsTempAccessKeyId(
146+
StringUtils.defaultString(stsTokenIdentifier.getTempAccessKeyId()));
147+
final UUID secretKeyId = stsTokenIdentifier.getSecretKeyId();
148+
s3AuthBuilder.setResolvedStsSecretKeyId(
149+
secretKeyId != null ? secretKeyId.toString() : "");
150+
} else {
151+
s3AuthBuilder.clearResolvedStsSessionPolicy();
152+
s3AuthBuilder.clearResolvedStsRoleArn();
153+
s3AuthBuilder.clearResolvedStsOriginalAccessKeyId();
154+
s3AuthBuilder.clearResolvedStsTempAccessKeyId();
155+
s3AuthBuilder.clearResolvedStsSecretKeyId();
156+
}
157+
158+
return s3AuthBuilder.build();
159+
}
160+
124161
/**
125162
* Performs any request specific failure handling during request
126163
* submission. An example of this would be an undo of any steps

hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/bucket/acl/OMBucketSetAclRequest.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,15 @@ public class OMBucketSetAclRequest extends OMBucketAclRequest {
5454

5555
@Override
5656
public OMRequest preExecute(OzoneManager ozoneManager) throws IOException {
57-
long modificationTime = Time.now();
58-
OzoneManagerProtocolProtos.SetAclRequest.Builder setAclRequestBuilder =
57+
final long modificationTime = Time.now();
58+
final OzoneManagerProtocolProtos.SetAclRequest.Builder setAclRequestBuilder =
5959
getOmRequest().getSetAclRequest().toBuilder()
6060
.setModificationTime(modificationTime);
6161

62-
return getOmRequest().toBuilder()
62+
// super.preExecute resolves S3Authentication (STS) for Ratis apply. Merge SetAclRequest changes on top.
63+
final OMRequest request = super.preExecute(ozoneManager);
64+
return request.toBuilder()
6365
.setSetAclRequest(setAclRequestBuilder)
64-
.setUserInfo(getUserInfo())
6566
.build();
6667
}
6768

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

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@
3131
import org.apache.hadoop.hdds.security.symmetric.ManagedSecretKey;
3232
import org.apache.hadoop.hdds.security.symmetric.SecretKeyClient;
3333
import org.apache.hadoop.ozone.om.exceptions.OMException;
34+
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos;
3435
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMTokenProto;
36+
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.S3Authentication;
3537
import org.apache.hadoop.security.token.SecretManager;
3638
import org.apache.hadoop.security.token.Token;
3739

@@ -175,5 +177,44 @@ static void ensureEssentialFieldsArePresentInToken(STSTokenIdentifier stsTokenId
175177
throw new SecretManager.InvalidToken("Invalid STS token - secretAccessKey is null/empty");
176178
}
177179
}
180+
181+
/**
182+
* Ensures STS-related {@link S3Authentication} fields are structurally consistent on the Ratis
183+
* apply path. Cryptographic validation (signature, expiry, secret key lookup) runs on the leader
184+
* RPC path (e.g. {@code S3SecurityUtil.validateS3Credential}). This method performs no crypto and does
185+
* not contact {@link SecretKeyClient}, keeping the apply thread deterministic and lightweight.
186+
*
187+
* @param request OM request possibly containing S3 authentication
188+
* @throws OMException if resolved fields and session token presence are inconsistent
189+
*/
190+
public static void ensureResolvedStsFieldsInvariants(OzoneManagerProtocolProtos.OMRequest request)
191+
throws OMException {
192+
if (!request.hasS3Authentication()) {
193+
return;
194+
}
195+
196+
final S3Authentication s3Auth = request.getS3Authentication();
197+
final boolean hasSessionToken = s3Auth.hasSessionToken() && !s3Auth.getSessionToken().isEmpty();
198+
199+
if (!hasSessionToken) {
200+
// If sessionToken is missing/empty, resolved fields must be empty.
201+
if (s3Auth.hasResolvedStsSessionPolicy() || s3Auth.hasResolvedStsRoleArn() ||
202+
s3Auth.hasResolvedStsOriginalAccessKeyId() || s3Auth.hasResolvedStsTempAccessKeyId() ||
203+
s3Auth.hasResolvedStsSecretKeyId()) {
204+
throw new OMException("Resolved STS fields must be empty when sessionToken is not present", INVALID_TOKEN);
205+
}
206+
return;
207+
}
208+
209+
ensureResolvedFieldsArePresent(s3Auth);
210+
}
211+
212+
private static void ensureResolvedFieldsArePresent(S3Authentication s3Auth) throws OMException {
213+
if (!s3Auth.hasResolvedStsSessionPolicy() || !s3Auth.hasResolvedStsRoleArn() ||
214+
!s3Auth.hasResolvedStsOriginalAccessKeyId() || !s3Auth.hasResolvedStsTempAccessKeyId() ||
215+
!s3Auth.hasResolvedStsSecretKeyId()) {
216+
throw new OMException("Resolved STS fields must be present when sessionToken is present", INVALID_TOKEN);
217+
}
218+
}
178219
}
179220

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

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,72 @@ public void testUserInfoWithSTSToken() throws IOException {
210210
}
211211
}
212212

213+
@Test
214+
public void testPreExecuteOverwritesResolvedStsFields() throws Exception {
215+
try (MockedStatic<Server> mockedRpcServer = mockStatic(Server.class)) {
216+
mockedRpcServer.when(Server::getRemoteUser).thenReturn(userGroupInformation);
217+
mockedRpcServer.when(Server::getRemoteIp).thenReturn(inetAddress);
218+
mockedRpcServer.when(Server::getRemoteAddress).thenReturn(inetAddress.toString());
219+
220+
final String accessId = "ASIA12345";
221+
final String signature = "Signature";
222+
final String stringToSign = "StringToSign";
223+
final String sessionToken = "SessionToken";
224+
final String originalAccessKeyId = "AKIAORIGINAL";
225+
final String roleArn = "arn:aws:iam::123456789012:role/test-role";
226+
final String sessionPolicy = "test-session-policy";
227+
final UUID secretKeyId = UUID.randomUUID();
228+
229+
final STSTokenIdentifier stsTokenIdentifier = mock(STSTokenIdentifier.class);
230+
when(stsTokenIdentifier.getSessionPolicy()).thenReturn(sessionPolicy);
231+
when(stsTokenIdentifier.getRoleArn()).thenReturn(roleArn);
232+
when(stsTokenIdentifier.getOriginalAccessKeyId()).thenReturn(originalAccessKeyId);
233+
when(stsTokenIdentifier.getTempAccessKeyId()).thenReturn(accessId);
234+
when(stsTokenIdentifier.getSecretKeyId()).thenReturn(secretKeyId);
235+
236+
final S3Authentication s3Authentication = S3Authentication.newBuilder()
237+
.setAccessId(accessId)
238+
.setSignature(signature)
239+
.setStringToSign(stringToSign)
240+
.setSessionToken(sessionToken)
241+
.setResolvedStsSessionPolicy("client-session-policy")
242+
.setResolvedStsRoleArn("client-role")
243+
.setResolvedStsOriginalAccessKeyId("client-original-access-key-id")
244+
.setResolvedStsTempAccessKeyId("client-temp-access-key-id")
245+
.setResolvedStsSecretKeyId("client-secret-key-id")
246+
.build();
247+
248+
OzoneManager.setS3Auth(s3Authentication);
249+
OzoneManager.setStsTokenIdentifier(stsTokenIdentifier);
250+
251+
try {
252+
final String bucketName = UUID.randomUUID().toString();
253+
final String volumeName = UUID.randomUUID().toString();
254+
final BucketInfo.Builder bucketInfo =
255+
newBucketInfoBuilder(bucketName, volumeName)
256+
.setIsVersionEnabled(true)
257+
.setStorageType(StorageTypeProto.DISK);
258+
259+
final OMRequest omRequest = newCreateBucketRequest(bucketInfo)
260+
.setS3Authentication(s3Authentication)
261+
.build();
262+
263+
final OMBucketCreateRequest omBucketCreateRequest = new OMBucketCreateRequest(omRequest);
264+
final OMRequest modifiedRequest = omBucketCreateRequest.preExecute(ozoneManager);
265+
final S3Authentication modifiedS3Auth = modifiedRequest.getS3Authentication();
266+
267+
assertEquals(sessionPolicy, modifiedS3Auth.getResolvedStsSessionPolicy());
268+
assertEquals(roleArn, modifiedS3Auth.getResolvedStsRoleArn());
269+
assertEquals(originalAccessKeyId, modifiedS3Auth.getResolvedStsOriginalAccessKeyId());
270+
assertEquals(accessId, modifiedS3Auth.getResolvedStsTempAccessKeyId());
271+
assertEquals(secretKeyId.toString(), modifiedS3Auth.getResolvedStsSecretKeyId());
272+
} finally {
273+
OzoneManager.setStsTokenIdentifier(null);
274+
OzoneManager.setS3Auth(null);
275+
}
276+
}
277+
}
278+
213279
@Test
214280
public void testUserInfoWithSTSAccessKeyMissingSessionToken() {
215281
final String accessId = "ASIA12345";

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

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@
3535
import org.apache.hadoop.hdds.security.symmetric.SecretKeyClient;
3636
import org.apache.hadoop.io.Text;
3737
import org.apache.hadoop.ozone.om.exceptions.OMException;
38+
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRequest;
3839
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMTokenProto;
40+
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.S3Authentication;
41+
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.Type;
3942
import org.apache.hadoop.security.token.SecretManager;
4043
import org.apache.hadoop.security.token.Token;
4144
import org.apache.ozone.test.TestClock;
@@ -238,6 +241,8 @@ public void testConstructValidateAndDecryptSTSTokenExpiredSecretKey() throws Exc
238241
// Create a mock secret key that is expired
239242
final ManagedSecretKey expiredSecretKey = mock(ManagedSecretKey.class);
240243
when(expiredSecretKey.isExpired()).thenReturn(true);
244+
final Instant now = Instant.now();
245+
when(expiredSecretKey.getExpiryTime()).thenReturn(now);
241246

242247
final SecretKeyClient mockKeyClient = mock(SecretKeyClient.class);
243248
when(mockKeyClient.getSecretKey(any())).thenReturn(expiredSecretKey);
@@ -373,4 +378,76 @@ public void testEnsureEssentialFieldsArePresentInTokenMissingSecretAccessKey() {
373378
.isInstanceOf(SecretManager.InvalidToken.class)
374379
.hasMessage("Invalid STS token - secretAccessKey is null/empty");
375380
}
381+
382+
@Test
383+
public void testEnsureResolvedStsFieldsInvariantsSuccess() throws Exception {
384+
final String tokenString = tokenSecretManager.createSTSTokenString(
385+
TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS, SECRET_ACCESS_KEY, SESSION_POLICY, clock);
386+
387+
final S3Authentication s3Auth = S3Authentication.newBuilder()
388+
.setSessionToken(tokenString)
389+
.setResolvedStsSessionPolicy(SESSION_POLICY)
390+
.setResolvedStsRoleArn(ROLE_ARN)
391+
.setResolvedStsOriginalAccessKeyId(ORIGINAL_ACCESS_KEY)
392+
.setResolvedStsTempAccessKeyId(TEMP_ACCESS_KEY)
393+
.setResolvedStsSecretKeyId(secretKeyId.toString())
394+
.build();
395+
396+
final OMRequest request = OMRequest.newBuilder()
397+
.setCmdType(Type.CreateBucket)
398+
.setClientId("client-id")
399+
.setS3Authentication(s3Auth)
400+
.build();
401+
402+
STSSecurityUtil.ensureResolvedStsFieldsInvariants(request);
403+
}
404+
405+
@Test
406+
public void testEnsureResolvedStsFieldsInvariantsMissingSessionToken() {
407+
final S3Authentication s3Auth = S3Authentication.newBuilder()
408+
.setResolvedStsSessionPolicy(SESSION_POLICY)
409+
.build();
410+
411+
final OMRequest request = OMRequest.newBuilder()
412+
.setCmdType(Type.CreateBucket)
413+
.setClientId("client-id")
414+
.setS3Authentication(s3Auth)
415+
.build();
416+
417+
assertThatThrownBy(() -> STSSecurityUtil.ensureResolvedStsFieldsInvariants(request))
418+
.isInstanceOf(OMException.class)
419+
.hasMessageContaining("Resolved STS fields must be empty when sessionToken is not present");
420+
}
421+
422+
@Test
423+
public void testEnsureResolvedStsFieldsInvariantsMissingResolvedFields() throws Exception {
424+
final String tokenString = tokenSecretManager.createSTSTokenString(
425+
TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS,
426+
SECRET_ACCESS_KEY, SESSION_POLICY, clock);
427+
428+
final S3Authentication s3Auth = S3Authentication.newBuilder()
429+
.setSessionToken(tokenString)
430+
.build();
431+
432+
final OMRequest request = OMRequest.newBuilder()
433+
.setCmdType(Type.CreateBucket)
434+
.setClientId("client-id")
435+
.setS3Authentication(s3Auth)
436+
.build();
437+
438+
assertThatThrownBy(() -> STSSecurityUtil.ensureResolvedStsFieldsInvariants(request))
439+
.isInstanceOf(OMException.class)
440+
.hasMessageContaining("Resolved STS fields must be present when sessionToken is present");
441+
}
442+
443+
@Test
444+
public void testEnsureResolvedStsFieldsInvariantsNoS3Auth() throws Exception {
445+
final OMRequest request = OMRequest.newBuilder()
446+
.setCmdType(Type.CreateBucket)
447+
.setClientId("client-id")
448+
.build();
449+
450+
// Should not throw
451+
STSSecurityUtil.ensureResolvedStsFieldsInvariants(request);
452+
}
376453
}

0 commit comments

Comments
 (0)