Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,9 @@ public interface HdfsClientConfigKeys {
"dfs.client.output.stream.uniq.default.key";
String DFS_OUTPUT_STREAM_UNIQ_DEFAULT_KEY_DEFAULT = "DEFAULT";

String DFS_CLIENT_WEBHDFS_USE_BASE_PATH_KEY = "dfs.client.webhdfs.use-base-path";
boolean DFS_CLIENT_WEBHDFS_USE_BASE_PATH_DEFAULT = false;

/**
* These are deprecated config keys to client code.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* 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.hdfs.web;

import org.apache.hadoop.security.authentication.client.ConnectionConfigurator;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

/**
* This class adds basic authentication to the connection,
* allowing users to access webhdfs over HTTP with basic authentication,
* for example when using Apache Knox.
*/
public class BasicAuthConfigurator implements ConnectionConfigurator {
private final ConnectionConfigurator parent;
private final String credentials;

static public ConnectionConfigurator getConfigurator(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this method has to be called by URLConnectionFactory which is in same package. Lets have it package-protected and remove public

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this method be moved to URLConnectionFactory class only?
Reason being, when external class calls BasicAuthConfigurator.getConfigurator, to developer it can look that we want something out of BasicAuthConfigurator, but instead it either gives BasicAuthConfigurator object or parent-configurator object. What you feel?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, we might as well move it to URLConnectionFactory and make it private there. I've made the change.

ConnectionConfigurator configurator, String basicAuthCred
) {
if (basicAuthCred != null && !basicAuthCred.isEmpty()) {
return new BasicAuthConfigurator(configurator, basicAuthCred);
} else {
return configurator;
}
}

/**
* @param credentials a string of the form "username:password"
*/
public BasicAuthConfigurator(
ConnectionConfigurator parent,
String credentials
) {
this.parent = parent;
this.credentials = credentials;
}

@Override
public HttpURLConnection configure(HttpURLConnection conn) throws IOException {
if (parent != null) {
parent.configure(conn);
}

if (credentials != null && !credentials.equals("")) {
conn.setRequestProperty(
"AUTHORIZATION",
"Basic " + Base64.getEncoder().encodeToString(
credentials.getBytes(StandardCharsets.UTF_8)
)
);
}

return conn;
}

public void destroy() {
if (parent != null && parent instanceof SSLConnectionConfigurator) {
((SSLConnectionConfigurator)parent).destroy();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ public HttpURLConnection configure(HttpURLConnection conn)
public static URLConnectionFactory newDefaultURLConnectionFactory(
Configuration conf) {
ConnectionConfigurator conn = getSSLConnectionConfiguration(
DEFAULT_SOCKET_TIMEOUT, DEFAULT_SOCKET_TIMEOUT, conf);
DEFAULT_SOCKET_TIMEOUT, DEFAULT_SOCKET_TIMEOUT, conf, null);

return new URLConnectionFactory(conn);
}
Expand All @@ -89,12 +89,28 @@ public static URLConnectionFactory newDefaultURLConnectionFactory(
public static URLConnectionFactory newDefaultURLConnectionFactory(
int connectTimeout, int readTimeout, Configuration conf) {
ConnectionConfigurator conn = getSSLConnectionConfiguration(
connectTimeout, readTimeout, conf);
connectTimeout, readTimeout, conf, null);
return new URLConnectionFactory(conn);
}

/**
* Construct a new URLConnectionFactory based on the configuration. It will
* honor connecTimeout and readTimeout when they are specified and allows
* specifying credentials for HTTP Basic Authentication in "username:password" format.
*/
public static URLConnectionFactory newDefaultURLConnectionFactory(
int connectTimeout, int readTimeout, Configuration conf, String basicAuthCredentials) {
ConnectionConfigurator conn = getSSLConnectionConfiguration(
connectTimeout, readTimeout, conf, basicAuthCredentials);
return new URLConnectionFactory(conn);
}

private static ConnectionConfigurator getSSLConnectionConfiguration(
final int connectTimeout, final int readTimeout, Configuration conf) {
final int connectTimeout,
final int readTimeout,
Configuration conf,
String basicAuthCredentials
) {
ConnectionConfigurator conn;
try {
conn = new SSLConnectionConfigurator(connectTimeout, readTimeout, conf);
Expand All @@ -120,7 +136,7 @@ public HttpURLConnection configure(HttpURLConnection connection)
}
}

return conn;
return BasicAuthConfigurator.getConfigurator(conn, basicAuthCredentials);
}

/**
Expand All @@ -130,6 +146,17 @@ public HttpURLConnection configure(HttpURLConnection connection)
public static URLConnectionFactory newOAuth2URLConnectionFactory(
int connectTimeout, int readTimeout, Configuration conf)
throws IOException {
return newOAuth2URLConnectionFactory(connectTimeout, readTimeout, conf, null);
}

/**
* Construct a new URLConnectionFactory that supports OAuth-based connections.
* It will also try to load the SSL configuration when they are specified. Furthermore, it allows
* specifying credentials for HTTP Basic Authentication in "username:password" format.
*/
public static URLConnectionFactory newOAuth2URLConnectionFactory(
int connectTimeout, int readTimeout, Configuration conf, String basicAuthCredentials)
throws IOException {
ConnectionConfigurator conn;
try {
ConnectionConfigurator sslConnConfigurator
Expand All @@ -139,6 +166,7 @@ public static URLConnectionFactory newOAuth2URLConnectionFactory(
} catch (Exception e) {
throw new IOException("Unable to load OAuth2 connection factory.", e);
}
conn = BasicAuthConfigurator.getConfigurator(conn, basicAuthCredentials);
return new URLConnectionFactory(conn);
}

Expand Down Expand Up @@ -214,5 +242,8 @@ public void destroy() {
if (connConfigurator instanceof SSLConnectionConfigurator) {
((SSLConnectionConfigurator) connConfigurator).destroy();
}
if (connConfigurator instanceof BasicAuthConfigurator) {
((BasicAuthConfigurator) connConfigurator).destroy();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ public class WebHdfsFileSystem extends FileSystem
private boolean isTLSKrb;

private boolean isServerHCFSCompatible = true;
private String pathPrefix = "";

/**
* Return the protocol scheme for the FileSystem.
Expand Down Expand Up @@ -238,17 +239,19 @@ public synchronized void initialize(URI uri, Configuration conf
if(isOAuth) {
LOG.debug("Enabling OAuth2 in WebHDFS");
connectionFactory = URLConnectionFactory
.newOAuth2URLConnectionFactory(connectTimeout, readTimeout, conf);
.newOAuth2URLConnectionFactory(connectTimeout, readTimeout, conf, uri.getUserInfo());
} else {
LOG.debug("Not enabling OAuth2 in WebHDFS");
connectionFactory = URLConnectionFactory
.newDefaultURLConnectionFactory(connectTimeout, readTimeout, conf);
.newDefaultURLConnectionFactory(connectTimeout, readTimeout, conf, uri.getUserInfo());
}

this.isTLSKrb = "HTTPS_ONLY".equals(conf.get(DFS_HTTP_POLICY_KEY));

ugi = UserGroupInformation.getCurrentUser();
this.uri = URI.create(uri.getScheme() + "://" + uri.getAuthority());
// Drop path and user:password from URI.
this.uri = URI.create(uri.getScheme() + "://" + uri.getHost()
+ (uri.getPort() == -1 ? "" : ":" + uri.getPort()));
this.nnAddrs = resolveNNAddr();

boolean isHA = HAUtilClient.isClientFailoverConfigured(conf, this.uri);
Expand Down Expand Up @@ -307,6 +310,20 @@ public StorageStatistics provide() {
return new DFSOpsCountStatistics();
}
});
pathPrefix = PATH_PREFIX;
boolean useBasePath = conf.getBoolean(
HdfsClientConfigKeys.DFS_CLIENT_WEBHDFS_USE_BASE_PATH_KEY,
HdfsClientConfigKeys.DFS_CLIENT_WEBHDFS_USE_BASE_PATH_DEFAULT
);
if (uri.getPath() != null && !uri.getPath().equals("") && useBasePath) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

although uri can not be null, but better to have null-check on that.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a uri != null check 👍

pathPrefix = uri.getPath();
if (pathPrefix.endsWith("/")) {
pathPrefix = pathPrefix.substring(0, pathPrefix.length() - 1);
}
if (!pathPrefix.endsWith(PATH_PREFIX)) {
pathPrefix += PATH_PREFIX;
}
}
}

/**
Expand Down Expand Up @@ -634,7 +651,7 @@ URL toUrl(final HttpOpParam.Op op, final Path fspath,
final Param<?,?>... parameters) throws IOException {
//initialize URI path and query

final String path = PATH_PREFIX
final String path = pathPrefix
+ (fspath == null? "/": makeQualified(fspath).toUri().getRawPath());
final String query = op.toQueryString()
+ Param.toSortedString("&", getAuthParameters(op))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/**
* 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.hdfs.web;

import org.apache.hadoop.security.authentication.client.ConnectionConfigurator;
import org.junit.Test;
import org.mockito.Mockito;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

public class TestBasicAuthConfigurator {
@Test
public void testNullCredentials() throws IOException {
ConnectionConfigurator conf = new BasicAuthConfigurator(null, null);
HttpURLConnection conn = Mockito.mock(HttpURLConnection.class);
conf.configure(conn);
Mockito.verify(conn, Mockito.never()).setRequestProperty(Mockito.any(), Mockito.any());
}

@Test
public void testEmptyCredentials() throws IOException {
ConnectionConfigurator conf = new BasicAuthConfigurator(null, "");
HttpURLConnection conn = Mockito.mock(HttpURLConnection.class);
conf.configure(conn);
Mockito.verify(conn, Mockito.never()).setRequestProperty(Mockito.any(), Mockito.any());
}

@Test
public void testCredentialsSet() throws IOException {
String credentials = "user:pass";
ConnectionConfigurator conf = new BasicAuthConfigurator(null, credentials);
HttpURLConnection conn = Mockito.mock(HttpURLConnection.class);
conf.configure(conn);
Mockito.verify(conn, Mockito.times(1)).setRequestProperty(
"AUTHORIZATION",
"Basic " + Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8))
);
}

@Test
public void testParentConfigurator() throws IOException {
ConnectionConfigurator parent = Mockito.mock(ConnectionConfigurator.class);
String credentials = "user:pass";
ConnectionConfigurator conf = new BasicAuthConfigurator(parent, credentials);
HttpURLConnection conn = Mockito.mock(HttpURLConnection.class);
conf.configure(conn);
Mockito.verify(conn, Mockito.times(1)).setRequestProperty(
"AUTHORIZATION",
"Basic " + Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8))
);
Mockito.verify(parent, Mockito.times(1)).configure(conn);
}

@Test
public void testCredentialsSetWithUtfAndSpecialCharacters() throws IOException {
String credentials = "\uD80C\uDD04@ą:pass";
ConnectionConfigurator conf = new BasicAuthConfigurator(null, credentials);
HttpURLConnection conn = Mockito.mock(HttpURLConnection.class);
conf.configure(conn);
Mockito.verify(conn, Mockito.times(1)).setRequestProperty(
"AUTHORIZATION",
"Basic " + Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8))
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6472,4 +6472,12 @@
Enables observer reads for clients. This should only be enabled when clients are using routers.
</description>
</property>
<property>
<name>dfs.client.webhdfs.use-base-path</name>
<value>false</value>
<description>
Enables webhdfs FS clients to use specified filesystem URL path as the prefix for API endpoint.
Useful when using a proxy server like Apache Knox, which requires prefixing requests with /gateway/name.
</description>
</property>
</configuration>
Loading