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