blob: d233234c26c0cb976b6e3af6114bd753dc942df9 [file] [log] [blame]
/**
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.security;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.CommonConfigurationKeys;
import org.apache.hadoop.minikdc.MiniKdc;
import org.apache.hadoop.security.UserGroupInformation.AuthenticationMethod;
import org.apache.hadoop.test.GenericTestUtils;
import org.apache.hadoop.util.Time;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.slf4j.event.Level;
import static org.apache.hadoop.fs.CommonConfigurationKeysPublic.HADOOP_KERBEROS_KEYTAB_LOGIN_AUTORENEWAL_ENABLED;
import static org.apache.hadoop.fs.CommonConfigurationKeysPublic.HADOOP_KERBEROS_MIN_SECONDS_BEFORE_RELOGIN;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.File;
import java.io.IOException;
import java.security.Principal;
import java.security.PrivilegedExceptionAction;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.security.auth.Subject;
import javax.security.auth.kerberos.KerberosPrincipal;
import javax.security.auth.kerberos.KerberosTicket;
import javax.security.auth.login.LoginContext;
/**
* Verify UGI login from keytab. Check that the UGI is
* configured to use keytab to catch regressions like
* HADOOP-10786.
*/
public class TestUGILoginFromKeytab {
private MiniKdc kdc;
private File workDir;
private ExecutorService executor;
@Rule
public final TemporaryFolder folder = new TemporaryFolder();
@Before
public void startMiniKdc() throws Exception {
// This setting below is required. If not enabled, UGI will abort
// any attempt to loginUserFromKeytab.
Configuration conf = new Configuration();
conf.set(CommonConfigurationKeys.HADOOP_SECURITY_AUTHENTICATION,
"kerberos");
UserGroupInformation.setConfiguration(conf);
UserGroupInformation.setShouldRenewImmediatelyForTests(true);
workDir = folder.getRoot();
kdc = new MiniKdc(MiniKdc.createConf(), workDir);
kdc.start();
executor = Executors.newCachedThreadPool();
}
@After
public void stopMiniKdc() {
if (kdc != null) {
kdc.stop();
}
if (executor != null) {
executor.shutdownNow();
}
}
/**
* Login from keytab using the MiniKDC.
*/
@Test
public void testUGILoginFromKeytab() throws Exception {
long beforeLogin = Time.now();
String principal = "foo";
File keytab = new File(workDir, "foo.keytab");
kdc.createPrincipal(keytab, principal);
UserGroupInformation.loginUserFromKeytab(principal, keytab.getPath());
UserGroupInformation ugi = UserGroupInformation.getLoginUser();
Assert.assertTrue("UGI should be configured to login from keytab",
ugi.isFromKeytab());
User user = getUser(ugi.getSubject());
Assert.assertNotNull(user.getLogin());
Assert.assertTrue("User login time is less than before login time, "
+ "beforeLoginTime:" + beforeLogin + " userLoginTime:" + user.getLastLogin(),
user.getLastLogin() > beforeLogin);
}
/**
* Login from keytab using the MiniKDC and verify the UGI can successfully
* relogin from keytab as well. This will catch regressions like HADOOP-10786.
*/
@Test
public void testUGIReLoginFromKeytab() throws Exception {
String principal = "foo";
File keytab = new File(workDir, "foo.keytab");
kdc.createPrincipal(keytab, principal);
UserGroupInformation.loginUserFromKeytab(principal, keytab.getPath());
UserGroupInformation ugi = UserGroupInformation.getLoginUser();
Assert.assertTrue("UGI should be configured to login from keytab",
ugi.isFromKeytab());
// Verify relogin from keytab.
User user = getUser(ugi.getSubject());
final long firstLogin = user.getLastLogin();
final LoginContext login1 = user.getLogin();
Assert.assertNotNull(login1);
// Sleep for 2 secs to have a difference between first and second login
Thread.sleep(2000);
ugi.reloginFromKeytab();
final long secondLogin = user.getLastLogin();
final LoginContext login2 = user.getLogin();
Assert.assertTrue("User should have been able to relogin from keytab",
secondLogin > firstLogin);
Assert.assertNotNull(login2);
Assert.assertNotSame(login1, login2);
}
@Test
public void testGetUGIFromKnownSubject() throws Exception {
KerberosPrincipal principal = new KerberosPrincipal("user");
File keytab = new File(workDir, "user.keytab");
kdc.createPrincipal(keytab, principal.getName());
UserGroupInformation ugi1 =
UserGroupInformation.loginUserFromKeytabAndReturnUGI(
principal.getName(), keytab.getPath());
Subject subject = ugi1.getSubject();
User user = getUser(subject);
Assert.assertNotNull(user);
LoginContext login = user.getLogin();
Assert.assertNotNull(login);
// User instance and/or login context should not change.
UserGroupInformation ugi2 = UserGroupInformation.getUGIFromSubject(subject);
Assert.assertSame(user, getUser(ugi2.getSubject()));
Assert.assertSame(login, user.getLogin());
}
@Test
public void testGetUGIFromExternalSubject() throws Exception {
KerberosPrincipal principal = new KerberosPrincipal("user");
File keytab = new File(workDir, "user.keytab");
kdc.createPrincipal(keytab, principal.getName());
UserGroupInformation ugi =
UserGroupInformation.loginUserFromKeytabAndReturnUGI(
principal.getName(), keytab.getPath());
Subject subject = ugi.getSubject();
removeUser(subject);
// first call to get the ugi should add the User instance w/o a login
// context.
UserGroupInformation ugi1 = UserGroupInformation.getUGIFromSubject(subject);
Assert.assertSame(subject, ugi1.getSubject());
User user = getUser(subject);
Assert.assertNotNull(user);
Assert.assertEquals(principal.getName(), user.getName());
Assert.assertNull(user.getLogin());
// subsequent call should not change the existing User instance.
UserGroupInformation ugi2 = UserGroupInformation.getUGIFromSubject(subject);
Assert.assertSame(subject, ugi2.getSubject());
Assert.assertSame(user, getUser(ugi2.getSubject()));
Assert.assertNull(user.getLogin());
}
@Test
public void testGetUGIFromExternalSubjectWithLogin() throws Exception {
KerberosPrincipal principal = new KerberosPrincipal("user");
File keytab = new File(workDir, "user.keytab");
kdc.createPrincipal(keytab, principal.getName());
// alter the User's login context to appear to be a foreign and
// unmanagable context.
UserGroupInformation ugi =
UserGroupInformation.loginUserFromKeytabAndReturnUGI(
principal.getName(), keytab.getPath());
Subject subject = ugi.getSubject();
User user = getUser(subject);
final LoginContext dummyLogin = Mockito.mock(LoginContext.class);
user.setLogin(dummyLogin);
// nothing should change.
UserGroupInformation ugi2 = UserGroupInformation.getUGIFromSubject(subject);
Assert.assertSame(subject, ugi2.getSubject());
Assert.assertSame(user, getUser(ugi2.getSubject()));
Assert.assertSame(dummyLogin, user.getLogin());
}
@Test
public void testUGIRefreshFromKeytab() throws Exception {
final Configuration conf = new Configuration();
conf.setBoolean(HADOOP_KERBEROS_KEYTAB_LOGIN_AUTORENEWAL_ENABLED, true);
SecurityUtil.setAuthenticationMethod(
UserGroupInformation.AuthenticationMethod.KERBEROS, conf);
UserGroupInformation.setConfiguration(conf);
String principal = "bar";
File keytab = new File(workDir, "bar.keytab");
kdc.createPrincipal(keytab, principal);
UserGroupInformation.loginUserFromKeytab(principal, keytab.getPath());
UserGroupInformation ugi = UserGroupInformation.getLoginUser();
Assert.assertEquals(UserGroupInformation.AuthenticationMethod.KERBEROS,
ugi.getAuthenticationMethod());
Assert.assertTrue(ugi.isFromKeytab());
Assert.assertTrue(
UserGroupInformation.isKerberosKeyTabLoginRenewalEnabled());
Assert.assertTrue(
UserGroupInformation.getKerberosLoginRenewalExecutor()
.isPresent());
}
@Test
public void testUGIRefreshFromKeytabDisabled() throws Exception {
GenericTestUtils.setLogLevel(UserGroupInformation.LOG, Level.DEBUG);
final Configuration conf = new Configuration();
conf.setLong(HADOOP_KERBEROS_MIN_SECONDS_BEFORE_RELOGIN, 1);
conf.setBoolean(HADOOP_KERBEROS_KEYTAB_LOGIN_AUTORENEWAL_ENABLED, false);
SecurityUtil.setAuthenticationMethod(
UserGroupInformation.AuthenticationMethod.KERBEROS, conf);
UserGroupInformation.setConfiguration(conf);
String principal = "bar";
File keytab = new File(workDir, "bar.keytab");
kdc.createPrincipal(keytab, principal);
UserGroupInformation.loginUserFromKeytab(principal, keytab.getPath());
UserGroupInformation ugi = UserGroupInformation.getLoginUser();
Assert.assertEquals(UserGroupInformation.AuthenticationMethod.KERBEROS,
ugi.getAuthenticationMethod());
Assert.assertTrue(ugi.isFromKeytab());
Assert.assertFalse(
UserGroupInformation.isKerberosKeyTabLoginRenewalEnabled());
Assert.assertFalse(
UserGroupInformation.getKerberosLoginRenewalExecutor()
.isPresent());
}
private static KerberosTicket getTicket(UserGroupInformation ugi) {
Set<KerberosTicket> tickets =
ugi.getSubject().getPrivateCredentials(KerberosTicket.class);
return tickets.isEmpty() ? null : tickets.iterator().next();
}
// verify ugi has expected principal, a keytab, and has a ticket for
// the expected principal.
private static KerberosTicket checkTicketAndKeytab(UserGroupInformation ugi,
KerberosPrincipal principal, boolean expectIsKeytab) {
Assert.assertEquals("wrong principal",
principal.getName(), ugi.getUserName());
Assert.assertEquals("is not keytab",
expectIsKeytab, ugi.isFromKeytab());
KerberosTicket ticket = getTicket(ugi);
Assert.assertNotNull("no ticket", ticket);
Assert.assertEquals("wrong principal", principal, ticket.getClient());
return ticket;
}
@Test
public void testReloginForUGIFromSubject() throws Exception {
KerberosPrincipal principal1 = new KerberosPrincipal("user1");
File keytab1 = new File(workDir, "user1.keytab");
kdc.createPrincipal(keytab1, principal1.getName());
KerberosPrincipal principal2 = new KerberosPrincipal("user2");
File keytab2 = new File(workDir, "user2.keytab");
kdc.createPrincipal(keytab2, principal2.getName());
// Login a user and remove the User instance so it looks like an
// "external" subject.
final Subject extSubject =
UserGroupInformation.loginUserFromKeytabAndReturnUGI(
principal2.getName(), keytab2.getPath()).getSubject();
removeUser(extSubject);
// Login another user.
UserGroupInformation.loginUserFromKeytab(
principal1.getName(), keytab1.getPath());
final UserGroupInformation loginUser = UserGroupInformation.getLoginUser();
loginUser.doAs(new PrivilegedExceptionAction<Void>() {
@Override
public Void run() throws IOException {
KerberosTicket loginTicket =
checkTicketAndKeytab(loginUser, principal1, true);
// get the ugi for the previously logged in subject.
UserGroupInformation extSubjectUser =
UserGroupInformation.getUGIFromSubject(extSubject);
KerberosTicket ticket =
checkTicketAndKeytab(extSubjectUser, principal2, false);
// verify login user got a new ticket.
loginUser.reloginFromKeytab();
KerberosTicket newLoginTicket =
checkTicketAndKeytab(loginUser, principal1, true);
Assert.assertNotEquals(loginTicket.getAuthTime(),
newLoginTicket.getAuthTime());
// verify an "external" subject ticket does not change.
extSubjectUser.reloginFromKeytab();
Assert.assertSame(ticket,
checkTicketAndKeytab(extSubjectUser, principal2, false));
// verify subject ugi relogin did not affect the login user.
Assert.assertSame(newLoginTicket,
checkTicketAndKeytab(loginUser, principal1, true));
return null;
}
});
}
@Test
public void testReloginForLoginFromSubject() throws Exception {
KerberosPrincipal principal1 = new KerberosPrincipal("user1");
File keytab1 = new File(workDir, "user1.keytab");
kdc.createPrincipal(keytab1, principal1.getName());
KerberosPrincipal principal2 = new KerberosPrincipal("user2");
File keytab2 = new File(workDir, "user2.keytab");
kdc.createPrincipal(keytab2, principal2.getName());
// login principal1 with a keytab.
UserGroupInformation.loginUserFromKeytab(
principal1.getName(), keytab1.getPath());
final UserGroupInformation originalLoginUser =
UserGroupInformation.getLoginUser();
Assert.assertNotNull(getUser(originalLoginUser.getSubject()).getLogin());
originalLoginUser.doAs(new PrivilegedExceptionAction<Void>() {
@Override
public Void run() throws IOException {
KerberosTicket originalLoginUserTicket =
checkTicketAndKeytab(originalLoginUser, principal1, true);
// login principal2 from a subject with keytab. it's external so
// no login context should be attached to the user.
final Subject subject =
UserGroupInformation.loginUserFromKeytabAndReturnUGI(
principal2.getName(), keytab2.getPath()).getSubject();
removeUser(subject);
// verify the new login user is external.
UserGroupInformation.loginUserFromSubject(subject);
Assert.assertNull(getUser(subject).getLogin());
UserGroupInformation extLoginUser =
UserGroupInformation.getLoginUser();
KerberosTicket extLoginUserTicket =
checkTicketAndKeytab(extLoginUser, principal2, false);
// verify subject-based login user does not get a new ticket, and
// original login user not affected.
extLoginUser.reloginFromKeytab();
Assert.assertSame(extLoginUserTicket,
checkTicketAndKeytab(extLoginUser, principal2, false));
Assert.assertSame(originalLoginUserTicket,
checkTicketAndKeytab(originalLoginUser, principal1, true));
// verify original login user gets a new ticket, new login user
// not affected.
originalLoginUser.reloginFromKeytab();
Assert.assertNotSame(originalLoginUserTicket,
checkTicketAndKeytab(originalLoginUser, principal1, true));
Assert.assertSame(extLoginUserTicket,
checkTicketAndKeytab(extLoginUser, principal2, false));
return null;
}
});
}
@Test
public void testReloginAfterFailedRelogin() throws Exception {
KerberosPrincipal principal = new KerberosPrincipal("user1");
File keytab = new File(workDir, "user1.keytab");
File keytabBackup = new File(keytab + ".backup");
kdc.createPrincipal(keytab, principal.getName());
UserGroupInformation.loginUserFromKeytab(
principal.getName(), keytab.getPath());
final UserGroupInformation loginUser = UserGroupInformation.getLoginUser();
checkTicketAndKeytab(loginUser, principal, true);
// move the keytab to induce a relogin failure.
Assert.assertTrue(keytab.renameTo(keytabBackup));
try {
loginUser.reloginFromKeytab();
Assert.fail("relogin should fail");
} catch (KerberosAuthException kae) {
// expected.
}
// even though no KeyTab object, ugi should know it's keytab based.
Assert.assertTrue(loginUser.isFromKeytab());
Assert.assertNull(getTicket(loginUser));
// move keytab back to enable relogin to succeed.
Assert.assertTrue(keytabBackup.renameTo(keytab));
loginUser.reloginFromKeytab();
checkTicketAndKeytab(loginUser, principal, true);
}
// verify getting concurrent relogins blocks to avoid indeterminate
// credentials corruption, but getting a ugi for the subject does not block.
@Test(timeout=180000)
public void testConcurrentRelogin() throws Exception {
final CyclicBarrier barrier = new CyclicBarrier(2);
final CountDownLatch latch = new CountDownLatch(1);
assertTrue(UserGroupInformation.isSecurityEnabled());
KerberosPrincipal principal = new KerberosPrincipal("testUser");
File keytab = new File(workDir, "user1.keytab");
kdc.createPrincipal(keytab, principal.getName());
// create a keytab ugi.
final UserGroupInformation loginUgi =
UserGroupInformation.loginUserFromKeytabAndReturnUGI(
principal.getName(), keytab.getPath());
assertEquals(AuthenticationMethod.KERBEROS,
loginUgi.getAuthenticationMethod());
assertTrue(loginUgi.isFromKeytab());
// create a new ugi instance based on subject from the logged in user.
final UserGroupInformation clonedUgi =
UserGroupInformation.getUGIFromSubject(loginUgi.getSubject());
assertEquals(AuthenticationMethod.KERBEROS,
clonedUgi.getAuthenticationMethod());
assertTrue(clonedUgi.isFromKeytab());
// cause first relogin to block on a barrier in logout to verify relogins
// are atomic.
User user = getUser(loginUgi.getSubject());
final LoginContext spyLogin = Mockito.spy(user.getLogin());
user.setLogin(spyLogin);
Mockito.doAnswer(new Answer<Void>(){
@Override
public Void answer(InvocationOnMock invocation)
throws Throwable {
invocation.callRealMethod();
latch.countDown();
barrier.await();
return null;
}
}).when(spyLogin).logout();
Future<Void> relogin = executor.submit(
new Callable<Void>(){
@Override
public Void call() throws Exception {
Thread.currentThread().setName("relogin");
loginUgi.reloginFromKeytab();
return null;
}
});
// wait for the thread to block on the barrier in the logout of the
// relogin.
assertTrue("first relogin didn't block",
latch.await(2, TimeUnit.SECONDS));
// although the logout removed the keytab instance, verify the ugi
// knows from its login params that it is supposed to be from a keytab.
assertTrue(clonedUgi.isFromKeytab());
// another concurrent re-login should block.
Mockito.doNothing().when(spyLogin).logout();
Mockito.doNothing().when(spyLogin).login();
Future<UserGroupInformation> clonedRelogin = executor.submit(
new Callable<UserGroupInformation>(){
@Override
public UserGroupInformation call() throws Exception {
Thread.currentThread().setName("clonedRelogin");
clonedUgi.reloginFromKeytab();
return clonedUgi;
}
});
try {
clonedRelogin.get(2, TimeUnit.SECONDS);
fail("second relogin didn't block!");
} catch (TimeoutException te) {
// expected
}
// concurrent UGI instantiation should not block and again should
// know it's supposed to be from a keytab.
loginUgi.doAs(new PrivilegedExceptionAction<Void>(){
@Override
public Void run() throws Exception {
UserGroupInformation ugi = UserGroupInformation.getCurrentUser();
assertEquals(principal.getName(), ugi.getUserName());
assertTrue(ugi.isFromKeytab());
return null;
}
});
clonedUgi.doAs(new PrivilegedExceptionAction<Void>(){
@Override
public Void run() throws Exception {
UserGroupInformation ugi = UserGroupInformation.getCurrentUser();
assertEquals(principal.getName(), ugi.getUserName());
assertTrue(ugi.isFromKeytab());
return null;
}
});
// second relogin should still be blocked until the original relogin
// is blocked.
assertFalse(clonedRelogin.isDone());
barrier.await();
relogin.get();
clonedRelogin.get();
}
private User getUser(Subject subject) {
Iterator<User> iter = subject.getPrincipals(User.class).iterator();
return iter.hasNext() ? iter.next() : null;
}
private void removeUser(Subject subject) {
// remove User instance so it appears to not be logged in.
for (Iterator<Principal> iter = subject.getPrincipals().iterator();
iter.hasNext(); ) {
if (iter.next() instanceof User) {
iter.remove();
}
}
}
}