/** * Copyright 2015 StreamSets Inc. * * Licensed under 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 com.streamsets.datacollector.security; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.streamsets.datacollector.main.RuntimeInfo; import com.streamsets.datacollector.util.Configuration; import org.apache.hadoop.minikdc.MiniKdc; import org.junit.AfterClass; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; import org.mockito.Mockito; import javax.security.auth.Subject; import javax.security.auth.kerberos.KerberosPrincipal; import javax.security.auth.kerberos.KerberosTicket; import java.io.File; import java.net.InetAddress; import java.util.Date; import java.util.Map; import java.util.UUID; import java.util.concurrent.TimeUnit; public class TestSecurityContext { private static File testDir; private static File keytabFile; private static MiniKdc miniKdc; @BeforeClass public static void startKdc() throws Exception { testDir = new File("target", UUID.randomUUID().toString()).getAbsoluteFile(); Assert.assertTrue(testDir.mkdirs()); File kdcDir = new File(testDir, "kdc"); Assert.assertTrue(kdcDir.mkdirs()); keytabFile = new File(testDir, "test.keytab"); miniKdc = new MiniKdc(MiniKdc.createConf(), testDir); miniKdc.start(); miniKdc.createPrincipal(keytabFile, "foo", "bar/localhost"); } @AfterClass public static void stopKdc() { if (miniKdc != null) { miniKdc.stop(); miniKdc = null; } } private RuntimeInfo getMockRuntimeInfo() { RuntimeInfo runtimeInfo = Mockito.mock(RuntimeInfo.class); Mockito.when(runtimeInfo.getConfigDir()).thenReturn(testDir.getAbsolutePath()); Mockito.when(runtimeInfo.getDataDir()).thenReturn(testDir.getAbsolutePath()); return runtimeInfo; } @Test public void testPrincipalResolution() { Configuration conf = new Configuration(); conf.set(SecurityConfiguration.KERBEROS_ENABLED_KEY, true); conf.set(SecurityConfiguration.KERBEROS_KEYTAB_KEY, "test.keytab"); String hostname = SecurityConfiguration.getLocalHostName(); Map<String, String> principals = ImmutableMap.<String, String>builder() .put("foo", "foo") .put("foo/bar","foo/bar") .put("foo/bar@REALM", "foo/bar@REALM") .put("foo/_HOST", "foo/" + hostname) .put("foo/_HOST@REALM", "foo/" + hostname + "@REALM") .put("foo/0.0.0.0", "foo/" + hostname) .put("foo/0.0.0.0@REALM", "foo/" + hostname + "@REALM").build(); for (Map.Entry<String, String> entry : principals.entrySet()) { conf.set(SecurityConfiguration.KERBEROS_PRINCIPAL_KEY, entry.getKey()); SecurityContext context = new SecurityContext(getMockRuntimeInfo(), conf); Assert.assertEquals(entry.getValue(), context.getSecurityConfiguration().getKerberosPrincipal()); } } @Test(expected = RuntimeException.class) public void invalidLogin() throws Exception { Configuration conf = new Configuration(); conf.set(SecurityConfiguration.KERBEROS_ENABLED_KEY, true); conf.set(SecurityConfiguration.KERBEROS_PRINCIPAL_KEY, "foo"); conf.set(SecurityConfiguration.KERBEROS_KEYTAB_KEY, "foo.keytab"); SecurityContext context = new SecurityContext(getMockRuntimeInfo(), conf); Assert.assertTrue(context.getSecurityConfiguration().isKerberosEnabled()); context.login(); } @Test public void notLoggedIn() throws Exception { Configuration conf = new Configuration(); conf.set(SecurityConfiguration.KERBEROS_ENABLED_KEY, true); conf.set(SecurityConfiguration.KERBEROS_PRINCIPAL_KEY, "foo"); conf.set(SecurityConfiguration.KERBEROS_KEYTAB_KEY, "test.keytab"); SecurityContext context = new SecurityContext(getMockRuntimeInfo(), conf); Assert.assertNull(context.getSubject()); } @Test public void loginKerberosDisabled() throws Exception { Configuration conf = new Configuration(); conf.set(SecurityConfiguration.KERBEROS_ENABLED_KEY, false); SecurityContext context = new SecurityContext(getMockRuntimeInfo(), conf); Assert.assertFalse(context.getSecurityConfiguration().isKerberosEnabled()); context.login(); Subject subject = context.getSubject(); Assert.assertNotNull(subject); System.out.println(subject); context.logout(); } @Test public void loginFromAbsoluteKeytab() throws Exception { loginFromKeytab(keytabFile.getAbsolutePath()); } @Test public void loginFromRelativeKeytab() throws Exception { loginFromKeytab(keytabFile.getName()); } private void loginFromKeytab(String keytab) throws Exception { Configuration conf = new Configuration(); conf.set(SecurityConfiguration.KERBEROS_ENABLED_KEY, true); conf.set(SecurityConfiguration.KERBEROS_PRINCIPAL_KEY, "foo"); conf.set(SecurityConfiguration.KERBEROS_KEYTAB_KEY, keytab); SecurityContext context = new SecurityContext(getMockRuntimeInfo(), conf); Assert.assertTrue(context.getSecurityConfiguration().isKerberosEnabled()); context.login(); Subject subject = context.getSubject(); Assert.assertNotNull(subject); System.out.println(subject); context.logout(); } @Test public void testRenewalCalculation() { Configuration conf = new Configuration(); SecurityContext context = new SecurityContext(getMockRuntimeInfo(), conf); context = Mockito.spy(context); Assert.assertEquals(context.getRenewalWindow(), context.getRenewalWindow(), 0.001); Assert.assertTrue(context.getRenewalWindow() >= 0.5); Assert.assertTrue(context.getRenewalWindow() < 0.7); // brute force test for (int i = 0; i < 1000; i++) { double window = context.computeRenewalWindow(); Assert.assertTrue(window >= 0.5); Assert.assertTrue(window < 0.7); } // renewal time Mockito.doReturn(0.5D).when(context).getRenewalWindow(); Assert.assertEquals(10 + (long)(0.5D * (20 - 10)), context.getRenewalTime(10, 20), 0.001); } private KerberosTicket createMockTGT(String name, Date start, Date end) { KerberosPrincipal userPrincipal = new KerberosPrincipal(name); KerberosPrincipal serverPrincipal = new KerberosPrincipal("krbtgt/" + userPrincipal.getRealm()); return new KerberosTicket(new byte[0], userPrincipal, serverPrincipal, new byte[0], 0, new boolean[0], start, start, end, end, new InetAddress[0]); } @Test public void testGetKerberosTicket() { long now = System.currentTimeMillis(); Date expired = new Date(now - TimeUnit.MINUTES.toMillis(30)); Date valid = new Date(now + TimeUnit.DAYS.toMillis(1)); KerberosTicket expiredTicket = createMockTGT("short", expired, expired); KerberosTicket validTicket = createMockTGT("valid", valid, valid); Configuration conf = new Configuration(); SecurityContext context = new SecurityContext(getMockRuntimeInfo(), conf); context = Mockito.spy(context); Mockito.doReturn(now).when(context).getTimeNow(); // 1 ticket, expired, we should return null and remove it from the subject Subject subject = new Subject(); Mockito.doReturn(subject).when(context).getSubject(); subject.getPrivateCredentials().add(expiredTicket); Assert.assertNull(context.getKerberosTicket()); Assert.assertTrue(subject.getPrivateCredentials().isEmpty()); // 2 tickets, 1 expired one not, one short other long expired, we should return the non expired one // and remove the expired on from subject subject = new Subject(); Mockito.doReturn(subject).when(context).getSubject(); Mockito.doReturn(now).when(context).getTimeNow(); subject.getPrivateCredentials().add(expiredTicket); subject.getPrivateCredentials().add(validTicket); Assert.assertEquals(validTicket, context.getKerberosTicket()); Assert.assertEquals(ImmutableSet.of(validTicket), subject.getPrivateCredentials()); } }