Skip to content

Commit 858f706

Browse files
[GOBBLIN-2234] Fix removal of existing ACLs issue for path existing in destination (#4149)
* Fix removal of existing ACL issue for path existing in destination
1 parent c12e321 commit 858f706

File tree

3 files changed

+343
-4
lines changed

3 files changed

+343
-4
lines changed

gobblin-data-management/src/main/java/org/apache/gobblin/data/management/copy/publisher/CopyDataPublisher.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,8 @@ private void preserveFileAttrInPublisher(CopyableFile copyableFile) throws IOExc
203203
FileStatus dstFile = this.fs.getFileStatus(copyableFile.getDestination());
204204
// User specifically try to copy dir metadata, so we change the group and permissions on destination even when the dir already existed
205205
log.info("Setting destination directory {} owner and permission to {}", dstFile.getPath(), copyableFile.getDestinationOwnerAndPermission().getFsPermission());
206-
FileAwareInputStreamDataWriter.setPathPermission(this.fs, dstFile, copyableFile.getDestinationOwnerAndPermission(), this.shouldFailWhenPermissionsFail);
206+
// Remove existing ACLs to ensure target matches source exactly (avoid ACL accumulation)
207+
FileAwareInputStreamDataWriter.setPathPermission(this.fs, dstFile, copyableFile.getDestinationOwnerAndPermission(), this.shouldFailWhenPermissionsFail, true);
207208
}
208209
if (preserveDirModTime || copyableFile.getFileStatus().isFile()) {
209210
// Preserving File ModTime, and set the access time to an initializing value when ModTime is declared to be preserved.

gobblin-data-management/src/main/java/org/apache/gobblin/data/management/copy/writer/FileAwareInputStreamDataWriter.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,10 +381,32 @@ public static Path getOutputDir(State state) {
381381
*/
382382
public static void setPathPermission(FileSystem fs, FileStatus file, OwnerAndPermission ownerAndPermission,
383383
boolean requirePermissionSetForSuccess) throws IOException {
384+
setPathPermission(fs, file, ownerAndPermission, requirePermissionSetForSuccess, false);
385+
}
386+
387+
/**
388+
* Sets the {@link FsPermission}, owner, group for the path passed. It uses `requirePermissionSetForSuccess` param
389+
* to determine whether an exception will be thrown or only error log printed in the case of failure.
390+
* @param requirePermissionSetForSuccess if true then throw exception, otherwise log error message and continue when
391+
* operations cannot be executed.
392+
* @param removeExistingAcls if true, removes existing ACLs before setting new ones to avoid accumulation.
393+
* This should be true when setting permissions on existing target files/directories
394+
* to ensure ACLs match the source exactly.
395+
*/
396+
public static void setPathPermission(FileSystem fs, FileStatus file, OwnerAndPermission ownerAndPermission,
397+
boolean requirePermissionSetForSuccess, boolean removeExistingAcls) throws IOException {
384398

385399
Path path = file.getPath();
386400
OwnerAndPermission targetOwnerAndPermission = setOwnerExecuteBitIfDirectory(file, ownerAndPermission);
387401
try {
402+
// Handle ACLs
403+
// Remove existing ACLs first if requested, even if source has no ACLs
404+
// This ensures destination matches source exactly (no ACLs if source has none)
405+
if (removeExistingAcls) {
406+
fs.removeAcl(path);
407+
}
408+
409+
// Set new ACLs if source has any
388410
if (!targetOwnerAndPermission.getAclEntries().isEmpty()) {
389411
// use modify acls instead of setAcl since latter requires all three acl entry types: user, group and others
390412
// while overwriting the acls for a given path. If anyone is absent it fails acl transformation validation.

gobblin-data-management/src/test/java/org/apache/gobblin/data/management/copy/writer/FileAwareInputStreamDataWriterTest.java

Lines changed: 319 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)