diff --git a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/security/acl/AssumeRoleRequest.java b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/security/acl/AssumeRoleRequest.java index 1272d5422ec1..20278a0ecfdb 100644 --- a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/security/acl/AssumeRoleRequest.java +++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/security/acl/AssumeRoleRequest.java @@ -18,6 +18,8 @@ package org.apache.hadoop.ozone.security.acl; import java.net.InetAddress; +import java.util.Collections; +import java.util.LinkedHashSet; import java.util.Objects; import java.util.Set; import net.jcip.annotations.Immutable; @@ -93,10 +95,24 @@ public int hashCode() { public static class OzoneGrant { private final Set objects; private final Set permissions; + /** + * S3 action names without the s3: prefix (e.g. GetObject) from the session policy. When present, the permissions + * will be further restricted by the set of available S3 actions. An empty (or null) set means this OzoneGrant + * does not enforce any restrictions on actions. + */ + private final Set s3Actions; public OzoneGrant(Set objects, Set permissions) { this.objects = objects; this.permissions = permissions; + this.s3Actions = Collections.emptySet(); + } + + public OzoneGrant(Set objects, Set permissions, + Set s3Actions) { + this.objects = objects; + this.permissions = permissions; + this.s3Actions = Collections.unmodifiableSet(new LinkedHashSet<>(s3Actions)); } public Set getObjects() { @@ -107,6 +123,10 @@ public Set getPermissions() { return permissions; } + public Set getS3Actions() { + return s3Actions; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -116,12 +136,19 @@ public boolean equals(Object o) { } final OzoneGrant that = (OzoneGrant) o; - return Objects.equals(objects, that.objects) && Objects.equals(permissions, that.permissions); + return Objects.equals(objects, that.objects) && Objects.equals(permissions, that.permissions) && + Objects.equals(s3Actions, that.s3Actions); } @Override public int hashCode() { - return Objects.hash(objects, permissions); + return Objects.hash(objects, permissions, s3Actions); + } + + @Override + public String toString() { + return "OzoneGrant{" + "objects=" + objects + ", permissions=" + permissions + ", s3Actions=" + + s3Actions + '}'; } } } diff --git a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/security/acl/RequestContext.java b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/security/acl/RequestContext.java index 44e0f9284abf..2d80fa2d7f96 100644 --- a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/security/acl/RequestContext.java +++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/security/acl/RequestContext.java @@ -50,6 +50,12 @@ public final class RequestContext { */ private final String sessionPolicy; + /** + * S3 action name for this request without the s3: prefix (e.g. PutObject), when the call originated from S3 Gateway. + * Null for non-S3 clients or when not applicable. + */ + private final String s3Action; + private RequestContext(Builder builder) { this.host = builder.host; this.ip = builder.ip; @@ -60,6 +66,7 @@ private RequestContext(Builder builder) { this.ownerName = builder.ownerName; this.recursiveAccessCheck = builder.recursiveAccessCheck; this.sessionPolicy = builder.sessionPolicy; + this.s3Action = builder.s3Action; } /** @@ -81,6 +88,7 @@ public static final class Builder { private boolean recursiveAccessCheck; private String sessionPolicy; + private String s3Action; private Builder() { @@ -135,6 +143,11 @@ public Builder setSessionPolicy(String sessionPolicy) { return this; } + public Builder setS3Action(String s3Action) { + this.s3Action = s3Action; + return this; + } + public RequestContext build() { return new RequestContext(this); } @@ -185,4 +198,22 @@ public boolean isRecursiveAccessCheck() { public String getSessionPolicy() { return sessionPolicy; } + + public String getS3Action() { + return s3Action; + } + + public Builder toBuilder() { + return newBuilder() + .setHost(host) + .setIp(ip) + .setClientUgi(clientUgi) + .setServiceId(serviceId) + .setAclType(aclType) + .setAclRights(aclRights) + .setOwnerName(ownerName) + .setRecursiveAccessCheck(recursiveAccessCheck) + .setSessionPolicy(sessionPolicy) + .setS3Action(s3Action); + } } diff --git a/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/security/acl/TestAssumeRoleRequest.java b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/security/acl/TestAssumeRoleRequest.java index e9d9c519bd11..c163517afd29 100644 --- a/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/security/acl/TestAssumeRoleRequest.java +++ b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/security/acl/TestAssumeRoleRequest.java @@ -21,9 +21,11 @@ import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; import java.util.Collections; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.Set; import org.apache.hadoop.security.UserGroupInformation; import org.junit.jupiter.api.Test; @@ -36,23 +38,32 @@ public class TestAssumeRoleRequest { @Test public void testConstructorAndGetters() { final UserGroupInformation ugi = UserGroupInformation.createRemoteUser("om"); + final IOzoneObj bucketObj = + OzoneObjInfo.Builder.newBuilder() + .setResType(OzoneObj.ResourceType.BUCKET) + .setStoreType(OzoneObj.StoreType.OZONE) + .setVolumeName("s3v") + .setBucketName("myBucket") + .build(); + final Set objects = Collections.singleton(bucketObj); + final Set permissions = Collections.singleton(IAccessAuthorizer.ACLType.READ); + final Set s3Actions = Collections.emptySet(); + final Set grants = new HashSet<>(); - grants.add( - new AssumeRoleRequest.OzoneGrant( - Collections.singleton( - OzoneObjInfo.Builder.newBuilder() - .setResType(OzoneObj.ResourceType.BUCKET) - .setStoreType(OzoneObj.StoreType.OZONE) - .setVolumeName("s3v") - .setBucketName("myBucket") - .build()), - Collections.singleton(IAccessAuthorizer.ACLType.READ))); + grants.add(new AssumeRoleRequest.OzoneGrant(objects, permissions, s3Actions)); final AssumeRoleRequest assumeRoleRequest1 = new AssumeRoleRequest( "host", null, ugi, "roleA", grants); final AssumeRoleRequest assumeRoleRequest2 = new AssumeRoleRequest( "host", null, ugi, "roleA", grants); + final AssumeRoleRequest.OzoneGrant grant = grants.iterator().next(); + assertEquals(objects, grant.getObjects()); + assertEquals(permissions, grant.getPermissions()); + assertEquals(s3Actions, grant.getS3Actions()); + // Ensure the s3 actions are not modifiable + assertThrows(UnsupportedOperationException.class, () -> grant.getS3Actions().add("GetObject")); + assertEquals("host", assumeRoleRequest1.getHost()); assertNull(assumeRoleRequest1.getIp()); assertSame(ugi, assumeRoleRequest1.getClientUgi()); @@ -66,6 +77,48 @@ public void testConstructorAndGetters() { "host", null, ugi, "roleB", null); assertNotEquals(assumeRoleRequest1, assumeRoleRequest3); } + + @Test + public void testGrantsWithS3Actions() { + final UserGroupInformation ugi = UserGroupInformation.createRemoteUser("om"); + + final IOzoneObj bucketObj = + OzoneObjInfo.Builder.newBuilder() + .setResType(OzoneObj.ResourceType.BUCKET) + .setStoreType(OzoneObj.StoreType.OZONE) + .setVolumeName("s3v") + .setBucketName("myBucket") + .build(); + + final Set objects = Collections.singleton(bucketObj); + final Set permissions = Collections.singleton(IAccessAuthorizer.ACLType.READ); + + final Set s3Actions = new LinkedHashSet<>(); + s3Actions.add("GetObject"); + s3Actions.add("PutObject"); + + final AssumeRoleRequest.OzoneGrant grantWithActions = new AssumeRoleRequest.OzoneGrant( + objects, permissions, s3Actions); + final AssumeRoleRequest.OzoneGrant grantWithoutActions = new AssumeRoleRequest.OzoneGrant( + objects, permissions, Collections.emptySet()); + + assertEquals(objects, grantWithActions.getObjects()); + assertEquals(permissions, grantWithActions.getPermissions()); + assertEquals(s3Actions, grantWithActions.getS3Actions()); + assertNotEquals(grantWithActions, grantWithoutActions); + assertNotEquals(grantWithActions.hashCode(), grantWithoutActions.hashCode()); + + final Set grantsWithActionsSet = Collections.singleton(grantWithActions); + final Set grantsWithoutActionsSet = Collections.singleton(grantWithoutActions); + + final AssumeRoleRequest requestWithActions = new AssumeRoleRequest( + "host", null, ugi, "roleA", grantsWithActionsSet); + final AssumeRoleRequest requestWithoutActions = new AssumeRoleRequest( + "host", null, ugi, "roleA", grantsWithoutActionsSet); + + assertNotEquals(requestWithActions, requestWithoutActions); + assertNotEquals(requestWithActions.hashCode(), requestWithoutActions.hashCode()); + } } diff --git a/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/security/acl/package-info.java b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/security/acl/package-info.java new file mode 100644 index 000000000000..ff6060adcb4d --- /dev/null +++ b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/security/acl/package-info.java @@ -0,0 +1,21 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Unit tests related to acl-related functionality. + */ +package org.apache.hadoop.ozone.security.acl; diff --git a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/acl/TestRequestContext.java b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/acl/TestRequestContext.java index a0b9bfbc7f81..45272946dac0 100644 --- a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/acl/TestRequestContext.java +++ b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/acl/TestRequestContext.java @@ -22,6 +22,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import org.apache.hadoop.security.UserGroupInformation; import org.junit.jupiter.api.Test; /** @@ -51,4 +52,89 @@ void testSessionPolicy() { builder.setSessionPolicy(policy); assertEquals(policy, builder.build().getSessionPolicy()); } + + @Test + public void testToBuilderWithNoModifications() { + // Create a RequestContext with all fields set + final UserGroupInformation ugi = UserGroupInformation.createRemoteUser("testUser"); + final String host = "testHost"; + final String serviceId = "testServiceId"; + final String ownerName = "testOwner"; + final String sessionPolicy = "{\"Statement\":[{\"Effect\":\"Allow\"}]}"; + final String s3Action = "GetObject"; + + final RequestContext original = RequestContext.newBuilder() + .setHost(host) + .setClientUgi(ugi) + .setServiceId(serviceId) + .setAclType(IAccessAuthorizer.ACLIdentityType.USER) + .setAclRights(IAccessAuthorizer.ACLType.READ) + .setOwnerName(ownerName) + .setRecursiveAccessCheck(true) + .setSessionPolicy(sessionPolicy) + .setS3Action(s3Action) + .build(); + + // Use toBuilder to create a new builder + final RequestContext.Builder builder = original.toBuilder(); + final RequestContext requestCtxFromToBuilder = builder.build(); + + // Verify all fields are preserved + assertEquals(original.getHost(), requestCtxFromToBuilder.getHost(), "Host should be preserved"); + assertNull(original.getIp(), "IP should be preserved"); + assertEquals(original.getClientUgi(), requestCtxFromToBuilder.getClientUgi(), "ClientUgi should be preserved"); + assertEquals(original.getServiceId(), requestCtxFromToBuilder.getServiceId(), "ServiceId should be preserved"); + assertEquals(original.getAclType(), requestCtxFromToBuilder.getAclType(), "AclType should be preserved"); + assertEquals(original.getAclRights(), requestCtxFromToBuilder.getAclRights(), "AclRights should be preserved"); + assertEquals(original.getOwnerName(), requestCtxFromToBuilder.getOwnerName(), "OwnerName should be preserved"); + assertTrue(original.isRecursiveAccessCheck(), "RecursiveAccessCheck should be preserved"); + assertEquals(original.getSessionPolicy(), requestCtxFromToBuilder.getSessionPolicy(), + "SessionPolicy should be preserved"); + assertEquals(original.getS3Action(), requestCtxFromToBuilder.getS3Action(), "S3 action should be preserved"); + } + + @Test + public void testToBuilderWithModifications() { + // Create an original RequestContext + final UserGroupInformation originalUgi = UserGroupInformation.createRemoteUser("user1"); + final RequestContext original = RequestContext.newBuilder() + .setHost("host1") + .setClientUgi(originalUgi) + .setServiceId("service1") + .setAclType(IAccessAuthorizer.ACLIdentityType.USER) + .setAclRights(IAccessAuthorizer.ACLType.READ) + .setOwnerName("owner1") + .setRecursiveAccessCheck(false) + .build(); + + // Use toBuilder and modify some fields + final UserGroupInformation newUgi = UserGroupInformation.createRemoteUser("user2"); + final RequestContext modified = original.toBuilder() + .setHost("host2") + .setClientUgi(newUgi) + .setAclRights(IAccessAuthorizer.ACLType.WRITE) + .setOwnerName("owner2") + .setRecursiveAccessCheck(true) + .setSessionPolicy("{\"Statement\":[]}") + .setS3Action("DeleteObject") + .build(); + + // Verify original is unchanged + assertEquals("host1", original.getHost(), "Original should be unchanged"); + assertEquals(originalUgi, original.getClientUgi(), "Original UGI should be unchanged"); + assertEquals(IAccessAuthorizer.ACLType.READ, original.getAclRights(), "Original ACL rights should be unchanged"); + assertEquals("owner1", original.getOwnerName(), "Original owner name should be unchanged"); + assertFalse(original.isRecursiveAccessCheck(), "Original recursive flag should be unchanged"); + assertNull(original.getSessionPolicy(), "Original session policy should be unchanged"); + assertNull(original.getS3Action(), "Original S3 action should be unchanged"); + + // Verify modified has new values + assertEquals("host2", modified.getHost(), "Modified host should be updated"); + assertEquals(newUgi, modified.getClientUgi(), "Modified UGI should be updated"); + assertEquals(IAccessAuthorizer.ACLType.WRITE, modified.getAclRights(), "Modified ACL rights should be updated"); + assertEquals("owner2", modified.getOwnerName(), "Modified owner should be updated"); + assertTrue(modified.isRecursiveAccessCheck(), "Modified recursive flag should be updated"); + assertEquals("{\"Statement\":[]}", modified.getSessionPolicy(), "Modified session policy should be updated"); + assertEquals("DeleteObject", modified.getS3Action(), "Modified S3 action should be updated"); + } }