001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.hbase.rest;
019
020import static org.apache.hadoop.hbase.rest.RESTServlet.HBASE_REST_SUPPORT_PROXYUSER;
021import static org.junit.jupiter.api.Assertions.assertEquals;
022import static org.junit.jupiter.api.Assertions.assertTrue;
023
024import com.fasterxml.jackson.databind.ObjectMapper;
025import java.io.File;
026import java.io.IOException;
027import java.net.HttpURLConnection;
028import java.net.URL;
029import java.security.Principal;
030import java.security.PrivilegedExceptionAction;
031import org.apache.commons.io.FileUtils;
032import org.apache.hadoop.conf.Configuration;
033import org.apache.hadoop.hbase.HBaseTestingUtil;
034import org.apache.hadoop.hbase.SingleProcessHBaseCluster;
035import org.apache.hadoop.hbase.StartTestingClusterOption;
036import org.apache.hadoop.hbase.TableName;
037import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder;
038import org.apache.hadoop.hbase.client.Connection;
039import org.apache.hadoop.hbase.client.ConnectionFactory;
040import org.apache.hadoop.hbase.client.Put;
041import org.apache.hadoop.hbase.client.Table;
042import org.apache.hadoop.hbase.client.TableDescriptor;
043import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
044import org.apache.hadoop.hbase.coprocessor.CoprocessorHost;
045import org.apache.hadoop.hbase.http.ssl.KeyStoreTestUtil;
046import org.apache.hadoop.hbase.rest.model.CellModel;
047import org.apache.hadoop.hbase.rest.model.CellSetModel;
048import org.apache.hadoop.hbase.rest.model.RowModel;
049import org.apache.hadoop.hbase.rest.model.ScannerModel;
050import org.apache.hadoop.hbase.security.HBaseKerberosUtils;
051import org.apache.hadoop.hbase.security.access.AccessControlClient;
052import org.apache.hadoop.hbase.security.access.AccessControlConstants;
053import org.apache.hadoop.hbase.security.access.AccessController;
054import org.apache.hadoop.hbase.security.access.Permission.Action;
055import org.apache.hadoop.hbase.security.token.TokenProvider;
056import org.apache.hadoop.hbase.testclassification.MediumTests;
057import org.apache.hadoop.hbase.testclassification.MiscTests;
058import org.apache.hadoop.hbase.util.Bytes;
059import org.apache.hadoop.hbase.util.Pair;
060import org.apache.hadoop.hdfs.DFSConfigKeys;
061import org.apache.hadoop.http.HttpConfig;
062import org.apache.hadoop.minikdc.MiniKdc;
063import org.apache.hadoop.security.UserGroupInformation;
064import org.apache.hadoop.security.authentication.util.KerberosName;
065import org.apache.http.HttpEntity;
066import org.apache.http.HttpHost;
067import org.apache.http.auth.AuthSchemeProvider;
068import org.apache.http.auth.AuthScope;
069import org.apache.http.auth.Credentials;
070import org.apache.http.client.AuthCache;
071import org.apache.http.client.CredentialsProvider;
072import org.apache.http.client.config.AuthSchemes;
073import org.apache.http.client.methods.CloseableHttpResponse;
074import org.apache.http.client.methods.HttpDelete;
075import org.apache.http.client.methods.HttpGet;
076import org.apache.http.client.methods.HttpPost;
077import org.apache.http.client.methods.HttpPut;
078import org.apache.http.client.protocol.HttpClientContext;
079import org.apache.http.config.Registry;
080import org.apache.http.config.RegistryBuilder;
081import org.apache.http.conn.HttpClientConnectionManager;
082import org.apache.http.entity.ContentType;
083import org.apache.http.entity.StringEntity;
084import org.apache.http.impl.auth.SPNegoSchemeFactory;
085import org.apache.http.impl.client.BasicAuthCache;
086import org.apache.http.impl.client.BasicCredentialsProvider;
087import org.apache.http.impl.client.CloseableHttpClient;
088import org.apache.http.impl.client.HttpClients;
089import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
090import org.apache.http.util.EntityUtils;
091import org.junit.jupiter.api.AfterAll;
092import org.junit.jupiter.api.BeforeAll;
093import org.junit.jupiter.api.Tag;
094import org.junit.jupiter.api.Test;
095import org.slf4j.Logger;
096import org.slf4j.LoggerFactory;
097
098import org.apache.hbase.thirdparty.com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider;
099import org.apache.hbase.thirdparty.javax.ws.rs.core.MediaType;
100
101/**
102 * Test class for SPNEGO authentication on the HttpServer. Uses Kerby's MiniKDC and Apache
103 * HttpComponents to verify that a simple Servlet is reachable via SPNEGO and unreachable w/o.
104 */
105@Tag(MiscTests.TAG)
106@Tag(MediumTests.TAG)
107public class TestSecureRESTServer {
108
109  private static final Logger LOG = LoggerFactory.getLogger(TestSecureRESTServer.class);
110  private static final HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil();
111  private static final HBaseRESTTestingUtility REST_TEST = new HBaseRESTTestingUtility();
112  private static SingleProcessHBaseCluster CLUSTER;
113
114  private static final String HOSTNAME = "localhost";
115  private static final String CLIENT_PRINCIPAL = "client";
116  private static final String CLIENT_PRINCIPAL2 = "client2";
117  private static final String WHEEL_PRINCIPAL = "wheel";
118  // The principal for accepting SPNEGO authn'ed requests (*must* be HTTP/fqdn)
119  private static final String SPNEGO_SERVICE_PRINCIPAL = "HTTP/" + HOSTNAME;
120  // The principal we use to connect to HBase
121  private static final String REST_SERVER_PRINCIPAL = "rest";
122  private static final String SERVICE_PRINCIPAL = "hbase/" + HOSTNAME;
123
124  private static URL baseUrl;
125  private static MiniKdc KDC;
126  private static RESTServer server;
127  private static File restServerKeytab;
128  private static File clientKeytab;
129  private static File wheelKeytab;
130  private static File serviceKeytab;
131
132  @BeforeAll
133  public static void setupServer() throws Exception {
134    final File target = new File(System.getProperty("user.dir"), "target");
135    assertTrue(target.exists());
136
137    /*
138     * Keytabs
139     */
140    File keytabDir = new File(target, TestSecureRESTServer.class.getSimpleName() + "_keytabs");
141    if (keytabDir.exists()) {
142      FileUtils.deleteDirectory(keytabDir);
143    }
144    keytabDir.mkdirs();
145    // Keytab for HBase services (RS, Master)
146    serviceKeytab = new File(keytabDir, "hbase.service.keytab");
147    // The keytab for the REST server
148    restServerKeytab = new File(keytabDir, "spnego.keytab");
149    // Keytab for the client
150    clientKeytab = new File(keytabDir, CLIENT_PRINCIPAL + ".keytab");
151    // Keytab for wheel
152    wheelKeytab = new File(keytabDir, WHEEL_PRINCIPAL + ".keytab");
153
154    /*
155     * Update UGI
156     */
157    Configuration conf = TEST_UTIL.getConfiguration();
158
159    /*
160     * Start KDC
161     */
162    KDC = TEST_UTIL.setupMiniKdc(serviceKeytab);
163    KDC.createPrincipal(clientKeytab, CLIENT_PRINCIPAL, CLIENT_PRINCIPAL2);
164    KDC.createPrincipal(wheelKeytab, WHEEL_PRINCIPAL);
165    KDC.createPrincipal(serviceKeytab, SERVICE_PRINCIPAL);
166    // REST server's keytab contains keys for both principals REST uses
167    KDC.createPrincipal(restServerKeytab, SPNEGO_SERVICE_PRINCIPAL, REST_SERVER_PRINCIPAL);
168
169    // Set configuration for HBase
170    HBaseKerberosUtils.setPrincipalForTesting(SERVICE_PRINCIPAL + "@" + KDC.getRealm());
171    HBaseKerberosUtils.setKeytabFileForTesting(serviceKeytab.getAbsolutePath());
172    // Why doesn't `setKeytabFileForTesting` do this?
173    conf.set("hbase.master.keytab.file", serviceKeytab.getAbsolutePath());
174    conf.set("hbase.unsafe.regionserver.hostname", "localhost");
175    conf.set("hbase.master.hostname", "localhost");
176    HBaseKerberosUtils.setSecuredConfiguration(conf, SERVICE_PRINCIPAL + "@" + KDC.getRealm(),
177      SPNEGO_SERVICE_PRINCIPAL + "@" + KDC.getRealm());
178    setHdfsSecuredConfiguration(conf);
179    conf.setStrings(CoprocessorHost.REGION_COPROCESSOR_CONF_KEY, TokenProvider.class.getName(),
180      AccessController.class.getName());
181    conf.setStrings(CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY, AccessController.class.getName());
182    conf.setStrings(CoprocessorHost.REGIONSERVER_COPROCESSOR_CONF_KEY,
183      AccessController.class.getName());
184    // Enable EXEC permission checking
185    conf.setBoolean(AccessControlConstants.EXEC_PERMISSION_CHECKS_KEY, true);
186    conf.set("hbase.superuser", "hbase");
187    conf.set("hadoop.proxyuser.rest.hosts", "*");
188    conf.set("hadoop.proxyuser.rest.users", "*");
189    conf.set("hadoop.proxyuser.wheel.hosts", "*");
190    conf.set("hadoop.proxyuser.wheel.users", "*");
191    UserGroupInformation.setConfiguration(conf);
192
193    updateKerberosConfiguration(conf, REST_SERVER_PRINCIPAL, SPNEGO_SERVICE_PRINCIPAL,
194      restServerKeytab);
195
196    // Start HBase
197    TEST_UTIL.startMiniCluster(StartTestingClusterOption.builder().numMasters(1).numRegionServers(1)
198      .numZkServers(1).build());
199
200    // Start REST
201    UserGroupInformation restUser = UserGroupInformation
202      .loginUserFromKeytabAndReturnUGI(REST_SERVER_PRINCIPAL, restServerKeytab.getAbsolutePath());
203    restUser.doAs(new PrivilegedExceptionAction<Void>() {
204      @Override
205      public Void run() throws Exception {
206        REST_TEST.startServletContainer(conf);
207        return null;
208      }
209    });
210    baseUrl = new URL("http://localhost:" + REST_TEST.getServletPort());
211
212    LOG.info("HTTP server started: " + baseUrl);
213    TEST_UTIL.waitTableAvailable(TableName.valueOf("hbase:acl"));
214
215    // Let the REST server create, read, and write globally
216    UserGroupInformation superuser = UserGroupInformation
217      .loginUserFromKeytabAndReturnUGI(SERVICE_PRINCIPAL, serviceKeytab.getAbsolutePath());
218    superuser.doAs(new PrivilegedExceptionAction<Void>() {
219      @Override
220      public Void run() throws Exception {
221        try (Connection conn = ConnectionFactory.createConnection(TEST_UTIL.getConfiguration())) {
222          AccessControlClient.grant(conn, REST_SERVER_PRINCIPAL, Action.CREATE, Action.READ,
223            Action.WRITE);
224        } catch (Throwable t) {
225          if (t instanceof Exception) {
226            throw (Exception) t;
227          } else {
228            throw new Exception(t);
229          }
230        }
231        return null;
232      }
233    });
234    instertData();
235  }
236
237  @AfterAll
238  public static void stopServer() throws Exception {
239    try {
240      if (null != server) {
241        server.stop();
242      }
243    } catch (Exception e) {
244      LOG.info("Failed to stop info server", e);
245    }
246    try {
247      if (CLUSTER != null) {
248        CLUSTER.shutdown();
249      }
250    } catch (Exception e) {
251      LOG.info("Failed to stop HBase cluster", e);
252    }
253    try {
254      if (null != KDC) {
255        KDC.stop();
256      }
257    } catch (Exception e) {
258      LOG.info("Failed to stop mini KDC", e);
259    }
260  }
261
262  private static void setHdfsSecuredConfiguration(Configuration conf) throws Exception {
263    // Set principal+keytab configuration for HDFS
264    conf.set(DFSConfigKeys.DFS_NAMENODE_KERBEROS_PRINCIPAL_KEY,
265      SERVICE_PRINCIPAL + "@" + KDC.getRealm());
266    conf.set(DFSConfigKeys.DFS_NAMENODE_KEYTAB_FILE_KEY, serviceKeytab.getAbsolutePath());
267    conf.set(DFSConfigKeys.DFS_DATANODE_KERBEROS_PRINCIPAL_KEY,
268      SERVICE_PRINCIPAL + "@" + KDC.getRealm());
269    conf.set(DFSConfigKeys.DFS_DATANODE_KEYTAB_FILE_KEY, serviceKeytab.getAbsolutePath());
270    conf.set(DFSConfigKeys.DFS_WEB_AUTHENTICATION_KERBEROS_PRINCIPAL_KEY,
271      SPNEGO_SERVICE_PRINCIPAL + "@" + KDC.getRealm());
272    // Enable token access for HDFS blocks
273    conf.setBoolean(DFSConfigKeys.DFS_BLOCK_ACCESS_TOKEN_ENABLE_KEY, true);
274    // Only use HTTPS (required because we aren't using "secure" ports)
275    conf.set(DFSConfigKeys.DFS_HTTP_POLICY_KEY, HttpConfig.Policy.HTTPS_ONLY.name());
276    // Bind on localhost for spnego to have a chance at working
277    conf.set(DFSConfigKeys.DFS_NAMENODE_HTTPS_ADDRESS_KEY, "localhost:0");
278    conf.set(DFSConfigKeys.DFS_DATANODE_HTTPS_ADDRESS_KEY, "localhost:0");
279
280    // Generate SSL certs
281    File keystoresDir = new File(TEST_UTIL.getDataTestDir("keystore").toUri().getPath());
282    keystoresDir.mkdirs();
283    String sslConfDir = KeyStoreTestUtil.getClasspathDir(TestSecureRESTServer.class);
284    KeyStoreTestUtil.setupSSLConfig(keystoresDir.getAbsolutePath(), sslConfDir, conf, false);
285
286    // Magic flag to tell hdfs to not fail on using ports above 1024
287    conf.setBoolean("ignore.secure.ports.for.testing", true);
288  }
289
290  private static void updateKerberosConfiguration(Configuration conf, String serverPrincipal,
291    String spnegoPrincipal, File serverKeytab) {
292    KerberosName.setRules("DEFAULT");
293
294    // Enable Kerberos (pre-req)
295    conf.set("hbase.security.authentication", "kerberos");
296    conf.set(RESTServer.REST_AUTHENTICATION_TYPE, "kerberos");
297    // User to talk to HBase as
298    conf.set(RESTServer.REST_KERBEROS_PRINCIPAL, serverPrincipal);
299    // User to accept SPNEGO-auth'd http calls as
300    conf.set("hbase.rest.authentication.kerberos.principal", spnegoPrincipal);
301    // Keytab for both principals above
302    conf.set(RESTServer.REST_KEYTAB_FILE, serverKeytab.getAbsolutePath());
303    conf.set("hbase.rest.authentication.kerberos.keytab", serverKeytab.getAbsolutePath());
304    conf.set(HBASE_REST_SUPPORT_PROXYUSER, "true");
305  }
306
307  private static void instertData() throws IOException, InterruptedException {
308    // Create a table, write a row to it, grant read perms to the client
309    UserGroupInformation superuser = UserGroupInformation
310      .loginUserFromKeytabAndReturnUGI(SERVICE_PRINCIPAL, serviceKeytab.getAbsolutePath());
311    final TableName table = TableName.valueOf("publicTable");
312    superuser.doAs(new PrivilegedExceptionAction<Void>() {
313      @Override
314      public Void run() throws Exception {
315        try (Connection conn = ConnectionFactory.createConnection(TEST_UTIL.getConfiguration())) {
316          TableDescriptor desc = TableDescriptorBuilder.newBuilder(table)
317            .setColumnFamily(ColumnFamilyDescriptorBuilder.of("f1")).build();
318          conn.getAdmin().createTable(desc);
319          try (Table t = conn.getTable(table)) {
320            Put p = new Put(Bytes.toBytes("a"));
321            p.addColumn(Bytes.toBytes("f1"), new byte[0], Bytes.toBytes("1"));
322            t.put(p);
323          }
324          AccessControlClient.grant(conn, CLIENT_PRINCIPAL, Action.READ);
325        } catch (Throwable e) {
326          if (e instanceof Exception) {
327            throw (Exception) e;
328          } else {
329            throw new Exception(e);
330          }
331        }
332        return null;
333      }
334    });
335  }
336
337  private void testProxy(String extraArgs, String PRINCIPAL, File keytab, int responseCode)
338    throws Exception {
339    UserGroupInformation.loginUserFromKeytabAndReturnUGI(SERVICE_PRINCIPAL,
340      serviceKeytab.getAbsolutePath());
341    final TableName table = TableName.valueOf("publicTable");
342
343    // Read that row as the client
344    Pair<CloseableHttpClient, HttpClientContext> pair = getClient();
345    CloseableHttpClient client = pair.getFirst();
346    HttpClientContext context = pair.getSecond();
347
348    HttpGet get = new HttpGet(new URL("http://localhost:" + REST_TEST.getServletPort()).toURI()
349      + "/" + table + "/a" + extraArgs);
350    get.addHeader("Accept", "application/json");
351    UserGroupInformation user =
352      UserGroupInformation.loginUserFromKeytabAndReturnUGI(PRINCIPAL, keytab.getAbsolutePath());
353    String jsonResponse = user.doAs(new PrivilegedExceptionAction<String>() {
354      @Override
355      public String run() throws Exception {
356        try (CloseableHttpResponse response = client.execute(get, context)) {
357          final int statusCode = response.getStatusLine().getStatusCode();
358          assertEquals(responseCode, statusCode, response.getStatusLine().toString());
359          HttpEntity entity = response.getEntity();
360          return EntityUtils.toString(entity);
361        }
362      }
363    });
364    if (responseCode == HttpURLConnection.HTTP_OK) {
365      ObjectMapper mapper = new JacksonJaxbJsonProvider().locateMapper(CellSetModel.class,
366        MediaType.APPLICATION_JSON_TYPE);
367      CellSetModel model = mapper.readValue(jsonResponse, CellSetModel.class);
368      assertEquals(1, model.getRows().size());
369      RowModel row = model.getRows().get(0);
370      assertEquals("a", Bytes.toString(row.getKey()));
371      assertEquals(1, row.getCells().size());
372      CellModel cell = row.getCells().get(0);
373      assertEquals("1", Bytes.toString(cell.getValue()));
374    }
375  }
376
377  @Test
378  public void testPositiveAuthorization() throws Exception {
379    testProxy("", CLIENT_PRINCIPAL, clientKeytab, HttpURLConnection.HTTP_OK);
380  }
381
382  @Test
383  public void testDoAs() throws Exception {
384    testProxy("?doAs=" + CLIENT_PRINCIPAL, WHEEL_PRINCIPAL, wheelKeytab, HttpURLConnection.HTTP_OK);
385  }
386
387  @Test
388  public void testDoas() throws Exception {
389    testProxy("?doas=" + CLIENT_PRINCIPAL, WHEEL_PRINCIPAL, wheelKeytab, HttpURLConnection.HTTP_OK);
390  }
391
392  @Test
393  public void testWithoutDoAs() throws Exception {
394    testProxy("", WHEEL_PRINCIPAL, wheelKeytab, HttpURLConnection.HTTP_FORBIDDEN);
395  }
396
397  @Test
398  public void testNegativeAuthorization() throws Exception {
399    Pair<CloseableHttpClient, HttpClientContext> pair = getClient();
400    CloseableHttpClient client = pair.getFirst();
401    HttpClientContext context = pair.getSecond();
402
403    StringEntity entity = new StringEntity(
404      "{\"name\":\"test\", \"ColumnSchema\":[{\"name\":\"f\"}]}", ContentType.APPLICATION_JSON);
405    HttpPut put = new HttpPut("http://localhost:" + REST_TEST.getServletPort() + "/test/schema");
406    put.setEntity(entity);
407
408    UserGroupInformation unprivileged = UserGroupInformation
409      .loginUserFromKeytabAndReturnUGI(CLIENT_PRINCIPAL, clientKeytab.getAbsolutePath());
410    unprivileged.doAs(new PrivilegedExceptionAction<Void>() {
411      @Override
412      public Void run() throws Exception {
413        try (CloseableHttpResponse response = client.execute(put, context)) {
414          final int statusCode = response.getStatusLine().getStatusCode();
415          HttpEntity entity = response.getEntity();
416          assertEquals(HttpURLConnection.HTTP_FORBIDDEN, statusCode,
417            "Got response: " + EntityUtils.toString(entity));
418        }
419        return null;
420      }
421    });
422  }
423
424  @Test
425  public void testScanWithDifferentClients() throws Exception {
426    Pair<CloseableHttpClient, HttpClientContext> pair = getClient();
427    CloseableHttpClient client = pair.getFirst();
428    HttpClientContext context = pair.getSecond();
429
430    UserGroupInformation ugi = UserGroupInformation
431      .loginUserFromKeytabAndReturnUGI(CLIENT_PRINCIPAL, clientKeytab.getAbsolutePath());
432
433    ObjectMapper mapper = new JacksonJaxbJsonProvider().locateMapper(ScannerModel.class,
434      MediaType.APPLICATION_JSON_TYPE);
435    TableName table = TableName.valueOf("publicTable");
436    ScannerModel model = new ScannerModel();
437    StringEntity entity =
438      new StringEntity(mapper.writeValueAsString(model), ContentType.APPLICATION_JSON);
439    HttpPost post =
440      new HttpPost("http://localhost:" + REST_TEST.getServletPort() + "/" + table + "/scanner");
441    post.setEntity(entity);
442    String scannerURI = ugi.doAs(new PrivilegedExceptionAction<String>() {
443
444      @Override
445      public String run() throws Exception {
446        try (CloseableHttpResponse response = client.execute(post, context)) {
447          final int statusCode = response.getStatusLine().getStatusCode();
448          assertEquals(HttpURLConnection.HTTP_CREATED, statusCode);
449          return response.getFirstHeader("Location").getValue();
450        }
451      }
452    });
453
454    Pair<CloseableHttpClient, HttpClientContext> pair2 = getClient();
455    CloseableHttpClient client2 = pair2.getFirst();
456    HttpClientContext context2 = pair2.getSecond();
457
458    UserGroupInformation ugi2 = UserGroupInformation
459      .loginUserFromKeytabAndReturnUGI(CLIENT_PRINCIPAL2, clientKeytab.getAbsolutePath());
460    ugi2.doAs(new PrivilegedExceptionAction<Void>() {
461
462      @Override
463      public Void run() throws Exception {
464        HttpGet get = new HttpGet(scannerURI + "?n=1");
465        try (CloseableHttpResponse response = client2.execute(get, context2)) {
466          final int statusCode = response.getStatusLine().getStatusCode();
467          assertEquals(HttpURLConnection.HTTP_FORBIDDEN, statusCode);
468        }
469        HttpDelete delete = new HttpDelete(scannerURI);
470        try (CloseableHttpResponse response = client2.execute(delete, context2)) {
471          final int statusCode = response.getStatusLine().getStatusCode();
472          assertEquals(HttpURLConnection.HTTP_FORBIDDEN, statusCode);
473        }
474        return null;
475      }
476    });
477
478    ugi.doAs(new PrivilegedExceptionAction<Void>() {
479
480      @Override
481      public Void run() throws Exception {
482        HttpGet get = new HttpGet(scannerURI + "?n=1");
483        try (CloseableHttpResponse response = client.execute(get, context)) {
484          final int statusCode = response.getStatusLine().getStatusCode();
485          assertEquals(HttpURLConnection.HTTP_OK, statusCode);
486        }
487        HttpDelete delete = new HttpDelete(scannerURI);
488        try (CloseableHttpResponse response = client.execute(delete, context)) {
489          final int statusCode = response.getStatusLine().getStatusCode();
490          assertEquals(HttpURLConnection.HTTP_OK, statusCode);
491        }
492        return null;
493      }
494    });
495
496  }
497
498  private Pair<CloseableHttpClient, HttpClientContext> getClient() {
499    HttpClientConnectionManager pool = new PoolingHttpClientConnectionManager();
500    HttpHost host = new HttpHost("localhost", REST_TEST.getServletPort());
501    Registry<AuthSchemeProvider> authRegistry = RegistryBuilder.<AuthSchemeProvider> create()
502      .register(AuthSchemes.SPNEGO, new SPNegoSchemeFactory(true, true)).build();
503    CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
504    credentialsProvider.setCredentials(AuthScope.ANY, EmptyCredentials.INSTANCE);
505    AuthCache authCache = new BasicAuthCache();
506
507    CloseableHttpClient client = HttpClients.custom().setDefaultAuthSchemeRegistry(authRegistry)
508      .setConnectionManager(pool).build();
509
510    HttpClientContext context = HttpClientContext.create();
511    context.setTargetHost(host);
512    context.setCredentialsProvider(credentialsProvider);
513    context.setAuthSchemeRegistry(authRegistry);
514    context.setAuthCache(authCache);
515
516    return new Pair<>(client, context);
517  }
518
519  private static class EmptyCredentials implements Credentials {
520    public static final EmptyCredentials INSTANCE = new EmptyCredentials();
521
522    @Override
523    public String getPassword() {
524      return null;
525    }
526
527    @Override
528    public Principal getUserPrincipal() {
529      return null;
530    }
531  }
532}