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.http;
019
020import static org.junit.Assert.assertEquals;
021import static org.junit.Assert.assertFalse;
022import static org.junit.Assert.assertTrue;
023
024import java.io.File;
025import java.lang.management.ManagementFactory;
026import java.net.HttpURLConnection;
027import java.net.URL;
028import java.security.PrivilegedExceptionAction;
029import javax.management.ObjectName;
030import org.apache.hadoop.conf.Configuration;
031import org.apache.hadoop.fs.CommonConfigurationKeys;
032import org.apache.hadoop.fs.Path;
033import org.apache.hadoop.hbase.HBaseClassTestRule;
034import org.apache.hadoop.hbase.HBaseTestingUtility;
035import org.apache.hadoop.hbase.HConstants;
036import org.apache.hadoop.hbase.LocalHBaseCluster;
037import org.apache.hadoop.hbase.TableName;
038import org.apache.hadoop.hbase.Waiter;
039import org.apache.hadoop.hbase.coprocessor.CoprocessorHost;
040import org.apache.hadoop.hbase.security.HBaseKerberosUtils;
041import org.apache.hadoop.hbase.security.token.TokenProvider;
042import org.apache.hadoop.hbase.testclassification.MediumTests;
043import org.apache.hadoop.hbase.testclassification.MiscTests;
044import org.apache.hadoop.hbase.util.CommonFSUtils;
045import org.apache.hadoop.hbase.util.Pair;
046import org.apache.hadoop.minikdc.MiniKdc;
047import org.apache.hadoop.security.UserGroupInformation;
048import org.apache.http.auth.AuthSchemeProvider;
049import org.apache.http.auth.AuthScope;
050import org.apache.http.auth.KerberosCredentials;
051import org.apache.http.client.config.AuthSchemes;
052import org.apache.http.client.methods.CloseableHttpResponse;
053import org.apache.http.client.methods.HttpGet;
054import org.apache.http.config.Lookup;
055import org.apache.http.config.RegistryBuilder;
056import org.apache.http.impl.auth.SPNegoSchemeFactory;
057import org.apache.http.impl.client.BasicCredentialsProvider;
058import org.apache.http.impl.client.CloseableHttpClient;
059import org.apache.http.impl.client.HttpClients;
060import org.apache.http.util.EntityUtils;
061import org.ietf.jgss.GSSCredential;
062import org.ietf.jgss.GSSManager;
063import org.ietf.jgss.GSSName;
064import org.ietf.jgss.Oid;
065import org.junit.AfterClass;
066import org.junit.BeforeClass;
067import org.junit.ClassRule;
068import org.junit.Rule;
069import org.junit.Test;
070import org.junit.experimental.categories.Category;
071import org.junit.rules.TestName;
072import org.slf4j.Logger;
073import org.slf4j.LoggerFactory;
074
075/**
076 * Testing info servers for admin acl.
077 */
078@Category({ MiscTests.class, MediumTests.class })
079public class TestInfoServersACL {
080
081  @ClassRule
082  public static final HBaseClassTestRule CLASS_RULE =
083    HBaseClassTestRule.forClass(TestInfoServersACL.class);
084
085  private static final Logger LOG = LoggerFactory.getLogger(TestInfoServersACL.class);
086  private final static HBaseTestingUtility UTIL = new HBaseTestingUtility();
087  private static Configuration conf;
088
089  protected static String USERNAME;
090  private static LocalHBaseCluster CLUSTER;
091  private static final File KEYTAB_FILE = new File(UTIL.getDataTestDir("keytab").toUri().getPath());
092  private static MiniKdc KDC;
093  private static String HOST = "localhost";
094  private static String PRINCIPAL;
095  private static String HTTP_PRINCIPAL;
096
097  @Rule
098  public TestName name = new TestName();
099
100  // user/group present in hbase.admin.acl
101  private static final String USER_ADMIN_STR = "admin";
102
103  // user with no permissions
104  private static final String USER_NONE_STR = "none";
105
106  @BeforeClass
107  public static void beforeClass() throws Exception {
108    conf = UTIL.getConfiguration();
109    KDC = UTIL.setupMiniKdc(KEYTAB_FILE);
110    USERNAME = UserGroupInformation.getLoginUser().getShortUserName();
111    PRINCIPAL = USERNAME + "/" + HOST;
112    HTTP_PRINCIPAL = "HTTP/" + HOST;
113    // Create principals for services and the test users
114    KDC.createPrincipal(KEYTAB_FILE, PRINCIPAL, HTTP_PRINCIPAL, USER_ADMIN_STR, USER_NONE_STR);
115    UTIL.startMiniZKCluster();
116
117    HBaseKerberosUtils.setSecuredConfiguration(conf, PRINCIPAL + "@" + KDC.getRealm(),
118      HTTP_PRINCIPAL + "@" + KDC.getRealm());
119    HBaseKerberosUtils.setSSLConfiguration(UTIL, TestInfoServersACL.class);
120
121    conf.setStrings(CoprocessorHost.REGION_COPROCESSOR_CONF_KEY, TokenProvider.class.getName());
122    UTIL.startMiniDFSCluster(1);
123    Path rootdir = UTIL.getDataTestDirOnTestFS("TestInfoServersACL");
124    CommonFSUtils.setRootDir(conf, rootdir);
125
126    // The info servers do not run in tests by default.
127    // Set them to ephemeral ports so they will start
128    // setup configuration
129    conf.setInt(HConstants.MASTER_INFO_PORT, 0);
130    conf.setInt(HConstants.REGIONSERVER_INFO_PORT, 0);
131
132    conf.set(HttpServer.HTTP_UI_AUTHENTICATION, "kerberos");
133    conf.set(HttpServer.HTTP_SPNEGO_AUTHENTICATION_PRINCIPAL_KEY, HTTP_PRINCIPAL);
134    conf.set(HttpServer.HTTP_SPNEGO_AUTHENTICATION_KEYTAB_KEY, KEYTAB_FILE.getAbsolutePath());
135
136    // ACL lists work only when "hadoop.security.authorization" is set to true
137    conf.setBoolean(CommonConfigurationKeys.HADOOP_SECURITY_AUTHORIZATION, true);
138    // only user admin will have acl access
139    conf.set(HttpServer.HTTP_SPNEGO_AUTHENTICATION_ADMIN_USERS_KEY, USER_ADMIN_STR);
140    // conf.set(HttpServer.FILTER_INITIALIZERS_PROPERTY, "");
141
142    CLUSTER = new LocalHBaseCluster(conf, 1);
143    CLUSTER.startup();
144    CLUSTER.getActiveMaster().waitForMetaOnline();
145  }
146
147  /**
148   * Helper method to shut down the cluster (if running)
149   */
150  @AfterClass
151  public static void shutDownMiniCluster() throws Exception {
152    if (CLUSTER != null) {
153      CLUSTER.shutdown();
154      CLUSTER.join();
155    }
156    if (KDC != null) {
157      KDC.stop();
158    }
159    UTIL.shutdownMiniCluster();
160  }
161
162  @Test
163  public void testAuthorizedUser() throws Exception {
164    UserGroupInformation admin = UserGroupInformation
165      .loginUserFromKeytabAndReturnUGI(USER_ADMIN_STR, KEYTAB_FILE.getAbsolutePath());
166    admin.doAs(new PrivilegedExceptionAction<Void>() {
167      @Override
168      public Void run() throws Exception {
169        // Check the expected content is present in the http response
170        String expectedContent = "Get Log Level";
171        Pair<Integer, String> pair = getLogLevelPage();
172        assertEquals(HttpURLConnection.HTTP_OK, pair.getFirst().intValue());
173        assertTrue("expected=" + expectedContent + ", content=" + pair.getSecond(),
174          pair.getSecond().contains(expectedContent));
175        return null;
176      }
177    });
178  }
179
180  @Test
181  public void testUnauthorizedUser() throws Exception {
182    UserGroupInformation nonAdmin = UserGroupInformation
183      .loginUserFromKeytabAndReturnUGI(USER_NONE_STR, KEYTAB_FILE.getAbsolutePath());
184    nonAdmin.doAs(new PrivilegedExceptionAction<Void>() {
185      @Override
186      public Void run() throws Exception {
187        Pair<Integer, String> pair = getLogLevelPage();
188        assertEquals(HttpURLConnection.HTTP_FORBIDDEN, pair.getFirst().intValue());
189        return null;
190      }
191    });
192  }
193
194  @Test
195  public void testTableActionsAvailableForAdmins() throws Exception {
196    final String expectedAuthorizedContent = "Actions:";
197    UserGroupInformation admin = UserGroupInformation
198      .loginUserFromKeytabAndReturnUGI(USER_ADMIN_STR, KEYTAB_FILE.getAbsolutePath());
199    admin.doAs(new PrivilegedExceptionAction<Void>() {
200      @Override
201      public Void run() throws Exception {
202        // Check the expected content is present in the http response
203        Pair<Integer, String> pair = getTablePage(TableName.META_TABLE_NAME);
204        assertEquals(HttpURLConnection.HTTP_OK, pair.getFirst().intValue());
205        assertTrue("expected=" + expectedAuthorizedContent + ", content=" + pair.getSecond(),
206          pair.getSecond().contains(expectedAuthorizedContent));
207        return null;
208      }
209    });
210
211    UserGroupInformation nonAdmin = UserGroupInformation
212      .loginUserFromKeytabAndReturnUGI(USER_NONE_STR, KEYTAB_FILE.getAbsolutePath());
213    nonAdmin.doAs(new PrivilegedExceptionAction<Void>() {
214      @Override
215      public Void run() throws Exception {
216        Pair<Integer, String> pair = getTablePage(TableName.META_TABLE_NAME);
217        assertEquals(HttpURLConnection.HTTP_OK, pair.getFirst().intValue());
218        assertFalse(
219          "should not find=" + expectedAuthorizedContent + ", content=" + pair.getSecond(),
220          pair.getSecond().contains(expectedAuthorizedContent));
221        return null;
222      }
223    });
224  }
225
226  @Test
227  public void testLogsAvailableForAdmins() throws Exception {
228    final String expectedAuthorizedContent = "Directory: /logs/";
229    UserGroupInformation admin = UserGroupInformation
230      .loginUserFromKeytabAndReturnUGI(USER_ADMIN_STR, KEYTAB_FILE.getAbsolutePath());
231    admin.doAs(new PrivilegedExceptionAction<Void>() {
232      @Override
233      public Void run() throws Exception {
234        // Check the expected content is present in the http response
235        Pair<Integer, String> pair = getLogsPage();
236        assertEquals(HttpURLConnection.HTTP_OK, pair.getFirst().intValue());
237        assertTrue("expected=" + expectedAuthorizedContent + ", content=" + pair.getSecond(),
238          pair.getSecond().contains(expectedAuthorizedContent));
239        return null;
240      }
241    });
242
243    UserGroupInformation nonAdmin = UserGroupInformation
244      .loginUserFromKeytabAndReturnUGI(USER_NONE_STR, KEYTAB_FILE.getAbsolutePath());
245    nonAdmin.doAs(new PrivilegedExceptionAction<Void>() {
246      @Override
247      public Void run() throws Exception {
248        Pair<Integer, String> pair = getLogsPage();
249        assertEquals(HttpURLConnection.HTTP_FORBIDDEN, pair.getFirst().intValue());
250        return null;
251      }
252    });
253  }
254
255  @Test
256  public void testDumpActionsAvailableForAdmins() throws Exception {
257    final String expectedAuthorizedContent = "Master status for";
258    UserGroupInformation admin = UserGroupInformation
259      .loginUserFromKeytabAndReturnUGI(USER_ADMIN_STR, KEYTAB_FILE.getAbsolutePath());
260    admin.doAs(new PrivilegedExceptionAction<Void>() {
261      @Override
262      public Void run() throws Exception {
263        // Check the expected content is present in the http response
264        Pair<Integer, String> pair = getMasterDumpPage();
265        assertEquals(HttpURLConnection.HTTP_OK, pair.getFirst().intValue());
266        assertTrue("expected=" + expectedAuthorizedContent + ", content=" + pair.getSecond(),
267          pair.getSecond().contains(expectedAuthorizedContent));
268        return null;
269      }
270    });
271
272    UserGroupInformation nonAdmin = UserGroupInformation
273      .loginUserFromKeytabAndReturnUGI(USER_NONE_STR, KEYTAB_FILE.getAbsolutePath());
274    nonAdmin.doAs(new PrivilegedExceptionAction<Void>() {
275      @Override
276      public Void run() throws Exception {
277        Pair<Integer, String> pair = getMasterDumpPage();
278        assertEquals(HttpURLConnection.HTTP_FORBIDDEN, pair.getFirst().intValue());
279        return null;
280      }
281    });
282  }
283
284  @Test
285  public void testStackActionsAvailableForAdmins() throws Exception {
286    final String expectedAuthorizedContent = "Process Thread Dump";
287    UserGroupInformation admin = UserGroupInformation
288      .loginUserFromKeytabAndReturnUGI(USER_ADMIN_STR, KEYTAB_FILE.getAbsolutePath());
289    admin.doAs(new PrivilegedExceptionAction<Void>() {
290      @Override
291      public Void run() throws Exception {
292        // Check the expected content is present in the http response
293        Pair<Integer, String> pair = getStacksPage();
294        assertEquals(HttpURLConnection.HTTP_OK, pair.getFirst().intValue());
295        assertTrue("expected=" + expectedAuthorizedContent + ", content=" + pair.getSecond(),
296          pair.getSecond().contains(expectedAuthorizedContent));
297        return null;
298      }
299    });
300
301    UserGroupInformation nonAdmin = UserGroupInformation
302      .loginUserFromKeytabAndReturnUGI(USER_NONE_STR, KEYTAB_FILE.getAbsolutePath());
303    nonAdmin.doAs(new PrivilegedExceptionAction<Void>() {
304      @Override
305      public Void run() throws Exception {
306        Pair<Integer, String> pair = getStacksPage();
307        assertEquals(HttpURLConnection.HTTP_FORBIDDEN, pair.getFirst().intValue());
308        return null;
309      }
310    });
311  }
312
313  @Test
314  public void testJmxAvailableForAdmins() throws Exception {
315    final String expectedAuthorizedContent = "Hadoop:service=HBase";
316    UTIL.waitFor(30000, new Waiter.Predicate<Exception>() {
317      @Override
318      public boolean evaluate() throws Exception {
319        for (ObjectName name : ManagementFactory.getPlatformMBeanServer()
320          .queryNames(new ObjectName("*:*"), null)) {
321          if (name.toString().contains(expectedAuthorizedContent)) {
322            LOG.info("{}", name);
323            return true;
324          }
325        }
326        return false;
327      }
328    });
329    UserGroupInformation admin = UserGroupInformation
330      .loginUserFromKeytabAndReturnUGI(USER_ADMIN_STR, KEYTAB_FILE.getAbsolutePath());
331    admin.doAs(new PrivilegedExceptionAction<Void>() {
332      @Override
333      public Void run() throws Exception {
334        // Check the expected content is present in the http response
335        Pair<Integer, String> pair = getJmxPage();
336        assertEquals(HttpURLConnection.HTTP_OK, pair.getFirst().intValue());
337        assertTrue("expected=" + expectedAuthorizedContent + ", content=" + pair.getSecond(),
338          pair.getSecond().contains(expectedAuthorizedContent));
339        return null;
340      }
341    });
342
343    UserGroupInformation nonAdmin = UserGroupInformation
344      .loginUserFromKeytabAndReturnUGI(USER_NONE_STR, KEYTAB_FILE.getAbsolutePath());
345    nonAdmin.doAs(new PrivilegedExceptionAction<Void>() {
346      @Override
347      public Void run() throws Exception {
348        Pair<Integer, String> pair = getJmxPage();
349        assertEquals(HttpURLConnection.HTTP_FORBIDDEN, pair.getFirst().intValue());
350        return null;
351      }
352    });
353  }
354
355  @Test
356  public void testMetricsAvailableForAdmins() throws Exception {
357    // Looks like there's nothing exported to this, but leave it since
358    // it's Hadoop2 only and will eventually be removed due to that.
359    final String expectedAuthorizedContent = "";
360    UserGroupInformation admin = UserGroupInformation
361      .loginUserFromKeytabAndReturnUGI(USER_ADMIN_STR, KEYTAB_FILE.getAbsolutePath());
362    admin.doAs(new PrivilegedExceptionAction<Void>() {
363      @Override
364      public Void run() throws Exception {
365        // Check the expected content is present in the http response
366        Pair<Integer, String> pair = getMetricsPage();
367        if (HttpURLConnection.HTTP_NOT_FOUND == pair.getFirst()) {
368          // Not on hadoop 2
369          return null;
370        }
371        assertEquals(HttpURLConnection.HTTP_OK, pair.getFirst().intValue());
372        assertTrue("expected=" + expectedAuthorizedContent + ", content=" + pair.getSecond(),
373          pair.getSecond().contains(expectedAuthorizedContent));
374        return null;
375      }
376    });
377
378    UserGroupInformation nonAdmin = UserGroupInformation
379      .loginUserFromKeytabAndReturnUGI(USER_NONE_STR, KEYTAB_FILE.getAbsolutePath());
380    nonAdmin.doAs(new PrivilegedExceptionAction<Void>() {
381      @Override
382      public Void run() throws Exception {
383        Pair<Integer, String> pair = getMetricsPage();
384        if (HttpURLConnection.HTTP_NOT_FOUND == pair.getFirst()) {
385          // Not on hadoop 2
386          return null;
387        }
388        assertEquals(HttpURLConnection.HTTP_FORBIDDEN, pair.getFirst().intValue());
389        return null;
390      }
391    });
392  }
393
394  private String getInfoServerHostAndPort() {
395    return "http://localhost:" + CLUSTER.getActiveMaster().getInfoServer().getPort();
396  }
397
398  private Pair<Integer, String> getLogLevelPage() throws Exception {
399    // Build the url which we want to connect to
400    URL url = new URL(getInfoServerHostAndPort() + "/logLevel");
401    return getUrlContent(url);
402  }
403
404  private Pair<Integer, String> getTablePage(TableName tn) throws Exception {
405    URL url = new URL(getInfoServerHostAndPort() + "/table.jsp?name=" + tn.getNameAsString());
406    return getUrlContent(url);
407  }
408
409  private Pair<Integer, String> getLogsPage() throws Exception {
410    URL url = new URL(getInfoServerHostAndPort() + "/logs/");
411    return getUrlContent(url);
412  }
413
414  private Pair<Integer, String> getMasterDumpPage() throws Exception {
415    URL url = new URL(getInfoServerHostAndPort() + "/dump");
416    return getUrlContent(url);
417  }
418
419  private Pair<Integer, String> getStacksPage() throws Exception {
420    URL url = new URL(getInfoServerHostAndPort() + "/stacks");
421    return getUrlContent(url);
422  }
423
424  private Pair<Integer, String> getJmxPage() throws Exception {
425    URL url = new URL(getInfoServerHostAndPort() + "/jmx");
426    return getUrlContent(url);
427  }
428
429  private Pair<Integer, String> getMetricsPage() throws Exception {
430    URL url = new URL(getInfoServerHostAndPort() + "/metrics");
431    return getUrlContent(url);
432  }
433
434  /**
435   * Retrieves the content of the specified URL. The content will only be returned if the status
436   * code for the operation was HTTP 200/OK.
437   */
438  private Pair<Integer, String> getUrlContent(URL url) throws Exception {
439    try (CloseableHttpClient client =
440      createHttpClient(UserGroupInformation.getCurrentUser().getUserName())) {
441      CloseableHttpResponse resp = client.execute(new HttpGet(url.toURI()));
442      int code = resp.getStatusLine().getStatusCode();
443      if (code == HttpURLConnection.HTTP_OK) {
444        return new Pair<>(code, EntityUtils.toString(resp.getEntity()));
445      }
446      return new Pair<>(code, null);
447    }
448  }
449
450  private CloseableHttpClient createHttpClient(String clientPrincipal) throws Exception {
451    // Logs in with Kerberos via GSS
452    GSSManager gssManager = GSSManager.getInstance();
453    // jGSS Kerberos login constant
454    Oid oid = new Oid("1.2.840.113554.1.2.2");
455    GSSName gssClient = gssManager.createName(clientPrincipal, GSSName.NT_USER_NAME);
456    GSSCredential credential = gssManager.createCredential(gssClient,
457      GSSCredential.DEFAULT_LIFETIME, oid, GSSCredential.INITIATE_ONLY);
458
459    Lookup<AuthSchemeProvider> authRegistry = RegistryBuilder.<AuthSchemeProvider> create()
460      .register(AuthSchemes.SPNEGO, new SPNegoSchemeFactory(true, true)).build();
461
462    BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
463    credentialsProvider.setCredentials(AuthScope.ANY, new KerberosCredentials(credential));
464
465    return HttpClients.custom().setDefaultAuthSchemeRegistry(authRegistry)
466      .setDefaultCredentialsProvider(credentialsProvider).build();
467  }
468}