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