Skip to content

Commit 697c093

Browse files
committed
HBASE-28501 Support non-SPNEGO authentication methods and implement session handling in REST java client library
1 parent 156e430 commit 697c093

File tree

1 file changed

+178
-30
lines changed
  • hbase-rest/src/main/java/org/apache/hadoop/hbase/rest/client

1 file changed

+178
-30
lines changed

hbase-rest/src/main/java/org/apache/hadoop/hbase/rest/client/Client.java

Lines changed: 178 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,16 @@
1818
package org.apache.hadoop.hbase.rest.client;
1919

2020
import java.io.BufferedInputStream;
21-
import java.io.ByteArrayInputStream;
2221
import java.io.File;
22+
import java.io.FileOutputStream;
2323
import java.io.IOException;
2424
import java.io.InputStream;
2525
import java.net.URI;
2626
import java.net.URISyntaxException;
2727
import java.net.URL;
2828
import java.nio.file.Files;
29+
import java.nio.file.Path;
30+
import java.security.GeneralSecurityException;
2931
import java.security.KeyManagementException;
3032
import java.security.KeyStore;
3133
import java.security.KeyStoreException;
@@ -44,9 +46,14 @@
4446
import org.apache.hadoop.security.authentication.client.AuthenticatedURL;
4547
import org.apache.hadoop.security.authentication.client.AuthenticationException;
4648
import org.apache.hadoop.security.authentication.client.KerberosAuthenticator;
49+
import org.apache.hadoop.security.ssl.SSLFactory;
50+
import org.apache.hadoop.security.ssl.SSLFactory.Mode;
4751
import org.apache.http.Header;
52+
import org.apache.http.HttpHeaders;
4853
import org.apache.http.HttpResponse;
4954
import org.apache.http.HttpStatus;
55+
import org.apache.http.auth.AuthScope;
56+
import org.apache.http.auth.UsernamePasswordCredentials;
5057
import org.apache.http.client.HttpClient;
5158
import org.apache.http.client.config.RequestConfig;
5259
import org.apache.http.client.methods.HttpDelete;
@@ -55,9 +62,12 @@
5562
import org.apache.http.client.methods.HttpPost;
5663
import org.apache.http.client.methods.HttpPut;
5764
import org.apache.http.client.methods.HttpUriRequest;
58-
import org.apache.http.entity.InputStreamEntity;
65+
import org.apache.http.client.protocol.HttpClientContext;
66+
import org.apache.http.entity.ByteArrayEntity;
67+
import org.apache.http.impl.client.BasicCredentialsProvider;
5968
import org.apache.http.impl.client.HttpClientBuilder;
6069
import org.apache.http.impl.client.HttpClients;
70+
import org.apache.http.impl.cookie.BasicClientCookie;
6171
import org.apache.http.message.BasicHeader;
6272
import org.apache.http.ssl.SSLContexts;
6373
import org.apache.http.util.EntityUtils;
@@ -86,8 +96,11 @@ public class Client {
8696
private boolean sslEnabled;
8797
private HttpResponse resp;
8898
private HttpGet httpGet = null;
89-
99+
private HttpClientContext stickyContext = null;
100+
private BasicCredentialsProvider provider;
101+
private Optional<KeyStore> trustStore;
90102
private Map<String, String> extraHeaders;
103+
private KerberosAuthenticator authenticator;
91104

92105
private static final String AUTH_COOKIE = "hadoop.auth";
93106
private static final String AUTH_COOKIE_EQ = AUTH_COOKIE + "=";
@@ -100,11 +113,13 @@ public Client() {
100113
this(null);
101114
}
102115

103-
private void initialize(Cluster cluster, Configuration conf, boolean sslEnabled,
104-
Optional<KeyStore> trustStore) {
116+
private void initialize(Cluster cluster, Configuration conf, boolean sslEnabled, boolean sticky,
117+
Optional<KeyStore> trustStore, Optional<String> userName, Optional<String> password,
118+
Optional<String> bearerToken) {
105119
this.cluster = cluster;
106120
this.conf = conf;
107121
this.sslEnabled = sslEnabled;
122+
this.trustStore = trustStore;
108123
extraHeaders = new ConcurrentHashMap<>();
109124
String clspath = System.getProperty("java.class.path");
110125
LOG.debug("classpath " + clspath);
@@ -136,38 +151,77 @@ private void initialize(Cluster cluster, Configuration conf, boolean sslEnabled,
136151
}
137152
}
138153

154+
if (userName.isPresent() && password.isPresent()) {
155+
// We want to stick to the old very limited authentication and session handling when sticky is
156+
// not set
157+
// to preserve backwards compatibility
158+
if (!sticky) {
159+
throw new IllegalArgumentException("BASIC auth is only implemented when sticky is set");
160+
}
161+
provider = new BasicCredentialsProvider();
162+
// AuthScope.ANY is required for pre-emptive auth. We only ever use a single auth method
163+
// anyway.
164+
AuthScope anyAuthScope = AuthScope.ANY;
165+
this.provider.setCredentials(anyAuthScope,
166+
new UsernamePasswordCredentials(userName.get(), password.get()));
167+
}
168+
169+
if (bearerToken.isPresent()) {
170+
// We want to stick to the old very limited authentication and session handling when sticky is
171+
// not set
172+
// to preserve backwards compatibility
173+
if (!sticky) {
174+
throw new IllegalArgumentException("BEARER auth is only implemented when sticky is set");
175+
}
176+
// We could also put the header into the context or connection, but that would have the same
177+
// effect.
178+
extraHeaders.put(HttpHeaders.AUTHORIZATION, "Bearer " + bearerToken.get());
179+
}
180+
139181
this.httpClient = httpClientBuilder.build();
182+
setSticky(sticky);
140183
}
141184

142185
/**
143-
* Constructor
186+
* Constructor This constructor will create an object using the old faulty load balancing logic.
187+
* When specifying multiple servers in the cluster object, it is highly recommended to call
188+
* setSticky() on the created client, or use one of the preferred constructors instead.
144189
* @param cluster the cluster definition
145190
*/
146191
public Client(Cluster cluster) {
147192
this(cluster, false);
148193
}
149194

150195
/**
151-
* Constructor
196+
* Constructor This constructor will create an object using the old faulty load balancing logic.
197+
* When specifying multiple servers in the cluster object, it is highly recommended to call
198+
* setSticky() on the created client, or use one of the preferred constructors instead.
152199
* @param cluster the cluster definition
153200
* @param sslEnabled enable SSL or not
154201
*/
155202
public Client(Cluster cluster, boolean sslEnabled) {
156-
initialize(cluster, HBaseConfiguration.create(), sslEnabled, Optional.empty());
203+
initialize(cluster, HBaseConfiguration.create(), sslEnabled, false, Optional.empty(),
204+
Optional.empty(), Optional.empty(), Optional.empty());
157205
}
158206

159207
/**
160-
* Constructor
208+
* Constructor This constructor will create an object using the old faulty load balancing logic.
209+
* When specifying multiple servers in the cluster object, it is highly recommended to call
210+
* setSticky() on the created client, or use one of the preferred constructors instead.
161211
* @param cluster the cluster definition
162212
* @param conf Configuration
163213
* @param sslEnabled enable SSL or not
164214
*/
165215
public Client(Cluster cluster, Configuration conf, boolean sslEnabled) {
166-
initialize(cluster, conf, sslEnabled, Optional.empty());
216+
initialize(cluster, conf, sslEnabled, false, Optional.empty(), Optional.empty(),
217+
Optional.empty(), Optional.empty());
167218
}
168219

169220
/**
170-
* Constructor, allowing to define custom trust store (only for SSL connections)
221+
* Constructor, allowing to define custom trust store (only for SSL connections) This constructor
222+
* will create an object using the old faulty load balancing logic. When specifying multiple
223+
* servers in the cluster object, it is highly recommended to call setSticky() on the created
224+
* client, or use one of the preferred constructors instead.
171225
* @param cluster the cluster definition
172226
* @param trustStorePath custom trust store to use for SSL connections
173227
* @param trustStorePassword password to use for custom trust store
@@ -176,22 +230,56 @@ public Client(Cluster cluster, Configuration conf, boolean sslEnabled) {
176230
*/
177231
public Client(Cluster cluster, String trustStorePath, Optional<String> trustStorePassword,
178232
Optional<String> trustStoreType) {
179-
this(cluster, HBaseConfiguration.create(), trustStorePath, trustStorePassword, trustStoreType);
233+
this(cluster, HBaseConfiguration.create(), true, trustStorePath, trustStorePassword,
234+
trustStoreType);
180235
}
181236

182237
/**
183-
* Constructor, allowing to define custom trust store (only for SSL connections)
238+
* Constructor that accepts an optional trustStore and authentication information for either BASIC
239+
* or BEARER authentication in sticky mode, which does not use the old faulty load balancing
240+
* logic, and enables correct session handling. If neither userName/password, nor the bearer token
241+
* is specified, the client falls back to SPNEGO auth. The loadTrustsore static method can be used
242+
* to load a local trustStore file. This is the preferred constructor to use.
243+
* @param cluster the cluster definition
244+
* @param conf HBase/Hadoop configuration
245+
* @param sslEnabled use HTTPS
246+
* @param trustStore the optional trustStore object
247+
* @param userName for BASIC auth
248+
* @param password for BASIC auth
249+
* @param bearerToken for BEAERER auth
250+
*/
251+
public Client(Cluster cluster, Configuration conf, boolean sslEnabled,
252+
Optional<KeyStore> trustStore, Optional<String> userName, Optional<String> password,
253+
Optional<String> bearerToken) {
254+
initialize(cluster, conf, sslEnabled, true, trustStore, userName, password, bearerToken);
255+
}
256+
257+
/**
258+
* Constructor, allowing to define custom trust store (only for SSL connections) This constructor
259+
* also enables sticky mode. This is a preferred constructor when not using BASIC or JWT
260+
* authentication. Clients created by this will use the old faulty load balancing logic.
184261
* @param cluster the cluster definition
185-
* @param conf Configuration
262+
* @param conf HBase/Hadoop Configuration
186263
* @param trustStorePath custom trust store to use for SSL connections
187264
* @param trustStorePassword password to use for custom trust store
188265
* @param trustStoreType type of custom trust store
189266
* @throws ClientTrustStoreInitializationException if the trust store file can not be loaded
190267
*/
191-
public Client(Cluster cluster, Configuration conf, String trustStorePath,
268+
public Client(Cluster cluster, Configuration conf, boolean sslEnabled, String trustStorePath,
192269
Optional<String> trustStorePassword, Optional<String> trustStoreType) {
270+
KeyStore trustStore = loadTruststore(trustStorePath, trustStorePassword, trustStoreType);
271+
initialize(cluster, conf, sslEnabled, false, Optional.of(trustStore), Optional.empty(),
272+
Optional.empty(), Optional.empty());
273+
}
274+
275+
/**
276+
* Loads a trustStore from the local fileSystem. Can be used to load the trustStore for the
277+
* preferred constructor.
278+
*/
279+
public static KeyStore loadTruststore(String trustStorePath, Optional<String> trustStorePassword,
280+
Optional<String> trustStoreType) {
193281

194-
char[] password = trustStorePassword.map(String::toCharArray).orElse(null);
282+
char[] truststorePassword = trustStorePassword.map(String::toCharArray).orElse(null);
195283
String type = trustStoreType.orElse(KeyStore.getDefaultType());
196284

197285
KeyStore trustStore;
@@ -202,13 +290,12 @@ public Client(Cluster cluster, Configuration conf, String trustStorePath,
202290
}
203291
try (InputStream inputStream =
204292
new BufferedInputStream(Files.newInputStream(new File(trustStorePath).toPath()))) {
205-
trustStore.load(inputStream, password);
293+
trustStore.load(inputStream, truststorePassword);
206294
} catch (CertificateException | NoSuchAlgorithmException | IOException e) {
207295
throw new ClientTrustStoreInitializationException("Trust store load error: " + trustStorePath,
208296
e);
209297
}
210-
211-
initialize(cluster, conf, true, Optional.of(trustStore));
298+
return trustStore;
212299
}
213300

214301
/**
@@ -337,12 +424,24 @@ public HttpResponse executeURI(HttpUriRequest method, Header[] headers, String u
337424
}
338425
long startTime = EnvironmentEdgeManager.currentTime();
339426
if (resp != null) EntityUtils.consumeQuietly(resp.getEntity());
340-
resp = httpClient.execute(method);
427+
if (stickyContext != null) {
428+
resp = httpClient.execute(method, stickyContext);
429+
} else {
430+
resp = httpClient.execute(method);
431+
}
341432
if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_UNAUTHORIZED) {
342433
// Authentication error
343434
LOG.debug("Performing negotiation with the server.");
344-
negotiate(method, uri);
345-
resp = httpClient.execute(method);
435+
try {
436+
negotiate(method, uri);
437+
} catch (GeneralSecurityException e) {
438+
throw new IOException(e);
439+
}
440+
if (stickyContext != null) {
441+
resp = httpClient.execute(method, stickyContext);
442+
} else {
443+
resp = httpClient.execute(method);
444+
}
346445
}
347446

348447
long endTime = EnvironmentEdgeManager.currentTime();
@@ -377,19 +476,58 @@ public HttpResponse execute(Cluster cluster, HttpUriRequest method, Header[] hea
377476
* @param uri the String to parse as a URL.
378477
* @throws IOException if unknown protocol is found.
379478
*/
380-
private void negotiate(HttpUriRequest method, String uri) throws IOException {
479+
private void negotiate(HttpUriRequest method, String uri)
480+
throws IOException, GeneralSecurityException {
381481
try {
382482
AuthenticatedURL.Token token = new AuthenticatedURL.Token();
383-
KerberosAuthenticator authenticator = new KerberosAuthenticator();
384-
authenticator.authenticate(new URL(uri), token);
385-
// Inject the obtained negotiated token in the method cookie
386-
injectToken(method, token);
483+
if (authenticator == null) {
484+
authenticator = new KerberosAuthenticator();
485+
if (trustStore.isPresent()) {
486+
// The authenticator does not use Apache HttpClient, so we need to
487+
// configure it separately to use the specified trustStore
488+
Configuration sslConf = setupTrustStoreForHadoop(trustStore.get());
489+
SSLFactory sslFactory = new SSLFactory(Mode.CLIENT, sslConf);
490+
sslFactory.init();
491+
authenticator.setConnectionConfigurator(sslFactory);
492+
}
493+
}
494+
URL url = new URL(uri);
495+
authenticator.authenticate(url, token);
496+
if (sticky) {
497+
BasicClientCookie authCookie = new BasicClientCookie("hadoop.auth", token.toString());
498+
// Hadoop eats the domain even if set by server
499+
authCookie.setDomain(url.getHost());
500+
stickyContext.getCookieStore().addCookie(authCookie);
501+
} else {
502+
// session cookie is NOT set for backwards compatibility for non-sticky mode
503+
// Inject the obtained negotiated token in the method cookie
504+
// This is only done for this single request, the next one will trigger a new SPENGO
505+
// handshake
506+
injectToken(method, token);
507+
}
387508
} catch (AuthenticationException e) {
388509
LOG.error("Failed to negotiate with the server.", e);
389510
throw new IOException(e);
390511
}
391512
}
392513

514+
private Configuration setupTrustStoreForHadoop(KeyStore trustStore)
515+
throws IOException, KeyStoreException, NoSuchAlgorithmException, CertificateException {
516+
Path tmpDirPath = Files.createTempDirectory("hbase_rest_client_truststore");
517+
File trustStoreFile = tmpDirPath.resolve("truststore.jks").toFile();
518+
// Shouldn't be needed with the secure temp dir, but let's generate a password anyway
519+
String password = Double.toString(Math.random());
520+
try (FileOutputStream fos = new FileOutputStream(trustStoreFile)) {
521+
trustStore.store(fos, password.toCharArray());
522+
}
523+
524+
Configuration sslConf = new Configuration();
525+
// Type is the Java default, we use the same JVM to read this back
526+
sslConf.set("ssl.client.keystore.location", trustStoreFile.getAbsolutePath());
527+
sslConf.set("ssl.client.keystore.password", password);
528+
return sslConf;
529+
}
530+
393531
/**
394532
* Helper method that injects an authentication token to send with the method.
395533
* @param method method to inject the authentication token into.
@@ -431,11 +569,21 @@ public boolean isSticky() {
431569
* The default behaviour is load balancing by sending each request to a random host. This DOES NOT
432570
* work with scans, which have state on the REST servers. Set sticky to true before attempting
433571
* Scan related operations if more than one host is defined in the cluster. Nodes must not be
434-
* added or removed from the Cluster object while sticky is true.
572+
* added or removed from the Cluster object while sticky is true. Setting the sticky flag also
573+
* enables session handling, which eliminates the need to re-authenticate each request, and lets
574+
* the client handle any other cookies (like the sticky cookie set by load balancers) correctly.
435575
* @param sticky whether subsequent requests will use the same host
436576
*/
437577
public void setSticky(boolean sticky) {
438578
lastNodeId = null;
579+
if (sticky) {
580+
stickyContext = new HttpClientContext();
581+
if (provider != null) {
582+
stickyContext.setCredentialsProvider(provider);
583+
}
584+
} else {
585+
stickyContext = null;
586+
}
439587
this.sticky = sticky;
440588
}
441589

@@ -654,7 +802,7 @@ public Response put(Cluster cluster, String path, Header[] headers, byte[] conte
654802
throws IOException {
655803
HttpPut method = new HttpPut(path);
656804
try {
657-
method.setEntity(new InputStreamEntity(new ByteArrayInputStream(content), content.length));
805+
method.setEntity(new ByteArrayEntity(content));
658806
HttpResponse resp = execute(cluster, method, headers, path);
659807
headers = resp.getAllHeaders();
660808
content = getResponseBody(resp);
@@ -748,7 +896,7 @@ public Response post(Cluster cluster, String path, Header[] headers, byte[] cont
748896
throws IOException {
749897
HttpPost method = new HttpPost(path);
750898
try {
751-
method.setEntity(new InputStreamEntity(new ByteArrayInputStream(content), content.length));
899+
method.setEntity(new ByteArrayEntity(content));
752900
HttpResponse resp = execute(cluster, method, headers, path);
753901
headers = resp.getAllHeaders();
754902
content = getResponseBody(resp);

0 commit comments

Comments
 (0)