Skip to content

Commit 3685237

Browse files
authored
HDDS-13997. [STS] Plumbing for passing STS token through S3 api processing (#9372)
1 parent 934f4c1 commit 3685237

14 files changed

Lines changed: 917 additions & 24 deletions

File tree

hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/S3Auth.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ public class S3Auth {
2727
public static final String S3_AUTH_CHECK = "ozone.s3.auth.check";
2828
// User principal to be used for KMS encryption and decryption
2929
private String userPrincipal;
30+
// Optional STS session token when using temporary credentials
31+
private String sessionToken;
3032

3133
public S3Auth(final String stringToSign,
3234
final String signature,
@@ -57,4 +59,12 @@ public String getUserPrincipal() {
5759
public void setUserPrincipal(String userPrincipal) {
5860
this.userPrincipal = userPrincipal;
5961
}
62+
63+
public String getSessionToken() {
64+
return sessionToken;
65+
}
66+
67+
public void setSessionToken(String sessionToken) {
68+
this.sessionToken = sessionToken;
69+
}
6070
}

hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -314,15 +314,21 @@ private OMResponse submitRequest(OMRequest omRequest)
314314
OMRequest.Builder builder = OMRequest.newBuilder(omRequest);
315315
// Insert S3 Authentication information for each request.
316316
if (getThreadLocalS3Auth() != null) {
317-
builder.setS3Authentication(
317+
final S3Authentication.Builder s3AuthBuilder =
318318
S3Authentication.newBuilder()
319319
.setSignature(
320320
threadLocalS3Auth.get().getSignature())
321321
.setStringToSign(
322322
threadLocalS3Auth.get().getStringTosSign())
323323
.setAccessId(
324-
threadLocalS3Auth.get().getAccessID())
325-
.build());
324+
threadLocalS3Auth.get().getAccessID());
325+
326+
// Include STS session token if present so OM can validate it
327+
if (threadLocalS3Auth.get().getSessionToken() != null) {
328+
s3AuthBuilder.setSessionToken(threadLocalS3Auth.get().getSessionToken());
329+
}
330+
331+
builder.setS3Authentication(s3AuthBuilder.build());
326332
}
327333
if (s3AuthCheck && getThreadLocalS3Auth() == null) {
328334
throw new IllegalArgumentException("S3 Auth expected to " +

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -488,8 +488,9 @@ void checkAcls(ResourceType resType, StoreType store,
488488
throws IOException {
489489
UserGroupInformation user;
490490
if (getS3Auth() != null) {
491+
final String effectiveAccessId = OzoneManager.getS3AuthEffectiveAccessId();
491492
String principal =
492-
OzoneAclUtils.accessIdToUserPrincipal(getS3Auth().getAccessId());
493+
OzoneAclUtils.accessIdToUserPrincipal(effectiveAccessId);
493494
user = UserGroupInformation.createRemoteUser(principal);
494495
} else {
495496
user = ProtobufRpcEngine.Server.getRemoteUser();
@@ -523,8 +524,9 @@ void checkAcls(ResourceType resType, StoreType store,
523524
throws IOException {
524525
UserGroupInformation user;
525526
if (getS3Auth() != null) {
527+
final String effectiveAccessId = OzoneManager.getS3AuthEffectiveAccessId();
526528
String principal =
527-
OzoneAclUtils.accessIdToUserPrincipal(getS3Auth().getAccessId());
529+
OzoneAclUtils.accessIdToUserPrincipal(effectiveAccessId);
528530
user = UserGroupInformation.createRemoteUser(principal);
529531
} else {
530532
user = ProtobufRpcEngine.Server.getRemoteUser();

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

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,7 @@
307307
import org.apache.hadoop.ozone.security.OMCertificateClient;
308308
import org.apache.hadoop.ozone.security.OzoneDelegationTokenSecretManager;
309309
import org.apache.hadoop.ozone.security.OzoneTokenIdentifier;
310+
import org.apache.hadoop.ozone.security.STSTokenIdentifier;
310311
import org.apache.hadoop.ozone.security.STSTokenSecretManager;
311312
import org.apache.hadoop.ozone.security.acl.IAccessAuthorizer;
312313
import org.apache.hadoop.ozone.security.acl.IAccessAuthorizer.ACLIdentityType;
@@ -378,6 +379,9 @@ public final class OzoneManager extends ServiceRuntimeInfoImpl
378379
private static final ThreadLocal<S3Authentication> S3_AUTH =
379380
new ThreadLocal<>();
380381

382+
// STS token (if present)
383+
private static final ThreadLocal<STSTokenIdentifier> STS_TOKEN = new ThreadLocal<>();
384+
381385
private static boolean securityEnabled = false;
382386

383387
private final ReconfigurationHandler reconfigurationHandler;
@@ -858,6 +862,47 @@ public static S3Authentication getS3Auth() {
858862
return S3_AUTH.get();
859863
}
860864

865+
/**
866+
* Set the STS token identifier for the current RPC handler thread.
867+
*/
868+
public static void setStsTokenIdentifier(STSTokenIdentifier val) {
869+
STS_TOKEN.set(val);
870+
}
871+
872+
/**
873+
* Get the STS token identifier for the current RPC handler thread.
874+
*/
875+
public static STSTokenIdentifier getStsTokenIdentifier() {
876+
return STS_TOKEN.get();
877+
}
878+
879+
/**
880+
* Returns the effective accessId for the current request. If STS temporary credentials are being used,
881+
* the access key id will be the original access key id (i.e. the creator of the token).
882+
*/
883+
public static String getS3AuthEffectiveAccessId() throws OMException {
884+
final S3Authentication s3Auth = getS3Auth();
885+
if (s3Auth == null) {
886+
return null;
887+
}
888+
889+
// If session token is present, try to resolve originalAccessKeyId from token
890+
if (s3Auth.hasSessionToken() && !s3Auth.getSessionToken().isEmpty()) {
891+
final STSTokenIdentifier stsTokenIdentifier = getStsTokenIdentifier();
892+
if (stsTokenIdentifier == null) {
893+
throw new OMException(
894+
"OMClientRequest has session token but no token identifier in OzoneManager", INVALID_REQUEST);
895+
}
896+
final String originalAccessKeyId = stsTokenIdentifier.getOriginalAccessKeyId();
897+
if (originalAccessKeyId != null && !originalAccessKeyId.isEmpty()) {
898+
return originalAccessKeyId;
899+
} else {
900+
throw new OMException("Invalid STS Token format - could not find originalAccessKeyId", INVALID_REQUEST);
901+
}
902+
}
903+
return s3Auth.getAccessId();
904+
}
905+
861906
/** Returns the ThreadName prefix for the current OM. */
862907
public String getThreadNamePrefix() {
863908
return threadPrefix;
@@ -1269,6 +1314,15 @@ private void stopSecretManager() {
12691314
}
12701315
}
12711316

1317+
/**
1318+
* Get the secret key client for this OzoneManager.
1319+
*
1320+
* @return the secret key client
1321+
*/
1322+
public SecretKeyClient getSecretKeyClient() {
1323+
return secretKeyClient;
1324+
}
1325+
12721326
@Override
12731327
public UUID refetchSecretKey() {
12741328
secretKeyClient.refetchSecretKey();
@@ -3836,7 +3890,12 @@ S3VolumeContext getS3VolumeContext(boolean skipChecks) throws IOException {
38363890
s3Volume);
38373891
}
38383892
} else {
3839-
String accessId = s3Auth.getAccessId();
3893+
// If this S3 request is authenticated with an STS session token, map
3894+
// the request to the *original* long-lived access ID so that the
3895+
// temporary credentials inherit that user's ACLs. Otherwise, fall back
3896+
// to the accessId included directly in the S3Authentication.
3897+
final String accessId = getS3AuthEffectiveAccessId();
3898+
38403899
// If S3 Multi-Tenancy is not enabled, all S3 requests will be redirected
38413900
// to the default s3v for compatibility
38423901
final Optional<String> optionalTenantId = isS3MultiTenancyEnabled() ?
@@ -4621,8 +4680,8 @@ public ResolvedBucket resolveBucketLink(Pair<String, String> requested,
46214680
if (aclEnabled) {
46224681
UserGroupInformation ugi = getRemoteUser();
46234682
if (getS3Auth() != null) {
4624-
ugi = UserGroupInformation.createRemoteUser(
4625-
OzoneAclUtils.accessIdToUserPrincipal(getS3Auth().getAccessId()));
4683+
final String principal = OzoneAclUtils.accessIdToUserPrincipal(getS3AuthEffectiveAccessId());
4684+
ugi = UserGroupInformation.createRemoteUser(principal);
46264685
}
46274686
InetAddress remoteIp = Server.getRemoteIp();
46284687
resolved = resolveBucketLink(requested, new HashSet<>(),

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

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import org.apache.hadoop.ozone.om.lock.OMLockDetails;
4949
import org.apache.hadoop.ozone.om.protocolPB.grpc.GrpcClientConstants;
5050
import org.apache.hadoop.ozone.om.ratis.utils.OzoneManagerRatisUtils;
51+
import org.apache.hadoop.ozone.om.request.s3.security.S3AssumeRoleRequest;
5152
import org.apache.hadoop.ozone.om.response.OMClientResponse;
5253
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos;
5354
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.LayoutVersion;
@@ -168,11 +169,33 @@ public OzoneManagerProtocolProtos.UserInfo getUserInfo() throws IOException {
168169
OzoneManagerProtocolProtos.UserInfo.Builder userInfo =
169170
OzoneManagerProtocolProtos.UserInfo.newBuilder();
170171

171-
// If S3 Authentication is set, determine user based on access ID.
172+
// If S3 Authentication is set, determine user based on STS token first,
173+
// falling back to accessId if session token not present.
172174
if (omRequest.hasS3Authentication()) {
173-
String principal = OzoneAclUtils.accessIdToUserPrincipal(
174-
omRequest.getS3Authentication().getAccessId());
175-
userInfo.setUserName(principal);
175+
final String accessKeyId = omRequest.getS3Authentication().getAccessId();
176+
if (accessKeyId.startsWith(S3AssumeRoleRequest.STS_TOKEN_PREFIX) &&
177+
!omRequest.getS3Authentication().hasSessionToken()) {
178+
throw new IOException("Error with STS token", new AuthenticationException(
179+
"Missing session token for accessKeyId: " + accessKeyId));
180+
}
181+
if (omRequest.getS3Authentication().hasSessionToken()) {
182+
try {
183+
final String originalAccessKeyId = OzoneManager.getS3AuthEffectiveAccessId();
184+
if (originalAccessKeyId != null && !originalAccessKeyId.isEmpty()) {
185+
final String principal = OzoneAclUtils.accessIdToUserPrincipal(originalAccessKeyId);
186+
userInfo.setUserName(principal);
187+
} else {
188+
throw new AuthenticationException(
189+
"Invalid STS Token - originalAccessKeyId was null or empty: " + originalAccessKeyId);
190+
}
191+
} catch (Exception e) {
192+
throw new IOException("Error with STS Token", e);
193+
}
194+
} else {
195+
String principal = OzoneAclUtils.accessIdToUserPrincipal(
196+
omRequest.getS3Authentication().getAccessId());
197+
userInfo.setUserName(principal);
198+
}
176199
} else if (user != null) {
177200
// Added not null checks, as in UT's these values might be null.
178201
userInfo.setUserName(user.getUserName());

hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3AssumeRoleRequest.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121
import java.io.IOException;
2222
import java.net.InetAddress;
2323
import java.security.SecureRandom;
24+
import java.time.Clock;
2425
import java.time.Instant;
26+
import java.time.ZoneOffset;
2527
import org.apache.commons.lang3.StringUtils;
2628
import org.apache.hadoop.ipc.ProtobufRpcEngine;
2729
import org.apache.hadoop.ozone.om.OzoneAclUtils;
@@ -58,7 +60,6 @@ public class S3AssumeRoleRequest extends OMClientRequest {
5860

5961
private static final int MIN_TOKEN_EXPIRATION_SECONDS = 900; // 15 minutes in seconds
6062
private static final int MAX_TOKEN_EXPIRATION_SECONDS = 43200; // 12 hours in seconds
61-
private static final String STS_TOKEN_PREFIX = "ASIA";
6263
private static final int STS_ACCESS_KEY_ID_LENGTH = 20;
6364
private static final int STS_SECRET_ACCESS_KEY_LENGTH = 40;
6465
private static final int STS_ROLE_ID_LENGTH = 16;
@@ -70,6 +71,9 @@ public class S3AssumeRoleRequest extends OMClientRequest {
7071
private static final String CHARS_FOR_SECRET_ACCESS_KEYS = CHARS_FOR_ACCESS_KEY_IDS +
7172
"abcdefghijklmnopqrstuvwxyz/+";
7273
private static final int CHARS_FOR_SECRET_ACCESS_KEYS_LENGTH = CHARS_FOR_SECRET_ACCESS_KEYS.length();
74+
private static final Clock CLOCK = Clock.system(ZoneOffset.UTC);
75+
76+
public static final String STS_TOKEN_PREFIX = "ASIA";
7377

7478
public S3AssumeRoleRequest(OMRequest omRequest) {
7579
super(omRequest);
@@ -197,7 +201,7 @@ private String generateSessionToken(String targetRoleName, OMRequest omRequest,
197201

198202
return ozoneManager.getSTSTokenSecretManager().createSTSTokenString(
199203
tempAccessKeyId, originalAccessKeyId, roleArn, assumeRoleRequest.getDurationSeconds(), secretAccessKey,
200-
sessionPolicy);
204+
sessionPolicy, CLOCK);
201205
}
202206

203207
/**

hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/protocolPB/OzoneManagerProtocolServerSideTranslatorPB.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ private OMResponse internalProcessRequest(OMRequest request) throws ServiceExcep
174174
return ozoneManager.getOmExecutionFlow().submit(request);
175175
} finally {
176176
OzoneManager.setS3Auth(null);
177+
OzoneManager.setStsTokenIdentifier(null);
177178
}
178179
}
179180

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
import static org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMTokenProto.Type.S3AUTHINFO;
2222

2323
import com.google.protobuf.ServiceException;
24+
import java.time.Clock;
25+
import java.time.ZoneOffset;
2426
import org.apache.hadoop.hdds.annotation.InterfaceAudience;
2527
import org.apache.hadoop.hdds.annotation.InterfaceStability;
2628
import org.apache.hadoop.io.Text;
@@ -41,6 +43,8 @@
4143
@InterfaceStability.Evolving
4244
public final class S3SecurityUtil {
4345

46+
private static final Clock CLOCK = Clock.system(ZoneOffset.UTC);
47+
4448
private S3SecurityUtil() {
4549
}
4650

@@ -54,6 +58,19 @@ private S3SecurityUtil() {
5458
public static void validateS3Credential(OMRequest omRequest,
5559
OzoneManager ozoneManager) throws ServiceException, OMException {
5660
if (ozoneManager.isSecurityEnabled()) {
61+
// If STS session token is present, decode, decrypt and validate it once and save in thread-local
62+
if (omRequest.getS3Authentication().hasSessionToken()) {
63+
final String token = omRequest.getS3Authentication().getSessionToken();
64+
if (!token.isEmpty()) {
65+
final STSTokenIdentifier stsTokenIdentifier = STSSecurityUtil.constructValidateAndDecryptSTSToken(
66+
token, ozoneManager.getSecretKeyClient(), CLOCK);
67+
// HMAC signature and expiration were validated above. Now validate AWS signature.
68+
validateSTSTokenAwsSignature(stsTokenIdentifier, omRequest);
69+
OzoneManager.setStsTokenIdentifier(stsTokenIdentifier);
70+
return;
71+
}
72+
}
73+
5774
OzoneTokenIdentifier s3Token = constructS3Token(omRequest);
5875
try {
5976
// authenticate user with signature verification through
@@ -89,4 +106,22 @@ private static OzoneTokenIdentifier constructS3Token(OMRequest omRequest) {
89106
s3Token.setOwner(new Text(auth.getAccessId()));
90107
return s3Token;
91108
}
109+
110+
/**
111+
* Validates the AWS signature of an STSTokenIdentifier that has already been decrypted.
112+
* @param stsTokenIdentifier the decrypted STS token
113+
* @param omRequest the OMRequest containing STS token
114+
* @throws OMException if the AWS signature validation fails
115+
*/
116+
private static void validateSTSTokenAwsSignature(STSTokenIdentifier stsTokenIdentifier, OMRequest omRequest)
117+
throws OMException {
118+
final String secretAccessKey = stsTokenIdentifier.getSecretAccessKey();
119+
final S3Authentication s3Authentication = omRequest.getS3Authentication();
120+
if (AWSV4AuthValidator.validateRequest(
121+
s3Authentication.getStringToSign(), s3Authentication.getSignature(), secretAccessKey)) {
122+
return;
123+
}
124+
throw new OMException(
125+
"STS token validation failed for token: " + omRequest.getS3Authentication().getSessionToken(), INVALID_TOKEN);
126+
}
92127
}

0 commit comments

Comments
 (0)