Skip to content

Commit 5bbfa24

Browse files
author
Fabian Morgan
committed
handle latent inconsistencies in s3 api acl checks
1 parent 37a224b commit 5bbfa24

7 files changed

Lines changed: 263 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

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

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

0 commit comments

Comments
 (0)