Skip to content

Commit 7fae413

Browse files
HADOOP-17261. s3a rename() needs s3:deleteObjectVersion permission (#2303)
Contributed by Steve Loughran.
1 parent 474fa80 commit 7fae413

File tree

3 files changed

+92
-10
lines changed

3 files changed

+92
-10
lines changed

hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/RenameOperation.java

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -215,41 +215,38 @@ private void completeActiveCopies(String reason) throws IOException {
215215
* This method must only be called from the primary thread.
216216
* @param path path to the object
217217
* @param key key of the object.
218-
* @param version object version.
219218
*/
220-
private void queueToDelete(Path path, String key, String version) {
219+
private void queueToDelete(Path path, String key) {
221220
LOG.debug("Queueing to delete {}", path);
222221
pathsToDelete.add(path);
223-
keysToDelete.add(new DeleteObjectsRequest.KeyVersion(key, version));
222+
keysToDelete.add(new DeleteObjectsRequest.KeyVersion(key));
224223
}
225224

226225
/**
227226
* Queue a list of markers for deletion.
228227
* <p></p>
229228
* no-op if the list is empty.
230229
* <p></p>
231-
* See {@link #queueToDelete(Path, String, String)} for
230+
* See {@link #queueToDelete(Path, String)} for
232231
* details on safe use of this method.
233232
*
234233
* @param markersToDelete markers
235234
*/
236235
private void queueToDelete(
237236
List<DirMarkerTracker.Marker> markersToDelete) {
238-
markersToDelete.forEach(m ->
239-
queueToDelete(m));
237+
markersToDelete.forEach(this::queueToDelete);
240238
}
241239

242240
/**
243241
* Queue a single marker for deletion.
244242
* <p></p>
245-
* See {@link #queueToDelete(Path, String, String)} for
243+
* See {@link #queueToDelete(Path, String)} for
246244
* details on safe use of this method.
247245
*
248246
* @param marker markers
249247
*/
250248
private void queueToDelete(final DirMarkerTracker.Marker marker) {
251-
queueToDelete(marker.getPath(), marker.getKey(),
252-
marker.getStatus().getVersionId());
249+
queueToDelete(marker.getPath(), marker.getKey());
253250
}
254251

255252
/**
@@ -418,6 +415,7 @@ protected void recursiveDirectoryRename() throws IOException {
418415
while (iterator.hasNext()) {
419416
// get the next entry in the listing.
420417
S3ALocatedFileStatus child = iterator.next();
418+
LOG.debug("To rename {}", child);
421419
// convert it to an S3 key.
422420
String k = storeContext.pathToKey(child.getPath());
423421
// possibly adding a "/" if it represents directory and it does
@@ -450,7 +448,7 @@ protected void recursiveDirectoryRename() throws IOException {
450448
Path childDestPath = storeContext.keyToPath(newDestKey);
451449

452450
// mark the source file for deletion on a successful copy.
453-
queueToDelete(childSourcePath, key, child.getVersionId());
451+
queueToDelete(childSourcePath, key);
454452
// now begin the single copy
455453
CompletableFuture<Path> copy = initiateCopy(child, key,
456454
childSourcePath, newDestKey, childDestPath);

hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/troubleshooting_s3a.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,45 @@ If the client using [assumed roles](assumed_roles.html), and a policy
423423
is set in `fs.s3a.assumed.role.policy`, then that policy declares
424424
_all_ the rights which the caller has.
425425

426+
### <a name="access_denied_renaming"></a> `AccessDeniedException` in rename, "MultiObjectDeleteException: One or more objects could not be deleted"
427+
428+
429+
```
430+
mv: rename s3a://london/dest to s3a://london/src on
431+
s3a://london/dest:
432+
com.amazonaws.services.s3.model.MultiObjectDeleteException: One or more objects
433+
could not be deleted (Service: null; Status Code: 200; Error Code: null; Request
434+
ID: 5C9018EF245F02C5; S3 Extended Request ID:
435+
5fQ2RVCPF0rdvADRv2XY3U4yb2J0gHRID/4jm1eqCXp7RxpU0dH9DliChYsCUD1aVCFtbwfWJWY=),
436+
S3 Extended Request ID:
437+
5fQ2RVCPF0rdvADRv2XY3U4yb2J0gHRID/4jm1eqCXp7RxpU0dH9DliChYsCUD1aVCFtbwfWJWY=:null:
438+
AccessDenied: dest/file10: Access Denied
439+
```
440+
441+
The S3A connector's emulation of file and directory rename is implemented by copying each file,
442+
then deleting the originals. This delete process is done in batches, by default in a single
443+
"multiple object delete request". If one or more of the objects listed in the request cannot
444+
be deleted, an error is returned in S3 listing which objects were not deleted.
445+
If the cause was "access denied", it is translated into an `AccessDeniedException`.
446+
447+
The rename is halted at this point: files may be present in both the source and destination directories.
448+
Those files which could not be deleted from the source directory will also have been copied
449+
into the destination directory. Files which were successfully deleted from the source
450+
directory will _only_ be in the destination. And files for which the rename operation
451+
had yet to commence -they will only be in the source tree.
452+
453+
The user has to recover from this themselves. Be assured: no data will have been deleted, it
454+
is just that the data may now be scattered across two directories.
455+
Note: this is one reason why any application which tries to atomically commit work
456+
via rename (classic Hadoop output committers, distcp with the `-atomic` option) are
457+
not safe to use with S3. It is not a file system.
458+
459+
For an 'AccessDenied' failure, the root cause is IAM permissions.
460+
The user/role/bucket must have the permission
461+
`s3:DeleteObject` on the source path. It is safest to grant `s3:Delete*` so
462+
that if a future version of the S3A connector supported extra operations
463+
(explicit deletion of versioned files, get/set/delete object tagging, ...),
464+
the client will have the permission to use them.
426465

427466
### <a name="kms_access_denied"></a> `AccessDeniedException` when using SSE-KMS
428467

hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/impl/ITestPartialRenamesDeletes.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import org.apache.hadoop.fs.contract.ContractTestUtils;
4949
import org.apache.hadoop.fs.s3a.AbstractS3ATestBase;
5050
import org.apache.hadoop.fs.s3a.S3AFileSystem;
51+
import org.apache.hadoop.io.IOUtils;
5152
import org.apache.hadoop.util.BlockingThreadPoolExecutorService;
5253
import org.apache.hadoop.util.DurationInfo;
5354

@@ -839,4 +840,48 @@ private static void buildPaths(
839840
public static String filenameOfIndex(final int i) {
840841
return String.format("%s%03d", PREFIX, i);
841842
}
843+
844+
/**
845+
* Verifies that s3:DeleteObjectVersion is not required for rename.
846+
* <p></p>
847+
* See HADOOP-17621.
848+
* <p></p>
849+
* This test will only show a regression if the bucket has versioning
850+
* enabled *and* S3Guard is enabled.
851+
*/
852+
@Test
853+
public void testRenamePermissionRequirements() throws Throwable {
854+
describe("Verify rename() only needs s3:DeleteObject permission");
855+
// close the existing roleFS
856+
IOUtils.cleanupWithLogger(LOG, roleFS);
857+
858+
// create an assumed role config which doesn't have
859+
// s3:DeleteObjectVersion permission, and attempt rename
860+
// and then delete.
861+
Configuration roleConfig = createAssumedRoleConfig();
862+
bindRolePolicyStatements(roleConfig,
863+
STATEMENT_S3GUARD_CLIENT,
864+
STATEMENT_ALLOW_SSE_KMS_RW,
865+
STATEMENT_ALL_BUCKET_READ_ACCESS, // root: r-x
866+
new Statement(Effects.Allow) // dest: rwx
867+
.addActions(S3_PATH_RW_OPERATIONS)
868+
.addResources(directory(basePath)),
869+
new Statement(Effects.Deny)
870+
.addActions(S3_DELETE_OBJECT_VERSION)
871+
.addResources(directory(basePath)));
872+
roleFS = (S3AFileSystem) basePath.getFileSystem(roleConfig);
873+
874+
Path srcDir = new Path(basePath, "src");
875+
Path destDir = new Path(basePath, "dest");
876+
roleFS.mkdirs(srcDir);
877+
878+
// the role FS has everything but that deleteObjectVersion permission, so
879+
// MUST be able to create files
880+
List<Path> createdFiles = createFiles(roleFS, srcDir, dirDepth, fileCount,
881+
dirCount);
882+
roleFS.rename(srcDir, destDir);
883+
roleFS.rename(destDir, srcDir);
884+
roleFS.delete(srcDir, true);
885+
886+
}
842887
}

0 commit comments

Comments
 (0)