Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -848,7 +848,8 @@ private Response createMultipartKey(OzoneVolume volume, OzoneBucket ozoneBucket,
String sourceBucket = result.getLeft();
String sourceKey = result.getRight();
if (S3Owner.hasBucketOwnershipVerificationConditions(getHeaders())) {
String sourceBucketOwner = volume.getBucket(sourceBucket).getOwner();
final String sourceBucketOwner = runWithS3ActionString(
"GetObject", () -> volume.getBucket(sourceBucket).getOwner());
S3Owner.verifyBucketOwnerConditionOnCopyOperation(getHeaders(), sourceBucket, sourceBucketOwner, bucketName,
ozoneBucket.getOwner());
}
Expand Down Expand Up @@ -1061,7 +1062,8 @@ private CopyObjectResponse copyObject(OzoneVolume volume,
final MessageDigest md5Digest = getMD5DigestInstance();

if (S3Owner.hasBucketOwnershipVerificationConditions(getHeaders())) {
String sourceBucketOwner = volume.getBucket(sourceBucket).getOwner();
final String sourceBucketOwner = runWithS3ActionString(
"GetObject", () -> volume.getBucket(sourceBucket).getOwner());
// The destBucket owner has already been checked in the caller method
S3Owner.verifyBucketOwnerConditionOnCopyOperation(getHeaders(), sourceBucket, sourceBucketOwner, null, null);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/*
* 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.
*/

package org.apache.hadoop.ozone.s3.endpoint;

import static org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.put;
import static org.apache.hadoop.ozone.s3.util.S3Consts.COPY_SOURCE_HEADER;
import static org.apache.hadoop.ozone.s3.util.S3Consts.EXPECTED_BUCKET_OWNER_HEADER;
import static org.apache.hadoop.ozone.s3.util.S3Consts.EXPECTED_SOURCE_BUCKET_OWNER_HEADER;
import static org.apache.hadoop.ozone.s3.util.S3Consts.STORAGE_CLASS_HEADER;
import static org.apache.hadoop.ozone.s3.util.S3Consts.UNSIGNED_PAYLOAD;
import static org.apache.hadoop.ozone.s3.util.S3Consts.X_AMZ_CONTENT_SHA256;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.util.concurrent.atomic.AtomicReference;
import javax.ws.rs.core.HttpHeaders;
import org.apache.hadoop.hdds.conf.OzoneConfiguration;
import org.apache.hadoop.ozone.client.ObjectStore;
import org.apache.hadoop.ozone.client.OzoneBucket;
import org.apache.hadoop.ozone.client.OzoneClient;
import org.apache.hadoop.ozone.client.OzoneVolume;
import org.apache.hadoop.ozone.client.protocol.ClientProtocol;
import org.apache.hadoop.ozone.om.exceptions.OMException;
import org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes;
import org.apache.hadoop.ozone.om.protocol.S3Auth;
import org.apache.hadoop.ozone.s3.signature.SignatureInfo;
import org.junit.jupiter.api.Test;

/**
* Verifies bucket-owner-condition verification (source bucket owner lookup) runs under the correct IAM S3 action
* string for copy-style operations.
*/
public class TestS3ActionOverrideForOwnerVerification {

private static final String DEST_BUCKET = "dest-bucket";
private static final String DEST_KEY = "dest-key";
private static final String SOURCE_BUCKET = "source-bucket";
private static final String SOURCE_KEY = "source-key";
private static final String SOURCE_OWNER = "source-owner";
private static final String DEST_OWNER = "dest-owner";

@Test
public void testUploadPartCopyUsesGetObjectActionForSourceBucketOwnerLookup() throws Exception {
final AtomicReference<String> actionAtSourceBucketOwnerLookup = new AtomicReference<>();
final ObjectEndpoint endpoint = newEndpoint(actionAtSourceBucketOwnerLookup);

// Trigger UploadPartCopy (MPU part upload with copy header).
final String uploadId = "upload-id";
assertThrows(Exception.class, () -> put(endpoint, DEST_BUCKET, DEST_KEY, 1, uploadId, ""));

assertEquals("GetObject", actionAtSourceBucketOwnerLookup.get());
}

@Test
public void testCopyObjectUsesGetObjectActionForSourceBucketOwnerLookup() throws Exception {
final AtomicReference<String> actionAtSourceBucketOwnerLookup = new AtomicReference<>();
final ObjectEndpoint endpoint = newEndpoint(actionAtSourceBucketOwnerLookup);

// Trigger CopyObject (PUT with copy header, no upload ID).
assertThrows(Exception.class, () -> put(endpoint, DEST_BUCKET, DEST_KEY, ""));

assertEquals("GetObject", actionAtSourceBucketOwnerLookup.get());
}

private static ObjectEndpoint newEndpoint(AtomicReference<String> actionAtSourceBucketOwnerLookup) throws Exception {
final HttpHeaders headers = mock(HttpHeaders.class);
when(headers.getHeaderString(X_AMZ_CONTENT_SHA256)).thenReturn(UNSIGNED_PAYLOAD);
when(headers.getHeaderString(STORAGE_CLASS_HEADER)).thenReturn("STANDARD");
when(headers.getHeaderString(COPY_SOURCE_HEADER)).thenReturn(SOURCE_BUCKET + "/" + SOURCE_KEY);
when(headers.getHeaderString(EXPECTED_SOURCE_BUCKET_OWNER_HEADER)).thenReturn(SOURCE_OWNER);
when(headers.getHeaderString(EXPECTED_BUCKET_OWNER_HEADER)).thenReturn(DEST_OWNER);

final SignatureInfo signatureInfo = mock(SignatureInfo.class);
when(signatureInfo.isSignPayload()).thenReturn(true);
when(signatureInfo.getStringToSign()).thenReturn("string-to-sign");
when(signatureInfo.getSignature()).thenReturn("signature");
when(signatureInfo.getAwsAccessId()).thenReturn("access-id");
when(signatureInfo.getSessionToken()).thenReturn(null);

final OzoneClient client = mock(OzoneClient.class);
final ObjectStore objectStore = mock(ObjectStore.class);
final ClientProtocol clientProtocol = mock(ClientProtocol.class);
final OzoneVolume volume = mock(OzoneVolume.class);
final OzoneBucket destBucket = mock(OzoneBucket.class);
final OzoneBucket sourceBucket = mock(OzoneBucket.class);

final AtomicReference<S3Auth> s3AuthRef = new AtomicReference<>();
doAnswer(invocationOnMock -> {
s3AuthRef.set(invocationOnMock.getArgument(0));
return null;
}).when(clientProtocol).setThreadLocalS3Auth(any(S3Auth.class));
doNothing().when(clientProtocol).setIsS3Request(true);

when(client.getObjectStore()).thenReturn(objectStore);
when(client.getProxy()).thenReturn(clientProtocol);
when(objectStore.getClientProxy()).thenReturn(clientProtocol);
when(objectStore.getS3Volume()).thenReturn(volume);

when(volume.getName()).thenReturn("s3Volume");

when(destBucket.getName()).thenReturn(DEST_BUCKET);
when(destBucket.getOwner()).thenReturn(DEST_OWNER);
when(volume.getBucket(DEST_BUCKET)).thenReturn(destBucket);

when(sourceBucket.getOwner()).thenAnswer(invocationOnMock -> {
final S3Auth s3Auth = s3AuthRef.get();
assertNotNull(s3Auth, "S3Auth must be initialized before owner lookup");
actionAtSourceBucketOwnerLookup.set(s3Auth.getS3Action());
return SOURCE_OWNER;
});
when(volume.getBucket(SOURCE_BUCKET)).thenReturn(sourceBucket);

// Stop the request after the source-bucket owner check, without needing to set up full copy behavior.
when(clientProtocol.getKeyDetails(anyString(), eq(SOURCE_BUCKET), eq(SOURCE_KEY)))
.thenThrow(new OMException("stop-after-owner-check", ResultCodes.KEY_NOT_FOUND));

final OzoneConfiguration conf = new OzoneConfiguration();
return EndpointBuilder.newObjectEndpointBuilder()
.setClient(client)
.setConfig(conf)
.setHeaders(headers)
.setSignatureInfo(signatureInfo)
.build();
}
}