This is an automated email from the ASF dual-hosted git repository.
ChenSammi pushed a commit to branch HDDS-13323-sts
in repository https://gitbox.apache.org/repos/asf/ozone.git
The following commit(s) were added to refs/heads/HDDS-13323-sts by this push:
new 0ab01a4777f HDDS-16313. [STS] Handle linked bucket and session
policies (#11196)
0ab01a4777f is described below
commit 0ab01a4777f3287203054b5ddaa9e4085056ad19
Author: fmorg-git <[email protected]>
AuthorDate: Thu Sep 3 03:11:17 2026 -0700
HDDS-16313. [STS] Handle linked bucket and session policies (#11196)
---
hadoop-hdds/docs/content/design/ozone-sts.md | 19 +-
.../security/ozone-secure-sts-multitenant.robot | 7 +-
.../smoketest/security/ozone-secure-sts.resource | 8 +
.../main/smoketest/security/ozone-secure-sts.robot | 91 +++++
.../src/main/proto/OmClientProtocol.proto | 2 +
.../org/apache/hadoop/ozone/om/OzoneManager.java | 31 +-
.../org/apache/hadoop/ozone/om/ResolvedBucket.java | 34 ++
.../request/s3/security/S3AssumeRoleRequest.java | 281 +++++++++++----
.../ozone/security/STSTokenSecretManager.java | 6 +-
.../s3/security/TestS3AssumeRoleRequest.java | 387 +++++++++++++++++++--
.../hadoop/ozone/security/TestSTSSecurityUtil.java | 50 +--
.../ozone/security/TestSTSTokenSecretManager.java | 10 +-
12 files changed, 760 insertions(+), 166 deletions(-)
diff --git a/hadoop-hdds/docs/content/design/ozone-sts.md
b/hadoop-hdds/docs/content/design/ozone-sts.md
index 3acbafdcf9a..9963534d848 100644
--- a/hadoop-hdds/docs/content/design/ozone-sts.md
+++ b/hadoop-hdds/docs/content/design/ozone-sts.md
@@ -117,6 +117,15 @@ team agreed that behavior is fine for actions, but does
not work for Conditions,
restrict calls by sourceIp, and if we silently ignore this, the client may
incorrectly think the temporary credentials
are restricted for use by that IP address, so the consensus was to reject the
request for that scenario.
+### 3.3.2 Additional Context on Linked Buckets
+
+In Ozone, one may configure a chain of bucket links. In the scenario where
one desires to call the AssumeRole API where the resource
+is a linked bucket, ensure the Ranger policies for the role have the proper
permissions for each link in the chain as well
+as the source bucket. For example, if there is a source bucket S, that is
linked to bucket A, which is linked to bucket B,
+and you want the token to be able to issue operations against linked bucket B,
ensure that the role has read access to bucket B,
+read access to bucket A, and the requisite access for bucket S (such as read
on keys for GetObject, create/write on keys for PutObject, etc.).
+The role must have at least read access to the volume(s) where these buckets
live as well.
+
## 3.4 SessionToken Format
As mentioned above, one of the return values from the AssumeRole call will be
the sessionToken. To support not
@@ -203,14 +212,14 @@ The format of this String is entirely up to the Ranger
team. What is required f
subsequent S3 API calls are made that use STS tokens. In order to achieve
this, the sessionPolicy String from Ranger will
be included in the sessionToken response to the AssumeRole API call (as
mentioned above), and Ozone will supply this String
to Ranger whenever STS tokens are used on S3 API calls via a new
`RequestContext.sessionPolicy` field in the
-`IAccessAuthorizer#checkAccess(IOzoneObj, RequestContext)` call. Another
requirement from the Ozone side is to pass the action (without the s3: prefix)
corresponding to the S3 api call into the `RequestContext.s3Action` field.
+`IAccessAuthorizer#checkAccess(IOzoneObj, RequestContext)` call. Another
requirement from the Ozone side is to pass the action (without the s3: prefix)
corresponding to the S3 API call into the `RequestContext.s3Action` field.
### 3.6.2 Additional Context on Permissions and Actions
In a prior iteration of this design, only permissions corresponding to Ozone
`ACLType` (i.e. read, write, create, read_acl, etc.) were included in Ranger
roles and session policies.
-However, after testing against AWS, it was found that ACLs used by Ozone and
Ranger are not granular enough. For example, read on volume, read on bucket,
and write on key can be used by either the S3 PutObjectTagging api (requiring
`s3:PutObjectTagging` action) or the S3 DeleteObjectTagging api (requiring
`s3:DeleteObjectTagging` action).
-Similarly, because the S3 PutObject api (`s3:PutObject` action) requires read
on volume, read on bucket, and create and write on key, someone with
`s3:PutObject` access could previously also call the S3 PutObjectTagging api,
even though they did not have access to the `s3:PutObjectTagging` action (as an
example).
-AWS does not allow an STS token that is restricted for one action to issue
calls to an api that is associated with a different action. To prevent having
more access than requested (or different access than requested), ACL
permissions can be constrained further by S3 actions.
+However, after testing against AWS, it was found that ACLs used by Ozone and
Ranger are not granular enough. For example, read on volume, read on bucket,
and write on key can be used by either the S3 PutObjectTagging API (requiring
`s3:PutObjectTagging` action) or the S3 DeleteObjectTagging API (requiring
`s3:DeleteObjectTagging` action).
+Similarly, because the S3 PutObject API (`s3:PutObject` action) requires read
on volume, read on bucket, and create and write on key, someone with
`s3:PutObject` access could previously also call the S3 PutObjectTagging API,
even though they did not have access to the `s3:PutObjectTagging` action (as an
example).
+AWS does not allow an STS token that is restricted for one action to issue
calls to an API that is associated with a different action. To prevent having
more access than requested (or different access than requested), ACL
permissions can be constrained further by S3 actions.
To do this constraining, the `RequestContext.s3Action` field is introduced so
that if populated, the RangerOzoneAuthorizer would further restrict the
permissions according to the action.
Additionally, the OzoneGrant would contain a Set<String> representing the S3
actions that are allowed for an inline policy. If all actions are allowed, then
the Set<String> would be empty or null.
@@ -224,7 +233,7 @@ created in Ranger as per the Prerequisites above.
- This authorized user (having permanent S3 credentials) makes the AssumeRole
STS call to Ozone.
- If successful, Ozone responds with the temporary credentials.
- A client makes S3 API calls with the temporary credentials for up to as long
as the credentials last.
-- When Ozone receives an S3 api call using temporary credentials, it will use
the Kerberos identity associated with the
+- When Ozone receives an S3 API call using temporary credentials, it will use
the Kerberos identity associated with the
originalAccessKeyId in the session token and perform the following checks:
- Ensure that if the accessKeyId starts with "ASIA", that a sessionToken was
included in the `x-amz-security-token` header
- Ensure the sessionToken is not expired
diff --git
a/hadoop-ozone/dist/src/main/smoketest/security/ozone-secure-sts-multitenant.robot
b/hadoop-ozone/dist/src/main/smoketest/security/ozone-secure-sts-multitenant.robot
index c7b367d74cf..f4c56839dca 100644
---
a/hadoop-ozone/dist/src/main/smoketest/security/ozone-secure-sts-multitenant.robot
+++
b/hadoop-ozone/dist/src/main/smoketest/security/ozone-secure-sts-multitenant.robot
@@ -17,7 +17,6 @@
Suite Setup Skip If '${RANGER_ENDPOINT_URL}' == '' No Ranger
Documentation Smoke test for S3 STS AssumeRole + Temp Creds
(Multi-Tenant Scenario)
Resource ./ozone-secure-sts.resource
-Resource ../admincli/lib.resource
Test Timeout 10 minutes
*** Variables ***
@@ -137,11 +136,7 @@ Create Iceberg Bucket and Another Bucket Table Access
policies
${policy_json} = Set Variable { "isEnabled":
true, "service": "dev_ozone", "name": "${TENANT_TWO}
${TENANT_TWO_ANOTHER_BUCKET} table access", "policyType": 0, "policyPriority":
0, "isAuditEnabled": true, "resources": { "volume": { "values": [
"${TENANT_TWO}" ], "isExcludes": false, "isRecursive": false }, "bucket": {
"values": [ "${TENANT_TWO_ANOTHER_BUCKET}" ], "isExcludes": false,
"isRecursive": false }, "key": { "values": [ "*" ], "isExcludes": false, [...]
Create Ranger Policy ${policy_json}
- # Update Ranger policy cache
- Kinit test user ${OM_ADMIN_USER}
${OM_ADMIN_USER}.keytab
- ${om_param} = Get OM Service Param
- ${output} = Execute ozone admin om
updateranger ${om_param}
- Should contain ${output} Operation
completed successfully
+ Refresh Ranger Policy Cache
Get S3 Credentials for Principals, Create Buckets, and Upload Files to Buckets
Kinit test user ${USER_A}
${USER_A}.keytab
diff --git
a/hadoop-ozone/dist/src/main/smoketest/security/ozone-secure-sts.resource
b/hadoop-ozone/dist/src/main/smoketest/security/ozone-secure-sts.resource
index 8e63f622545..ebfbe55a653 100644
--- a/hadoop-ozone/dist/src/main/smoketest/security/ozone-secure-sts.resource
+++ b/hadoop-ozone/dist/src/main/smoketest/security/ozone-secure-sts.resource
@@ -19,6 +19,7 @@ Library String
Library BuiltIn
Library DateTime
Library Collections
+Resource ../admincli/lib.resource
Resource ../commonlib.robot
Resource ../s3/commonawslib.robot
@@ -85,6 +86,13 @@ Update Ranger Policy Items
${result} = Execute curl --silent
--show-error --include --location --netrc -X PUT -H "Content-Type:
application/json" -H "accept: application/json" --data '${updated}'
"${RANGER_ENDPOINT_URL}/service/public/v2/api/policy/${policy_id}"
Should Contain ${result} HTTP/1.1 200
+Refresh Ranger Policy Cache
+ [Arguments] ${admin_user}=hdfs
${admin_keytab}=hdfs.keytab
+ Kinit test user ${admin_user} ${admin_keytab}
+ ${om_param} = Get OM Service Param
+ ${output} = Execute ozone admin om
updateranger ${om_param}
+ Should Contain ${output} Operation
completed successfully
+
Assume Role And Get Temporary Credentials
[Arguments] ${perm_access_key_id} ${perm_secret_key}
${policy_json}=${EMPTY} ${role_arn}=${ROLE_ARN_OBS}
${role_session_name}=${ROLE_SESSION_NAME} ${duration_seconds}=900
Configure AWS Profile permanent ${perm_access_key_id}
${perm_secret_key}
diff --git
a/hadoop-ozone/dist/src/main/smoketest/security/ozone-secure-sts.robot
b/hadoop-ozone/dist/src/main/smoketest/security/ozone-secure-sts.robot
index c07ee6afc98..b07a38e1a23 100644
--- a/hadoop-ozone/dist/src/main/smoketest/security/ozone-secure-sts.robot
+++ b/hadoop-ozone/dist/src/main/smoketest/security/ozone-secure-sts.robot
@@ -66,6 +66,15 @@ ${ACTION_MATCHES_PUTOBJECT_CREATE_WRITE_ROLE_ARN}
arn:aws:iam::123456789012:rol
${ACTION_MATCHES_GETOBJECT_PUTOBJECT_ROLE_ARN}
arn:aws:iam::123456789012:role/${ACTION_MATCHES_GETOBJECT_PUTOBJECT_ROLE}
${ACTION_MATCHES_UPLOADPARTCOPY_EXPECTED_OWNER_ROLE_ARN}
arn:aws:iam::123456789012:role/${ACTION_MATCHES_UPLOADPARTCOPY_EXPECTED_OWNER_ROLE}
${ACTION_MATCHES_GET_STAR_READ_ROLE_ARN}
arn:aws:iam::123456789012:role/${ACTION_MATCHES_GET_STAR_READ_ROLE}
+${STS_LINK_BUCKET_ROLE} sts-link-bucket-role
+${STS_LINK_BUCKET_ROLE_ARN}
arn:aws:iam::123456789012:role/${STS_LINK_BUCKET_ROLE}
+${STS_LINK_BUCKET_SOURCE} sts-link-bucket-source
+${STS_LINK_BUCKET_LINKED} s3v-sts-link-bucket-source
+${STS_LINK_BUCKET_TESTFILE} link-bucket-testfile.txt
+${STS_LINK_BUCKET_CHAIN_SOURCE} sts-chain-source
+${STS_LINK_BUCKET_CHAIN_INTERMEDIATE} s3v-sts-chain-intermediate
+${STS_LINK_BUCKET_CHAIN_FINAL} s3v-sts-chain-final
+${STS_LINK_BUCKET_CHAIN_TESTFILE} chain-link-testfile.txt
${TEST_USER_ADMIN} testuser
${TEST_USER_NON_ADMIN} testuser2
@{ICEBERG_OBJECT_KEYS} file1.txt file1again.txt
folder/pepper.txt folder/salt.txt userA/userA.txt userB/userB.txt
userAfile.txt
@@ -94,6 +103,27 @@ Populate Iceberg Bucket
Create File ${TEMP_DIR}/zero-byte-marker
Execute ozone sh key put
/s3v/${bucket}/zeroByteFolder/ ${TEMP_DIR}/zero-byte-marker
+Setup Same Volume Link Bucket
+ # Create a source bucket and a same-volume linked bucket (s3v/source ->
s3v/s3v-source).
+ Kinit test user hdfs hdfs.keytab
+ Execute ozone sh bucket create --layout
${ICEBERG_LAYOUT_OBS} /s3v/${STS_LINK_BUCKET_SOURCE}
+ Create File ${TEMP_DIR}/${STS_LINK_BUCKET_TESTFILE}
link bucket test content
+ Execute ozone sh key put
/s3v/${STS_LINK_BUCKET_SOURCE}/${STS_LINK_BUCKET_TESTFILE}
${TEMP_DIR}/${STS_LINK_BUCKET_TESTFILE}
+ Execute ozone sh bucket link
/s3v/${STS_LINK_BUCKET_SOURCE} /s3v/${STS_LINK_BUCKET_LINKED}
+
+Setup Chained Same Volume Link Buckets
+ # Create source <- intermediate <- final chained links in s3v.
+ Kinit test user hdfs hdfs.keytab
+ Execute ozone sh bucket create --layout
${ICEBERG_LAYOUT_OBS} /s3v/${STS_LINK_BUCKET_CHAIN_SOURCE}
+ Create File
${TEMP_DIR}/${STS_LINK_BUCKET_CHAIN_TESTFILE} chained link test content
+ Execute ozone sh key put
/s3v/${STS_LINK_BUCKET_CHAIN_SOURCE}/${STS_LINK_BUCKET_CHAIN_TESTFILE}
${TEMP_DIR}/${STS_LINK_BUCKET_CHAIN_TESTFILE}
+ Execute ozone sh bucket link
/s3v/${STS_LINK_BUCKET_CHAIN_SOURCE} /s3v/${STS_LINK_BUCKET_CHAIN_INTERMEDIATE}
+ Execute ozone sh bucket link
/s3v/${STS_LINK_BUCKET_CHAIN_INTERMEDIATE} /s3v/${STS_LINK_BUCKET_CHAIN_FINAL}
+
+Head Bucket Should Succeed
+ [Arguments] ${bucket} ${profile}=sts
+ Execute and checkrc aws s3api --endpoint-url ${S3G_ENDPOINT_URL}
head-bucket --bucket ${bucket} --profile ${profile} 0
+
Run List Prefix And Delimiter Policy Matrix For Bucket And Api
[Arguments] ${bucket} ${role_arn} ${api}
# Capture baseline (non-STS) behavior using the permanent credentials.
When using STS, it will behave just like
@@ -337,6 +367,31 @@ Create Iceberg Multi-Bucket Role Policies
${key_policy} = Set Variable { "isEnabled":
true, "service": "dev_ozone", "name": "iceberg multi table access",
"policyType": 0, "policyPriority": 0, "isAuditEnabled": true, "resources": {
"volume": { "values": [ "s3v" ], "isExcludes": false, "isRecursive": false },
"bucket": { "values": [ "${ICEBERG_BUCKET_OBS}", "${ICEBERG_BUCKET_FSO}" ],
"isExcludes": false, "isRecursive": false }, "key": { "values": [ "*" ],
"isExcludes": false, "isRecursive": true [...]
Create Ranger Policy ${key_policy}
+Create STS Link Bucket Role in Ranger
+ ${role_json} = Set Variable { "name":
"${STS_LINK_BUCKET_ROLE}", "description": "STS linked bucket authorization
regression role" }
+ Create Ranger Role ${role_json}
+ Create Ranger Assume Role Policy ${STS_LINK_BUCKET_ROLE}
${ICEBERG_SVC_CATALOG_USER}
+
+Create STS Link Bucket Access Policies
+ # Grant volume access and linked-bucket bucket/key policies for
session-policy scoping.
+ # Also grant source-bucket key access on the role so Ranger allows the
resolved key path; the STS
+ # session policy (linked bucket only) is what this regression exercises.
+ ${policy_items} = Set Variable [ {
"accesses": [ { "type": "read", "isAllowed": true }, { "type": "list",
"isAllowed": true } ], "roles": [ "${STS_LINK_BUCKET_ROLE}" ], "delegateAdmin":
false } ]
+ Update Ranger Policy Items iceberg volume access ${policy_items}
+ ${bucket_policy} = Set Variable { "isEnabled":
true, "service": "dev_ozone", "name": "sts linked bucket access", "policyType":
0, "policyPriority": 0, "isAuditEnabled": true, "resources": { "volume": {
"values": [ "s3v" ], "isExcludes": false, "isRecursive": false }, "bucket": {
"values": [ "${STS_LINK_BUCKET_LINKED}" ], "isExcludes": false, "isRecursive":
false } }, "policyItems": [ { "accesses": [ { "type": "all", "isAllowed": true
} ], "roles": [ "${STS [...]
+ Create Ranger Policy ${bucket_policy}
+ ${key_policy} = Set Variable { "isEnabled":
true, "service": "dev_ozone", "name": "sts linked bucket table access",
"policyType": 0, "policyPriority": 0, "isAuditEnabled": true, "resources": {
"volume": { "values": [ "s3v" ], "isExcludes": false, "isRecursive": false },
"bucket": { "values": [ "${STS_LINK_BUCKET_LINKED}" ], "isExcludes": false,
"isRecursive": false }, "key": { "values": [ "*" ], "isExcludes": false,
"isRecursive": true } }, "policyItems [...]
+ Create Ranger Policy ${key_policy}
+ ${source_key_policy} = Set Variable { "isEnabled":
true, "service": "dev_ozone", "name": "sts linked bucket source table access",
"policyType": 0, "policyPriority": 0, "isAuditEnabled": true, "resources": {
"volume": { "values": [ "s3v" ], "isExcludes": false, "isRecursive": false },
"bucket": { "values": [ "${STS_LINK_BUCKET_SOURCE}" ], "isExcludes": false,
"isRecursive": false }, "key": { "values": [ "*" ], "isExcludes": false,
"isRecursive": true } }, "poli [...]
+ Create Ranger Policy ${source_key_policy}
+ ${chain_bucket_policy} = Set Variable { "isEnabled":
true, "service": "dev_ozone", "name": "sts chained link bucket access",
"policyType": 0, "policyPriority": 0, "isAuditEnabled": true, "resources": {
"volume": { "values": [ "s3v" ], "isExcludes": false, "isRecursive": false },
"bucket": { "values": [ "${STS_LINK_BUCKET_CHAIN_FINAL}",
"${STS_LINK_BUCKET_CHAIN_INTERMEDIATE}" ], "isExcludes": false, "isRecursive":
false } }, "policyItems": [ { "accesses": [ { "ty [...]
+ Create Ranger Policy ${chain_bucket_policy}
+ ${chain_key_policy} = Set Variable { "isEnabled":
true, "service": "dev_ozone", "name": "sts chained link bucket table access",
"policyType": 0, "policyPriority": 0, "isAuditEnabled": true, "resources": {
"volume": { "values": [ "s3v" ], "isExcludes": false, "isRecursive": false },
"bucket": { "values": [ "${STS_LINK_BUCKET_CHAIN_FINAL}" ], "isExcludes":
false, "isRecursive": false }, "key": { "values": [ "*" ], "isExcludes": false,
"isRecursive": true } }, " [...]
+ Create Ranger Policy ${chain_key_policy}
+ ${chain_source_key_policy} = Set Variable { "isEnabled":
true, "service": "dev_ozone", "name": "sts chained link bucket source table
access", "policyType": 0, "policyPriority": 0, "isAuditEnabled": true,
"resources": { "volume": { "values": [ "s3v" ], "isExcludes": false,
"isRecursive": false }, "bucket": { "values": [
"${STS_LINK_BUCKET_CHAIN_SOURCE}" ], "isExcludes": false, "isRecursive": false
}, "key": { "values": [ "*" ], "isExcludes": false, "isRecursive": tru [...]
+ Create Ranger Policy ${chain_source_key_policy}
+ Refresh Ranger Policy Cache
+
Create Partial Access Roles in Ranger
FOR ${role} IN ${PARTIAL_LIST_ALL_BUCKETS_VOL_READ_ROLE}
${PARTIAL_LIST_ALL_BUCKETS_VOL_LIST_ROLE} ${PARTIAL_BUCKET_READ_ROLE}
${PARTIAL_BUCKET_READ_UPLOAD_PREFIX_ROLE} ${PARTIAL_BUCKET_LIST_ROLE}
${PARTIAL_BUCKET_READ_ACL_ROLE} ${PARTIAL_PUT_OBJECT_KEY_CREATE_ROLE}
${PARTIAL_PUT_OBJECT_KEY_WRITE_ROLE}
${role_json} = Set Variable { "name":
"${role}", "description": "Partial access role" }
@@ -416,6 +471,8 @@ Get S3 Credentials for Service Catalog Principal, Create
Iceberg Buckets, and Up
Execute ozone sh bucket create --layout
${ICEBERG_LAYOUT_FSO} /s3v/${ICEBERG_BUCKET_FSO}
Populate Iceberg Bucket ${ICEBERG_BUCKET_OBS}
Populate Iceberg Bucket ${ICEBERG_BUCKET_FSO}
+ Setup Same Volume Link Bucket
+ Setup Chained Same Volume Link Buckets
# Switch back to the service catalog principal for running S3/STS requests.
Kinit test user ${ICEBERG_SVC_CATALOG_USER}
${ICEBERG_SVC_CATALOG_USER}.keytab
@@ -1276,6 +1333,40 @@ STS session policy containing only GetObject must deny
DeleteObjects
${output} = Execute aws s3api
--endpoint-url ${S3G_ENDPOINT_URL} delete-bucket --bucket ${bucket} --profile
sts
Should Not Contain ${output} AccessDenied
+STS Session Policy On Linked Bucket Grants GetObject On Source Bucket
+ # Same-volume linked buckets (e.g. s3v/s3v-iceberg -> s3v/iceberg):
session policy scoped to the linked
+ # bucket must also authorize ListBucket and GetObject on the source bucket.
+ Kinit test user ${ICEBERG_SVC_CATALOG_USER}
${ICEBERG_SVC_CATALOG_USER}.keytab
+ ${session_policy} = Set Variable
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:ListBucket","s3:GetObject"],"Resource":["arn:aws:s3:::${STS_LINK_BUCKET_LINKED}","arn:aws:s3:::${STS_LINK_BUCKET_LINKED}/*"]}]}
+ Assume Role And Configure STS Profile
policy_json=${session_policy} perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}
perm_secret_key=${PERMANENT_SECRET_KEY} role_arn=${STS_LINK_BUCKET_ROLE_ARN}
+ Head Bucket Should Succeed ${STS_LINK_BUCKET_LINKED}
+ Get Object Should Succeed ${STS_LINK_BUCKET_LINKED}
${STS_LINK_BUCKET_TESTFILE}
+
+STS Role On Linked Bucket Grants GetObject On Source Bucket
+ # Same-volume linked buckets: role permissions on the linked bucket must
authorize
+ # key access on the source bucket when no inline session policy is
supplied.
+ Kinit test user ${ICEBERG_SVC_CATALOG_USER}
${ICEBERG_SVC_CATALOG_USER}.keytab
+ Assume Role And Configure STS Profile
perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}
perm_secret_key=${PERMANENT_SECRET_KEY} role_arn=${STS_LINK_BUCKET_ROLE_ARN}
+ Head Bucket Should Succeed ${STS_LINK_BUCKET_LINKED}
+ Get Object Should Succeed ${STS_LINK_BUCKET_LINKED}
${STS_LINK_BUCKET_TESTFILE}
+
+STS Session Policy On Chained Linked Bucket Grants GetObject On Source Bucket
+ # Chained linked buckets (source <- intermediate <- final): session policy
scoped to the final
+ # linked bucket must also authorize ListBucket and GetObject on the source
bucket.
+ Kinit test user ${ICEBERG_SVC_CATALOG_USER}
${ICEBERG_SVC_CATALOG_USER}.keytab
+ ${session_policy} = Set Variable
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:ListBucket","s3:GetObject"],"Resource":["arn:aws:s3:::${STS_LINK_BUCKET_CHAIN_FINAL}","arn:aws:s3:::${STS_LINK_BUCKET_CHAIN_FINAL}/*"]}]}
+ Assume Role And Configure STS Profile
policy_json=${session_policy} perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}
perm_secret_key=${PERMANENT_SECRET_KEY} role_arn=${STS_LINK_BUCKET_ROLE_ARN}
+ Head Bucket Should Succeed ${STS_LINK_BUCKET_CHAIN_FINAL}
+ Get Object Should Succeed ${STS_LINK_BUCKET_CHAIN_FINAL}
${STS_LINK_BUCKET_CHAIN_TESTFILE}
+
+STS Role On Chained Linked Bucket Grants GetObject On Source Bucket
+ # Chained linked buckets: role permissions on the final linked bucket must
authorize
+ # key access on the source bucket when no inline session policy is
supplied.
+ Kinit test user ${ICEBERG_SVC_CATALOG_USER}
${ICEBERG_SVC_CATALOG_USER}.keytab
+ Assume Role And Configure STS Profile
perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}
perm_secret_key=${PERMANENT_SECRET_KEY} role_arn=${STS_LINK_BUCKET_ROLE_ARN}
+ Head Bucket Should Succeed ${STS_LINK_BUCKET_CHAIN_FINAL}
+ Get Object Should Succeed ${STS_LINK_BUCKET_CHAIN_FINAL}
${STS_LINK_BUCKET_CHAIN_TESTFILE}
+
Expired STS temporary credentials must return ExpiredToken on S3 APIs
# Increase timeout to account for 15 minute STS token expiration plus the
time to execute the api calls
[Timeout] 25 minutes
diff --git
a/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
b/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
index 638cf99bd1c..53f7c20e5d9 100644
--- a/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
+++ b/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
@@ -2531,6 +2531,8 @@ message UpdateAssumeRoleRequest {
required string tempAccessKeyId = 6;
required string secretAccessKey = 7;
required string roleId = 8;
+ optional string sessionToken = 9;
+ optional uint64 expirationEpochSeconds = 10;
}
message RevokeSTSTokenRequest {
diff --git
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
index 04455a525a9..f8c4d0a244a 100644
---
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
+++
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
@@ -145,6 +145,7 @@
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -5119,19 +5120,16 @@ public ResolvedBucket resolveBucketLink(OmKeyArgs args)
public ResolvedBucket resolveBucketLink(Pair<String, String> requested,
OMClientRequest omClientRequest)
throws IOException {
+ final Set<Pair<String, String>> linkChain = new LinkedHashSet<>();
OmBucketInfo resolved;
if (getAclsEnabled()) {
- resolved = resolveBucketLink(requested, new HashSet<>(),
- omClientRequest.createUGIForApi(),
- omClientRequest.getRemoteAddress(),
- omClientRequest.getHostName(),
- false);
+ resolved = resolveBucketLink(
+ requested, linkChain, omClientRequest.createUGIForApi(),
omClientRequest.getRemoteAddress(),
+ omClientRequest.getHostName(), false);
} else {
- resolved = resolveBucketLink(requested, new HashSet<>(),
- null, null, null, false);
+ resolved = resolveBucketLink(requested, linkChain, null, null, null,
false);
}
- return new ResolvedBucket(requested.getLeft(), requested.getRight(),
- resolved);
+ return new ResolvedBucket(requested.getLeft(), requested.getRight(),
resolved, linkChain);
}
public ResolvedBucket resolveBucketLink(Pair<String, String> requested,
@@ -5143,6 +5141,7 @@ public ResolvedBucket resolveBucketLink(Pair<String,
String> requested,
boolean allowDanglingBuckets,
boolean aclEnabled)
throws IOException {
+ final Set<Pair<String, String>> linkChain = new LinkedHashSet<>();
OmBucketInfo resolved;
if (aclEnabled) {
UserGroupInformation ugi = getRemoteUser();
@@ -5151,17 +5150,13 @@ public ResolvedBucket resolveBucketLink(Pair<String,
String> requested,
ugi = UserGroupInformation.createRemoteUser(principal);
}
InetAddress remoteIp = Server.getRemoteIp();
- resolved = resolveBucketLink(requested, new HashSet<>(),
- ugi,
- remoteIp != null ? remoteIp : omRpcAddress.getAddress(),
- remoteIp != null ? remoteIp.getHostName() :
- omRpcAddress.getHostName(), allowDanglingBuckets, aclEnabled);
+ resolved = resolveBucketLink(
+ requested, linkChain, ugi, remoteIp != null ? remoteIp :
omRpcAddress.getAddress(),
+ remoteIp != null ? remoteIp.getHostName() :
omRpcAddress.getHostName(), allowDanglingBuckets, aclEnabled);
} else {
- resolved = resolveBucketLink(requested, new HashSet<>(),
- null, null, null, allowDanglingBuckets, aclEnabled);
+ resolved = resolveBucketLink(requested, linkChain, null, null, null,
allowDanglingBuckets, aclEnabled);
}
- return new ResolvedBucket(requested.getLeft(), requested.getRight(),
- resolved);
+ return new ResolvedBucket(requested.getLeft(), requested.getRight(),
resolved, linkChain);
}
private OmBucketInfo resolveBucketLink(
diff --git
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ResolvedBucket.java
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ResolvedBucket.java
index 19b41355d09..6dd50feff61 100644
---
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ResolvedBucket.java
+++
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ResolvedBucket.java
@@ -17,6 +17,9 @@
package org.apache.hadoop.ozone.om;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
@@ -44,11 +47,18 @@ public class ResolvedBucket {
private final String realBucket;
private final String bucketOwner;
private final BucketLayout bucketLayout;
+ private final Collection<Pair<String, String>> linkChain;
public ResolvedBucket(String requestedVolume, String requestedBucket,
OmBucketInfo resolved) {
+ this(requestedVolume, requestedBucket, resolved, Collections.emptyList());
+ }
+
+ public ResolvedBucket(String requestedVolume, String requestedBucket,
+ OmBucketInfo resolved, Collection<Pair<String, String>> linkChain) {
this.requestedVolume = requestedVolume;
this.requestedBucket = requestedBucket;
+ this.linkChain = unmodifiableLinkChain(linkChain);
if (resolved != null) {
this.realVolume = resolved.getVolumeName();
this.realBucket = resolved.getBucketName();
@@ -65,12 +75,28 @@ public ResolvedBucket(String requestedVolume, String
requestedBucket,
public ResolvedBucket(String requestedVolume, String requestedBucket,
String realVolume, String realBucket, String bucketOwner,
BucketLayout bucketLayout) {
+ this(requestedVolume, requestedBucket, realVolume, realBucket,
bucketOwner, bucketLayout,
+ Collections.emptyList());
+ }
+
+ public ResolvedBucket(String requestedVolume, String requestedBucket,
+ String realVolume, String realBucket, String bucketOwner,
+ BucketLayout bucketLayout, Collection<Pair<String, String>> linkChain) {
this.requestedVolume = requestedVolume;
this.requestedBucket = requestedBucket;
this.realVolume = realVolume;
this.realBucket = realBucket;
this.bucketOwner = bucketOwner;
this.bucketLayout = bucketLayout;
+ this.linkChain = unmodifiableLinkChain(linkChain);
+ }
+
+ private static Collection<Pair<String, String>> unmodifiableLinkChain(
+ Collection<Pair<String, String>> linkChain) {
+ if (linkChain == null || linkChain.isEmpty()) {
+ return Collections.emptyList();
+ }
+ return Collections.unmodifiableList(new ArrayList<>(linkChain));
}
public ResolvedBucket(Pair<String, String> requested,
@@ -104,6 +130,14 @@ public BucketLayout bucketLayout() {
return bucketLayout;
}
+ /**
+ * Ordered link buckets followed when resolving from the requested bucket to
the real bucket.
+ * Each pair is {@code (volume, bucket)}. Empty when the requested bucket is
not a link.
+ */
+ public Collection<Pair<String, String>> linkChain() {
+ return linkChain;
+ }
+
public OmKeyArgs update(OmKeyArgs args) {
return isLink()
? args.toBuilder()
diff --git
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3AssumeRoleRequest.java
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3AssumeRoleRequest.java
index b6d650cc439..693d280419b 100644
---
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3AssumeRoleRequest.java
+++
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3AssumeRoleRequest.java
@@ -29,10 +29,15 @@
import java.net.InetAddress;
import java.security.SecureRandom;
import java.time.Clock;
+import java.time.Instant;
+import java.util.EnumSet;
import java.util.HashMap;
+import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.tuple.Pair;
import org.apache.hadoop.hdds.scm.client.HddsClientUtils;
import org.apache.hadoop.ipc_.ProtobufRpcEngine;
import org.apache.hadoop.ozone.OzoneConsts;
@@ -40,6 +45,7 @@
import org.apache.hadoop.ozone.audit.OMAction;
import org.apache.hadoop.ozone.om.OzoneAclUtils;
import org.apache.hadoop.ozone.om.OzoneManager;
+import org.apache.hadoop.ozone.om.ResolvedBucket;
import org.apache.hadoop.ozone.om.exceptions.OMException;
import org.apache.hadoop.ozone.om.execution.flowcontrol.ExecutionContext;
import org.apache.hadoop.ozone.om.helpers.AwsRoleArnValidator;
@@ -53,6 +59,10 @@
import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.AssumeRoleResponse;
import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRequest;
import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.UpdateAssumeRoleRequest;
+import org.apache.hadoop.ozone.security.acl.IAccessAuthorizer.ACLType;
+import org.apache.hadoop.ozone.security.acl.IOzoneObj;
+import org.apache.hadoop.ozone.security.acl.OzoneObj;
+import org.apache.hadoop.ozone.security.acl.OzoneObjInfo;
import org.apache.hadoop.ozone.security.acl.iam.IamSessionPolicyResolver;
import org.apache.hadoop.security.UserGroupInformation;
@@ -94,42 +104,81 @@ public OMRequest preExecute(OzoneManager ozoneManager)
throws IOException {
final OMRequest omRequest = super.preExecute(ozoneManager);
final AssumeRoleRequest assumeRoleRequest =
omRequest.getAssumeRoleRequest();
- // Brief overview of flow:
- // The STS Endpoint makes the AssumeRole call, which when received by OM
leader (via this method),
- // it will generate the temporary credentials (tempAccessKeyId,
secretAccessKey) and roleId.
- // The original AssumeRole request is converted to an
UpdateAssumeRoleRequest with the generated
- // credentials. This update request will be submitted to Ratis and the
credentials
- // created by the leader will be replicated across all OMs. All OMs in
- // HA mode therefore will have identical audit logs with the same
tempAccessKeyId.
-
- // Generate temporary AWS credentials using cryptographically strong
SecureRandom
- final String tempAccessKeyId = STS_TOKEN_PREFIX +
generateSecureRandomStringUsingChars(
- STS_ACCESS_KEY_ID_ALLOWED_CHARS,
STS_ACCESS_KEY_ID_ALLOWED_CHARS_LENGTH,
- STS_ACCESS_KEY_ID_RANDOM_LENGTH);
- final String secretAccessKey = generateSecureRandomStringUsingChars(
- CHARS_FOR_SECRET_ACCESS_KEYS, CHARS_FOR_SECRET_ACCESS_KEYS_LENGTH,
STS_SECRET_ACCESS_KEY_LENGTH);
- final String roleId = ASSUME_ROLE_ID_PREFIX +
generateSecureRandomStringUsingChars(
- STS_ACCESS_KEY_ID_ALLOWED_CHARS,
STS_ACCESS_KEY_ID_ALLOWED_CHARS_LENGTH,
- STS_ROLE_ID_LENGTH);
-
- // Build UpdateAssumeRoleRequest with leader-generated credentials
- final UpdateAssumeRoleRequest.Builder updateAssumeRoleRequestBuilder =
- UpdateAssumeRoleRequest.newBuilder()
- .setRoleArn(assumeRoleRequest.getRoleArn())
- .setRoleSessionName(assumeRoleRequest.getRoleSessionName())
- .setDurationSeconds(assumeRoleRequest.getDurationSeconds())
- .setRequestId(assumeRoleRequest.getRequestId())
- .setTempAccessKeyId(tempAccessKeyId)
- .setSecretAccessKey(secretAccessKey)
- .setRoleId(roleId);
-
- if (assumeRoleRequest.hasAwsIamSessionPolicy()) {
-
updateAssumeRoleRequestBuilder.setAwsIamSessionPolicy(assumeRoleRequest.getAwsIamSessionPolicy());
- }
+ final int durationSeconds = assumeRoleRequest.getDurationSeconds();
+ final String roleSessionName = assumeRoleRequest.getRoleSessionName();
+ final String roleArn = assumeRoleRequest.getRoleArn();
+ final String awsIamSessionPolicy =
assumeRoleRequest.getAwsIamSessionPolicy();
+ final String requestId = assumeRoleRequest.getRequestId();
+ final OzoneManagerProtocolProtos.UserInfo userInfo =
omRequest.getUserInfo();
+ final AuditLogger auditLogger = ozoneManager.getAuditLogger();
+ final Map<String, String> auditMap = new HashMap<>();
+ S3STSUtils.addAssumeRoleAuditParams(
+ auditMap, roleArn, roleSessionName, awsIamSessionPolicy,
durationSeconds, requestId);
- return omRequest.toBuilder()
- .setUpdateAssumeRoleRequest(updateAssumeRoleRequestBuilder.build())
- .build();
+ try {
+ if (!omRequest.hasS3Authentication()) {
+ throw new OMException(
+ "S3AssumeRoleRequest does not have S3 authentication",
OMException.ResultCodes.INVALID_REQUEST);
+ }
+
+ // Brief overview of flow:
+ // The STS Endpoint makes the AssumeRole call, which when received by OM
leader (via this method),
+ // it will validate the request, authorize via Ranger, generate
temporary credentials
+ // (tempAccessKeyId, secretAccessKey), roleId, and the signed session
token.
+ // The original AssumeRole request is converted to an
UpdateAssumeRoleRequest with the generated
+ // values. This update request will be submitted to Ratis and replicated
across all OMs.
+ // All OMs in HA mode therefore will have identical audit logs with the
same tempAccessKeyId.
+ S3STSUtils.validateDuration(durationSeconds);
+ S3STSUtils.validateRoleSessionName(roleSessionName);
+ final String targetRoleName =
AwsRoleArnValidator.validateAndExtractRoleNameFromArn(roleArn);
+
+ // Generate temporary AWS credentials using cryptographically strong
SecureRandom
+ final String tempAccessKeyId = STS_TOKEN_PREFIX +
generateSecureRandomStringUsingChars(
+ STS_ACCESS_KEY_ID_ALLOWED_CHARS,
STS_ACCESS_KEY_ID_ALLOWED_CHARS_LENGTH,
+ STS_ACCESS_KEY_ID_RANDOM_LENGTH);
+ final String secretAccessKey = generateSecureRandomStringUsingChars(
+ CHARS_FOR_SECRET_ACCESS_KEYS, CHARS_FOR_SECRET_ACCESS_KEYS_LENGTH,
STS_SECRET_ACCESS_KEY_LENGTH);
+ final String roleId = ASSUME_ROLE_ID_PREFIX +
generateSecureRandomStringUsingChars(
+ STS_ACCESS_KEY_ID_ALLOWED_CHARS,
STS_ACCESS_KEY_ID_ALLOWED_CHARS_LENGTH,
+ STS_ROLE_ID_LENGTH);
+
+ final Instant creationInstant = clock.instant();
+ final String sessionToken = generateSessionToken(
+ targetRoleName, omRequest, ozoneManager, assumeRoleRequest,
secretAccessKey, tempAccessKeyId,
+ creationInstant);
+ final long expirationEpochSeconds =
creationInstant.plusSeconds(durationSeconds).getEpochSecond();
+
+ auditMap.put(OzoneConsts.S3_STS_TEMP_ACCESS_KEY_ID, tempAccessKeyId);
+
+ // Build UpdateAssumeRoleRequest with leader-generated credentials and
session token
+ final UpdateAssumeRoleRequest.Builder updateAssumeRoleRequestBuilder =
+ UpdateAssumeRoleRequest.newBuilder()
+ .setRoleArn(roleArn)
+ .setRoleSessionName(roleSessionName)
+ .setDurationSeconds(durationSeconds)
+ .setRequestId(requestId)
+ .setTempAccessKeyId(tempAccessKeyId)
+ .setSecretAccessKey(secretAccessKey)
+ .setRoleId(roleId)
+ .setSessionToken(sessionToken)
+ .setExpirationEpochSeconds(expirationEpochSeconds);
+
+ if (assumeRoleRequest.hasAwsIamSessionPolicy()) {
+
updateAssumeRoleRequestBuilder.setAwsIamSessionPolicy(awsIamSessionPolicy);
+ }
+
+ return omRequest.toBuilder()
+ .setUpdateAssumeRoleRequest(updateAssumeRoleRequestBuilder.build())
+ .build();
+ } catch (OMException e) {
+ markForAudit(auditLogger, buildAuditMessage(OMAction.S3_ASSUME_ROLE,
auditMap, e, userInfo));
+ throw e;
+ } catch (IOException e) {
+ final OMException omException = new OMException(
+ "Failed to generate STS token for role: " + roleArn, e,
OMException.ResultCodes.INTERNAL_ERROR);
+ markForAudit(auditLogger, buildAuditMessage(OMAction.S3_ASSUME_ROLE,
auditMap, omException, userInfo));
+ throw omException;
+ }
}
@Override
@@ -148,6 +197,8 @@ public OMClientResponse validateAndUpdateCache(OzoneManager
ozoneManager, Execut
final String tempAccessKeyId =
updateAssumeRoleRequest.getTempAccessKeyId();
final String secretAccessKey =
updateAssumeRoleRequest.getSecretAccessKey();
final String roleId = updateAssumeRoleRequest.getRoleId();
+ final String sessionToken = updateAssumeRoleRequest.getSessionToken();
+ final long expirationEpochSeconds =
updateAssumeRoleRequest.getExpirationEpochSeconds();
final Map<String, String> auditMap = new HashMap<>();
final AuditLogger auditLogger = ozoneManager.getAuditLogger();
@@ -158,33 +209,15 @@ public OMClientResponse
validateAndUpdateCache(OzoneManager ozoneManager, Execut
Exception exception = null;
OMClientResponse omClientResponse;
try {
- // Validate duration
- S3STSUtils.validateDuration(durationSeconds);
-
- // Validate role session name
- S3STSUtils.validateRoleSessionName(roleSessionName);
-
- // Validate role ARN and extract role
- final String targetRoleName =
AwsRoleArnValidator.validateAndExtractRoleNameFromArn(roleArn);
-
- // Note: The IamSessionPolicyResolver validates the awsIamPolicy length
internally
-
- if (!omRequest.hasS3Authentication()) {
+ if (Strings.isNullOrEmpty(tempAccessKeyId) ||
Strings.isNullOrEmpty(secretAccessKey) ||
+ Strings.isNullOrEmpty(roleId) || Strings.isNullOrEmpty(sessionToken)
|| expirationEpochSeconds <= 0) {
throw new OMException(
- "S3AssumeRoleRequest does not have S3 authentication",
OMException.ResultCodes.INVALID_REQUEST);
+ "UpdateAssumeRoleRequest is missing leader-generated AssumeRole
fields",
+ OMException.ResultCodes.INVALID_REQUEST);
}
- // Generate session token using leader-generated credentials
- final String sessionToken = generateSessionToken(
- targetRoleName, omRequest, ozoneManager, assumeRoleRequest,
secretAccessKey, tempAccessKeyId);
-
- // Generate AssumedRoleId for response using leader-generated roleId
final String assumedRoleId = roleId + ":" + roleSessionName;
- // Calculate expiration of session token
- final long expirationEpochSeconds =
clock.instant().plusSeconds(durationSeconds).getEpochSecond();
-
- // Add tempAccessKeyId to the log so it can be determined which
permanent user created the tempAccessKeyId
auditMap.put(OzoneConsts.S3_STS_TEMP_ACCESS_KEY_ID, tempAccessKeyId);
final AssumeRoleResponse.Builder responseBuilder =
AssumeRoleResponse.newBuilder()
@@ -202,15 +235,8 @@ public OMClientResponse
validateAndUpdateCache(OzoneManager ozoneManager, Execut
exception = e;
omClientResponse = new S3AssumeRoleResponse(
createErrorOMResponse(OmResponseUtil.getOMResponseBuilder(omRequest), e));
- } catch (IOException e) {
- final OMException omException = new OMException(
- "Failed to generate STS token for role: " + roleArn, e,
OMException.ResultCodes.INTERNAL_ERROR);
- exception = omException;
- omClientResponse = new S3AssumeRoleResponse(
-
createErrorOMResponse(OmResponseUtil.getOMResponseBuilder(omRequest),
omException));
}
- // Audit log
markForAudit(auditLogger, buildAuditMessage(OMAction.S3_ASSUME_ROLE,
auditMap, exception, userInfo));
return omClientResponse;
@@ -221,7 +247,7 @@ public OMClientResponse validateAndUpdateCache(OzoneManager
ozoneManager, Execut
*/
private String generateSessionToken(String targetRoleName, OMRequest
omRequest,
OzoneManager ozoneManager, AssumeRoleRequest assumeRoleRequest, String
secretAccessKey,
- String tempAccessKeyId) throws IOException {
+ String tempAccessKeyId, Instant creationInstant) throws IOException {
InetAddress remoteIp = ProtobufRpcEngine.Server.getRemoteIp();
if (remoteIp == null) {
@@ -246,7 +272,7 @@ private String generateSessionToken(String targetRoleName,
OMRequest omRequest,
return ozoneManager.getSTSTokenSecretManager().createSTSTokenString(
tempAccessKeyId, originalAccessKeyId, roleArn,
assumeRoleRequest.getDurationSeconds(), secretAccessKey,
- sessionPolicy, clock);
+ sessionPolicy, creationInstant);
}
/**
@@ -274,13 +300,134 @@ String getSessionPolicy(OzoneManager ozoneManager,
String originalAccessKeyId, S
final Set<OzoneGrant> grants = Strings.isNullOrEmpty(awsIamPolicy) ?
null :
- IamSessionPolicyResolver.resolve(awsIamPolicy, volumeName,
IamSessionPolicyResolver.AuthorizerType.RANGER);
+ resolveGrantsAgainstBucketLinks(
+ IamSessionPolicyResolver.resolve(awsIamPolicy, volumeName,
IamSessionPolicyResolver.AuthorizerType.RANGER),
+ (linkVolume, linkBucket) ->
ozoneManager.resolveBucketLink(Pair.of(linkVolume, linkBucket), true, false));
return ozoneManager.getAccessAuthorizer().generateAssumeRoleSessionPolicy(
new org.apache.hadoop.ozone.security.acl.AssumeRoleRequest(
hostName, remoteIp, ugi, targetRoleName, grants));
}
+ /**
+ * Rewrites the resolved session-policy grants so that any bucket, key, or
prefix resource that names a
+ * bucket link is anchored to the link's source volume and bucket - the
resource paths the OM authorizes
+ * against once the link is resolved at request time. READ on each link
bucket in the chain (and, when the
+ * chain crosses volumes, READ on each distinct volume except the requested
one) is retained so OM can follow
+ * every hop at request time, which keeps the generated token as small as
possible.
+ * <p>
+ * The link target is resolved when the token is generated, so the token
grants access to whatever the link
+ * points to at that moment. If the link is later re-pointed, the token no
longer grants access to the new
+ * target.
+ *
+ * @param grants the grants produced by {@link
IamSessionPolicyResolver}, possibly {@code null}
+ * @param linkResolver resolves a (volume, bucket) pair to its link target
+ * @return the link-aware grants, or the input unchanged when there is
nothing to resolve
+ */
+ @VisibleForTesting
+ static Set<OzoneGrant> resolveGrantsAgainstBucketLinks(Set<OzoneGrant>
grants,
+ BucketLinkResolver linkResolver) throws IOException {
+ if (grants == null || grants.isEmpty()) {
+ return grants;
+ }
+
+ final Map<Pair<String, String>, ResolvedBucket> resolutionCache = new
HashMap<>();
+ final Set<IOzoneObj> linkFollowObjects = new LinkedHashSet<>();
+ final Set<OzoneGrant> resolvedGrants = new LinkedHashSet<>();
+
+ for (OzoneGrant grant : grants) {
+ final Set<IOzoneObj> resolvedObjects = new LinkedHashSet<>();
+ for (IOzoneObj object : grant.getObjects()) {
+ resolvedObjects.add(
+ resolveObjectAgainstBucketLink((OzoneObj) object, linkResolver,
resolutionCache, linkFollowObjects));
+ }
+ resolvedGrants.add(new OzoneGrant(resolvedObjects,
grant.getPermissions(), grant.getS3Actions()));
+ }
+
+ // Retain only the READ required to follow each link hop at request time.
+ if (!linkFollowObjects.isEmpty()) {
+ resolvedGrants.add(new OzoneGrant(linkFollowObjects,
EnumSet.of(ACLType.READ)));
+ }
+
+ return resolvedGrants;
+ }
+
+ /**
+ * Resolves a single grant object against its bucket link. Bucket, key, and
prefix objects that name a
+ * link bucket are rewritten to the link's source volume and bucket, and the
READ needed to follow each hop
+ * in the link chain is collected in {@code linkFollowObjects}. All other
objects (volume resources and
+ * wildcard buckets) are returned unchanged.
+ */
+ private static IOzoneObj resolveObjectAgainstBucketLink(OzoneObj object,
BucketLinkResolver linkResolver,
+ Map<Pair<String, String>, ResolvedBucket> resolutionCache,
Set<IOzoneObj> linkFollowObjects)
+ throws IOException {
+ final OzoneObj.ResourceType resourceType = object.getResourceType();
+ if (resourceType != OzoneObj.ResourceType.BUCKET
+ && resourceType != OzoneObj.ResourceType.KEY
+ && resourceType != OzoneObj.ResourceType.PREFIX) {
+ return object;
+ }
+
+ final String volumeName = object.getVolumeName();
+ final String bucketName = object.getBucketName();
+ // Wildcard or unspecified names cannot correspond to a concrete link
bucket.
+ if (StringUtils.isBlank(volumeName) || StringUtils.isBlank(bucketName) ||
hasWildcard(volumeName) ||
+ hasWildcard(bucketName)) {
+ return object;
+ }
+
+ final Pair<String, String> requested = Pair.of(volumeName, bucketName);
+ ResolvedBucket resolved = resolutionCache.get(requested);
+ if (resolved == null) {
+ resolved = linkResolver.resolve(volumeName, bucketName);
+ resolutionCache.put(requested, resolved);
+ }
+ if (resolved == null || resolved.isDangling() || !resolved.isLink()) {
+ return object;
+ }
+
+ final Set<String> chainVolumes = new LinkedHashSet<>();
+ for (Pair<String, String> link : resolved.linkChain()) {
+ linkFollowObjects.add(newResourceObj(OzoneObj.ResourceType.BUCKET,
link.getLeft(), link.getRight()));
+ chainVolumes.add(link.getLeft());
+ }
+ chainVolumes.add(resolved.realVolume());
+ chainVolumes.remove(volumeName);
+ for (String vol : chainVolumes) {
+ linkFollowObjects.add(newResourceObj(OzoneObj.ResourceType.VOLUME, vol,
null));
+ }
+
+ return OzoneObjInfo.Builder.fromOzoneObj(object)
+ .setVolumeName(resolved.realVolume())
+ .setBucketName(resolved.realBucket())
+ .build();
+ }
+
+ private static IOzoneObj newResourceObj(OzoneObj.ResourceType resourceType,
String volumeName, String bucketName) {
+ final OzoneObjInfo.Builder builder = OzoneObjInfo.Builder.newBuilder()
+ .setResType(resourceType)
+ .setStoreType(OzoneObj.StoreType.OZONE)
+ .setVolumeName(volumeName);
+ if (bucketName != null) {
+ builder.setBucketName(bucketName);
+ }
+ return builder.build();
+ }
+
+ private static boolean hasWildcard(String name) {
+ return name.indexOf('*') >= 0 || name.indexOf('?') >= 0;
+ }
+
+ /**
+ * Resolves a (volume, bucket) pair to its link target, following bucket
links. Implementations must not
+ * enforce ACLs, so that session-policy generation stays deterministic
across OMs and does not depend on
+ * the external authorizer.
+ */
+ @FunctionalInterface
+ interface BucketLinkResolver {
+ ResolvedBucket resolve(String volumeName, String bucketName) throws
IOException;
+ }
+
/**
* Generates a cryptographically strong String of the supplied stringLength
using supplied chars.
*/
diff --git
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSTokenSecretManager.java
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSTokenSecretManager.java
index 63c4d8121ed..d9e5c4caf76 100644
---
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSTokenSecretManager.java
+++
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSTokenSecretManager.java
@@ -18,7 +18,6 @@
package org.apache.hadoop.ozone.security;
import java.io.IOException;
-import java.time.Clock;
import java.time.Instant;
import java.util.Objects;
import org.apache.hadoop.hdds.annotation.InterfaceAudience;
@@ -83,12 +82,11 @@ public Token<STSTokenIdentifier>
generateToken(STSTokenIdentifier tokenIdentifie
* @param secretAccessKey the secret access key associated with the
temporary access key ID
* @param sessionPolicy an optional opaque identifier that further
limits the scope of
* the permissions granted by the role
- * @param clock the system clock
+ * @param creationTime token creation time
* @return base64 encoded token string
*/
public String createSTSTokenString(String tempAccessKeyId, String
originalAccessKeyId, String roleArn,
- int durationSeconds, String secretAccessKey, String sessionPolicy, Clock
clock) throws IOException {
- final Instant creationTime = clock.instant();
+ int durationSeconds, String secretAccessKey, String sessionPolicy,
Instant creationTime) throws IOException {
final Instant expiration = creationTime.plusSeconds(durationSeconds);
final STSTokenIdentifier identifier = new
STSTokenIdentifier(STSTokenIdentifier.Params.newBuilder()
diff --git
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/security/TestS3AssumeRoleRequest.java
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/security/TestS3AssumeRoleRequest.java
index 3ae775b716b..57068d1a169 100644
---
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/security/TestS3AssumeRoleRequest.java
+++
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/security/TestS3AssumeRoleRequest.java
@@ -19,6 +19,7 @@
import static java.util.Collections.emptySet;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
@@ -33,13 +34,17 @@
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.ZoneOffset;
+import java.util.Arrays;
import java.util.Collections;
+import java.util.EnumSet;
+import java.util.LinkedHashSet;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Pattern;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
+import org.apache.commons.lang3.tuple.Pair;
import org.apache.hadoop.hdds.conf.OzoneConfiguration;
import org.apache.hadoop.hdds.security.symmetric.ManagedSecretKey;
import org.apache.hadoop.hdds.security.symmetric.SecretKeySignerClient;
@@ -47,7 +52,10 @@
import org.apache.hadoop.ozone.audit.AuditMessage;
import org.apache.hadoop.ozone.om.OMMultiTenantManager;
import org.apache.hadoop.ozone.om.OzoneManager;
+import org.apache.hadoop.ozone.om.ResolvedBucket;
+import org.apache.hadoop.ozone.om.exceptions.OMException;
import org.apache.hadoop.ozone.om.execution.flowcontrol.ExecutionContext;
+import org.apache.hadoop.ozone.om.helpers.BucketLayout;
import org.apache.hadoop.ozone.om.helpers.OMAuditLogger;
import org.apache.hadoop.ozone.om.response.OMClientResponse;
import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.AssumeRoleRequest;
@@ -57,9 +65,14 @@
import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.S3Authentication;
import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.Status;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.Type;
+import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.UpdateAssumeRoleRequest;
import org.apache.hadoop.ozone.security.STSTokenSecretManager;
import org.apache.hadoop.ozone.security.acl.AssumeRoleRequest.OzoneGrant;
import org.apache.hadoop.ozone.security.acl.IAccessAuthorizer;
+import org.apache.hadoop.ozone.security.acl.IAccessAuthorizer.ACLType;
+import org.apache.hadoop.ozone.security.acl.IOzoneObj;
+import org.apache.hadoop.ozone.security.acl.OzoneObj;
+import org.apache.hadoop.ozone.security.acl.OzoneObjInfo;
import org.apache.hadoop.ozone.security.acl.iam.IamSessionPolicyResolver;
import org.apache.hadoop.security.UserGroupInformation;
import org.apache.hadoop.security.token.TokenIdentifier;
@@ -150,13 +163,11 @@ public void testInvalidDurationTooShort() {
).build();
final S3AssumeRoleRequest request = new S3AssumeRoleRequest(omRequest,
CLOCK);
- final OMClientResponse response =
request.validateAndUpdateCache(ozoneManager, context);
- final OMResponse omResponse = response.getOMResponse();
+ final OMException exception = assertThrows(OMException.class, () ->
request.preExecute(ozoneManager));
- assertThat(omResponse.getStatus()).isEqualTo(Status.INVALID_REQUEST);
- assertThat(omResponse.getMessage()).isEqualTo(
+
assertThat(exception.getResult()).isEqualTo(OMException.ResultCodes.INVALID_REQUEST);
+ assertThat(exception.getMessage()).isEqualTo(
"Invalid Value: DurationSeconds must be between 900 and 43200
seconds");
- assertThat(omResponse.hasAssumeRoleResponse()).isFalse();
assertMarkForAuditCalled(request);
}
@@ -172,13 +183,11 @@ public void testInvalidDurationTooLong() {
).build();
final S3AssumeRoleRequest request = new S3AssumeRoleRequest(omRequest,
CLOCK);
- final OMClientResponse response =
request.validateAndUpdateCache(ozoneManager, context);
- final OMResponse omResponse = response.getOMResponse();
+ final OMException exception = assertThrows(OMException.class, () ->
request.preExecute(ozoneManager));
- assertThat(omResponse.getStatus()).isEqualTo(Status.INVALID_REQUEST);
- assertThat(omResponse.getMessage()).isEqualTo(
+
assertThat(exception.getResult()).isEqualTo(OMException.ResultCodes.INVALID_REQUEST);
+ assertThat(exception.getMessage()).isEqualTo(
"Invalid Value: DurationSeconds must be between 900 and 43200
seconds");
- assertThat(omResponse.hasAssumeRoleResponse()).isFalse();
assertMarkForAuditCalled(request);
}
@@ -196,12 +205,17 @@ public void testValidDurationMaxBoundary() throws
IOException {
// Call preExecute first to generate credentials
final S3AssumeRoleRequest request = new S3AssumeRoleRequest(omRequest,
CLOCK);
final OMRequest preExecutedRequest = request.preExecute(ozoneManager);
+
+ assertLeaderGeneratedAssumeRoleFields(preExecutedRequest, 43200);
+
final S3AssumeRoleRequest requestWithCredentials = new
S3AssumeRoleRequest(preExecutedRequest, CLOCK);
final OMClientResponse response =
requestWithCredentials.validateAndUpdateCache(ozoneManager, context);
final OMResponse omResponse = response.getOMResponse();
assertThat(omResponse.getStatus()).isEqualTo(Status.OK);
assertThat(omResponse.hasAssumeRoleResponse()).isTrue();
+ verify(accessAuthorizer).generateAssumeRoleSessionPolicy(
+ any(org.apache.hadoop.ozone.security.acl.AssumeRoleRequest.class));
assertMarkForAuditCalled(requestWithCredentials);
}
@@ -219,12 +233,17 @@ public void testValidDurationMinBoundary() throws
IOException {
// Call preExecute first to generate credentials
final S3AssumeRoleRequest request = new S3AssumeRoleRequest(omRequest,
CLOCK);
final OMRequest preExecutedRequest = request.preExecute(ozoneManager);
+
+ assertLeaderGeneratedAssumeRoleFields(preExecutedRequest, 900);
+
final S3AssumeRoleRequest requestWithCredentials = new
S3AssumeRoleRequest(preExecutedRequest, CLOCK);
final OMClientResponse response =
requestWithCredentials.validateAndUpdateCache(ozoneManager, context);
final OMResponse omResponse = response.getOMResponse();
assertThat(omResponse.getStatus()).isEqualTo(Status.OK);
assertThat(omResponse.hasAssumeRoleResponse()).isTrue();
+ verify(accessAuthorizer).generateAssumeRoleSessionPolicy(
+ any(org.apache.hadoop.ozone.security.acl.AssumeRoleRequest.class));
assertMarkForAuditCalled(requestWithCredentials);
}
@@ -242,12 +261,10 @@ public void testMissingS3Authentication() {
).build();
final S3AssumeRoleRequest request = new S3AssumeRoleRequest(omRequest,
CLOCK);
- final OMClientResponse response =
request.validateAndUpdateCache(ozoneManager, context);
- final OMResponse omResponse = response.getOMResponse();
+ final OMException exception = assertThrows(OMException.class, () ->
request.preExecute(ozoneManager));
- assertThat(omResponse.getStatus()).isEqualTo(Status.INVALID_REQUEST);
- assertThat(omResponse.getMessage()).isEqualTo("S3AssumeRoleRequest does
not have S3 authentication");
- assertThat(omResponse.hasAssumeRoleResponse()).isFalse();
+
assertThat(exception.getResult()).isEqualTo(OMException.ResultCodes.INVALID_REQUEST);
+ assertThat(exception.getMessage()).isEqualTo("S3AssumeRoleRequest does not
have S3 authentication");
assertMarkForAuditCalled(request);
}
@@ -266,6 +283,9 @@ public void testSuccessfulAssumeRoleGeneratesCredentials()
throws IOException {
final S3AssumeRoleRequest request = new S3AssumeRoleRequest(omRequest,
CLOCK);
// Call preExecute first to generate credentials
final OMRequest preExecutedRequest = request.preExecute(ozoneManager);
+
+ assertLeaderGeneratedAssumeRoleFields(preExecutedRequest, durationSeconds);
+
final S3AssumeRoleRequest requestWithCredentials = new
S3AssumeRoleRequest(preExecutedRequest, CLOCK);
final OMClientResponse clientResponse =
requestWithCredentials.validateAndUpdateCache(ozoneManager, context);
final OMResponse omResponse = clientResponse.getOMResponse();
@@ -275,6 +295,8 @@ public void testSuccessfulAssumeRoleGeneratesCredentials()
throws IOException {
assertThat(omResponse.getCmdType()).isEqualTo(Type.AssumeRole);
final AssumeRoleResponse assumeRoleResponse =
omResponse.getAssumeRoleResponse();
+ assertThat(assumeRoleResponse.getSessionToken()).isEqualTo(
+ preExecutedRequest.getUpdateAssumeRoleRequest().getSessionToken());
// AccessKeyId: prefix ASIA + 20 chars
assertThat(assumeRoleResponse.getAccessKeyId()).startsWith("ASIA");
@@ -359,6 +381,29 @@ public void testAssumeRoleCredentialsAreUnique() throws
IOException {
verify(auditLogger, times(2)).logWrite(any(AuditMessage.class));
}
+ @Test
+ public void testValidateAndUpdateCacheDoesNotCallAuthorizer() throws
IOException {
+ final OMRequest omRequest = baseOmRequestBuilder()
+ .setAssumeRoleRequest(
+ AssumeRoleRequest.newBuilder()
+ .setRoleArn(ROLE_ARN_1)
+ .setRoleSessionName(SESSION_NAME)
+ .setDurationSeconds(3600)
+ .setRequestId(REQUEST_ID)
+ ).build();
+
+ final S3AssumeRoleRequest request = new S3AssumeRoleRequest(omRequest,
CLOCK);
+ final OMRequest preExecutedRequest = request.preExecute(ozoneManager);
+ verify(accessAuthorizer).generateAssumeRoleSessionPolicy(
+ any(org.apache.hadoop.ozone.security.acl.AssumeRoleRequest.class));
+
+ final S3AssumeRoleRequest requestWithCredentials = new
S3AssumeRoleRequest(preExecutedRequest, CLOCK);
+ requestWithCredentials.validateAndUpdateCache(ozoneManager, context);
+
+ verify(accessAuthorizer, times(1)).generateAssumeRoleSessionPolicy(
+ any(org.apache.hadoop.ozone.security.acl.AssumeRoleRequest.class));
+ }
+
@Test
public void testAssumeRoleWithEmptySessionName() {
final OMRequest omRequest = baseOmRequestBuilder()
@@ -371,9 +416,9 @@ public void testAssumeRoleWithEmptySessionName() {
).build();
final S3AssumeRoleRequest request = new S3AssumeRoleRequest(omRequest,
CLOCK);
- final OMClientResponse response =
request.validateAndUpdateCache(ozoneManager, context);
-
assertThat(response.getOMResponse().getStatus()).isEqualTo(Status.INVALID_REQUEST);
- assertThat(response.getOMResponse().getMessage()).isEqualTo(
+ final OMException exception = assertThrows(OMException.class, () ->
request.preExecute(ozoneManager));
+
assertThat(exception.getResult()).isEqualTo(OMException.ResultCodes.INVALID_REQUEST);
+ assertThat(exception.getMessage()).isEqualTo(
"Value null at 'roleSessionName' failed to satisfy constraint: Member
must not be null");
assertMarkForAuditCalled(request);
}
@@ -389,14 +434,12 @@ public void testInvalidAssumeRoleSessionNameTooShort() {
).build();
final S3AssumeRoleRequest request = new S3AssumeRoleRequest(omRequest,
CLOCK);
- final OMClientResponse response =
request.validateAndUpdateCache(ozoneManager, context);
- final OMResponse omResponse = response.getOMResponse();
+ final OMException exception = assertThrows(OMException.class, () ->
request.preExecute(ozoneManager));
- assertThat(omResponse.getStatus()).isEqualTo(Status.INVALID_REQUEST);
- assertThat(omResponse.getMessage()).isEqualTo(
+
assertThat(exception.getResult()).isEqualTo(OMException.ResultCodes.INVALID_REQUEST);
+ assertThat(exception.getMessage()).isEqualTo(
"Invalid RoleSessionName length 1: it must be 2-64 characters long and
contain only alphanumeric " +
"characters and +, =, ,, ., @, -");
- assertThat(omResponse.hasAssumeRoleResponse()).isFalse();
assertMarkForAuditCalled(request);
}
@@ -412,15 +455,13 @@ public void testInvalidRoleSessionNameTooLong() {
).build();
final S3AssumeRoleRequest request = new S3AssumeRoleRequest(omRequest,
CLOCK);
- final OMClientResponse response =
request.validateAndUpdateCache(ozoneManager, context);
- final OMResponse omResponse = response.getOMResponse();
+ final OMException exception = assertThrows(OMException.class, () ->
request.preExecute(ozoneManager));
- assertThat(omResponse.getStatus()).isEqualTo(Status.INVALID_REQUEST);
- assertThat(omResponse.getMessage()).isEqualTo(
+
assertThat(exception.getResult()).isEqualTo(OMException.ResultCodes.INVALID_REQUEST);
+ assertThat(exception.getMessage()).isEqualTo(
"Invalid RoleSessionName length 70: it must be 2-64 characters long
and contain only alphanumeric " +
"characters and +, =, ,, ., @, -"
);
- assertThat(omResponse.hasAssumeRoleResponse()).isFalse();
assertMarkForAuditCalled(request);
}
@@ -511,17 +552,13 @@ public void
testMalformedSessionPolicyDoesNotIssueCredentials() throws IOExcepti
).build();
final S3AssumeRoleRequest request = new S3AssumeRoleRequest(omRequest,
CLOCK);
- final OMRequest preExecutedRequest = request.preExecute(ozoneManager);
- final S3AssumeRoleRequest requestWithCredentials = new
S3AssumeRoleRequest(preExecutedRequest, CLOCK);
- final OMClientResponse response =
requestWithCredentials.validateAndUpdateCache(ozoneManager, context);
- final OMResponse omResponse = response.getOMResponse();
+ final OMException exception = assertThrows(OMException.class, () ->
request.preExecute(ozoneManager));
-
assertThat(omResponse.getStatus()).isEqualTo(Status.MALFORMED_POLICY_DOCUMENT);
- assertThat(omResponse.getMessage()).isEqualTo("IAM session policy:
Duplicate field 'Action' in session policy");
- assertThat(omResponse.hasAssumeRoleResponse()).isFalse();
+
assertThat(exception.getResult()).isEqualTo(OMException.ResultCodes.MALFORMED_POLICY_DOCUMENT);
+ assertThat(exception.getMessage()).isEqualTo("IAM session policy:
Duplicate field 'Action' in session policy");
verify(accessAuthorizer, never()).generateAssumeRoleSessionPolicy(
any(org.apache.hadoop.ozone.security.acl.AssumeRoleRequest.class));
- assertMarkForAuditCalled(requestWithCredentials);
+ assertMarkForAuditCalled(request);
}
@Test
@@ -604,6 +641,273 @@ public void
testGetSessionPolicyWithBlankAwsPolicyCapturesNullGrants() throws Ex
assertThat(capturedAssumeRoleRequest.getGrants()).isNull();
}
+ @Test
+ public void
testResolveGrantsAgainstBucketLinksLeavesNonLinkGrantsUnchanged() throws
IOException {
+ final Set<OzoneGrant> grants = Collections.singleton(
+ new OzoneGrant(
+ objectsOf(
+ obj(OzoneObj.ResourceType.VOLUME, "s3v", null, null),
+ obj(OzoneObj.ResourceType.BUCKET, "s3v", "mybucket", null),
+ obj(OzoneObj.ResourceType.KEY, "s3v", "mybucket", "*")),
+ EnumSet.of(ACLType.READ), Collections.singleton("GetObject")));
+
+ final Set<OzoneGrant> result =
S3AssumeRoleRequest.resolveGrantsAgainstBucketLinks(
+ grants, (volume, bucket) -> new ResolvedBucket(
+ volume, bucket, volume, bucket, "owner",
BucketLayout.OBJECT_STORE));
+
+ assertThat(result).isEqualTo(grants);
+ }
+
+ @Test
+ public void
testResolveGrantsAgainstBucketLinksRewritesToSourceForSameVolumeLink() throws
IOException {
+ final Set<OzoneGrant> grants = Collections.singleton(
+ new OzoneGrant(
+ objectsOf(
+ obj(OzoneObj.ResourceType.VOLUME, "s3v", null, null),
+ obj(OzoneObj.ResourceType.BUCKET, "s3v", "s3v-iceberg", null),
+ obj(OzoneObj.ResourceType.KEY, "s3v", "s3v-iceberg", "*")),
+ EnumSet.of(ACLType.READ), Collections.singleton("GetObject")));
+
+ final Set<OzoneGrant> result =
S3AssumeRoleRequest.resolveGrantsAgainstBucketLinks(
+ grants, (volume, bucket) -> resolved(
+ volume, bucket, "s3v", "iceberg", Pair.of("s3v", "s3v-iceberg")));
+
+ final IOzoneObj linkBucket = obj(OzoneObj.ResourceType.BUCKET, "s3v",
"s3v-iceberg", null);
+ final Set<IOzoneObj> allObjects = allObjectsIn(result);
+ // Key and bucket are anchored to the link's source.
+ assertThat(allObjects).contains(obj(OzoneObj.ResourceType.KEY, "s3v",
"iceberg", "*"));
+ assertThat(allObjects).contains(obj(OzoneObj.ResourceType.BUCKET, "s3v",
"iceberg", null));
+ // The link key is gone; only a READ on the link bucket remains, for
following the link.
+ assertThat(allObjects).doesNotContain(obj(OzoneObj.ResourceType.KEY,
"s3v", "s3v-iceberg", "*"));
+ assertThat(allObjects).contains(linkBucket);
+ // Same volume, so no extra source-volume grant is added.
+ assertThat(allObjects).doesNotContain(obj(OzoneObj.ResourceType.VOLUME,
"iceberg", null, null));
+
+ final OzoneGrant followGrant = grantContaining(result, linkBucket);
+ assertThat(followGrant.getPermissions()).containsExactly(ACLType.READ);
+ assertThat(followGrant.getS3Actions()).isEmpty();
+ }
+
+ @Test
+ public void
testResolveGrantsAgainstBucketLinksRewritesPrefixOnSameVolumeLink() throws
IOException {
+ final Set<OzoneGrant> grants = Collections.singleton(
+ new OzoneGrant(
+ objectsOf(
+ prefixObj("s3v", "s3v-iceberg", "folder/")),
+ EnumSet.of(ACLType.READ, ACLType.LIST),
Collections.singleton("ListBucket")));
+
+ final Set<OzoneGrant> result =
S3AssumeRoleRequest.resolveGrantsAgainstBucketLinks(
+ grants, (volume, bucket) -> resolved(
+ volume, bucket, "s3v", "iceberg", Pair.of("s3v", "s3v-iceberg")));
+
+ final IOzoneObj linkBucket = obj(OzoneObj.ResourceType.BUCKET, "s3v",
"s3v-iceberg", null);
+ final Set<IOzoneObj> allObjects = allObjectsIn(result);
+ assertThat(allObjects).contains(prefixObj("s3v", "iceberg", "folder/"));
+ assertThat(allObjects).doesNotContain(prefixObj("s3v", "s3v-iceberg",
"folder/"));
+ assertThat(allObjects).contains(linkBucket);
+
+ final OzoneGrant followGrant = grantContaining(result, linkBucket);
+ assertThat(followGrant.getPermissions()).containsExactly(ACLType.READ);
+ assertThat(followGrant.getS3Actions()).isEmpty();
+ }
+
+ @Test
+ public void
testResolveGrantsAgainstBucketLinksAddsSourceVolumeReadForCrossVolumeLink()
throws IOException {
+ final Set<OzoneGrant> grants = Collections.singleton(
+ new OzoneGrant(
+ objectsOf(
+ obj(OzoneObj.ResourceType.VOLUME, "s3v", null, null),
+ obj(OzoneObj.ResourceType.BUCKET, "s3v", "s3v-iceberg", null),
+ obj(OzoneObj.ResourceType.KEY, "s3v", "s3v-iceberg", "*")),
+ EnumSet.of(ACLType.READ), Collections.singleton("GetObject")));
+
+ final Set<OzoneGrant> result =
S3AssumeRoleRequest.resolveGrantsAgainstBucketLinks(
+ grants, (volume, bucket) -> resolved(
+ volume, bucket, "tenantvol", "iceberg", Pair.of("s3v",
"s3v-iceberg")));
+
+ final IOzoneObj linkBucket = obj(OzoneObj.ResourceType.BUCKET, "s3v",
"s3v-iceberg", null);
+ final IOzoneObj sourceVolume = obj(OzoneObj.ResourceType.VOLUME,
"tenantvol", null, null);
+ final Set<IOzoneObj> allObjects = allObjectsIn(result);
+ assertThat(allObjects).contains(obj(OzoneObj.ResourceType.KEY,
"tenantvol", "iceberg", "*"));
+ assertThat(allObjects).contains(linkBucket);
+ assertThat(allObjects).contains(sourceVolume);
+
+ final OzoneGrant followGrant = grantContaining(result, sourceVolume);
+ assertThat(followGrant.getPermissions()).containsExactly(ACLType.READ);
+ assertThat(followGrant.getObjects()).contains(linkBucket);
+ }
+
+ @Test
+ public void testResolveGrantsAgainstBucketLinksSkipsWildcardBuckets() throws
IOException {
+ final Set<OzoneGrant> grants = Collections.singleton(
+ new OzoneGrant(
+ objectsOf(
+ obj(OzoneObj.ResourceType.VOLUME, "s3v", null, null),
+ obj(OzoneObj.ResourceType.BUCKET, "s3v", "*", null),
+ obj(OzoneObj.ResourceType.KEY, "s3v", "*", "*")),
+ EnumSet.of(ACLType.READ), Collections.singleton("GetObject")));
+
+ final Set<OzoneGrant> result =
S3AssumeRoleRequest.resolveGrantsAgainstBucketLinks(
+ grants, (volume, bucket) -> {
+ throw new AssertionError("link resolver must not be called for
wildcard buckets");
+ });
+
+ assertThat(result).isEqualTo(grants);
+ }
+
+ @Test
+ public void
testResolveGrantsAgainstBucketLinksLeavesDanglingBucketsUnchanged() throws
IOException {
+ final Set<OzoneGrant> grants = Collections.singleton(
+ new OzoneGrant(
+ objectsOf(
+ obj(OzoneObj.ResourceType.KEY, "s3v", "danglingBucket", "*")),
+ EnumSet.of(ACLType.READ), Collections.singleton("GetObject")));
+
+ final Set<OzoneGrant> result =
S3AssumeRoleRequest.resolveGrantsAgainstBucketLinks(
+ grants, (volume, bucket) -> new ResolvedBucket(volume, bucket, null,
null, null, null));
+
+ assertThat(result).isEqualTo(grants);
+ }
+
+ @Test
+ public void
testResolveGrantsAgainstBucketLinksRewritesChainedSameVolumeLink() throws
IOException {
+ final Set<OzoneGrant> grants = Collections.singleton(
+ new OzoneGrant(
+ objectsOf(
+ obj(OzoneObj.ResourceType.BUCKET, "s3v", "linkA", null),
+ obj(OzoneObj.ResourceType.KEY, "s3v", "linkA", "*")),
+ EnumSet.of(ACLType.READ), Collections.singleton("GetObject")));
+
+ final Set<OzoneGrant> result =
S3AssumeRoleRequest.resolveGrantsAgainstBucketLinks(
+ grants, (volume, bucket) -> resolved(
+ volume, bucket, "s3v", "source", Pair.of("s3v", "linkA"),
Pair.of("s3v", "linkB")));
+
+ final IOzoneObj linkA = obj(OzoneObj.ResourceType.BUCKET, "s3v", "linkA",
null);
+ final IOzoneObj linkB = obj(OzoneObj.ResourceType.BUCKET, "s3v", "linkB",
null);
+ final Set<IOzoneObj> allObjects = allObjectsIn(result);
+ assertThat(allObjects).contains(obj(OzoneObj.ResourceType.KEY, "s3v",
"source", "*"));
+ assertThat(allObjects).contains(linkA);
+ assertThat(allObjects).contains(linkB);
+ assertThat(allObjects).doesNotContain(obj(OzoneObj.ResourceType.VOLUME,
"s3v", null, null));
+ }
+
+ @Test
+ public void
testResolveGrantsAgainstBucketLinksAddsVolumeReadForChainedCrossVolumeHop()
throws IOException {
+ final Set<OzoneGrant> grants = Collections.singleton(
+ new OzoneGrant(
+ objectsOf(
+ obj(OzoneObj.ResourceType.BUCKET, "s3v", "linkA", null),
+ obj(OzoneObj.ResourceType.KEY, "s3v", "linkA", "*")),
+ EnumSet.of(ACLType.READ), Collections.singleton("GetObject")));
+
+ final Set<OzoneGrant> result =
S3AssumeRoleRequest.resolveGrantsAgainstBucketLinks(
+ grants, (volume, bucket) -> resolved(
+ volume, bucket, "tenant", "source", Pair.of("s3v", "linkA"),
Pair.of("tenant", "linkB")));
+
+ final IOzoneObj linkA = obj(OzoneObj.ResourceType.BUCKET, "s3v", "linkA",
null);
+ final IOzoneObj linkB = obj(OzoneObj.ResourceType.BUCKET, "tenant",
"linkB", null);
+ final IOzoneObj tenantVolume = obj(OzoneObj.ResourceType.VOLUME, "tenant",
null, null);
+ final Set<IOzoneObj> allObjects = allObjectsIn(result);
+ assertThat(allObjects).contains(obj(OzoneObj.ResourceType.KEY, "tenant",
"source", "*"));
+ assertThat(allObjects).contains(linkA);
+ assertThat(allObjects).contains(linkB);
+ assertThat(allObjects).contains(tenantVolume);
+ assertThat(allObjects).doesNotContain(obj(OzoneObj.ResourceType.VOLUME,
"s3v", null, null));
+ }
+
+ @Test
+ public void testGetSessionPolicyRewritesLinkBucketGrantsToSource() throws
Exception {
+ when(ozoneManager.isS3MultiTenancyEnabled()).thenReturn(false);
+
+ final Set<OzoneGrant> resolverGrants = Collections.singleton(
+ new OzoneGrant(
+ objectsOf(
+ obj(OzoneObj.ResourceType.VOLUME, "s3v", null, null),
+ obj(OzoneObj.ResourceType.BUCKET, "s3v", "s3v-iceberg", null),
+ obj(OzoneObj.ResourceType.KEY, "s3v", "s3v-iceberg", "*")),
+ EnumSet.of(ACLType.READ), Collections.singleton("GetObject")));
+
+ when(ozoneManager.resolveBucketLink(Pair.of("s3v", "s3v-iceberg"), true,
false))
+ .thenReturn(resolved("s3v", "s3v-iceberg", "s3v", "iceberg",
Pair.of("s3v", "s3v-iceberg")));
+
+ try (MockedStatic<IamSessionPolicyResolver> resolverMock =
mockStatic(IamSessionPolicyResolver.class)) {
+ resolverMock.when(() -> IamSessionPolicyResolver.resolve(
+ AWS_IAM_POLICY, "s3v",
IamSessionPolicyResolver.AuthorizerType.RANGER))
+ .thenReturn(resolverGrants);
+
+ final String result = new
S3AssumeRoleRequest(baseOmRequestBuilder().build(), CLOCK)
+ .getSessionPolicy(
+ ozoneManager, ORIGINAL_ACCESS_KEY_ID, AWS_IAM_POLICY, OM_HOST,
LOOPBACK_IP,
+ UserGroupInformation.createRemoteUser("userNameLink"),
TARGET_ROLE_NAME);
+ // Ensure no exception was thrown and that the method actually delegated
to generateAssumeRoleSessionPolicy
+ // and returned its value (not null, not something else).
+ assertThat(result).isEqualTo(SESSION_POLICY_VALUE);
+ }
+
+ final
ArgumentCaptor<org.apache.hadoop.ozone.security.acl.AssumeRoleRequest> captor =
ArgumentCaptor.forClass(
+ org.apache.hadoop.ozone.security.acl.AssumeRoleRequest.class);
+ verify(accessAuthorizer).generateAssumeRoleSessionPolicy(captor.capture());
+
+ final Set<IOzoneObj> allObjects =
allObjectsIn(captor.getValue().getGrants());
+ assertThat(allObjects).contains(obj(OzoneObj.ResourceType.KEY, "s3v",
"iceberg", "*"));
+ assertThat(allObjects).contains(obj(OzoneObj.ResourceType.BUCKET, "s3v",
"s3v-iceberg", null));
+ assertThat(allObjects).doesNotContain(obj(OzoneObj.ResourceType.KEY,
"s3v", "s3v-iceberg", "*"));
+
+ verify(ozoneManager).resolveBucketLink(Pair.of("s3v", "s3v-iceberg"),
true, false);
+ }
+
+ @SafeVarargs
+ private static ResolvedBucket resolved(String requestedVol, String
requestedBucket, String realVol, String realBucket,
+ Pair<String, String>... linkChain) {
+ return new ResolvedBucket(
+ requestedVol, requestedBucket, realVol, realBucket, "owner",
+ BucketLayout.OBJECT_STORE, Arrays.asList(linkChain));
+ }
+
+ private static IOzoneObj obj(OzoneObj.ResourceType type, String volume,
String bucket, String key) {
+ final OzoneObjInfo.Builder builder = OzoneObjInfo.Builder.newBuilder()
+ .setResType(type)
+ .setStoreType(OzoneObj.StoreType.OZONE)
+ .setVolumeName(volume);
+ if (bucket != null) {
+ builder.setBucketName(bucket);
+ }
+ if (key != null) {
+ builder.setKeyName(key);
+ }
+ return builder.build();
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ private static IOzoneObj prefixObj(String volume, String bucket, String
prefix) {
+ return OzoneObjInfo.Builder.newBuilder()
+ .setResType(OzoneObj.ResourceType.PREFIX)
+ .setStoreType(OzoneObj.StoreType.OZONE)
+ .setVolumeName(volume)
+ .setBucketName(bucket)
+ .setPrefixName(prefix)
+ .build();
+ }
+
+ private static Set<IOzoneObj> objectsOf(IOzoneObj... objects) {
+ return new LinkedHashSet<>(Arrays.asList(objects));
+ }
+
+ private static Set<IOzoneObj> allObjectsIn(Set<OzoneGrant> grants) {
+ final Set<IOzoneObj> all = new LinkedHashSet<>();
+ for (OzoneGrant grant : grants) {
+ all.addAll(grant.getObjects());
+ }
+ return all;
+ }
+
+ private static OzoneGrant grantContaining(Set<OzoneGrant> grants, IOzoneObj
object) {
+ return grants.stream()
+ .filter(grant -> grant.getObjects().contains(object))
+ .findFirst()
+ .orElseThrow(() -> new AssertionError("No grant contains " + object));
+ }
+
private org.apache.hadoop.ozone.security.acl.AssumeRoleRequest
captureAssumeRoleRequest(String volumeName,
String userName) throws Exception {
try (MockedStatic<IamSessionPolicyResolver> resolverMock =
mockStatic(IamSessionPolicyResolver.class)) {
@@ -637,6 +941,17 @@ private static OMRequest.Builder baseOmRequestBuilder() {
);
}
+ private void assertLeaderGeneratedAssumeRoleFields(OMRequest
preExecutedRequest, int durationSeconds) {
+ assertThat(preExecutedRequest.hasUpdateAssumeRoleRequest()).isTrue();
+ final UpdateAssumeRoleRequest updateAssumeRoleRequest =
preExecutedRequest.getUpdateAssumeRoleRequest();
+
assertThat(updateAssumeRoleRequest.getTempAccessKeyId()).startsWith("ASIA");
+ assertThat(updateAssumeRoleRequest.getSecretAccessKey()).isNotEmpty();
+ assertThat(updateAssumeRoleRequest.getRoleId()).startsWith("AROA");
+ assertThat(updateAssumeRoleRequest.getSessionToken()).isNotEmpty();
+ assertThat(updateAssumeRoleRequest.getExpirationEpochSeconds())
+ .isEqualTo(CLOCK.instant().getEpochSecond() + durationSeconds);
+ }
+
private void assertMarkForAuditCalled(S3AssumeRoleRequest request) {
OMAuditLogger.log(request.getAuditBuilder());
verify(auditLogger).logWrite(any(AuditMessage.class));
diff --git
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSSecurityUtil.java
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSSecurityUtil.java
index 608be8f6603..2f3a0350b57 100644
---
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSSecurityUtil.java
+++
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSSecurityUtil.java
@@ -80,8 +80,8 @@ public void
testConstructValidateAndDecryptSTSTokenInvalidProtobuf() throws IOEx
@Test
public void testConstructValidateAndDecryptSTSTokenSuccess() throws
IOException {
// Create a valid token
- final String tokenString = tokenSecretManager.createSTSTokenString(
- TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS,
SECRET_ACCESS_KEY, SESSION_POLICY, clock);
+ final String tokenString =
tokenSecretManager.createSTSTokenString(TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY,
+ ROLE_ARN, DURATION_SECONDS, SECRET_ACCESS_KEY, SESSION_POLICY,
clock.instant());
// Validate and decrypt the token
final STSTokenIdentifier result =
STSSecurityUtil.constructValidateAndDecryptSTSToken(
@@ -103,7 +103,7 @@ public void
testConstructValidateAndDecryptSTSTokenSuccess() throws IOException
public void
testConstructValidateAndDecryptSTSTokenSuccessWithNullSessionPolicy() throws
Exception {
// Create a valid token with null session policy
final String tokenString = tokenSecretManager.createSTSTokenString(
- TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS,
SECRET_ACCESS_KEY, null, clock);
+ TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS,
SECRET_ACCESS_KEY, null, clock.instant());
// Validate and decrypt the token
final STSTokenIdentifier result =
STSSecurityUtil.constructValidateAndDecryptSTSToken(
@@ -135,8 +135,8 @@ public void
testConstructValidateAndDecryptSTSTokenRuntimeDecodeFailure() {
@Test
public void testConstructValidateAndDecryptSTSTokenInvalidKind() throws
Exception {
// Create a valid identifier to use as base
- final String validTokenString = tokenSecretManager.createSTSTokenString(
- TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS,
SECRET_ACCESS_KEY, SESSION_POLICY, clock);
+ final String validTokenString =
tokenSecretManager.createSTSTokenString(TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY,
+ ROLE_ARN, DURATION_SECONDS, SECRET_ACCESS_KEY, SESSION_POLICY,
clock.instant());
final Token<STSTokenIdentifier> validToken = new Token<>();
validToken.decodeFromUrlString(validTokenString);
@@ -158,8 +158,8 @@ public void
testConstructValidateAndDecryptSTSTokenInvalidKind() throws Exceptio
@Test
public void testConstructValidateAndDecryptSTSTokenInvalidService() throws
Exception {
// Create a token with incorrect service
- final String validTokenString = tokenSecretManager.createSTSTokenString(
- TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS,
SECRET_ACCESS_KEY, SESSION_POLICY, clock);
+ final String validTokenString =
tokenSecretManager.createSTSTokenString(TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY,
+ ROLE_ARN, DURATION_SECONDS, SECRET_ACCESS_KEY, SESSION_POLICY,
clock.instant());
final Token<STSTokenIdentifier> validToken = new Token<>();
validToken.decodeFromUrlString(validTokenString);
@@ -180,7 +180,7 @@ public void
testConstructValidateAndDecryptSTSTokenInvalidService() throws Excep
public void testConstructValidateAndDecryptSTSTokenExpired() throws
Exception {
// Create a token that expires immediately (durationSeconds of 0)
final String tokenString = tokenSecretManager.createSTSTokenString(
- TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, 0, SECRET_ACCESS_KEY,
SESSION_POLICY, clock);
+ TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, 0, SECRET_ACCESS_KEY,
SESSION_POLICY, clock.instant());
// Fast-forward time to ensure token is expired
clock.fastForward(100);
@@ -196,8 +196,8 @@ public void
testConstructValidateAndDecryptSTSTokenExpired() throws Exception {
@Test
public void testConstructValidateAndDecryptSTSTokenSecretKeyNotFound()
throws Exception {
// Create a valid token string
- final String validTokenString = tokenSecretManager.createSTSTokenString(
- TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS,
SECRET_ACCESS_KEY, SESSION_POLICY, clock);
+ final String validTokenString =
tokenSecretManager.createSTSTokenString(TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY,
+ ROLE_ARN, DURATION_SECONDS, SECRET_ACCESS_KEY, SESSION_POLICY,
clock.instant());
// Create a mock secret key client that returns null for the key
final SecretKeyClient mockKeyClient = mock(SecretKeyClient.class);
@@ -215,8 +215,8 @@ public void
testConstructValidateAndDecryptSTSTokenSecretKeyNotFound() throws Ex
@Test
public void testConstructValidateAndDecryptSTSTokenInvalidSecretKeyId()
throws Exception {
// Create a valid identifier to use as base
- final String validTokenString = tokenSecretManager.createSTSTokenString(
- TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS,
SECRET_ACCESS_KEY, SESSION_POLICY, clock);
+ final String validTokenString =
tokenSecretManager.createSTSTokenString(TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY,
+ ROLE_ARN, DURATION_SECONDS, SECRET_ACCESS_KEY, SESSION_POLICY,
clock.instant());
final Token<STSTokenIdentifier> validToken = new Token<>();
validToken.decodeFromUrlString(validTokenString);
@@ -241,8 +241,8 @@ public void
testConstructValidateAndDecryptSTSTokenInvalidSecretKeyId() throws E
@Test
public void testConstructValidateAndDecryptSTSTokenExpiredSecretKey() throws
Exception {
// Create a valid token string
- final String validTokenString = tokenSecretManager.createSTSTokenString(
- TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS,
SECRET_ACCESS_KEY, SESSION_POLICY, clock);
+ final String validTokenString =
tokenSecretManager.createSTSTokenString(TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY,
+ ROLE_ARN, DURATION_SECONDS, SECRET_ACCESS_KEY, SESSION_POLICY,
clock.instant());
// Create a mock secret key that is expired
final ManagedSecretKey expiredSecretKey = mock(ManagedSecretKey.class);
@@ -264,8 +264,8 @@ public void
testConstructValidateAndDecryptSTSTokenExpiredSecretKey() throws Exc
@Test
public void
testConstructValidateAndDecryptSTSTokenSecretKeyRetrievalException() throws
Exception {
// Create a valid token string
- final String validTokenString = tokenSecretManager.createSTSTokenString(
- TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS,
SECRET_ACCESS_KEY, SESSION_POLICY, clock);
+ final String validTokenString =
tokenSecretManager.createSTSTokenString(TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY,
+ ROLE_ARN, DURATION_SECONDS, SECRET_ACCESS_KEY, SESSION_POLICY,
clock.instant());
// Create a mock secret key client that throws an exception
final SecretKeyClient mockKeyClient = mock(SecretKeyClient.class);
@@ -283,8 +283,8 @@ public void
testConstructValidateAndDecryptSTSTokenSecretKeyRetrievalException()
@Test
public void testConstructValidateAndDecryptSTSTokenInvalidSignature() throws
Exception {
// Create a valid token string
- final String validTokenString = tokenSecretManager.createSTSTokenString(
- TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS,
SECRET_ACCESS_KEY, SESSION_POLICY, clock);
+ final String validTokenString =
tokenSecretManager.createSTSTokenString(TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY,
+ ROLE_ARN, DURATION_SECONDS, SECRET_ACCESS_KEY, SESSION_POLICY,
clock.instant());
final Token<STSTokenIdentifier> validToken = new Token<>();
validToken.decodeFromUrlString(validTokenString);
@@ -307,7 +307,7 @@ public void
testConstructValidateAndDecryptSTSTokenInvalidSignature() throws Exc
public void testConstructValidateAndDecryptSTSTokenRejectsDoubledToken()
throws Exception {
final String tokenString = tokenSecretManager.createSTSTokenString(
TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS,
- SECRET_ACCESS_KEY, SESSION_POLICY, clock);
+ SECRET_ACCESS_KEY, SESSION_POLICY, clock.instant());
assertThatThrownBy(() ->
STSSecurityUtil.constructValidateAndDecryptSTSToken(tokenString +
tokenString, secretKeyClient, clock))
@@ -320,7 +320,7 @@ public void
testConstructValidateAndDecryptSTSTokenRejectsDoubledToken() throws
public void testConstructValidateAndDecryptSTSTokenRejectsTokenWithSuffix()
throws Exception {
final String tokenString = tokenSecretManager.createSTSTokenString(
TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS,
- SECRET_ACCESS_KEY, SESSION_POLICY, clock);
+ SECRET_ACCESS_KEY, SESSION_POLICY, clock.instant());
assertThatThrownBy(() ->
STSSecurityUtil.constructValidateAndDecryptSTSToken(tokenString +
"garbage", secretKeyClient, clock))
@@ -344,11 +344,11 @@ public void
testConstructValidateAndDecryptMultipleTokens() throws Exception {
// Create multiple tokens and validate them all
final String token1 = tokenSecretManager.createSTSTokenString(
"temp-key-1", "orig-key-1", "role-arn-1", DURATION_SECONDS,
- "secret-key-1", "policy-1", clock);
+ "secret-key-1", "policy-1", clock.instant());
final String token2 = tokenSecretManager.createSTSTokenString(
"temp-key-2", "orig-key-2", "role-arn-2", DURATION_SECONDS,
- "secret-key-2", "policy-2", clock);
+ "secret-key-2", "policy-2", clock.instant());
final STSTokenIdentifier result1 =
STSSecurityUtil.constructValidateAndDecryptSTSToken(
token1, secretKeyClient, clock);
@@ -418,8 +418,8 @@ public void
testEnsureEssentialFieldsArePresentInTokenMissingCreationTime() {
@Test
public void testEnsureResolvedStsFieldsInvariantsSuccess() throws Exception {
- final String tokenString = tokenSecretManager.createSTSTokenString(
- TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS,
SECRET_ACCESS_KEY, SESSION_POLICY, clock);
+ final String tokenString =
tokenSecretManager.createSTSTokenString(TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY,
+ ROLE_ARN, DURATION_SECONDS, SECRET_ACCESS_KEY, SESSION_POLICY,
clock.instant());
final S3Authentication s3Auth = S3Authentication.newBuilder()
.setSessionToken(tokenString)
@@ -460,7 +460,7 @@ public void
testEnsureResolvedStsFieldsInvariantsMissingSessionToken() {
public void testEnsureResolvedStsFieldsInvariantsMissingResolvedFields()
throws Exception {
final String tokenString = tokenSecretManager.createSTSTokenString(
TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS,
- SECRET_ACCESS_KEY, SESSION_POLICY, clock);
+ SECRET_ACCESS_KEY, SESSION_POLICY, clock.instant());
final S3Authentication s3Auth = S3Authentication.newBuilder()
.setSessionToken(tokenString)
diff --git
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenSecretManager.java
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenSecretManager.java
index 4408652dbb9..fe20c9dd2a4 100644
---
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenSecretManager.java
+++
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenSecretManager.java
@@ -84,8 +84,8 @@ public void setUp() throws Exception {
@Test
public void testCreateSTSTokenStringContainsCorrectFields() throws
IOException {
- final String tokenString = secretManager.createSTSTokenString(
- TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS,
SECRET_ACCESS_KEY, SESSION_POLICY, clock);
+ final String tokenString =
secretManager.createSTSTokenString(TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY,
+ ROLE_ARN, DURATION_SECONDS, SECRET_ACCESS_KEY, SESSION_POLICY,
clock.instant());
// Decode the token
final Token<STSTokenIdentifier> token = new Token<>();
@@ -114,7 +114,7 @@ public void testCreateSTSTokenStringContainsCorrectFields()
throws IOException {
@Test
public void testCreateSTSTokenStringWithNullSessionPolicy() throws
IOException {
final String tokenString = secretManager.createSTSTokenString(
- TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS,
SECRET_ACCESS_KEY, null, clock);
+ TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS,
SECRET_ACCESS_KEY, null, clock.instant());
// Decode the token
final Token<STSTokenIdentifier> token = new Token<>();
@@ -150,8 +150,8 @@ public void
testCreateSTSTokenStringValidatesWhenSecretKeyRotatesDuringCreation(
encryptionKey, signingKey);
final STSTokenSecretManager rotatingSecretManager = new
STSTokenSecretManager(rotatingSecretKeyClient);
- final String tokenString = rotatingSecretManager.createSTSTokenString(
- TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS,
SECRET_ACCESS_KEY, SESSION_POLICY, clock);
+ final String tokenString =
rotatingSecretManager.createSTSTokenString(TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY,
+ ROLE_ARN, DURATION_SECONDS, SECRET_ACCESS_KEY, SESSION_POLICY,
clock.instant());
final STSTokenIdentifier result =
STSSecurityUtil.constructValidateAndDecryptSTSToken(
tokenString, rotatingSecretKeyClient, clock);
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]