/* * Copyright (C) 2015 The Async HBase Authors. All rights reserved. * This file is part of Async HBase. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * - Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * - Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * - Neither the name of the StumbleUpon nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package org.hbase.async.auth; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyLong; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.io.IOException; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; import javax.security.auth.Subject; import javax.security.auth.callback.CallbackHandler; import javax.security.auth.kerberos.KerberosPrincipal; import javax.security.auth.kerberos.KerberosTicket; import javax.security.auth.login.AppConfigurationEntry; import javax.security.auth.login.Configuration; import javax.security.auth.login.LoginContext; import javax.security.auth.login.LoginException; import org.apache.zookeeper.Shell; import org.hbase.async.Config; import org.hbase.async.auth.Login.TicketRenewalTask; import org.jboss.netty.util.HashedWheelTimer; import org.jboss.netty.util.TimerTask; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.powermock.api.mockito.PowerMockito; import org.powermock.core.classloader.annotations.PowerMockIgnore; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; import org.powermock.reflect.Whitebox; @RunWith(PowerMockRunner.class) @PowerMockIgnore({"javax.management.*", "javax.xml.*", "ch.qos.*", "org.slf4j.*", "com.sum.*", "org.xml.*"}) @PrepareForTest({ HashedWheelTimer.class, Configuration.class, Subject.class, AppConfigurationEntry.class, LoginContext.class, Login.class, KerberosTicket.class, KerberosPrincipal.class, System.class, Shell.class }) public class TestLogin { private final static String CONTEXT_NAME = "Uberwald"; private HashedWheelTimer timer; private Config config; private CallbackHandler callback; private AppConfigurationEntry[] app_config; private AppConfigurationEntry app_config_entry; private Map<String, Object> app_config_options; private LoginContext login_context; private Subject subject; private KerberosTicket ticket; private Set<KerberosTicket> tickets; private KerberosPrincipal server; private Date start_time; private Date end_time; @SuppressWarnings("unchecked") @Before public void before() throws Exception { // always make sure to start the unit tests off by setting the login to null // since we can't gaurantee order. Whitebox.setInternalState(Login.class, "current_login", (Login)null); start_time = new Date(1388534400000L); end_time = new Date(1388538000000L); config = new Config(); timer = mock(HashedWheelTimer.class); callback = mock(CallbackHandler.class); app_config_entry = mock(AppConfigurationEntry.class); app_config = new AppConfigurationEntry[] { app_config_entry }; app_config_options = new HashMap<String, Object>(2); app_config_options.put("useTicketCache", "true"); app_config_options.put("principal", "Vetinari"); login_context = mock(LoginContext.class); subject = mock(Subject.class); ticket = PowerMockito.mock(KerberosTicket.class); server = mock(KerberosPrincipal.class); final Configuration app_conf = mock(Configuration.class); when(app_conf.getAppConfigurationEntry(anyString())).thenReturn(app_config); PowerMockito.mockStatic(Configuration.class); PowerMockito.when(Configuration.getConfiguration()).thenReturn(app_conf); PowerMockito.mockStatic(LoginContext.class); PowerMockito.whenNew(LoginContext.class) .withAnyArguments().thenReturn(login_context); when(login_context.getSubject()).thenReturn(subject); tickets = new HashSet<KerberosTicket>(); tickets.add(ticket); when(subject.getPrivateCredentials(any(Class.class))).thenReturn(tickets); when(ticket.getServer()).thenReturn(server); when(ticket.getStartTime()).thenReturn(start_time); when(ticket.getEndTime()).thenReturn(end_time); when(server.getName()).thenReturn("krbtgt/Lancre@Lancre"); when(server.getRealm()).thenReturn("Lancre"); // do NOT shell out during a unit test! PowerMockito.mockStatic(Shell.class); PowerMockito.mockStatic(System.class); PowerMockito.when(System.currentTimeMillis()).thenReturn(1388534460000L); } @Test public void initUserIfNeeded() throws Exception { Login.initUserIfNeeded(config, timer, CONTEXT_NAME, callback); // no-ops Login.initUserIfNeeded(config, timer, CONTEXT_NAME, callback); Login.initUserIfNeeded(config, timer, CONTEXT_NAME, callback); Login.initUserIfNeeded(config, timer, CONTEXT_NAME, callback); verify(login_context, never()).logout(); verify(login_context, times(1)).login(); verify(timer, times(1)).newTimeout((TimerTask)any(), anyLong(), eq(TimeUnit.MILLISECONDS)); } @Test public void ctorWithKerberos() throws Exception { final Login login = new Login(config, timer, CONTEXT_NAME, callback); assertEquals(subject, login.getSubject()); verify(login_context, never()).logout(); verify(login_context, times(1)).login(); verify(timer, times(1)).newTimeout((TimerTask)any(), anyLong(), eq(TimeUnit.MILLISECONDS)); } @Test public void ctorNotKerberos() throws Exception { when(subject.getPrivateCredentials(KerberosTicket.class)) .thenReturn(Collections.<KerberosTicket>emptySet()); final Login login = new Login(config, timer, CONTEXT_NAME, callback); assertEquals(subject, login.getSubject()); verify(login_context, never()).logout(); verify(login_context, times(1)).login(); verify(timer, never()).newTimeout((TimerTask)any(), anyLong(), eq(TimeUnit.MILLISECONDS)); } @Test (expected = LoginException.class) public void ctorNullContextName() throws Exception { new Login(config, timer, null, callback); } @Test (expected = LoginException.class) public void ctorEmptyContextName() throws Exception { new Login(config, timer, "", callback); } @Test (expected = LoginException.class) public void ctorFailed() throws Exception { doThrow(new LoginException("Boo!")).when(login_context).login(); new Login(config, timer, CONTEXT_NAME, callback); } @Test public void getRefreshDelay() throws Exception { final Login login = new Login(config, timer, CONTEXT_NAME, callback); final long delay = (Long)Whitebox.invokeMethod(login, "getRefreshDelay", ticket); // should be within 80% of the end time assertTrue(delay < end_time.getTime()); assertTrue(delay >= (end_time.getTime() - start_time.getTime()) * 0.80); } @Test public void getRefreshDelayNoTicket() throws Exception { final Login login = new Login(config, timer, CONTEXT_NAME, callback); final long delay = (Long)Whitebox.invokeMethod(login, "getRefreshDelay", (KerberosTicket)null); assertEquals(Login.MIN_TIME_BEFORE_RELOGIN, delay); } @Test public void getRefreshDelayCantRenew() throws Exception { when(ticket.getRenewTill()).thenReturn(end_time); final Login login = new Login(config, timer, CONTEXT_NAME, callback); Whitebox.setInternalState(login, "using_ticket_cache", true); final long delay = (Long)Whitebox.invokeMethod(login, "getRefreshDelay", ticket); assertEquals(Login.MIN_TIME_BEFORE_RELOGIN, delay); } @Test public void getRefreshPastExpiration() throws Exception { final Login login = new Login(config, timer, CONTEXT_NAME, callback); PowerMockito.when(System.currentTimeMillis()).thenReturn(1388538060000L); final long delay = (Long)Whitebox.invokeMethod(login, "getRefreshDelay", ticket); assertEquals(Login.MIN_TIME_BEFORE_RELOGIN, delay); } @Test public void getRefreshWithinMinTime() throws Exception { final Login login = new Login(config, timer, CONTEXT_NAME, callback); PowerMockito.when(System.currentTimeMillis()).thenReturn(1388537942000L); final long delay = (Long)Whitebox.invokeMethod(login, "getRefreshDelay", ticket); assertEquals(0, delay); } @Test public void getRefreshSuperShortExpiration() throws Exception { // I guess this prevents possible dos attacks if someone set the lifetime to // be less than a minute final Login login = new Login(config, timer, CONTEXT_NAME, callback); start_time.setTime(1388537942000L); final long delay = (Long)Whitebox.invokeMethod(login, "getRefreshDelay", ticket); assertEquals(Login.MIN_TIME_BEFORE_RELOGIN, delay); } @Test public void getRefreshFlippedTicketTimes() throws Exception { final Login login = new Login(config, timer, CONTEXT_NAME, callback); // Friends don't let friend's KDC issue funky tickets like this end_time.setTime(1388534400000L); start_time.setTime(1388538000000L); final long delay = (Long)Whitebox.invokeMethod(login, "getRefreshDelay", ticket); assertEquals(Login.MIN_TIME_BEFORE_RELOGIN, delay); } @Test public void getRefreshSameTicketTimes() throws Exception { final Login login = new Login(config, timer, CONTEXT_NAME, callback); end_time.setTime(1388534400000L); final long delay = (Long)Whitebox.invokeMethod(login, "getRefreshDelay", ticket); assertEquals(Login.MIN_TIME_BEFORE_RELOGIN, delay); } @Test public void getTGT() throws Exception { final Login login = new Login(config, timer, CONTEXT_NAME, callback); final KerberosTicket tgt = Whitebox.invokeMethod(login, "getTGT"); assertEquals(tgt, ticket); } @Test public void getTGTNoMatch() throws Exception { final Login login = new Login(config, timer, CONTEXT_NAME, callback); when(server.getName()).thenReturn("quirm"); final KerberosTicket tgt = Whitebox.invokeMethod(login, "getTGT"); assertNull(tgt); } @Test public void getTGTNoTickets() throws Exception { final Login login = new Login(config, timer, CONTEXT_NAME, callback); when(subject.getPrivateCredentials(KerberosTicket.class)) .thenReturn(Collections.<KerberosTicket>emptySet()); final KerberosTicket tgt = Whitebox.invokeMethod(login, "getTGT"); assertNull(tgt); } @Test public void refreshTicketCache() throws Exception { final Login login = new Login(config, timer, CONTEXT_NAME, callback); Whitebox.invokeMethod(login, "refreshTicketCache"); PowerMockito.verifyStatic(times(1)); Shell.execCommand("/usr/bin/kinit", "-R"); } @Test public void refreshTicketCacheConfigPath() throws Exception { config.overrideConfig("asynchbase.security.auth.kinit", "/usr/local/bin/kinit"); final Login login = new Login(config, timer, CONTEXT_NAME, callback); Whitebox.invokeMethod(login, "refreshTicketCache"); PowerMockito.verifyStatic(times(1)); Shell.execCommand("/usr/local/bin/kinit", "-R"); } @Test (expected = RuntimeException.class) public void refreshTicketCacheIOException() throws Exception { final Login login = new Login(config, timer, CONTEXT_NAME, callback); PowerMockito.when(Shell.execCommand(anyString(), anyString())) .thenThrow(new IOException("Boo!")); Whitebox.invokeMethod(login, "refreshTicketCache"); } @Test (expected = RuntimeException.class) public void refreshTicketCacheException() throws Exception { final Login login = new Login(config, timer, CONTEXT_NAME, callback); PowerMockito.when(Shell.execCommand(anyString(), anyString())) .thenThrow(new Exception("Boo!")); Whitebox.invokeMethod(login, "refreshTicketCache"); } @Test public void refreshTicketCacheEmptyCommand() throws Exception { config.overrideConfig("asynchbase.security.auth.kinit", ""); final Login login = new Login(config, timer, CONTEXT_NAME, callback); Whitebox.invokeMethod(login, "refreshTicketCache"); PowerMockito.verifyStatic(times(1)); Shell.execCommand("/usr/bin/kinit", "-R"); } @Test public void reLogin() throws Exception { final Login login = new Login(config, timer, CONTEXT_NAME, callback); Whitebox.invokeMethod(login, "reLogin"); verify(login_context, times(1)).logout(); verify(login_context, times(2)).login(); } @Test public void reLoginNotKerberos() throws Exception { when(subject.getPrivateCredentials(KerberosTicket.class)) .thenReturn(Collections.<KerberosTicket>emptySet()); final Login login = new Login(config, timer, CONTEXT_NAME, callback); Whitebox.invokeMethod(login, "reLogin"); verify(login_context, never()).logout(); verify(login_context, times(1)).login(); } @Test (expected = LoginException.class) public void reLoginNotLoggedInYet() throws Exception { final Login login = new Login(config, timer, CONTEXT_NAME, callback); Whitebox.setInternalState(login, "login_context", (LoginContext)null); Whitebox.invokeMethod(login, "reLogin"); } @Test (expected = LoginException.class) public void reLoginLoginFailed() throws Exception { final Login login = new Login(config, timer, CONTEXT_NAME, callback); doThrow(new LoginException("Boo!")).when(login_context).login(); Whitebox.invokeMethod(login, "reLogin"); } @Test public void ticketRenewalTask() throws Exception { final Login login = new Login(config, timer, CONTEXT_NAME, callback); final TicketRenewalTask task = login.new TicketRenewalTask(); task.run(null); verify(timer, times(2)).newTimeout((TimerTask)any(), anyLong(), eq(TimeUnit.MILLISECONDS)); verify(login_context, times(1)).logout(); verify(login_context, times(2)).login(); PowerMockito.verifyStatic(never()); Shell.execCommand("/usr/bin/kinit", "-R"); } @Test public void ticketRenewalTaskRefreshCache() throws Exception { final Login login = new Login(config, timer, CONTEXT_NAME, callback); Whitebox.setInternalState(login, "using_ticket_cache", true); final TicketRenewalTask task = login.new TicketRenewalTask(); task.run(null); verify(timer, times(2)).newTimeout((TimerTask)any(), anyLong(), eq(TimeUnit.MILLISECONDS)); verify(login_context, times(1)).logout(); verify(login_context, times(2)).login(); PowerMockito.verifyStatic(times(1)); Shell.execCommand("/usr/bin/kinit", "-R"); } @Test public void ticketRenewalTaskLoginException() throws Exception { final Login login = new Login(config, timer, CONTEXT_NAME, callback); Whitebox.setInternalState(login, "login_context", (LoginContext)null); final TicketRenewalTask task = login.new TicketRenewalTask(); task.run(null); verify(timer, times(2)).newTimeout((TimerTask)any(), anyLong(), eq(TimeUnit.MILLISECONDS)); // catch the default refresh rate verify(timer, times(1)).newTimeout((TimerTask)any(), eq(Login.MIN_TIME_BEFORE_RELOGIN), eq(TimeUnit.MILLISECONDS)); verify(login_context, never()).logout(); verify(login_context, times(1)).login(); PowerMockito.verifyStatic(never()); Shell.execCommand("/usr/bin/kinit", "-R"); } @Test public void ticketRenewalTaskException() throws Exception { final Login login = new Login(config, timer, CONTEXT_NAME, callback); doThrow(new RuntimeException("Boo!")).when(login_context).login(); final TicketRenewalTask task = login.new TicketRenewalTask(); task.run(null); verify(timer, times(2)).newTimeout((TimerTask)any(), anyLong(), eq(TimeUnit.MILLISECONDS)); // catch the default refresh rate verify(timer, times(1)).newTimeout((TimerTask)any(), eq(Login.MIN_TIME_BEFORE_RELOGIN), eq(TimeUnit.MILLISECONDS)); verify(login_context, times(1)).logout(); verify(login_context, times(2)).login(); PowerMockito.verifyStatic(never()); Shell.execCommand("/usr/bin/kinit", "-R"); } }