diff --git a/hadoop-common-project/hadoop-common/src/main/resources/core-default.xml b/hadoop-common-project/hadoop-common/src/main/resources/core-default.xml index 35be56bf81..52b58eddb1 100644 --- a/hadoop-common-project/hadoop-common/src/main/resources/core-default.xml +++ b/hadoop-common-project/hadoop-common/src/main/resources/core-default.xml @@ -1292,6 +1292,16 @@ to specify the time (such as 2s, 2m, 1h, etc.). + + fs.azure.authorization + false + + Config flag to enable authorization support in WASB. Setting it to "true" enables + authorization support to WASB. Currently WASB authorization requires a remote service + to provide authorization that needs to be specified via fs.azure.authorization.remote.service.url + configuration + + diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/conf/TestCommonConfigurationFields.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/conf/TestCommonConfigurationFields.java index 966a8ac0d0..cbfb6d1a7c 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/conf/TestCommonConfigurationFields.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/conf/TestCommonConfigurationFields.java @@ -114,6 +114,7 @@ public void initializeMemberVariables() { xmlPropsToSkipCompare.add("fs.azure.sas.expiry.period"); xmlPropsToSkipCompare.add("fs.azure.local.sas.key.mode"); xmlPropsToSkipCompare.add("fs.azure.secure.mode"); + xmlPropsToSkipCompare.add("fs.azure.authorization"); // Deprecated properties. These should eventually be removed from the // class. diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azure/AzureNativeFileSystemStore.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azure/AzureNativeFileSystemStore.java index 07c389ce0c..a8708ecafd 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azure/AzureNativeFileSystemStore.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azure/AzureNativeFileSystemStore.java @@ -249,7 +249,7 @@ public class AzureNativeFileSystemStore implements NativeFileSystemStore { * Default values to control SAS Key mode. * By default we set the values to false. */ - private static final boolean DEFAULT_USE_SECURE_MODE = false; + public static final boolean DEFAULT_USE_SECURE_MODE = false; private static final boolean DEFAULT_USE_LOCAL_SAS_KEY_MODE = false; /** @@ -849,6 +849,9 @@ private void connectToAzureStorageInSecureMode(String accountName, rootDirectory = container.getDirectoryReference(""); canCreateOrModifyContainer = true; + + configureAzureStorageSession(); + tolerateOobAppends = false; } /** diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azure/NativeAzureFileSystem.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azure/NativeAzureFileSystem.java index 4184a53941..6de0a285a8 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azure/NativeAzureFileSystem.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azure/NativeAzureFileSystem.java @@ -1106,7 +1106,31 @@ private void restoreKey() throws IOException { // A counter to create unique (within-process) names for my metrics sources. private static AtomicInteger metricsSourceNameCounter = new AtomicInteger(); private boolean appendSupportEnabled = false; - + + /** + * Configuration key to enable authorization support in WASB. + */ + public static final String KEY_AZURE_AUTHORIZATION = + "fs.azure.authorization"; + + /** + * Default value for the authorization support in WASB. + */ + private static final boolean DEFAULT_AZURE_AUTHORIZATION = false; + + /** + * Flag controlling authorization support in WASB. + */ + private boolean azureAuthorization = false; + + /** + * Authorizer to use when authorization support is enabled in + * WASB. + */ + private WasbAuthorizerInterface authorizer = null; + + private String delegationToken = null; + public NativeAzureFileSystem() { // set store in initialize() } @@ -1146,11 +1170,11 @@ public static String newMetricsSourceName() { return baseName + number; } } - + /** * Checks if the given URI scheme is a scheme that's affiliated with the Azure * File System. - * + * * @param scheme * The URI scheme. * @return true iff it's an Azure File System URI scheme. @@ -1167,7 +1191,7 @@ private static boolean isWasbScheme(String scheme) { /** * Puts in the authority of the default file system if it is a WASB file * system and the given URI's authority is null. - * + * * @return The URI with reconstructed authority if necessary and possible. */ private static URI reconstructAuthorityIfNeeded(URI uri, Configuration conf) { @@ -1237,6 +1261,24 @@ public void initialize(URI uri, Configuration conf) // Initialize thread counts from user configuration deleteThreadCount = conf.getInt(AZURE_DELETE_THREADS, DEFAULT_AZURE_DELETE_THREADS); renameThreadCount = conf.getInt(AZURE_RENAME_THREADS, DEFAULT_AZURE_RENAME_THREADS); + + boolean useSecureMode = conf.getBoolean(AzureNativeFileSystemStore.KEY_USE_SECURE_MODE, + AzureNativeFileSystemStore.DEFAULT_USE_SECURE_MODE); + + this.azureAuthorization = useSecureMode && + conf.getBoolean(KEY_AZURE_AUTHORIZATION, DEFAULT_AZURE_AUTHORIZATION); + + if (this.azureAuthorization) { + + this.authorizer = + new RemoteWasbAuthorizerImpl(); + authorizer.init(conf); + } + } + + @VisibleForTesting + public void updateWasbAuthorizer(WasbAuthorizerInterface authorizer) { + this.authorizer = authorizer; } private NativeFileSystemStore createDefaultStore(Configuration conf) { @@ -1338,18 +1380,28 @@ public Path makeAbsolute(Path path) { /** * For unit test purposes, retrieves the AzureNativeFileSystemStore store * backing this file system. - * + * * @return The store object. */ @VisibleForTesting public AzureNativeFileSystemStore getStore() { return actualStore; } - + NativeFileSystemStore getStoreInterface() { return store; } + private void performAuthCheck(String path, String accessType, + String operation) throws WasbAuthorizationException, IOException { + + if (azureAuthorization && this.authorizer != null && + !this.authorizer.authorize(path, accessType, delegationToken)) { + throw new WasbAuthorizationException(operation + + " operation for Path : " + path + " not allowed"); + } + } + /** * Gets the metrics source for this file system. * This is mainly here for unit testing purposes. @@ -1372,6 +1424,10 @@ public FSDataOutputStream append(Path f, int bufferSize, Progressable progress) LOG.debug("Opening file: {} for append", f); Path absolutePath = makeAbsolute(f); + + performAuthCheck(absolutePath.toString(), + WasbAuthorizationOperations.WRITE.toString(), "append"); + String key = pathToKey(absolutePath); FileMetadata meta = null; try { @@ -1434,6 +1490,7 @@ public FSDataOutputStream create(Path f, FsPermission permission, * Get a self-renewing lease on the specified file. * @param path path whose lease to be renewed. * @return Lease + * @throws AzureException when not being able to acquire a lease on the path */ public SelfRenewingLease acquireLease(Path path) throws AzureException { String fullKey = pathToKey(makeAbsolute(path)); @@ -1572,6 +1629,10 @@ private FSDataOutputStream create(Path f, FsPermission permission, } Path absolutePath = makeAbsolute(f); + + performAuthCheck(absolutePath.toString(), + WasbAuthorizationOperations.WRITE.toString(), "create"); + String key = pathToKey(absolutePath); FileMetadata existingMetadata = store.retrieveMetadata(key); @@ -1652,10 +1713,10 @@ private FSDataOutputStream create(Path f, FsPermission permission, // Construct the data output stream from the buffered output stream. FSDataOutputStream fsOut = new FSDataOutputStream(bufOutStream, statistics); - + // Increment the counter instrumentation.fileCreated(); - + // Return data output stream to caller. return fsOut; } @@ -1694,6 +1755,10 @@ public boolean delete(Path f, boolean recursive, LOG.debug("Deleting file: {}", f.toString()); Path absolutePath = makeAbsolute(f); + + performAuthCheck(absolutePath.toString(), + WasbAuthorizationOperations.EXECUTE.toString(), "delete"); + String key = pathToKey(absolutePath); // Capture the metadata for the path. @@ -1964,6 +2029,10 @@ public FileStatus getFileStatus(Path f) throws FileNotFoundException, IOExceptio // Capture the absolute path and the path to key. Path absolutePath = makeAbsolute(f); + + performAuthCheck(absolutePath.toString(), + WasbAuthorizationOperations.EXECUTE.toString(), "getFileStatus"); + String key = pathToKey(absolutePath); if (key.length() == 0) { // root always exists return newDirectory(null, absolutePath); @@ -2062,6 +2131,10 @@ public FileStatus[] listStatus(Path f) throws FileNotFoundException, IOException LOG.debug("Listing status for {}", f.toString()); Path absolutePath = makeAbsolute(f); + + performAuthCheck(absolutePath.toString(), + WasbAuthorizationOperations.EXECUTE.toString(), "list"); + String key = pathToKey(absolutePath); Set status = new TreeSet(); FileMetadata meta = null; @@ -2228,7 +2301,7 @@ private static enum UMaskApplyMode { /** * Applies the applicable UMASK's on the given permission. - * + * * @param permission * The permission to mask. * @param applyMode @@ -2250,7 +2323,7 @@ private FsPermission applyUMask(final FsPermission permission, /** * Creates the PermissionStatus object to use for the given permission, based * on the current user in context. - * + * * @param permission * The permission for the file. * @return The permission status object to use. @@ -2284,6 +2357,10 @@ public boolean mkdirs(Path f, FsPermission permission, boolean noUmask) throws I } Path absolutePath = makeAbsolute(f); + + performAuthCheck(absolutePath.toString(), + WasbAuthorizationOperations.EXECUTE.toString(), "mkdirs"); + PermissionStatus permissionStatus = null; if(noUmask) { // ensure owner still has wx permissions at the minimum @@ -2337,6 +2414,10 @@ public FSDataInputStream open(Path f, int bufferSize) throws FileNotFoundExcepti LOG.debug("Opening file: {}", f.toString()); Path absolutePath = makeAbsolute(f); + + performAuthCheck(absolutePath.toString(), + WasbAuthorizationOperations.READ.toString(), "read"); + String key = pathToKey(absolutePath); FileMetadata meta = null; try { @@ -2393,7 +2474,12 @@ public boolean rename(Path src, Path dst) throws FileNotFoundException, IOExcept + " through WASB that has colons in the name"); } - String srcKey = pathToKey(makeAbsolute(src)); + Path absolutePath = makeAbsolute(src); + + performAuthCheck(absolutePath.toString(), + WasbAuthorizationOperations.EXECUTE.toString(), "rename"); + + String srcKey = pathToKey(absolutePath); if (srcKey.length() == 0) { // Cannot rename root of file system @@ -2695,6 +2781,10 @@ public Path getWorkingDirectory() { @Override public void setPermission(Path p, FsPermission permission) throws FileNotFoundException, IOException { Path absolutePath = makeAbsolute(p); + + performAuthCheck(absolutePath.toString(), + WasbAuthorizationOperations.EXECUTE.toString(), "setPermission"); + String key = pathToKey(absolutePath); FileMetadata metadata = null; try { @@ -2733,6 +2823,10 @@ public void setPermission(Path p, FsPermission permission) throws FileNotFoundEx public void setOwner(Path p, String username, String groupname) throws IOException { Path absolutePath = makeAbsolute(p); + + performAuthCheck(absolutePath.toString(), + WasbAuthorizationOperations.EXECUTE.toString(), "setOwner"); + String key = pathToKey(absolutePath); FileMetadata metadata = null; diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azure/RemoteWasbAuthorizerImpl.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azure/RemoteWasbAuthorizerImpl.java new file mode 100644 index 0000000000..5f2265bc73 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azure/RemoteWasbAuthorizerImpl.java @@ -0,0 +1,190 @@ +/** + * 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.fs.azure; + +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.annotations.VisibleForTesting; +import org.apache.hadoop.conf.Configuration; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.utils.URIBuilder; + +import java.io.IOException; +import java.net.URISyntaxException; + +import static org.apache.hadoop.fs.azure.WasbRemoteCallHelper.REMOTE_CALL_SUCCESS_CODE; + +/** + * Class implementing WasbAuthorizerInterface using a remote + * service that implements the authorization operation. This + * class expects the url of the remote service to be passed + * via config. + */ +public class RemoteWasbAuthorizerImpl implements WasbAuthorizerInterface { + + private String remoteAuthorizerServiceUrl = ""; + + /** + * Configuration parameter name expected in the Configuration object to + * provide the url of the remote service. {@value} + */ + public static final String KEY_REMOTE_AUTH_SERVICE_URL = + "fs.azure.authorization.remote.service.url"; + + /** + * Authorization operation OP name in the remote service {@value} + */ + private static final String CHECK_AUTHORIZATION_OP = + "CHECK_AUTHORIZATION"; + + /** + * Query parameter specifying the access operation type. {@value} + */ + private static final String ACCESS_OPERATION_QUERY_PARAM_NAME = + "operation_type"; + + /** + * Query parameter specifying the wasb absolute path. {@value} + */ + private static final String WASB_ABSOLUTE_PATH_QUERY_PARAM_NAME = + "wasb_absolute_path"; + + /** + * Query parameter name for user info {@value} + */ + private static final String DELEGATION_TOKEN_QUERY_PARAM_NAME = + "delegation_token"; + + private WasbRemoteCallHelper remoteCallHelper = null; + + @VisibleForTesting + public void updateWasbRemoteCallHelper(WasbRemoteCallHelper helper) { + this.remoteCallHelper = helper; + } + + @Override + public void init(Configuration conf) + throws WasbAuthorizationException, IOException { + + remoteAuthorizerServiceUrl = conf.get(KEY_REMOTE_AUTH_SERVICE_URL); + + if (remoteAuthorizerServiceUrl == null + || remoteAuthorizerServiceUrl.isEmpty()) { + throw new WasbAuthorizationException( + "fs.azure.authorization.remote.service.url config not set" + + " in configuration."); + } + + this.remoteCallHelper = new WasbRemoteCallHelper(); + } + + @Override + public boolean authorize(String wasbAbsolutePath, String accessType, + String delegationToken) throws WasbAuthorizationException, IOException { + + try { + URIBuilder uriBuilder = new URIBuilder(remoteAuthorizerServiceUrl); + uriBuilder.setPath("/" + CHECK_AUTHORIZATION_OP); + uriBuilder.addParameter(WASB_ABSOLUTE_PATH_QUERY_PARAM_NAME, + wasbAbsolutePath); + uriBuilder.addParameter(ACCESS_OPERATION_QUERY_PARAM_NAME, + accessType); + uriBuilder.addParameter(DELEGATION_TOKEN_QUERY_PARAM_NAME, + delegationToken); + + String responseBody = remoteCallHelper.makeRemoteGetRequest( + new HttpGet(uriBuilder.build())); + + ObjectMapper objectMapper = new ObjectMapper(); + RemoteAuthorizerResponse authorizerResponse = + objectMapper.readValue(responseBody, RemoteAuthorizerResponse.class); + + if (authorizerResponse == null) { + throw new WasbAuthorizationException( + "RemoteAuthorizerResponse object null from remote call"); + } else if (authorizerResponse.getResponseCode() + == REMOTE_CALL_SUCCESS_CODE) { + return authorizerResponse.getAuthorizationResult(); + } else { + throw new WasbAuthorizationException("Remote authorization" + + " service encountered an error " + + authorizerResponse.getResponseMessage()); + } + } catch (URISyntaxException | WasbRemoteCallException + | JsonParseException | JsonMappingException ex) { + throw new WasbAuthorizationException(ex); + } + } +} + +/** + * POJO representing the response expected from a remote + * authorization service. + * The remote service is expected to return the authorization + * response in the following JSON format + * { + * "responseCode" : 0 or non-zero , + * "responseMessage" : relavant message of failure + * "authorizationResult" : authorization result + * true - if auhorization allowed + * false - otherwise. + * + * } + */ +class RemoteAuthorizerResponse { + + private int responseCode; + private boolean authorizationResult; + private String responseMessage; + + public RemoteAuthorizerResponse(int responseCode, + boolean authorizationResult, String message) { + this.responseCode = responseCode; + this.authorizationResult = authorizationResult; + this.responseMessage = message; + } + + public RemoteAuthorizerResponse() { + } + + public int getResponseCode() { + return responseCode; + } + + public void setResponseCode(int responseCode) { + this.responseCode = responseCode; + } + + public boolean getAuthorizationResult() { + return authorizationResult; + } + + public void setAuthorizationResult(boolean authorizationResult) { + this.authorizationResult = authorizationResult; + } + + public String getResponseMessage() { + return responseMessage; + } + + public void setResponseMessage(String message) { + this.responseMessage = message; + } +} \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azure/WasbAuthorizationException.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azure/WasbAuthorizationException.java new file mode 100644 index 0000000000..eff9248dff --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azure/WasbAuthorizationException.java @@ -0,0 +1,40 @@ +/** + * 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.fs.azure; + +/** + * Exception that gets thrown during the authorization failures + * in WASB. + */ +public class WasbAuthorizationException extends AzureException { + + private static final long serialVersionUID = 1L; + + public WasbAuthorizationException(String message) { + super(message); + } + + public WasbAuthorizationException(String message, Throwable cause) { + super(message, cause); + } + + public WasbAuthorizationException(Throwable t) { + super(t); + } +} \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azure/WasbAuthorizationOperations.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azure/WasbAuthorizationOperations.java new file mode 100644 index 0000000000..41ca2b3226 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azure/WasbAuthorizationOperations.java @@ -0,0 +1,44 @@ +/** + * 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.fs.azure; + +/** + * Different authorization operations supported + * in WASB. + */ + +public enum WasbAuthorizationOperations { + + READ, WRITE, EXECUTE; + + @Override + public String toString() { + switch(this) { + case READ: + return "read"; + case WRITE: + return "write"; + case EXECUTE: + return "execute"; + default: + throw new IllegalArgumentException( + "Invalid Authorization Operation"); + } + } +} \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azure/WasbAuthorizerInterface.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azure/WasbAuthorizerInterface.java new file mode 100644 index 0000000000..f391851095 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azure/WasbAuthorizerInterface.java @@ -0,0 +1,53 @@ +/** + * 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.fs.azure; + +import java.io.IOException; + +import org.apache.hadoop.conf.Configuration; + +/** + * Interface to implement authorization support in WASB. + * API's of this interface will be implemented in the + * StorageInterface Layer before making calls to Azure + * Storage. + */ +public interface WasbAuthorizerInterface { + /** + * Initializer method + * @param conf - Configuration object + * @throws WasbAuthorizationException - On authorization exceptions + * @throws IOException - When not able to reach the authorizer + */ + public void init(Configuration conf) + throws WasbAuthorizationException, IOException; + + /** + * Authorizer API to authorize access in WASB. + + * @param wasbAbolutePath : Absolute WASB Path used for access. + * @param accessType : Type of access + * @param delegationToken : The user information. + * @return : true - If access allowed false - If access is not allowed. + * @throws WasbAuthorizationException - On authorization exceptions + * @throws IOException - When not able to reach the authorizer + */ + public boolean authorize(String wasbAbolutePath, String accessType, + String delegationToken) throws WasbAuthorizationException, IOException; +} \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azure/WasbRemoteCallHelper.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azure/WasbRemoteCallHelper.java index 543c899f98..09ea0847ee 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azure/WasbRemoteCallHelper.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azure/WasbRemoteCallHelper.java @@ -18,18 +18,21 @@ package org.apache.hadoop.fs.azure; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; - +import com.google.common.annotations.VisibleForTesting; +import org.apache.http.Header; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; +import org.apache.http.StatusLine; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.HttpClientBuilder; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; + /** * Helper class the has constants and helper methods * used in WASB when integrating with a remote http cred @@ -48,6 +51,11 @@ class WasbRemoteCallHelper { */ private HttpClient client = null; + @VisibleForTesting + public void updateHttpClient(HttpClient client) { + this.client = client; + } + public WasbRemoteCallHelper() { this.client = HttpClientBuilder.create().build(); } @@ -58,17 +66,54 @@ public WasbRemoteCallHelper() { * @return Http Response body returned as a string. The caller * is expected to semantically understand the response. * @throws WasbRemoteCallException + * @throws IOException */ public String makeRemoteGetRequest(HttpGet getRequest) - throws WasbRemoteCallException { + throws WasbRemoteCallException, IOException { try { + final String APPLICATION_JSON = "application/json"; + final int MAX_CONTENT_LENGTH = 1024; + + getRequest.setHeader("Accept", APPLICATION_JSON); + HttpResponse response = client.execute(getRequest); - if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) { - throw new WasbRemoteCallException( - response.getStatusLine().toString()); + StatusLine statusLine = response.getStatusLine(); + if (statusLine == null || statusLine.getStatusCode() != HttpStatus.SC_OK) { + throw new WasbRemoteCallException(getRequest.getURI().toString() + ":" + + ((statusLine!=null) ? statusLine.toString() : "NULL") + ); + } + + Header contentTypeHeader = response.getFirstHeader("Content-Type"); + if (contentTypeHeader == null || contentTypeHeader.getValue() != APPLICATION_JSON) { + throw new WasbRemoteCallException(getRequest.getURI().toString() + ":" + + "Content-Type mismatch: expected: " + APPLICATION_JSON + + ", got " + ((contentTypeHeader!=null) ? contentTypeHeader.getValue() : "NULL") + ); + } + + Header contentLengthHeader = response.getFirstHeader("Content-Length"); + if (contentLengthHeader == null) { + throw new WasbRemoteCallException(getRequest.getURI().toString() + ":" + + "Content-Length header missing" + ); + } + + try { + if (Integer.parseInt(contentLengthHeader.getValue()) > MAX_CONTENT_LENGTH) { + throw new WasbRemoteCallException(getRequest.getURI().toString() + ":" + + "Content-Length:" + contentLengthHeader.getValue() + + "exceeded max:" + MAX_CONTENT_LENGTH + ); + } + } + catch (NumberFormatException nfe) { + throw new WasbRemoteCallException(getRequest.getURI().toString() + ":" + + "Invalid Content-Length value :" + contentLengthHeader.getValue() + ); } BufferedReader rd = new BufferedReader( @@ -83,11 +128,11 @@ public String makeRemoteGetRequest(HttpGet getRequest) return responseBody.toString(); } catch (ClientProtocolException clientProtocolEx) { - throw new WasbRemoteCallException("Encountered ClientProtocolException" - + " while making remote call", clientProtocolEx); + throw new WasbRemoteCallException(getRequest.getURI().toString() + ":" + + "Encountered ClientProtocolException while making remote call", clientProtocolEx); } catch (IOException ioEx) { - throw new WasbRemoteCallException("Encountered IOException while making" - + " remote call", ioEx); + throw new WasbRemoteCallException(getRequest.getURI().toString() + ":" + + "Encountered IOException while making remote call", ioEx); } } } \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/site/markdown/index.md b/hadoop-tools/hadoop-azure/src/site/markdown/index.md index 2865223acd..1d1274b930 100644 --- a/hadoop-tools/hadoop-azure/src/site/markdown/index.md +++ b/hadoop-tools/hadoop-azure/src/site/markdown/index.md @@ -333,6 +333,40 @@ The service is expected to return a response in JSON format: "sasKey" : Requested SAS Key } ``` + +## Authorization Support in WASB. + +Authorization support can be enabled in WASB using the following configuration: + +``` + + fs.azure.authorization + true + +``` + The current implementation of authorization relies on the presence of an external service that can enforce + the authorization. The service is expected to be running on a URL provided by the following config. + +``` + + fs.azure.authorization.remote.service.url + {URL} + +``` + + The remote service is expected to provide support for the following REST call: ```{URL}/CHECK_AUTHORIZATION``` + An example request: + ```{URL}/CHECK_AUTHORIZATION?wasb_absolute_path=&operation_type=&delegation_token=``` + + The service is expected to return a response in JSON format: + ``` + { + "responseCode" : 0 or non-zero , + "responseMessage" : relevant message on failure , + "authorizationResult" : true/false + } + ``` + ## Testing the hadoop-azure Module The hadoop-azure module includes a full suite of unit tests. Most of the tests diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azure/AzureBlobStorageTestAccount.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azure/AzureBlobStorageTestAccount.java index 5353663bc8..5f66fd2f0a 100644 --- a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azure/AzureBlobStorageTestAccount.java +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azure/AzureBlobStorageTestAccount.java @@ -18,17 +18,9 @@ package org.apache.hadoop.fs.azure; -import static org.apache.hadoop.fs.azure.AzureNativeFileSystemStore.DEFAULT_STORAGE_EMULATOR_ACCOUNT_NAME; - -import java.net.URI; -import java.net.URISyntaxException; -import java.util.Calendar; -import java.util.Date; -import java.util.EnumSet; -import java.util.GregorianCalendar; -import java.util.TimeZone; -import java.util.concurrent.ConcurrentLinkedQueue; - +import com.microsoft.azure.storage.*; +import com.microsoft.azure.storage.blob.*; +import com.microsoft.azure.storage.core.Base64; import org.apache.commons.configuration2.SubsetConfiguration; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.Path; @@ -39,22 +31,14 @@ import org.apache.hadoop.metrics2.MetricsSink; import org.apache.hadoop.metrics2.MetricsTag; import org.apache.hadoop.metrics2.impl.TestMetricsConfig; -import org.apache.hadoop.metrics2.lib.DefaultMetricsSystem; -import com.microsoft.azure.storage.AccessCondition; -import com.microsoft.azure.storage.CloudStorageAccount; -import com.microsoft.azure.storage.StorageCredentials; -import com.microsoft.azure.storage.StorageCredentialsAccountAndKey; -import com.microsoft.azure.storage.StorageCredentialsAnonymous; -import com.microsoft.azure.storage.blob.BlobContainerPermissions; -import com.microsoft.azure.storage.blob.BlobContainerPublicAccessType; -import com.microsoft.azure.storage.blob.BlobOutputStream; -import com.microsoft.azure.storage.blob.CloudBlobClient; -import com.microsoft.azure.storage.blob.CloudBlobContainer; -import com.microsoft.azure.storage.blob.CloudBlockBlob; -import com.microsoft.azure.storage.blob.SharedAccessBlobPermissions; -import com.microsoft.azure.storage.blob.SharedAccessBlobPolicy; -import com.microsoft.azure.storage.core.Base64; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.*; +import java.util.concurrent.ConcurrentLinkedQueue; + +import static org.apache.hadoop.fs.azure.AzureNativeFileSystemStore.DEFAULT_STORAGE_EMULATOR_ACCOUNT_NAME; +import static org.apache.hadoop.fs.azure.AzureNativeFileSystemStore.KEY_USE_LOCAL_SAS_KEY_MODE; /** * Helper class to create WASB file systems backed by either a mock in-memory @@ -92,7 +76,7 @@ public final class AzureBlobStorageTestAccount { private static final ConcurrentLinkedQueue allMetrics = new ConcurrentLinkedQueue(); private static boolean metricsConfigSaved = false; - + private AzureBlobStorageTestAccount(NativeAzureFileSystem fs, CloudStorageAccount account, CloudBlobContainer container) { @@ -272,6 +256,7 @@ public static AzureBlobStorageTestAccount createMock(Configuration conf) throws store.setAzureStorageInteractionLayer(mockStorage); NativeAzureFileSystem fs = new NativeAzureFileSystem(store); setMockAccountKey(conf); + configureSecureModeTestSettings(conf); // register the fs provider. fs.initialize(new URI(MOCK_WASB_URI), conf); @@ -332,6 +317,8 @@ public static AzureBlobStorageTestAccount createForEmulator() // Set account URI and initialize Azure file system. URI accountUri = createAccountUri(DEFAULT_STORAGE_EMULATOR_ACCOUNT_NAME, containerName); + configureSecureModeTestSettings(conf); + fs.initialize(accountUri, conf); // Create test account initializing the appropriate member variables. @@ -368,6 +355,7 @@ public static AzureBlobStorageTestAccount createOutOfBandStore( // out-of-band appends. conf.setBoolean(KEY_DISABLE_THROTTLING, true); conf.setBoolean(KEY_READ_TOLERATE_CONCURRENT_APPEND, true); + configureSecureModeTestSettings(conf); // Set account URI and initialize Azure file system. URI accountUri = createAccountUri(accountName, containerName); @@ -408,6 +396,17 @@ public static void setMockAccountKey(Configuration conf) { setMockAccountKey(conf, MOCK_ACCOUNT_NAME); } + /** + * Configure default values for Secure Mode testing. + * These values are relevant only when testing in Secure Mode. + * + * @param conf + * The configuration. + */ + public static void configureSecureModeTestSettings(Configuration conf) { + conf.set(KEY_USE_LOCAL_SAS_KEY_MODE, "true"); // always use local sas-key mode for testing + } + /** * Sets the mock account key in the given configuration. * @@ -556,6 +555,8 @@ public static AzureBlobStorageTestAccount create(String containerNameSuffix, conf.setBoolean(KEY_DISABLE_THROTTLING, true); } + configureSecureModeTestSettings(conf); + // Set account URI and initialize Azure file system. URI accountUri = createAccountUri(accountName, containerName); fs.initialize(accountUri, conf); @@ -693,6 +694,8 @@ public static AzureBlobStorageTestAccount createAnonymous( // Capture the account URL and the account name. String accountName = conf.get(TEST_ACCOUNT_NAME_PROPERTY_NAME); + configureSecureModeTestSettings(conf); + // Generate a container name and create a shared access signature string for // it. // @@ -764,6 +767,8 @@ public static AzureBlobStorageTestAccount createRoot(final String blobName, // Capture the account URL and the account name. String accountName = conf.get(TEST_ACCOUNT_NAME_PROPERTY_NAME); + configureSecureModeTestSettings(conf); + // Set up public container with the specified blob name. CloudBlockBlob blobRoot = primeRootContainer(blobClient, accountName, blobName, fileSize); diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azure/MockWasbAuthorizerImpl.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azure/MockWasbAuthorizerImpl.java new file mode 100644 index 0000000000..8f7cb2ae5e --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azure/MockWasbAuthorizerImpl.java @@ -0,0 +1,102 @@ +/** + * 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.fs.azure; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.hadoop.conf.Configuration; + +/** + * A mock wasb authorizer implementation. + */ + +public class MockWasbAuthorizerImpl implements WasbAuthorizerInterface { + + private Map authRules; + + @Override + public void init(Configuration conf) { + authRules = new HashMap(); + } + + public void addAuthRule(String wasbAbsolutePath, + String accessType, boolean access) { + AuthorizationComponent component = + new AuthorizationComponent(wasbAbsolutePath, accessType); + this.authRules.put(component, access); + } + + @Override + public boolean authorize(String wasbAbsolutePath, String accessType, + String delegationToken) throws WasbAuthorizationException { + + AuthorizationComponent component = + new AuthorizationComponent(wasbAbsolutePath, accessType); + + if (authRules.containsKey(component)) { + return authRules.get(component); + } else { + return false; + } + } +} + +class AuthorizationComponent { + + private String wasbAbsolutePath; + private String accessType; + + public AuthorizationComponent(String wasbAbsolutePath, + String accessType) { + this.wasbAbsolutePath = wasbAbsolutePath; + this.accessType = accessType; + } + + @Override + public int hashCode() { + return this.wasbAbsolutePath.hashCode() ^ this.accessType.hashCode(); + } + + @Override + public boolean equals(Object obj) { + + if (obj == this) { + return true; + } + + if (obj == null + || !(obj instanceof AuthorizationComponent)) { + return false; + } + + return ((AuthorizationComponent)obj). + getWasbAbsolutePath().equals(this.wasbAbsolutePath) + && ((AuthorizationComponent)obj). + getAccessType().equals(this.accessType); + } + + public String getWasbAbsolutePath() { + return this.wasbAbsolutePath; + } + + public String getAccessType() { + return accessType; + } +} \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azure/TestNativeAzureFileSystemAuthorization.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azure/TestNativeAzureFileSystemAuthorization.java new file mode 100644 index 0000000000..a2bbeb1765 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azure/TestNativeAzureFileSystemAuthorization.java @@ -0,0 +1,344 @@ +/** + * 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.fs.azure; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.FSDataInputStream; +import org.apache.hadoop.fs.FSDataOutputStream; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.fs.contract.ContractTestUtils; +import org.junit.Assume; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import com.sun.tools.javac.util.Assert; +import org.junit.rules.ExpectedException; + +import java.io.Console; + +import static org.apache.hadoop.fs.azure.AzureNativeFileSystemStore.KEY_USE_SECURE_MODE; + +/** + * Test class to hold all WASB authorization tests. + */ +public class TestNativeAzureFileSystemAuthorization + extends AbstractWasbTestBase { + + @Override + protected AzureBlobStorageTestAccount createTestAccount() throws Exception { + Configuration conf = new Configuration(); + conf.set(NativeAzureFileSystem.KEY_AZURE_AUTHORIZATION, "true"); + conf.set(RemoteWasbAuthorizerImpl.KEY_REMOTE_AUTH_SERVICE_URL, "http://localhost/"); + return AzureBlobStorageTestAccount.create(conf); + } + + + @Before + public void beforeMethod() { + boolean useSecureMode = fs.getConf().getBoolean(KEY_USE_SECURE_MODE, false); + boolean useAuthorization = fs.getConf().getBoolean(NativeAzureFileSystem.KEY_AZURE_AUTHORIZATION, false); + Assume.assumeTrue("Test valid when both SecureMode and Authorization are enabled .. skipping", + useSecureMode && useAuthorization); + + Assume.assumeTrue( + useSecureMode && useAuthorization + ); + } + + + @Rule + public ExpectedException expectedEx = ExpectedException.none(); + + /** + * Positive test to verify Create and delete access check + * @throws Throwable + */ + @Test + public void testCreateAccessCheckPositive() throws Throwable { + + AzureBlobStorageTestAccount testAccount = createTestAccount(); + NativeAzureFileSystem fs = testAccount.getFileSystem(); + + Path parentDir = new Path("/testCreateAccessCheckPositive"); + Path testPath = new Path(parentDir, "test.dat"); + + MockWasbAuthorizerImpl authorizer = new MockWasbAuthorizerImpl(); + authorizer.init(null); + authorizer.addAuthRule(testPath.toString(), WasbAuthorizationOperations.WRITE.toString(), true); + authorizer.addAuthRule(testPath.toString(), WasbAuthorizationOperations.EXECUTE.toString(), true); + authorizer.addAuthRule(parentDir.toString(), WasbAuthorizationOperations.EXECUTE.toString(), true); + fs.updateWasbAuthorizer(authorizer); + + fs.create(testPath); + ContractTestUtils.assertPathExists(fs, "testPath was not created", testPath); + fs.delete(parentDir, true); + } + + /** + * Negative test to verify Create access check + * @throws Throwable + */ + + @Test // (expected=WasbAuthorizationException.class) + public void testCreateAccessCheckNegative() throws Throwable { + + expectedEx.expect(WasbAuthorizationException.class); + expectedEx.expectMessage("create operation for Path : /testCreateAccessCheckNegative/test.dat not allowed"); + + AzureBlobStorageTestAccount testAccount = createTestAccount(); + NativeAzureFileSystem fs = testAccount.getFileSystem(); + + Path parentDir = new Path("/testCreateAccessCheckNegative"); + Path testPath = new Path(parentDir, "test.dat"); + + MockWasbAuthorizerImpl authorizer = new MockWasbAuthorizerImpl(); + authorizer.init(null); + authorizer.addAuthRule(testPath.toString(),WasbAuthorizationOperations.WRITE.toString(), false); + authorizer.addAuthRule(parentDir.toString(),WasbAuthorizationOperations.EXECUTE.toString(), true); + fs.updateWasbAuthorizer(authorizer); + + try { + fs.create(testPath); + } + finally { + fs.delete(parentDir, true); + } + } + + /** + * Positive test to verify Create and delete access check + * @throws Throwable + */ + @Test + public void testListAccessCheckPositive() throws Throwable { + + AzureBlobStorageTestAccount testAccount = createTestAccount(); + NativeAzureFileSystem fs = testAccount.getFileSystem(); + + Path parentDir = new Path("/testListAccessCheckPositive"); + Path testPath = new Path(parentDir, "test.dat"); + + MockWasbAuthorizerImpl authorizer = new MockWasbAuthorizerImpl(); + authorizer.init(null); + authorizer.addAuthRule(testPath.toString(), WasbAuthorizationOperations.WRITE.toString(), true); + authorizer.addAuthRule(testPath.toString(), WasbAuthorizationOperations.EXECUTE.toString(), true); + authorizer.addAuthRule(parentDir.toString(), WasbAuthorizationOperations.EXECUTE.toString(), true); + fs.updateWasbAuthorizer(authorizer); + + fs.create(testPath); + ContractTestUtils.assertPathExists(fs, "testPath does not exist", testPath); + + try { + fs.listStatus(testPath); + } + finally { + fs.delete(parentDir, true); + } + } + + /** + * Negative test to verify Create access check + * @throws Throwable + */ + + @Test //(expected=WasbAuthorizationException.class) + public void testListAccessCheckNegative() throws Throwable { + + expectedEx.expect(WasbAuthorizationException.class); + expectedEx.expectMessage("getFileStatus operation for Path : /testListAccessCheckNegative/test.dat not allowed"); + + AzureBlobStorageTestAccount testAccount = createTestAccount(); + NativeAzureFileSystem fs = testAccount.getFileSystem(); + + Path parentDir = new Path("/testListAccessCheckNegative"); + Path testPath = new Path(parentDir, "test.dat"); + + MockWasbAuthorizerImpl authorizer = new MockWasbAuthorizerImpl(); + authorizer.init(null); + authorizer.addAuthRule(testPath.toString(), WasbAuthorizationOperations.WRITE.toString(), true); + authorizer.addAuthRule(testPath.toString(), WasbAuthorizationOperations.EXECUTE.toString(), false); + authorizer.addAuthRule(parentDir.toString(), WasbAuthorizationOperations.EXECUTE.toString(), true); + fs.updateWasbAuthorizer(authorizer); + + fs.create(testPath); + ContractTestUtils.assertPathExists(fs, "testPath does not exist", testPath); + + try { + fs.listStatus(testPath); + } + finally { + fs.delete(parentDir, true); + } + } + + /** + * Positive test to verify rename access check. + * @throws Throwable + */ + @Test + public void testRenameAccessCheckPositive() throws Throwable { + + AzureBlobStorageTestAccount testAccount = createTestAccount(); + NativeAzureFileSystem fs = testAccount.getFileSystem(); + + Path parentDir = new Path("/testRenameAccessCheckPositive"); + Path testPath = new Path(parentDir, "test.dat"); + Path renamePath = new Path(parentDir, "test2.dat"); + + MockWasbAuthorizerImpl authorizer = new MockWasbAuthorizerImpl(); + authorizer.init(null); + authorizer.addAuthRule(testPath.toString(), WasbAuthorizationOperations.WRITE.toString(), true); + authorizer.addAuthRule(testPath.toString(), WasbAuthorizationOperations.EXECUTE.toString(), true); + authorizer.addAuthRule(renamePath.toString(), WasbAuthorizationOperations.EXECUTE.toString(), true); + authorizer.addAuthRule(parentDir.toString(), WasbAuthorizationOperations.EXECUTE.toString(), true); + fs.updateWasbAuthorizer(authorizer); + + fs.create(testPath); + ContractTestUtils.assertPathExists(fs, "sourcePath does not exist", testPath); + + try { + fs.rename(testPath, renamePath); + ContractTestUtils.assertPathExists(fs, "destPath does not exist", renamePath); + } + finally { + fs.delete(parentDir, true); + } + } + + /** + * Negative test to verify rename access check. + * @throws Throwable + */ + @Test //(expected=WasbAuthorizationException.class) + public void testRenameAccessCheckNegative() throws Throwable { + + expectedEx.expect(WasbAuthorizationException.class); + expectedEx.expectMessage("rename operation for Path : /testRenameAccessCheckNegative/test.dat not allowed"); + + AzureBlobStorageTestAccount testAccount = createTestAccount(); + NativeAzureFileSystem fs = testAccount.getFileSystem(); + Path parentDir = new Path("/testRenameAccessCheckNegative"); + Path testPath = new Path(parentDir, "test.dat"); + Path renamePath = new Path(parentDir, "test2.dat"); + + MockWasbAuthorizerImpl authorizer = new MockWasbAuthorizerImpl(); + authorizer.init(null); + authorizer.addAuthRule(testPath.toString(), WasbAuthorizationOperations.WRITE.toString(), true); + // set EXECUTE to true for initial assert right after creation. + authorizer.addAuthRule(testPath.toString(), WasbAuthorizationOperations.EXECUTE.toString(), true); + authorizer.addAuthRule(parentDir.toString(), WasbAuthorizationOperations.EXECUTE.toString(), true); + fs.updateWasbAuthorizer(authorizer); + + fs.create(testPath); + ContractTestUtils.assertPathExists(fs, "sourcePath does not exist", testPath); + + // Set EXECUTE to false for actual rename-failure test + authorizer.addAuthRule(testPath.toString(), WasbAuthorizationOperations.EXECUTE.toString(), false); + fs.updateWasbAuthorizer(authorizer); + + try { + fs.rename(testPath, renamePath); + ContractTestUtils.assertPathExists(fs, "destPath does not exist", renamePath); + } finally { + fs.delete(parentDir, true); + } + } + + /** + * Positive test for read access check. + * @throws Throwable + */ + @Test + public void testReadAccessCheckPositive() throws Throwable { + + AzureBlobStorageTestAccount testAccount = createTestAccount(); + NativeAzureFileSystem fs = testAccount.getFileSystem(); + Path parentDir = new Path("/testReadAccessCheckPositive"); + Path testPath = new Path(parentDir, "test.dat"); + + MockWasbAuthorizerImpl authorizer = new MockWasbAuthorizerImpl(); + authorizer.init(null); + authorizer.addAuthRule(testPath.toString(), WasbAuthorizationOperations.WRITE.toString(), true); + authorizer.addAuthRule(testPath.toString(), WasbAuthorizationOperations.EXECUTE.toString(), true); + authorizer.addAuthRule(testPath.toString(), WasbAuthorizationOperations.READ.toString(), true); + authorizer.addAuthRule(parentDir.toString(), WasbAuthorizationOperations.EXECUTE.toString(), true); + fs.updateWasbAuthorizer(authorizer); + + FSDataOutputStream fso = fs.create(testPath); + String data = "Hello World"; + fso.writeBytes(data); + fso.close(); + ContractTestUtils.assertPathExists(fs, "testPath does not exist", testPath); + + FSDataInputStream inputStream = null; + try { + inputStream = fs.open(testPath); + ContractTestUtils.verifyRead(inputStream, data.getBytes(), 0, data.length()); + } + finally { + if(inputStream != null) { + inputStream.close(); + } + fs.delete(parentDir, true); + } + } + + /** + * Negative test to verify read access check. + * @throws Throwable + */ + + @Test //(expected=WasbAuthorizationException.class) + public void testReadAccessCheckNegative() throws Throwable { + + expectedEx.expect(WasbAuthorizationException.class); + expectedEx.expectMessage("read operation for Path : /testReadAccessCheckNegative/test.dat not allowed"); + + AzureBlobStorageTestAccount testAccount = createTestAccount(); + NativeAzureFileSystem fs = testAccount.getFileSystem(); + Path parentDir = new Path("/testReadAccessCheckNegative"); + Path testPath = new Path(parentDir, "test.dat"); + + MockWasbAuthorizerImpl authorizer = new MockWasbAuthorizerImpl(); + authorizer.init(null); + authorizer.addAuthRule(testPath.toString(), WasbAuthorizationOperations.WRITE.toString(), true); + authorizer.addAuthRule(testPath.toString(), WasbAuthorizationOperations.EXECUTE.toString(), true); + authorizer.addAuthRule(testPath.toString(), WasbAuthorizationOperations.READ.toString(), false); + authorizer.addAuthRule(parentDir.toString(), WasbAuthorizationOperations.EXECUTE.toString(), true); + fs.updateWasbAuthorizer(authorizer); + + FSDataOutputStream fso = fs.create(testPath); + String data = "Hello World"; + fso.writeBytes(data); + fso.close(); + ContractTestUtils.assertPathExists(fs, "testPath does not exist", testPath); + + FSDataInputStream inputStream = null; + try { + inputStream = fs.open(testPath); + ContractTestUtils.verifyRead(inputStream, data.getBytes(), 0, data.length()); + } finally { + if (inputStream != null) { + inputStream.close(); + } + fs.delete(parentDir, true); + } + } +} \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azure/TestWasbRemoteCallHelper.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azure/TestWasbRemoteCallHelper.java new file mode 100644 index 0000000000..b13e5e9057 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azure/TestWasbRemoteCallHelper.java @@ -0,0 +1,344 @@ +/** + * 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.fs.azure; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.fs.contract.ContractTestUtils; +import org.apache.http.*; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.junit.Assume; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.mockito.Mockito; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; + +import static org.apache.hadoop.fs.azure.AzureNativeFileSystemStore.KEY_USE_SECURE_MODE; + +/** + * Test class to hold all WasbRemoteCallHelper tests + */ +public class TestWasbRemoteCallHelper + extends AbstractWasbTestBase { + + @Override + protected AzureBlobStorageTestAccount createTestAccount() throws Exception { + Configuration conf = new Configuration(); + conf.set(NativeAzureFileSystem.KEY_AZURE_AUTHORIZATION, "true"); + conf.set(RemoteWasbAuthorizerImpl.KEY_REMOTE_AUTH_SERVICE_URL, "http://localhost/"); + return AzureBlobStorageTestAccount.create(conf); + } + + @Before + public void beforeMethod() { + boolean useSecureMode = fs.getConf().getBoolean(KEY_USE_SECURE_MODE, false); + boolean useAuthorization = fs.getConf().getBoolean(NativeAzureFileSystem.KEY_AZURE_AUTHORIZATION, false); + Assume.assumeTrue("Test valid when both SecureMode and Authorization are enabled .. skipping", + useSecureMode && useAuthorization); + + Assume.assumeTrue( + useSecureMode && useAuthorization + ); + } + + @Rule + public ExpectedException expectedEx = ExpectedException.none(); + + /** + * Test invalid status-code + * @throws Throwable + */ + @Test // (expected = WasbAuthorizationException.class) + public void testInvalidStatusCode() throws Throwable { + + setupExpectations(); + + // set up mocks + HttpClient mockHttpClient = Mockito.mock(HttpClient.class); + HttpResponse mockHttpResponse = Mockito.mock(HttpResponse.class); + Mockito.when(mockHttpClient.execute(Mockito.any())).thenReturn(mockHttpResponse); + Mockito.when(mockHttpResponse.getStatusLine()).thenReturn(newStatusLine(999)); + // finished setting up mocks + + performop(mockHttpClient); + } + + /** + * Test invalid Content-Type + * @throws Throwable + */ + @Test // (expected = WasbAuthorizationException.class) + public void testInvalidContentType() throws Throwable { + + setupExpectations(); + + // set up mocks + HttpClient mockHttpClient = Mockito.mock(HttpClient.class); + HttpResponse mockHttpResponse = Mockito.mock(HttpResponse.class); + Mockito.when(mockHttpClient.execute(Mockito.any())).thenReturn(mockHttpResponse); + Mockito.when(mockHttpResponse.getStatusLine()).thenReturn(newStatusLine(200)); + Mockito.when(mockHttpResponse.getFirstHeader("Content-Type")) + .thenReturn(newHeader("Content-Type", "text/plain")); + // finished setting up mocks + + performop(mockHttpClient); + } + + /** + * Test missing Content-Length + * @throws Throwable + */ + @Test // (expected = WasbAuthorizationException.class) + public void testMissingContentLength() throws Throwable { + + setupExpectations(); + + // set up mocks + HttpClient mockHttpClient = Mockito.mock(HttpClient.class); + HttpResponse mockHttpResponse = Mockito.mock(HttpResponse.class); + Mockito.when(mockHttpClient.execute(Mockito.any())).thenReturn(mockHttpResponse); + Mockito.when(mockHttpResponse.getStatusLine()).thenReturn(newStatusLine(200)); + Mockito.when(mockHttpResponse.getFirstHeader("Content-Type")) + .thenReturn(newHeader("Content-Type", "application/json")); + // finished setting up mocks + + performop(mockHttpClient); + } + + /** + * Test Content-Length exceeds max + * @throws Throwable + */ + @Test // (expected = WasbAuthorizationException.class) + public void testContentLengthExceedsMax() throws Throwable { + + setupExpectations(); + + // set up mocks + HttpClient mockHttpClient = Mockito.mock(HttpClient.class); + HttpResponse mockHttpResponse = Mockito.mock(HttpResponse.class); + Mockito.when(mockHttpClient.execute(Mockito.any())).thenReturn(mockHttpResponse); + Mockito.when(mockHttpResponse.getStatusLine()).thenReturn(newStatusLine(200)); + Mockito.when(mockHttpResponse.getFirstHeader("Content-Type")) + .thenReturn(newHeader("Content-Type", "application/json")); + Mockito.when(mockHttpResponse.getFirstHeader("Content-Length")) + .thenReturn(newHeader("Content-Length", "2048")); + // finished setting up mocks + + performop(mockHttpClient); + } + + /** + * Test invalid Content-Length value + * @throws Throwable + */ + @Test // (expected = WasbAuthorizationException.class) + public void testInvalidContentLengthValue() throws Throwable { + + setupExpectations(); + + // set up mocks + HttpClient mockHttpClient = Mockito.mock(HttpClient.class); + HttpResponse mockHttpResponse = Mockito.mock(HttpResponse.class); + Mockito.when(mockHttpClient.execute(Mockito.any())).thenReturn(mockHttpResponse); + Mockito.when(mockHttpResponse.getStatusLine()).thenReturn(newStatusLine(200)); + Mockito.when(mockHttpResponse.getFirstHeader("Content-Type")) + .thenReturn(newHeader("Content-Type", "application/json")); + Mockito.when(mockHttpResponse.getFirstHeader("Content-Length")) + .thenReturn(newHeader("Content-Length", "20abc48")); + // finished setting up mocks + + performop(mockHttpClient); + } + + /** + * Test valid JSON response + * @throws Throwable + */ + @Test + public void testValidJSONResponse() throws Throwable { + + // set up mocks + HttpClient mockHttpClient = Mockito.mock(HttpClient.class); + + HttpResponse mockHttpResponse = Mockito.mock(HttpResponse.class); + HttpEntity mockHttpEntity = Mockito.mock(HttpEntity.class); + + Mockito.when(mockHttpClient.execute(Mockito.any())).thenReturn(mockHttpResponse); + Mockito.when(mockHttpResponse.getStatusLine()).thenReturn(newStatusLine(200)); + Mockito.when(mockHttpResponse.getFirstHeader("Content-Type")) + .thenReturn(newHeader("Content-Type", "application/json")); + Mockito.when(mockHttpResponse.getFirstHeader("Content-Length")) + .thenReturn(newHeader("Content-Length", "1024")); + Mockito.when(mockHttpResponse.getEntity()).thenReturn(mockHttpEntity); + Mockito.when(mockHttpEntity.getContent()) + .thenReturn(new ByteArrayInputStream(validJsonResponse().getBytes(StandardCharsets.UTF_8))) + .thenReturn(new ByteArrayInputStream(validJsonResponse().getBytes(StandardCharsets.UTF_8))) + .thenReturn(new ByteArrayInputStream(validJsonResponse().getBytes(StandardCharsets.UTF_8))); + // finished setting up mocks + + performop(mockHttpClient); + } + + /** + * Test malformed JSON response + * @throws Throwable + */ + @Test // (expected = WasbAuthorizationException.class) + public void testMalFormedJSONResponse() throws Throwable { + + expectedEx.expect(WasbAuthorizationException.class); + expectedEx.expectMessage("com.fasterxml.jackson.core.JsonParseException: Unexpected end-of-input in FIELD_NAME"); + + // set up mocks + HttpClient mockHttpClient = Mockito.mock(HttpClient.class); + + HttpResponse mockHttpResponse = Mockito.mock(HttpResponse.class); + HttpEntity mockHttpEntity = Mockito.mock(HttpEntity.class); + + Mockito.when(mockHttpClient.execute(Mockito.any())).thenReturn(mockHttpResponse); + Mockito.when(mockHttpResponse.getStatusLine()).thenReturn(newStatusLine(200)); + Mockito.when(mockHttpResponse.getFirstHeader("Content-Type")) + .thenReturn(newHeader("Content-Type", "application/json")); + Mockito.when(mockHttpResponse.getFirstHeader("Content-Length")) + .thenReturn(newHeader("Content-Length", "1024")); + Mockito.when(mockHttpResponse.getEntity()).thenReturn(mockHttpEntity); + Mockito.when(mockHttpEntity.getContent()) + .thenReturn(new ByteArrayInputStream(malformedJsonResponse().getBytes(StandardCharsets.UTF_8))); + // finished setting up mocks + + performop(mockHttpClient); + } + + /** + * Test valid JSON response failure response code + * @throws Throwable + */ + @Test // (expected = WasbAuthorizationException.class) + public void testFailureCodeJSONResponse() throws Throwable { + + expectedEx.expect(WasbAuthorizationException.class); + expectedEx.expectMessage("Remote authorization service encountered an error Unauthorized"); + + // set up mocks + HttpClient mockHttpClient = Mockito.mock(HttpClient.class); + + HttpResponse mockHttpResponse = Mockito.mock(HttpResponse.class); + HttpEntity mockHttpEntity = Mockito.mock(HttpEntity.class); + + Mockito.when(mockHttpClient.execute(Mockito.any())).thenReturn(mockHttpResponse); + Mockito.when(mockHttpResponse.getStatusLine()).thenReturn(newStatusLine(200)); + Mockito.when(mockHttpResponse.getFirstHeader("Content-Type")) + .thenReturn(newHeader("Content-Type", "application/json")); + Mockito.when(mockHttpResponse.getFirstHeader("Content-Length")) + .thenReturn(newHeader("Content-Length", "1024")); + Mockito.when(mockHttpResponse.getEntity()).thenReturn(mockHttpEntity); + Mockito.when(mockHttpEntity.getContent()) + .thenReturn(new ByteArrayInputStream(failureCodeJsonResponse().getBytes(StandardCharsets.UTF_8))); + // finished setting up mocks + + performop(mockHttpClient); + } + + private void setupExpectations() { + expectedEx.expect(WasbAuthorizationException.class); + expectedEx.expectMessage("org.apache.hadoop.fs.azure.WasbRemoteCallException: " + + "http://localhost/CHECK_AUTHORIZATION?wasb_absolute_path=%2Ftest.dat&" + + "operation_type=write&delegation_token:Encountered IOException while making remote call"); + } + + private void performop(HttpClient mockHttpClient) throws Throwable { + AzureBlobStorageTestAccount testAccount = createTestAccount(); + NativeAzureFileSystem fs = testAccount.getFileSystem(); + + Path testPath = new Path("/", "test.dat"); + + RemoteWasbAuthorizerImpl authorizer = new RemoteWasbAuthorizerImpl(); + authorizer.init(fs.getConf()); + WasbRemoteCallHelper mockWasbRemoteCallHelper = new WasbRemoteCallHelper(); + mockWasbRemoteCallHelper.updateHttpClient(mockHttpClient); + authorizer.updateWasbRemoteCallHelper(mockWasbRemoteCallHelper); + fs.updateWasbAuthorizer(authorizer); + + fs.create(testPath); + ContractTestUtils.assertPathExists(fs, "testPath was not created", testPath); + fs.delete(testPath, false); + } + + private String validJsonResponse() { + return new String( + "{\"responseCode\": 0, \"authorizationResult\": true, \"responseMessage\": \"Authorized\"}" + ); + } + + private String malformedJsonResponse() { + return new String( + "{\"responseCode\": 0, \"authorizationResult\": true, \"responseMessage\":" + ); + } + + private String failureCodeJsonResponse() { + return new String( + "{\"responseCode\": 1, \"authorizationResult\": false, \"responseMessage\": \"Unauthorized\"}" + ); + } + + private StatusLine newStatusLine(int statusCode) { + return new StatusLine() { + @Override + public ProtocolVersion getProtocolVersion() { + return new ProtocolVersion("HTTP", 1, 1); + } + + @Override + public int getStatusCode() { + return statusCode; + } + + @Override + public String getReasonPhrase() { + return "Reason Phrase"; + } + }; + } + + private Header newHeader(String name, String value) { + return new Header() { + @Override + public String getName() { + return name; + } + + @Override + public String getValue() { + return value; + } + + @Override + public HeaderElement[] getElements() throws ParseException { + return new HeaderElement[0]; + } + }; + } +} \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/test/resources/azure-test.xml b/hadoop-tools/hadoop-azure/src/test/resources/azure-test.xml index e898aa631e..c73d6d8489 100644 --- a/hadoop-tools/hadoop-azure/src/test/resources/azure-test.xml +++ b/hadoop-tools/hadoop-azure/src/test/resources/azure-test.xml @@ -19,27 +19,21 @@ + fs.azure.secure.mode false - - fs.azure.local.sas.key.mode - false - - - fs.azure.cred.service.url - {CRED_SERIVCE_URL} - - --> +