@@ -684,15 +684,331 @@ public void cleanup() {
684684 }
685685
686686 /**
687- * Created this class to support `setAcl` method for {@link LocalFileSystem} for unit testing since LocalFileSystem
688- * doesn't provide any implementation for `setAcl` method
687+ * Test that setPathPermission with removeExistingAcls=true removes existing ACLs before adding new ones.
688+ */
689+ @ Test
690+ public void testSetPathPermissionRemovesExistingAcls () throws Exception {
691+ Path testPath = new Path (testTempPath , "testAclReplacement" );
692+ fs .mkdirs (testPath );
693+ FileStatus fileStatus = fs .getFileStatus (testPath );
694+ testPath = fileStatus .getPath ();
695+
696+ // Simulate existing ACLs on target directory
697+ List <AclEntry > existingAcls = Lists .newArrayList (
698+ AclEntry .parseAclEntry ("user:olduser:rwx" , true ),
699+ AclEntry .parseAclEntry ("user:anotheruser:r-x" , true )
700+ );
701+ fs .modifyAclEntries (testPath , existingAcls );
702+ Assert .assertEquals (existingAcls , fs .getAclEntries (testPath ));
703+
704+ // Set new ACLs with removeExistingAcls=true
705+ List <AclEntry > newAcls = Lists .newArrayList (
706+ AclEntry .parseAclEntry ("user:newuser:rw-" , true )
707+ );
708+ OwnerAndPermission ownerAndPermission = new OwnerAndPermission (null , null ,
709+ FsPermission .valueOf ("drwxr-xr-x" ), newAcls );
710+
711+ FileAwareInputStreamDataWriter .setPathPermission (fs , fileStatus , ownerAndPermission , false , true );
712+
713+ // Verify old ACLs were removed and only new ACLs exist
714+ List <AclEntry > resultAcls = fs .getAclEntries (testPath );
715+ Assert .assertEquals (1 , resultAcls .size ());
716+ Assert .assertEquals (newAcls , resultAcls );
717+ Assert .assertTrue (fs .wasRemoveAclCalled (testPath ));
718+ }
719+
720+ /**
721+ * Test that setPathPermission with removeExistingAcls=false does NOT remove existing ACLs.
722+ * This tests the default behavior where ACLs are added without removal.
723+ */
724+ @ Test
725+ public void testSetPathPermissionDoesNotRemoveExistingAcls () throws Exception {
726+ Path testPath = new Path (testTempPath , "testAclAddition" );
727+ fs .mkdirs (testPath );
728+ FileStatus fileStatus = fs .getFileStatus (testPath );
729+ testPath = fileStatus .getPath ();
730+
731+ // Set initial ACLs
732+ List <AclEntry > initialAcls = Lists .newArrayList (
733+ AclEntry .parseAclEntry ("user:existinguser:rwx" , true )
734+ );
735+ fs .modifyAclEntries (testPath , initialAcls );
736+
737+ // Add new ACLs with removeExistingAcls=false
738+ List <AclEntry > newAcls = Lists .newArrayList (
739+ AclEntry .parseAclEntry ("user:newuser:r-x" , true )
740+ );
741+ OwnerAndPermission ownerAndPermission = new OwnerAndPermission (null , null ,
742+ FsPermission .valueOf ("drwxr-xr-x" ), newAcls );
743+
744+ FileAwareInputStreamDataWriter .setPathPermission (fs , fileStatus , ownerAndPermission , false , false );
745+
746+ // Verify removeAcl was NOT called
747+ Assert .assertFalse (fs .wasRemoveAclCalled (testPath ));
748+ // Verify new ACLs were added
749+ List <AclEntry > resultAcls = fs .getAclEntries (testPath );
750+ Assert .assertNotNull (resultAcls );
751+ // Should have old as well as new acls
752+ initialAcls .addAll (newAcls );
753+ Assert .assertEquals (initialAcls , resultAcls );
754+ }
755+
756+ /**
757+ * Test that setPathPermission with default parameter (no removeExistingAcls) defaults to false.
758+ */
759+ @ Test
760+ public void testSetPathPermissionDefaultBehaviorDoesNotRemoveAcls () throws Exception {
761+ Path testPath = new Path (testTempPath , "testDefaultBehavior" );
762+ fs .mkdirs (testPath );
763+ FileStatus fileStatus = fs .getFileStatus (testPath );
764+ testPath = fileStatus .getPath ();
765+
766+ List <AclEntry > acls = Lists .newArrayList (
767+ AclEntry .parseAclEntry ("user:testuser:rwx" , true )
768+ );
769+ OwnerAndPermission ownerAndPermission = new OwnerAndPermission (null , null ,
770+ FsPermission .valueOf ("drwxr-xr-x" ), acls );
771+
772+ // Call without removeExistingAcls parameter (should default to false)
773+ FileAwareInputStreamDataWriter .setPathPermission (fs , fileStatus , ownerAndPermission , false );
774+
775+ // Verify removeAcl was NOT called (default is false)
776+ Assert .assertFalse (fs .wasRemoveAclCalled (testPath ));
777+ // Verify ACLs were set (if filesystem supports it)
778+ List <AclEntry > resultAcls = fs .getAclEntries (testPath );
779+ if (resultAcls != null ) {
780+ Assert .assertEquals (acls , resultAcls );
781+ }
782+ }
783+
784+ /**
785+ * Test that setPathPermission works correctly for files (not just directories).
786+ */
787+ @ Test
788+ public void testSetPathPermissionForFile () throws Exception {
789+ Path testPath = new Path (testTempPath , "testFile.txt" );
790+ fs .create (testPath ).close ();
791+ FileStatus fileStatus = fs .getFileStatus (testPath );
792+ testPath = fileStatus .getPath ();
793+
794+ // Set initial ACLs on file
795+ List <AclEntry > initialAcls = Lists .newArrayList (
796+ AclEntry .parseAclEntry ("user:olduser:rwx" , true )
797+ );
798+ fs .modifyAclEntries (testPath , initialAcls );
799+
800+ // Replace with new ACLs
801+ List <AclEntry > newAcls = Lists .newArrayList (
802+ AclEntry .parseAclEntry ("user:newuser:r--" , true )
803+ );
804+ OwnerAndPermission ownerAndPermission = new OwnerAndPermission (null , null ,
805+ FsPermission .valueOf ("-rw-r--r--" ), newAcls );
806+
807+ FileAwareInputStreamDataWriter .setPathPermission (fs , fileStatus , ownerAndPermission , false , true );
808+
809+ // Verify ACLs were replaced
810+ Assert .assertTrue (fs .wasRemoveAclCalled (testPath ));
811+ List <AclEntry > resultAcls = fs .getAclEntries (testPath );
812+ Assert .assertNotNull (resultAcls );
813+ Assert .assertEquals (newAcls , resultAcls );
814+ }
815+
816+ /**
817+ * Test that setPathPermission handles multiple ACL entries correctly.
818+ */
819+ @ Test
820+ public void testSetPathPermissionWithMultipleAclEntries () throws Exception {
821+ Path testPath = new Path (testTempPath , "testMultipleAcls" );
822+ fs .mkdirs (testPath );
823+ FileStatus fileStatus = fs .getFileStatus (testPath );
824+ testPath = fileStatus .getPath ();
825+
826+ // Create multiple ACL entries
827+ List <AclEntry > multipleAcls = Lists .newArrayList (
828+ AclEntry .parseAclEntry ("user:alice:rwx" , true ),
829+ AclEntry .parseAclEntry ("user:bob:r-x" , true ),
830+ AclEntry .parseAclEntry ("user:charlie:rw-" , true ),
831+ AclEntry .parseAclEntry ("group:developers:rwx" , true )
832+ );
833+ OwnerAndPermission ownerAndPermission = new OwnerAndPermission (null , null ,
834+ FsPermission .valueOf ("drwxr-xr-x" ), multipleAcls );
835+
836+ FileAwareInputStreamDataWriter .setPathPermission (fs , fileStatus , ownerAndPermission , false , true );
837+
838+ // Verify all ACLs were set
839+ Assert .assertTrue (fs .wasRemoveAclCalled (testPath ));
840+ List <AclEntry > resultAcls = fs .getAclEntries (testPath );
841+ Assert .assertNotNull (resultAcls );
842+ Assert .assertEquals (4 , resultAcls .size ());
843+ Assert .assertEquals (multipleAcls , resultAcls );
844+ }
845+
846+ /**
847+ * Test that setPathPermission works with directory execute bit handling.
848+ * Directories without owner execute bit get it added automatically.
849+ */
850+ @ Test
851+ public void testSetPathPermissionWithDirectoryExecuteBit () throws Exception {
852+ Path testPath = new Path (testTempPath , "testDirExecuteBit" );
853+ fs .mkdirs (testPath );
854+ FileStatus fileStatus = fs .getFileStatus (testPath );
855+ testPath = fileStatus .getPath ();
856+
857+ List <AclEntry > acls = Lists .newArrayList (
858+ AclEntry .parseAclEntry ("user:testuser:rw-" , true )
859+ );
860+ // Directory permission without owner execute bit
861+ OwnerAndPermission ownerAndPermission = new OwnerAndPermission (null , null ,
862+ FsPermission .valueOf ("drw-rw-rw-" ), acls );
863+
864+ FileAwareInputStreamDataWriter .setPathPermission (fs , fileStatus , ownerAndPermission , false , true );
865+
866+ // Verify ACLs were set even with execute bit handling
867+ Assert .assertTrue (fs .wasRemoveAclCalled (testPath ));
868+ List <AclEntry > resultAcls = fs .getAclEntries (testPath );
869+ Assert .assertNotNull (resultAcls );
870+ Assert .assertEquals (acls , resultAcls );
871+ }
872+
873+ /**
874+ * Test that setPathPermission correctly handles the scenario where a directory
875+ * has existing ACLs that need to be completely replaced.
876+ */
877+ @ Test
878+ public void testSetPathPermissionReplacesAllExistingAcls () throws Exception {
879+ Path testPath = new Path (testTempPath , "testCompleteReplacement" );
880+ fs .mkdirs (testPath );
881+ FileStatus fileStatus = fs .getFileStatus (testPath );
882+ testPath = fileStatus .getPath ();
883+
884+ // Set multiple existing ACLs
885+ List <AclEntry > existingAcls = Lists .newArrayList (
886+ AclEntry .parseAclEntry ("user:user1:rwx" , true ),
887+ AclEntry .parseAclEntry ("user:user2:r-x" , true ),
888+ AclEntry .parseAclEntry ("group:group1:rwx" , true )
889+ );
890+ fs .modifyAclEntries (testPath , existingAcls );
891+
892+ // Replace with completely different ACLs
893+ List <AclEntry > newAcls = Lists .newArrayList (
894+ AclEntry .parseAclEntry ("user:differentuser:r--" , true )
895+ );
896+ OwnerAndPermission ownerAndPermission = new OwnerAndPermission (null , null ,
897+ FsPermission .valueOf ("drwxr-xr-x" ), newAcls );
898+
899+ FileAwareInputStreamDataWriter .setPathPermission (fs , fileStatus , ownerAndPermission , false , true );
900+
901+ // Verify complete replacement
902+ Assert .assertTrue (fs .wasRemoveAclCalled (testPath ));
903+ List <AclEntry > resultAcls = fs .getAclEntries (testPath );
904+ Assert .assertNotNull (resultAcls );
905+ Assert .assertEquals (1 , resultAcls .size ());
906+ Assert .assertEquals (newAcls , resultAcls );
907+ // Verify none of the old ACLs exist
908+ for (AclEntry oldAcl : existingAcls ) {
909+ Assert .assertFalse (resultAcls .contains (oldAcl ));
910+ }
911+ }
912+
913+ /**
914+ * Test that permissions are still set correctly when ACL operations are performed.
915+ */
916+ @ Test
917+ public void testSetPathPermissionSetsPermissionsCorrectly () throws Exception {
918+ Path testPath = new Path (testTempPath , "testPermissions" );
919+ fs .mkdirs (testPath );
920+ FileStatus fileStatus = fs .getFileStatus (testPath );
921+ testPath = fileStatus .getPath ();
922+
923+ List <AclEntry > acls = Lists .newArrayList (
924+ AclEntry .parseAclEntry ("user:testuser:rwx" , true )
925+ );
926+ FsPermission permission = FsPermission .valueOf ("drwxr-x---" );
927+ OwnerAndPermission ownerAndPermission = new OwnerAndPermission (null , null , permission , acls );
928+
929+ FileAwareInputStreamDataWriter .setPathPermission (fs , fileStatus , ownerAndPermission , false , true );
930+
931+ // Verify ACLs were set
932+ Assert .assertTrue (fs .wasRemoveAclCalled (testPath ));
933+ Assert .assertNotNull (fs .getAclEntries (testPath ));
934+
935+ // Verify permissions were also set (LocalFileSystem adds execute bit for directories)
936+ FileStatus updatedStatus = fs .getFileStatus (testPath );
937+ Assert .assertTrue (updatedStatus .getPermission ().getUserAction ().implies (FsAction .EXECUTE ));
938+ }
939+
940+ /**
941+ * Test that when destination has ACLs but source has no ACLs (empty list),
942+ * all ACLs are removed from destination when removeExistingAcls=true.
943+ * This ensures destination matches source exactly (no ACLs).
944+ */
945+ @ Test
946+ public void testSetPathPermissionRemovesDestinationAclsWhenSourceHasNoAcls () throws Exception {
947+ Path testPath = new Path (testTempPath , "testEmptySourceAcls" );
948+ fs .mkdirs (testPath );
949+ FileStatus fileStatus = fs .getFileStatus (testPath );
950+ testPath = fileStatus .getPath ();
951+
952+ // Destination has existing ACLs
953+ List <AclEntry > existingAcls = Lists .newArrayList (
954+ AclEntry .parseAclEntry ("user:user1:rwx" , true ),
955+ AclEntry .parseAclEntry ("user:user2:r-x" , true ),
956+ AclEntry .parseAclEntry ("group:group1:rw-" , true )
957+ );
958+ fs .modifyAclEntries (testPath , existingAcls );
959+ Assert .assertEquals (existingAcls , fs .getAclEntries (testPath ));
960+
961+ // Source has no ACLs (empty list)
962+ List <AclEntry > emptyAcls = Lists .newArrayList ();
963+ OwnerAndPermission ownerAndPermission = new OwnerAndPermission (null , null ,
964+ FsPermission .valueOf ("drwxr-xr-x" ), emptyAcls );
965+
966+ // Call with removeExistingAcls=true
967+ FileAwareInputStreamDataWriter .setPathPermission (fs , fileStatus , ownerAndPermission , false , true );
968+
969+ // Verify removeAcl was called to clear all ACLs
970+ Assert .assertTrue (fs .wasRemoveAclCalled (testPath ));
971+ // Verify no ACLs exist on destination (matches source which has no ACLs)
972+ List <AclEntry > resultAcls = fs .getAclEntries (testPath );
973+ Assert .assertNull (resultAcls ,
974+ "Destination should have no ACLs when source has no ACLs and removeExistingAcls=true" );
975+ }
976+
977+ /**
978+ * Enhanced TestLocalFileSystem to support ACL testing with tracking of removeAcl calls.
689979 */
690980 protected class TestLocalFileSystem extends LocalFileSystem {
691981 private final ConcurrentHashMap <Path , List <AclEntry >> pathToAclEntries = new ConcurrentHashMap <>();
982+ private final ConcurrentHashMap <Path , Boolean > removeAclCalls = new ConcurrentHashMap <>();
983+
692984 @ Override
693985 public void modifyAclEntries (Path path , List <AclEntry > aclEntries ) {
694- pathToAclEntries .put (path , aclEntries );
986+ // Use computeIfAbsent to reflect true behavior: add to existing ACLs, don't replace
987+ pathToAclEntries .computeIfAbsent (path , k -> Lists .newArrayList ()).addAll (aclEntries );
988+ }
989+
990+ @ Override
991+ public void removeAcl (Path path ) {
992+ removeAclCalls .put (path , true );
993+ pathToAclEntries .remove (path );
994+ }
995+
996+ /**
997+ * Get ACL entries for a given path.
998+ * @return List of ACL entries, or null if no ACLs are set
999+ */
1000+ public List <AclEntry > getAclEntries (Path path ) {
1001+ return pathToAclEntries .get (path );
6951002 }
1003+
1004+ /**
1005+ * Check if removeAcl was called for a given path.
1006+ * @return true if removeAcl was called, false otherwise
1007+ */
1008+ public boolean wasRemoveAclCalled (Path path ) {
1009+ return removeAclCalls .getOrDefault (path , false );
1010+ }
1011+
6961012 public ImmutableMap <Path , List <AclEntry >> getPathToAclEntries () {
6971013 return ImmutableMap .copyOf (pathToAclEntries );
6981014 }
0 commit comments