| /** |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * <p> |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * <p> |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.hadoop.http; |
| |
| import org.apache.hadoop.conf.Configuration; |
| import org.apache.hadoop.fs.CommonConfigurationKeys; |
| import org.apache.hadoop.minikdc.MiniKdc; |
| import org.apache.hadoop.net.NetUtils; |
| import org.apache.hadoop.security.AuthenticationFilterInitializer; |
| import org.apache.hadoop.security.SecurityUtil; |
| import org.apache.hadoop.security.UserGroupInformation; |
| import org.apache.hadoop.security.UserGroupInformation.AuthenticationMethod; |
| import org.apache.hadoop.security.authentication.KerberosTestUtils; |
| import org.apache.hadoop.security.authentication.client.AuthenticatedURL; |
| import org.apache.hadoop.security.authentication.client.AuthenticationException; |
| import org.apache.hadoop.security.authentication.server.AuthenticationFilter; |
| import org.apache.hadoop.security.authentication.server.AuthenticationToken; |
| import org.apache.hadoop.security.authentication.util.Signer; |
| import org.apache.hadoop.security.authentication.util.SignerSecretProvider; |
| import org.apache.hadoop.security.authentication.util.StringSignerSecretProviderCreator; |
| import org.apache.hadoop.security.authorize.AccessControlList; |
| import org.apache.hadoop.security.authorize.ProxyUsers; |
| import org.ietf.jgss.GSSException; |
| import org.junit.AfterClass; |
| import org.junit.BeforeClass; |
| import org.junit.Test; |
| import org.junit.Assert; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import java.io.File; |
| import java.io.FileWriter; |
| import java.io.Writer; |
| import java.net.HttpURLConnection; |
| import java.net.URI; |
| import java.net.URL; |
| import java.security.AccessController; |
| import java.security.PrivilegedExceptionAction; |
| import java.util.HashSet; |
| import java.util.Properties; |
| import java.util.Set; |
| import javax.security.auth.Subject; |
| import javax.servlet.ServletContext; |
| |
| import static org.junit.Assert.assertTrue; |
| |
| /** |
| * This class is tested for http server with SPENGO authentication. |
| */ |
| public class TestHttpServerWithSpengo { |
| |
| static final Logger LOG = |
| LoggerFactory.getLogger(TestHttpServerWithSpengo.class); |
| |
| private static final String SECRET_STR = "secret"; |
| private static final String HTTP_USER = "HTTP"; |
| private static final String PREFIX = "hadoop.http.authentication."; |
| private static final long TIMEOUT = 20000; |
| |
| private static File httpSpnegoKeytabFile = new File( |
| KerberosTestUtils.getKeytabFile()); |
| private static String httpSpnegoPrincipal = |
| KerberosTestUtils.getServerPrincipal(); |
| private static String realm = KerberosTestUtils.getRealm(); |
| |
| private static File testRootDir = new File("target", |
| TestHttpServerWithSpengo.class.getName() + "-root"); |
| private static MiniKdc testMiniKDC; |
| private static File secretFile = new File(testRootDir, SECRET_STR); |
| |
| private static UserGroupInformation authUgi; |
| |
| @BeforeClass |
| public static void setUp() throws Exception { |
| try { |
| testMiniKDC = new MiniKdc(MiniKdc.createConf(), testRootDir); |
| testMiniKDC.start(); |
| testMiniKDC.createPrincipal( |
| httpSpnegoKeytabFile, HTTP_USER + "/localhost", "keytab-user"); |
| } catch (Exception e) { |
| assertTrue("Couldn't setup MiniKDC", false); |
| } |
| |
| System.setProperty("sun.security.krb5.debug", "true"); |
| Configuration conf = new Configuration(); |
| SecurityUtil.setAuthenticationMethod(AuthenticationMethod.KERBEROS, conf); |
| UserGroupInformation.setConfiguration(conf); |
| authUgi = UserGroupInformation.loginUserFromKeytabAndReturnUGI( |
| "keytab-user", httpSpnegoKeytabFile.toString()); |
| Writer w = new FileWriter(secretFile); |
| w.write("secret"); |
| w.close(); |
| } |
| |
| @AfterClass |
| public static void tearDown() { |
| if (testMiniKDC != null) { |
| testMiniKDC.stop(); |
| } |
| } |
| |
| /** |
| * groupA |
| * - userA |
| * groupB |
| * - userA, userB |
| * groupC |
| * - userC |
| * SPNEGO filter has been enabled. |
| * userA has the privilege to impersonate users in groupB. |
| * userA has admin access to all default servlets, but userB |
| * and userC don't have. So "/logs" can only be accessed by userA. |
| * @throws Exception |
| */ |
| @Test |
| public void testAuthenticationWithProxyUser() throws Exception { |
| |
| Configuration spengoConf = getSpengoConf(new Configuration()); |
| |
| //setup logs dir |
| System.setProperty("hadoop.log.dir", testRootDir.getAbsolutePath()); |
| |
| // Setup user group |
| UserGroupInformation.createUserForTesting("userA", |
| new String[]{"groupA", "groupB"}); |
| UserGroupInformation.createUserForTesting("userB", |
| new String[]{"groupB"}); |
| UserGroupInformation.createUserForTesting("userC", |
| new String[]{"groupC"}); |
| |
| // Make userA impersonate users in groupB |
| spengoConf.set("hadoop.proxyuser.userA.hosts", "*"); |
| spengoConf.set("hadoop.proxyuser.userA.groups", "groupB"); |
| ProxyUsers.refreshSuperUserGroupsConfiguration(spengoConf); |
| |
| HttpServer2 httpServer = null; |
| try { |
| // Create http server to test. |
| httpServer = getCommonBuilder() |
| .setConf(spengoConf) |
| .setACL(new AccessControlList("userA groupA")) |
| .build(); |
| httpServer.start(); |
| |
| // Get signer to encrypt token |
| Signer signer = getSignerToEncrypt(); |
| |
| // setup auth token for userA |
| AuthenticatedURL.Token token = getEncryptedAuthToken(signer, "userA"); |
| |
| String serverURL = "http://" + |
| NetUtils.getHostPortString(httpServer.getConnectorAddress(0)) + "/"; |
| |
| // The default authenticator is kerberos. |
| AuthenticatedURL authUrl = new AuthenticatedURL(); |
| |
| // userA impersonates userB, it's allowed. |
| for (String servlet : |
| new String[]{"stacks", "jmx", "conf"}) { |
| HttpURLConnection conn = authUrl |
| .openConnection(new URL(serverURL + servlet + "?doAs=userB"), |
| token); |
| Assert.assertEquals(HttpURLConnection.HTTP_OK, conn.getResponseCode()); |
| } |
| |
| // userA cannot impersonate userC, it fails. |
| for (String servlet : |
| new String[]{"stacks", "jmx", "conf"}){ |
| HttpURLConnection conn = authUrl |
| .openConnection(new URL(serverURL + servlet + "?doAs=userC"), |
| token); |
| Assert.assertEquals(HttpURLConnection.HTTP_FORBIDDEN, |
| conn.getResponseCode()); |
| } |
| |
| // "/logs" and "/logLevel" require admin authorization, |
| // only userA has the access. |
| for (String servlet : |
| new String[]{"logLevel", "logs"}) { |
| HttpURLConnection conn = authUrl |
| .openConnection(new URL(serverURL + servlet), token); |
| Assert.assertEquals(HttpURLConnection.HTTP_OK, conn.getResponseCode()); |
| } |
| |
| // Setup token for userB |
| token = getEncryptedAuthToken(signer, "userB"); |
| |
| // userB cannot access these servlets. |
| for (String servlet : |
| new String[]{"logLevel", "logs"}) { |
| HttpURLConnection conn = authUrl |
| .openConnection(new URL(serverURL + servlet), token); |
| Assert.assertEquals(HttpURLConnection.HTTP_FORBIDDEN, |
| conn.getResponseCode()); |
| } |
| |
| } finally { |
| if (httpServer != null) { |
| httpServer.stop(); |
| } |
| } |
| } |
| |
| @Test |
| public void testSessionCookie() throws Exception { |
| Configuration conf = new Configuration(); |
| conf.set(HttpServer2.FILTER_INITIALIZER_PROPERTY, |
| AuthenticationFilterInitializer.class.getName()); |
| conf.set(PREFIX + "type", "kerberos"); |
| conf.setBoolean(PREFIX + "simple.anonymous.allowed", false); |
| conf.set(PREFIX + "signer.secret.provider", |
| TestSignerSecretProvider.class.getName()); |
| |
| conf.set(PREFIX + "kerberos.keytab", |
| httpSpnegoKeytabFile.getAbsolutePath()); |
| conf.set(PREFIX + "kerberos.principal", httpSpnegoPrincipal); |
| conf.set(PREFIX + "cookie.domain", realm); |
| conf.setBoolean(CommonConfigurationKeys.HADOOP_SECURITY_AUTHORIZATION, |
| true); |
| |
| //setup logs dir |
| System.setProperty("hadoop.log.dir", testRootDir.getAbsolutePath()); |
| |
| HttpServer2 httpServer = null; |
| // Create http server to test. |
| httpServer = getCommonBuilder() |
| .setConf(conf) |
| .build(); |
| httpServer.start(); |
| |
| // Get signer to encrypt token |
| final Signer signer = new Signer(new TestSignerSecretProvider()); |
| final AuthenticatedURL authUrl = new AuthenticatedURL(); |
| |
| final URL url = new URL("http://" + NetUtils.getHostPortString( |
| httpServer.getConnectorAddress(0)) + "/conf"); |
| |
| // this illustrates an inconsistency with AuthenticatedURL. the |
| // authenticator is only called when the token is not set. if the |
| // authenticator fails then it must throw an AuthenticationException to |
| // the caller, yet the caller may see 401 for subsequent requests |
| // that require re-authentication like token expiration. |
| final UserGroupInformation simpleUgi = |
| UserGroupInformation.createRemoteUser("simple-user"); |
| |
| authUgi.doAs(new PrivilegedExceptionAction<Void>() { |
| @Override |
| public Void run() throws Exception { |
| TestSignerSecretProvider.rollSecret(); |
| HttpURLConnection conn = null; |
| AuthenticatedURL.Token token = new AuthenticatedURL.Token(); |
| |
| // initial request should trigger authentication and set the token. |
| conn = authUrl.openConnection(url, token); |
| Assert.assertEquals(HttpURLConnection.HTTP_OK, conn.getResponseCode()); |
| Assert.assertTrue(token.isSet()); |
| String cookie = token.toString(); |
| |
| // token should not change. |
| conn = authUrl.openConnection(url, token); |
| Assert.assertEquals(HttpURLConnection.HTTP_OK, conn.getResponseCode()); |
| Assert.assertTrue(token.isSet()); |
| Assert.assertEquals(cookie, token.toString()); |
| |
| // roll secret to invalidate token. |
| TestSignerSecretProvider.rollSecret(); |
| conn = authUrl.openConnection(url, token); |
| // this may or may not happen. under normal circumstances the |
| // jdk will silently renegotiate and the client never sees a 401. |
| // however in some cases the jdk will give up doing spnego. since |
| // the token is already set, the authenticator isn't invoked (which |
| // would do the spnego if the jdk doesn't), which causes the client |
| // to see a 401. |
| if (conn.getResponseCode() == HttpURLConnection.HTTP_UNAUTHORIZED) { |
| // if this happens, the token should be cleared which means the |
| // next request should succeed and receive a new token. |
| Assert.assertFalse(token.isSet()); |
| conn = authUrl.openConnection(url, token); |
| } |
| |
| // token should change. |
| Assert.assertEquals(HttpURLConnection.HTTP_OK, conn.getResponseCode()); |
| Assert.assertTrue(token.isSet()); |
| Assert.assertNotEquals(cookie, token.toString()); |
| cookie = token.toString(); |
| |
| // token should not change. |
| for (int i=0; i < 3; i++) { |
| conn = authUrl.openConnection(url, token); |
| Assert.assertEquals("attempt"+i, |
| HttpURLConnection.HTTP_OK, conn.getResponseCode()); |
| Assert.assertTrue(token.isSet()); |
| Assert.assertEquals(cookie, token.toString()); |
| } |
| |
| // blow out the kerberos creds test only auth token is used. |
| Subject s = Subject.getSubject(AccessController.getContext()); |
| Set<Object> oldCreds = new HashSet<>(s.getPrivateCredentials()); |
| s.getPrivateCredentials().clear(); |
| |
| // token should not change. |
| for (int i=0; i < 3; i++) { |
| try { |
| conn = authUrl.openConnection(url, token); |
| Assert.assertEquals("attempt"+i, |
| HttpURLConnection.HTTP_OK, conn.getResponseCode()); |
| } catch (AuthenticationException ae) { |
| Assert.fail("attempt"+i+" "+ae); |
| } |
| Assert.assertTrue(token.isSet()); |
| Assert.assertEquals(cookie, token.toString()); |
| } |
| |
| // invalidate token. connections should fail now and token should be |
| // unset. |
| TestSignerSecretProvider.rollSecret(); |
| conn = authUrl.openConnection(url, token); |
| Assert.assertEquals( |
| HttpURLConnection.HTTP_UNAUTHORIZED, conn.getResponseCode()); |
| Assert.assertFalse(token.isSet()); |
| Assert.assertEquals("", token.toString()); |
| |
| // restore the kerberos creds, should work again. |
| s.getPrivateCredentials().addAll(oldCreds); |
| conn = authUrl.openConnection(url, token); |
| Assert.assertEquals( |
| HttpURLConnection.HTTP_OK, conn.getResponseCode()); |
| Assert.assertTrue(token.isSet()); |
| cookie = token.toString(); |
| |
| // token should not change. |
| for (int i=0; i < 3; i++) { |
| conn = authUrl.openConnection(url, token); |
| Assert.assertEquals("attempt"+i, |
| HttpURLConnection.HTTP_OK, conn.getResponseCode()); |
| Assert.assertTrue(token.isSet()); |
| Assert.assertEquals(cookie, token.toString()); |
| } |
| return null; |
| } |
| }); |
| |
| simpleUgi.doAs(new PrivilegedExceptionAction<Void>() { |
| @Override |
| public Void run() throws Exception { |
| TestSignerSecretProvider.rollSecret(); |
| AuthenticatedURL authUrl = new AuthenticatedURL(); |
| AuthenticatedURL.Token token = new AuthenticatedURL.Token(); |
| HttpURLConnection conn = null; |
| |
| // initial connect with unset token will trigger authenticator which |
| // should fail since we have no creds and leave token unset. |
| try { |
| authUrl.openConnection(url, token); |
| Assert.fail("should fail with no credentials"); |
| } catch (AuthenticationException ae) { |
| Assert.assertNotNull(ae.getCause()); |
| Assert.assertEquals(GSSException.class, ae.getCause().getClass()); |
| GSSException gsse = (GSSException)ae.getCause(); |
| Assert.assertEquals(GSSException.NO_CRED, gsse.getMajor()); |
| } catch (Throwable t) { |
| Assert.fail("Unexpected exception" + t); |
| } |
| Assert.assertFalse(token.isSet()); |
| |
| // create a valid token and save its value. |
| token = getEncryptedAuthToken(signer, "valid"); |
| String cookie = token.toString(); |
| |
| // server should accept token. after the request the token should |
| // be set to the same value (ie. server didn't reissue cookie) |
| conn = authUrl.openConnection(url, token); |
| Assert.assertEquals(HttpURLConnection.HTTP_OK, conn.getResponseCode()); |
| Assert.assertTrue(token.isSet()); |
| Assert.assertEquals(cookie, token.toString()); |
| |
| conn = authUrl.openConnection(url, token); |
| Assert.assertEquals(HttpURLConnection.HTTP_OK, conn.getResponseCode()); |
| Assert.assertTrue(token.isSet()); |
| Assert.assertEquals(cookie, token.toString()); |
| |
| // change the secret to effectively invalidate the cookie. see above |
| // regarding inconsistency. the authenticator has no way to know the |
| // token is bad, so the client will encounter a 401 instead of |
| // AuthenticationException. |
| TestSignerSecretProvider.rollSecret(); |
| conn = authUrl.openConnection(url, token); |
| Assert.assertEquals( |
| HttpURLConnection.HTTP_UNAUTHORIZED, conn.getResponseCode()); |
| Assert.assertFalse(token.isSet()); |
| Assert.assertEquals("", token.toString()); |
| return null; |
| } |
| }); |
| } |
| |
| public static class TestSignerSecretProvider extends SignerSecretProvider { |
| static int n = 0; |
| static byte[] secret; |
| |
| static void rollSecret() { |
| secret = ("secret[" + (n++) + "]").getBytes(); |
| } |
| |
| public TestSignerSecretProvider() { |
| } |
| |
| @Override |
| public void init(Properties config, ServletContext servletContext, |
| long tokenValidity) throws Exception { |
| rollSecret(); |
| } |
| |
| @Override |
| public byte[] getCurrentSecret() { |
| return secret; |
| } |
| |
| @Override |
| public byte[][] getAllSecrets() { |
| return new byte[][]{secret}; |
| } |
| } |
| |
| private AuthenticatedURL.Token getEncryptedAuthToken(Signer signer, |
| String user) throws Exception { |
| AuthenticationToken token = |
| new AuthenticationToken(user, user, "kerberos"); |
| token.setExpires(System.currentTimeMillis() + TIMEOUT); |
| return new AuthenticatedURL.Token(signer.sign(token.toString())); |
| } |
| |
| private Signer getSignerToEncrypt() throws Exception { |
| SignerSecretProvider secretProvider = |
| StringSignerSecretProviderCreator.newStringSignerSecretProvider(); |
| Properties secretProviderProps = new Properties(); |
| secretProviderProps.setProperty( |
| AuthenticationFilter.SIGNATURE_SECRET, SECRET_STR); |
| secretProvider.init(secretProviderProps, null, TIMEOUT); |
| return new Signer(secretProvider); |
| } |
| |
| private Configuration getSpengoConf(Configuration conf) { |
| conf = new Configuration(); |
| conf.set(HttpServer2.FILTER_INITIALIZER_PROPERTY, |
| AuthenticationFilterInitializer.class.getName()); |
| conf.set(PREFIX + "type", "kerberos"); |
| conf.setBoolean(PREFIX + "simple.anonymous.allowed", false); |
| conf.set(PREFIX + "signature.secret.file", |
| secretFile.getAbsolutePath()); |
| conf.set(PREFIX + "kerberos.keytab", |
| httpSpnegoKeytabFile.getAbsolutePath()); |
| conf.set(PREFIX + "kerberos.principal", httpSpnegoPrincipal); |
| conf.set(PREFIX + "cookie.domain", realm); |
| conf.setBoolean(CommonConfigurationKeys.HADOOP_SECURITY_AUTHORIZATION, |
| true); |
| return conf; |
| } |
| |
| private HttpServer2.Builder getCommonBuilder() throws Exception { |
| return new HttpServer2.Builder().setName("test") |
| .addEndpoint(new URI("http://localhost:0")) |
| .setFindPort(true); |
| } |
| } |