diff --git a/hadoop-hdds/common/src/main/resources/ozone-default.xml b/hadoop-hdds/common/src/main/resources/ozone-default.xml index f08e03c03eb..38a660b4a4f 100644 --- a/hadoop-hdds/common/src/main/resources/ozone-default.xml +++ b/hadoop-hdds/common/src/main/resources/ozone-default.xml @@ -1757,6 +1757,14 @@ service principal. + + ozone.s3g.readonly + false + OZONE, S3GATEWAY + Whether the S3Gateway blocks PUT/POST/DELETE methods or not. + Mostly used for system maintenance. + + ozone.om.save.metrics.interval 5m diff --git a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/OzoneClient.java b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/OzoneClient.java index 3a63a593469..4d156b198fe 100644 --- a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/OzoneClient.java +++ b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/OzoneClient.java @@ -92,11 +92,19 @@ public OzoneClient(ConfigurationSource conf, ClientProtocol proxy) { @VisibleForTesting protected OzoneClient(ObjectStore objectStore, ClientProtocol clientProtocol) { + this(objectStore, clientProtocol, new OzoneConfiguration()); + } + + @VisibleForTesting + protected OzoneClient(ObjectStore objectStore, + ClientProtocol clientProtocol, + OzoneConfiguration conf) { this.objectStore = objectStore; this.proxy = clientProtocol; // For the unit test - this.conf = new OzoneConfiguration(); + this.conf = conf; } + /** * Returns the object store associated with the Ozone Cluster. * @return ObjectStore diff --git a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/Gateway.java b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/Gateway.java index b8153f0209d..f4de110a4c2 100644 --- a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/Gateway.java +++ b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/Gateway.java @@ -41,6 +41,7 @@ import static org.apache.hadoop.ozone.conf.OzoneServiceConfig.DEFAULT_SHUTDOWN_HOOK_PRIORITY; import static org.apache.hadoop.ozone.s3.S3GatewayConfigKeys.OZONE_S3G_KERBEROS_KEYTAB_FILE_KEY; import static org.apache.hadoop.ozone.s3.S3GatewayConfigKeys.OZONE_S3G_KERBEROS_PRINCIPAL_KEY; +import static org.apache.hadoop.ozone.s3.S3GatewayConfigKeys.OZONE_S3G_READONLY; /** * This class is used to start/stop S3 compatible rest server. @@ -92,6 +93,8 @@ public void start() throws IOException { LOG.info("Starting Ozone S3 gateway"); HddsServerUtil.initializeMetrics(ozoneConfiguration, "S3Gateway"); + LOG.info("S3 Gateway Readonly mode: {}={}", OZONE_S3G_READONLY, + ozoneConfiguration.get(OZONE_S3G_READONLY)); httpServer.start(); } diff --git a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/S3GatewayConfigKeys.java b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/S3GatewayConfigKeys.java index af8575300e1..56d1162c7cc 100644 --- a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/S3GatewayConfigKeys.java +++ b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/S3GatewayConfigKeys.java @@ -64,7 +64,8 @@ public final class S3GatewayConfigKeys { public static final String OZONE_S3G_KERBEROS_PRINCIPAL_KEY = "ozone.s3g.kerberos.principal"; - + public static final String OZONE_S3G_READONLY = "ozone.s3g.readonly"; + public static final boolean OZONE_S3G_READONLY_DEFAULT = false; /** * Never constructed. */ diff --git a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketEndpoint.java b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketEndpoint.java index 6bbd1a5b30a..3f1bc561058 100644 --- a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketEndpoint.java +++ b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketEndpoint.java @@ -39,6 +39,7 @@ import org.apache.hadoop.ozone.s3.util.ContinueToken; import org.apache.hadoop.ozone.s3.util.S3StorageType; import org.apache.hadoop.ozone.security.acl.IAccessAuthorizer; + import org.apache.http.HttpStatus; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -64,6 +65,7 @@ import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Optional; import java.util.Set; import static org.apache.hadoop.ozone.OzoneAcl.AclScope.ACCESS; @@ -243,6 +245,12 @@ public Response put(@PathParam("bucket") String bucketName, InputStream body) throws IOException, OS3Exception { S3GAction s3GAction = S3GAction.CREATE_BUCKET; + // Check if the S3Gateway status is readonly + Optional checkResult = checkIfReadonly(); + if (checkResult.isPresent()) { + return checkResult.get(); + } + try { if (aclMarker != null) { s3GAction = S3GAction.PUT_ACL; @@ -347,6 +355,12 @@ public Response delete(@PathParam("bucket") String bucketName) throws IOException, OS3Exception { S3GAction s3GAction = S3GAction.DELETE_BUCKET; + // Check if the S3Gateway status is readonly + Optional checkResult = checkIfReadonly(); + if (checkResult.isPresent()) { + return checkResult.get(); + } + try { deleteS3Bucket(bucketName); } catch (OMException ex) { @@ -390,9 +404,18 @@ public MultiDeleteResponse multiDelete(@PathParam("bucket") String bucketName, MultiDeleteRequest request) throws OS3Exception, IOException { S3GAction s3GAction = S3GAction.MULTI_DELETE; + MultiDeleteResponse result = new MultiDeleteResponse(); + + // Check if the S3Gateway status is readonly + Optional checkResult = checkIfReadonly(); + if (checkResult.isPresent()) { + Response res = checkResult.get(); + result.addError(new Error("", res.getStatusInfo().getReasonPhrase(), + "The S3Gateway is in read-only mode.")); + } OzoneBucket bucket = getBucket(bucketName); - MultiDeleteResponse result = new MultiDeleteResponse(); + if (request.getObjects() != null) { for (DeleteObject keyToDelete : request.getObjects()) { try { diff --git a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/EndpointBase.java b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/EndpointBase.java index 56a187fd004..cfdc357979e 100644 --- a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/EndpointBase.java +++ b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/EndpointBase.java @@ -24,14 +24,15 @@ import javax.ws.rs.container.ContainerRequestContext; import javax.ws.rs.core.Context; import java.io.IOException; -import java.util.Set; -import java.util.HashSet; import java.util.Arrays; -import java.util.Map; +import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.List; -import java.util.Collections; +import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; @@ -51,6 +52,7 @@ 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.S3GatewayConfigKeys; import org.apache.hadoop.ozone.s3.exception.OS3Exception; import org.apache.hadoop.ozone.s3.exception.S3ErrorTable; @@ -58,6 +60,7 @@ import org.apache.hadoop.ozone.s3.metrics.S3GatewayMetrics; import org.apache.hadoop.ozone.s3.util.AuditUtils; +import org.apache.http.HttpStatus; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -380,4 +383,14 @@ protected boolean isAccessDenied(OMException ex) { || result == ResultCodes.INVALID_TOKEN; } + protected Optional checkIfReadonly() { + // Check if the S3Gateway is in read-only mode or not. + if (getClient().getConfiguration().getBoolean( + S3GatewayConfigKeys.OZONE_S3G_READONLY, + S3GatewayConfigKeys.OZONE_S3G_READONLY_DEFAULT)) { + return Optional.of(Response.status(HttpStatus.SC_METHOD_NOT_ALLOWED). + header("Allow", "GET,HEAD").build()); + } + return Optional.empty(); + } } diff --git a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectEndpoint.java b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectEndpoint.java index 488dc9ce492..78b6ac30352 100644 --- a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectEndpoint.java +++ b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectEndpoint.java @@ -48,9 +48,10 @@ import java.time.Instant; import java.time.ZoneId; import java.time.ZonedDateTime; -import java.util.Map; import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import java.util.Optional; import java.util.OptionalLong; import org.apache.commons.lang3.StringUtils; @@ -175,6 +176,12 @@ public Response put( @QueryParam("uploadId") @DefaultValue("") String uploadID, InputStream body) throws IOException, OS3Exception { + // Check if the S3Gateway status is readonly + Optional checkResult = checkIfReadonly(); + if (checkResult.isPresent()) { + return checkResult.get(); + } + S3GAction s3GAction = S3GAction.CREATE_KEY; boolean auditSuccess = true; @@ -527,6 +534,12 @@ public Response delete( @QueryParam("uploadId") @DefaultValue("") String uploadId) throws IOException, OS3Exception { + // Check if the S3Gateway status is readonly + Optional checkResult = checkIfReadonly(); + if (checkResult.isPresent()) { + return checkResult.get(); + } + S3GAction s3GAction = S3GAction.DELETE_KEY; try { @@ -591,6 +604,13 @@ public Response initializeMultipartUpload( @PathParam("path") String key ) throws IOException, OS3Exception { + + // Check if the S3Gateway status is readonly + Optional checkResult = checkIfReadonly(); + if (checkResult.isPresent()) { + return checkResult.get(); + } + S3GAction s3GAction = S3GAction.INIT_MULTIPART_UPLOAD; try { @@ -658,6 +678,13 @@ public Response completeMultipartUpload(@PathParam("bucket") String bucket, @QueryParam("uploadId") @DefaultValue("") String uploadID, CompleteMultipartUploadRequest multipartUploadRequest) throws IOException, OS3Exception { + + // Check if the S3Gateway status is readonly + Optional checkResult = checkIfReadonly(); + if (checkResult.isPresent()) { + return checkResult.get(); + } + S3GAction s3GAction = S3GAction.COMPLETE_MULTIPART_UPLOAD; OzoneVolume volume = getVolume(); // Using LinkedHashMap to preserve ordering of parts list. diff --git a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/OzoneClientStub.java b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/OzoneClientStub.java index 64f515060b4..84198b9e035 100644 --- a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/OzoneClientStub.java +++ b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/OzoneClientStub.java @@ -19,6 +19,8 @@ */ package org.apache.hadoop.ozone.client; +import org.apache.hadoop.hdds.conf.OzoneConfiguration; + /** * In-memory OzoneClient for testing. */ @@ -31,6 +33,11 @@ public OzoneClientStub(ObjectStoreStub objectStoreStub) { super(objectStoreStub, new ClientProtocolStub(objectStoreStub)); } + public OzoneClientStub(ObjectStoreStub objectStoreStub, + OzoneConfiguration conf) { + super(objectStoreStub, new ClientProtocolStub(objectStoreStub), conf); + } + @Override public void close() { //NOOP. diff --git a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestReadonlyEndpoint.java b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestReadonlyEndpoint.java new file mode 100644 index 00000000000..418b1171014 --- /dev/null +++ b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestReadonlyEndpoint.java @@ -0,0 +1,188 @@ +/* + * 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 org.apache.commons.io.IOUtils; +import org.apache.hadoop.hdds.conf.OzoneConfiguration; +import org.apache.hadoop.ozone.client.ObjectStoreStub; +import org.apache.hadoop.ozone.client.OzoneBucket; +import org.apache.hadoop.ozone.client.OzoneClient; +import org.apache.hadoop.ozone.client.OzoneClientStub; +import org.apache.hadoop.ozone.client.io.OzoneInputStream; +import org.apache.hadoop.ozone.client.io.OzoneOutputStream; +import org.apache.hadoop.ozone.s3.S3GatewayConfigKeys; +import org.apache.hadoop.ozone.s3.exception.OS3Exception; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MultivaluedHashMap; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status;; +import javax.ws.rs.core.UriInfo; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.time.format.DateTimeFormatter; + +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * Test put object. + */ +public class TestReadonlyEndpoint { + public static final String CONTENT = "0123456789"; + private String bucketName = "b1"; + private String keyName = "k1/1"; + private String destBucket = "b2"; + private OzoneClient clientStub; + private ObjectEndpoint objectEndpoint; + private BucketEndpoint bucketEndpoint; + + private HttpHeaders headers; + private ByteArrayInputStream body; + private ContainerRequestContext context; + + @Before + public void setup() throws IOException { + //Create client stub and object store stub. + OzoneConfiguration conf = new OzoneConfiguration(); + conf.setBoolean(S3GatewayConfigKeys.OZONE_S3G_READONLY, true); + ObjectStoreStub objectStoreStub = new ObjectStoreStub(); + clientStub = new OzoneClientStub(objectStoreStub, conf); + + // Create bucket + clientStub.getObjectStore().createS3Bucket(bucketName); + clientStub.getObjectStore().createS3Bucket(destBucket); + + // Create PutObject and setClient to OzoneClientStub + objectEndpoint = new ObjectEndpoint(); + objectEndpoint.setClient(clientStub); + objectEndpoint.setOzoneConfiguration(new OzoneConfiguration()); + + OzoneBucket bucket = clientStub.getObjectStore().getS3Bucket(bucketName); + OzoneOutputStream keyStream = + bucket.createKey("key1", CONTENT.getBytes(UTF_8).length); + keyStream.write(CONTENT.getBytes(UTF_8)); + keyStream.close(); + + // Return data for get + headers = Mockito.mock(HttpHeaders.class); + objectEndpoint.setHeaders(headers); + body = new ByteArrayInputStream(CONTENT.getBytes(UTF_8)); + + context = Mockito.mock(ContainerRequestContext.class); + Mockito.when(context.getUriInfo()).thenReturn(Mockito.mock(UriInfo.class)); + Mockito.when(context.getUriInfo().getQueryParameters()) + .thenReturn(new MultivaluedHashMap<>()); + objectEndpoint.setContext(context); + + bucketEndpoint = new BucketEndpoint(); + bucketEndpoint.setClient(clientStub); + } + + // Put should fail when configured as readonly + @Test + public void testPutObject() throws IOException, OS3Exception { + Response response = objectEndpoint.put(bucketName, keyName, CONTENT + .length(), 1, null, body); + + Assert.assertEquals(Status.METHOD_NOT_ALLOWED.getStatusCode(), + response.getStatus()); + } + + // Get should succeed + @Test + public void get() throws IOException, OS3Exception { + //WHEN + Response response = objectEndpoint.get(bucketName, "key1", + null, 0, null, body); + + //THEN + OzoneInputStream ozoneInputStream = + clientStub.getObjectStore().getS3Bucket(bucketName) + .readKey("key1"); + String keyContent = + IOUtils.toString(ozoneInputStream, UTF_8); + + Assert.assertEquals(CONTENT, keyContent); + Assert.assertEquals("" + keyContent.length(), + response.getHeaderString("Content-Length")); + + DateTimeFormatter.RFC_1123_DATE_TIME + .parse(response.getHeaderString("Last-Modified")); + } + + // Copy is also treated as write + @Test + public void testCopyObject() throws IOException, OS3Exception { + // Put object in to source bucket + objectEndpoint.setHeaders(headers); + + Response response = objectEndpoint.put(bucketName, keyName, + CONTENT.length(), 1, null, body); + Assert.assertEquals(Status.METHOD_NOT_ALLOWED.getStatusCode(), + response.getStatus()); + } + + @Test + public void testDelete() throws IOException, OS3Exception { + //GIVEN + OzoneBucket bucket = + clientStub.getObjectStore().getS3Bucket("b1"); + + bucket.createKey("key1", 0).close(); + + //WHEN + Response response = objectEndpoint.delete("b1", "key1", null); + + //THEN + Assert.assertEquals(Status.METHOD_NOT_ALLOWED.getStatusCode(), + response.getStatus()); + } + + @Test + public void testBucketPut() throws IOException, OS3Exception { + Response response = bucketEndpoint.put(bucketName, null, null, null); + Assert.assertEquals(Status.METHOD_NOT_ALLOWED.getStatusCode(), + response.getStatus()); + } + + @Test + public void listDir() throws OS3Exception, IOException { + Response response = + bucketEndpoint.get(bucketName, "/", null, null, 100, + "dir1", null, null, null, null, null); + + Assert.assertEquals(Status.OK.getStatusCode(), response.getStatus()); + } + + @Test + public void testInitiateMultipartUpload() throws Exception { + Response response = objectEndpoint.initializeMultipartUpload(bucketName, + keyName); + + Assert.assertEquals(Status.METHOD_NOT_ALLOWED.getStatusCode(), + response.getStatus()); + } +} diff --git a/pom.xml b/pom.xml index 1f387ee1f81..c496a61525d 100644 --- a/pom.xml +++ b/pom.xml @@ -66,7 +66,7 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xs ${ozone.version} - 1.3.0 + 1.3.2-SNAPSHOT Grand Canyon ${hdds.version} ${ozone.version}