diff --git a/hadoop-hdds/common/src/main/resources/ozone-default.xml b/hadoop-hdds/common/src/main/resources/ozone-default.xml index 21ba1b45451..333d2f229f5 100644 --- a/hadoop-hdds/common/src/main/resources/ozone-default.xml +++ b/hadoop-hdds/common/src/main/resources/ozone-default.xml @@ -1928,6 +1928,13 @@ will be used for http authentication. + + 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 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 6fc55ac0127..ed61423b9a4 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 @@ -44,6 +44,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. @@ -101,6 +102,8 @@ public void start() throws IOException { LOG.info("Starting Ozone S3 gateway"); HddsServerUtil.initializeMetrics(ozoneConfiguration, "S3Gateway"); jvmPauseMonitor.start(); + 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 179c5eeee79..6fa808504e9 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 @@ -81,6 +81,8 @@ public final class S3GatewayConfigKeys { public static final boolean OZONE_S3G_LIST_KEYS_SHALLOW_ENABLED_DEFAULT = true; + 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 b8cd56d5f95..fce9515ec6f 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 @@ -67,6 +67,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.audit.AuditLogger.PerformanceStringBuilder; @@ -291,6 +292,12 @@ public Response put(@PathParam("bucket") String bucketName, long startNanos = Time.monotonicNowNanos(); 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; @@ -398,6 +405,12 @@ public Response delete(@PathParam("bucket") String bucketName) long startNanos = Time.monotonicNowNanos(); 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) { @@ -441,9 +454,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()) { long startNanos = Time.monotonicNowNanos(); 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 6f0f3c48472..f833e2d6879 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; @@ -52,6 +53,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; @@ -61,6 +63,7 @@ import org.apache.hadoop.ozone.s3.signature.SignatureInfo; import org.apache.hadoop.ozone.s3.util.AuditUtils; import org.apache.hadoop.util.Time; +import org.apache.http.HttpStatus; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -410,4 +413,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 1e247c8eb85..f09fa76db89 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 @@ -44,6 +44,20 @@ import javax.ws.rs.core.StreamingOutput; import javax.xml.bind.DatatypeConverter; import org.apache.commons.io.IOUtils; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.text.ParseException; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +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; import org.apache.commons.lang3.tuple.Pair; import org.apache.hadoop.hdds.client.ECReplicationConfig; @@ -218,6 +232,13 @@ public Response put( @QueryParam("uploadId") @DefaultValue("") String uploadID, InputStream body) throws IOException, OS3Exception { long startNanos = Time.monotonicNowNanos(); + + // Check if the S3Gateway status is readonly + Optional checkResult = checkIfReadonly(); + if (checkResult.isPresent()) { + return checkResult.get(); + } + S3GAction s3GAction = S3GAction.CREATE_KEY; boolean auditSuccess = true; PerformanceStringBuilder perf = new PerformanceStringBuilder(); @@ -664,6 +685,13 @@ public Response delete( @QueryParam("uploadId") @DefaultValue("") String uploadId) throws IOException, OS3Exception { long startNanos = Time.monotonicNowNanos(); + + // Check if the S3Gateway status is readonly + Optional checkResult = checkIfReadonly(); + if (checkResult.isPresent()) { + return checkResult.get(); + } + S3GAction s3GAction = S3GAction.DELETE_KEY; try { @@ -729,6 +757,13 @@ public Response initializeMultipartUpload( ) throws IOException, OS3Exception { long startNanos = Time.monotonicNowNanos(); + + // Check if the S3Gateway status is readonly + Optional checkResult = checkIfReadonly(); + if (checkResult.isPresent()) { + return checkResult.get(); + } + S3GAction s3GAction = S3GAction.INIT_MULTIPART_UPLOAD; try { @@ -797,6 +832,13 @@ public Response completeMultipartUpload(@PathParam("bucket") String bucket, CompleteMultipartUploadRequest multipartUploadRequest) throws IOException, OS3Exception { long startNanos = Time.monotonicNowNanos(); + + // 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..d5cf27d9d89 --- /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.jupiter.api.Test; +import org.junit.jupiter.api.BeforeAll; +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; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * 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; + + @BeforeAll + 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); + + 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", + 0, null, 0, null); + + //THEN + OzoneInputStream ozoneInputStream = + clientStub.getObjectStore().getS3Bucket(bucketName) + .readKey("key1"); + String keyContent = + IOUtils.toString(ozoneInputStream, UTF_8); + + assertEquals(CONTENT, keyContent); + 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); + 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 + assertEquals(Status.METHOD_NOT_ALLOWED.getStatusCode(), + response.getStatus()); + } + + @Test + public void testBucketPut() throws IOException, OS3Exception { + Response response = bucketEndpoint.put(bucketName, null, null, null); + 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); + + assertEquals(Status.OK.getStatusCode(), response.getStatus()); + } + + @Test + public void testInitiateMultipartUpload() throws Exception { + Response response = objectEndpoint.initializeMultipartUpload(bucketName, + keyName); + + assertEquals(Status.METHOD_NOT_ALLOWED.getStatusCode(), + response.getStatus()); + } +}