blob: cf2a8b6af3cd8712efbc8e324df22afa5466f19e [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.CommonConfigurationKeysPublic;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.io.retry.RetryPolicies;
import org.apache.hadoop.io.retry.RetryPolicy;
import org.apache.hadoop.ipc.TestRpcBase.TestTokenIdentifier;
import org.apache.hadoop.metrics2.MetricsRecordBuilder;
import org.apache.hadoop.security.SaslRpcServer.AuthMethod;
import org.apache.hadoop.security.UserGroupInformation.AuthenticationMethod;
import org.apache.hadoop.security.authentication.util.KerberosName;
import org.apache.hadoop.security.token.Token;
import org.apache.hadoop.security.token.TokenIdentifier;
import org.apache.hadoop.test.GenericTestUtils;
import org.apache.hadoop.util.Shell;
import org.apache.hadoop.util.StringUtils;
import org.apache.hadoop.util.Time;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.event.Level;
import javax.security.auth.Subject;
import javax.security.auth.kerberos.KerberosPrincipal;
import javax.security.auth.kerberos.KerberosTicket;
import javax.security.auth.kerberos.KeyTab;
import javax.security.auth.login.AppConfigurationEntry;
import javax.security.auth.login.LoginContext;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
import java.security.Principal;
import java.security.PrivilegedExceptionAction;
import java.util.Collection;
import java.util.ConcurrentModificationException;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import static org.apache.hadoop.fs.CommonConfigurationKeys.HADOOP_USER_GROUP_METRICS_PERCENTILES_INTERVALS;
import static org.apache.hadoop.fs.CommonConfigurationKeysPublic.HADOOP_KERBEROS_MIN_SECONDS_BEFORE_RELOGIN;
import static org.apache.hadoop.fs.CommonConfigurationKeysPublic.HADOOP_SECURITY_AUTH_TO_LOCAL;
import static org.apache.hadoop.fs.CommonConfigurationKeysPublic.HADOOP_SECURITY_AUTH_TO_LOCAL_MECHANISM;
import static org.apache.hadoop.test.MetricsAsserts.assertCounter;
import static org.apache.hadoop.test.MetricsAsserts.assertCounterGt;
import static org.apache.hadoop.test.MetricsAsserts.assertGaugeGt;
import static org.apache.hadoop.test.MetricsAsserts.assertQuantileGauges;
import static org.apache.hadoop.test.MetricsAsserts.getDoubleGauge;
import static org.apache.hadoop.test.MetricsAsserts.getMetrics;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
public class TestUserGroupInformation {
static final Logger LOG = LoggerFactory.getLogger(
TestUserGroupInformation.class);
final private static String USER_NAME = "user1@HADOOP.APACHE.ORG";
final private static String GROUP1_NAME = "group1";
final private static String GROUP2_NAME = "group2";
final private static String GROUP3_NAME = "group3";
final private static String[] GROUP_NAMES =
new String[]{GROUP1_NAME, GROUP2_NAME, GROUP3_NAME};
// Rollover interval of percentile metrics (in seconds)
private static final int PERCENTILES_INTERVAL = 1;
private static Configuration conf;
/**
* UGI should not use the default security conf, else it will collide
* with other classes that may change the default conf. Using this dummy
* class that simply throws an exception will ensure that the tests fail
* if UGI uses the static default config instead of its own config
*/
private static class DummyLoginConfiguration extends
javax.security.auth.login.Configuration
{
@Override
public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
throw new RuntimeException("UGI is not using its own security conf!");
}
}
// must be set immediately to avoid inconsistent testing issues.
static {
// fake the realm is kerberos is enabled
System.setProperty("java.security.krb5.kdc", "");
System.setProperty("java.security.krb5.realm", "DEFAULT.REALM");
}
/** configure ugi */
@BeforeClass
public static void setup() {
javax.security.auth.login.Configuration.setConfiguration(
new DummyLoginConfiguration());
// doesn't matter what it is, but getGroups needs it set...
// use HADOOP_HOME environment variable to prevent interfering with logic
// that finds winutils.exe
String home = System.getenv("HADOOP_HOME");
System.setProperty("hadoop.home.dir", (home != null ? home : "."));
}
@Before
public void setupUgi() {
conf = new Configuration();
UserGroupInformation.reset();
UserGroupInformation.setConfiguration(conf);
}
@After
public void resetUgi() {
UserGroupInformation.setLoginUser(null);
}
@Test(timeout = 30000)
public void testSimpleLogin() throws IOException {
tryLoginAuthenticationMethod(AuthenticationMethod.SIMPLE, true);
}
@Test (timeout = 30000)
public void testTokenLogin() throws IOException {
tryLoginAuthenticationMethod(AuthenticationMethod.TOKEN, false);
}
@Test (timeout = 30000)
public void testProxyLogin() throws IOException {
tryLoginAuthenticationMethod(AuthenticationMethod.PROXY, false);
}
private void tryLoginAuthenticationMethod(AuthenticationMethod method,
boolean expectSuccess)
throws IOException {
SecurityUtil.setAuthenticationMethod(method, conf);
UserGroupInformation.setConfiguration(conf); // pick up changed auth
UserGroupInformation ugi = null;
Exception ex = null;
try {
ugi = UserGroupInformation.getLoginUser();
} catch (Exception e) {
ex = e;
}
if (expectSuccess) {
assertNotNull(ugi);
assertEquals(method, ugi.getAuthenticationMethod());
} else {
assertNotNull(ex);
assertEquals(UnsupportedOperationException.class, ex.getClass());
assertEquals(method + " login authentication is not supported",
ex.getMessage());
}
}
@Test (timeout = 30000)
public void testGetRealAuthenticationMethod() {
UserGroupInformation ugi = UserGroupInformation.createRemoteUser("user1");
ugi.setAuthenticationMethod(AuthenticationMethod.SIMPLE);
assertEquals(AuthenticationMethod.SIMPLE, ugi.getAuthenticationMethod());
assertEquals(AuthenticationMethod.SIMPLE, ugi.getRealAuthenticationMethod());
ugi = UserGroupInformation.createProxyUser("user2", ugi);
assertEquals(AuthenticationMethod.PROXY, ugi.getAuthenticationMethod());
assertEquals(AuthenticationMethod.SIMPLE, ugi.getRealAuthenticationMethod());
}
@Test (timeout = 30000)
public void testCreateRemoteUser() {
UserGroupInformation ugi = UserGroupInformation.createRemoteUser("user1");
assertEquals(AuthenticationMethod.SIMPLE, ugi.getAuthenticationMethod());
assertTrue (ugi.toString().contains("(auth:SIMPLE)"));
ugi = UserGroupInformation.createRemoteUser("user1",
AuthMethod.KERBEROS);
assertEquals(AuthenticationMethod.KERBEROS, ugi.getAuthenticationMethod());
assertTrue (ugi.toString().contains("(auth:KERBEROS)"));
}
/** Test login method */
@Test (timeout = 30000)
public void testLogin() throws Exception {
conf.set(HADOOP_USER_GROUP_METRICS_PERCENTILES_INTERVALS,
String.valueOf(PERCENTILES_INTERVAL));
UserGroupInformation.setConfiguration(conf);
// login from unix
UserGroupInformation ugi = UserGroupInformation.getCurrentUser();
assertEquals(UserGroupInformation.getCurrentUser(),
UserGroupInformation.getLoginUser());
assertTrue(ugi.getGroupNames().length >= 1);
verifyGroupMetrics(1);
// ensure that doAs works correctly
UserGroupInformation userGroupInfo =
UserGroupInformation.createUserForTesting(USER_NAME, GROUP_NAMES);
UserGroupInformation curUGI =
userGroupInfo.doAs(new PrivilegedExceptionAction<UserGroupInformation>(){
@Override
public UserGroupInformation run() throws IOException {
return UserGroupInformation.getCurrentUser();
}});
// make sure in the scope of the doAs, the right user is current
assertEquals(curUGI, userGroupInfo);
// make sure it is not the same as the login user
assertFalse(curUGI.equals(UserGroupInformation.getLoginUser()));
}
/**
* given user name - get all the groups.
* Needs to happen before creating the test users
*/
@Test (timeout = 30000)
public void testGetServerSideGroups() throws IOException,
InterruptedException {
// get the user name
Process pp = Runtime.getRuntime().exec("whoami");
BufferedReader br = new BufferedReader
(new InputStreamReader(pp.getInputStream()));
String userName = br.readLine().trim();
// If on windows domain, token format is DOMAIN\\user and we want to
// extract only the user name
if(Shell.WINDOWS) {
int sp = userName.lastIndexOf('\\');
if (sp != -1) {
userName = userName.substring(sp + 1);
}
// user names are case insensitive on Windows. Make consistent
userName = StringUtils.toLowerCase(userName);
}
// get the groups
pp = Runtime.getRuntime().exec(Shell.WINDOWS ?
Shell.getWinUtilsPath() + " groups -F"
: "id -Gn " + userName);
br = new BufferedReader(new InputStreamReader(pp.getInputStream()));
String line = br.readLine();
System.out.println(userName + ":" + line);
Set<String> groups = new LinkedHashSet<String> ();
String[] tokens = line.split(Shell.TOKEN_SEPARATOR_REGEX);
for(String s: tokens) {
groups.add(s);
}
final UserGroupInformation login = UserGroupInformation.getCurrentUser();
String loginUserName = login.getShortUserName();
if(Shell.WINDOWS) {
// user names are case insensitive on Windows. Make consistent
loginUserName = StringUtils.toLowerCase(loginUserName);
}
assertEquals(userName, loginUserName);
String[] gi = login.getGroupNames();
assertEquals(groups.size(), gi.length);
for(int i=0; i < gi.length; i++) {
assertTrue(groups.contains(gi[i]));
}
final UserGroupInformation fakeUser =
UserGroupInformation.createRemoteUser("foo.bar");
fakeUser.doAs(new PrivilegedExceptionAction<Object>(){
@Override
public Object run() throws IOException {
UserGroupInformation current = UserGroupInformation.getCurrentUser();
assertFalse(current.equals(login));
assertEquals(current, fakeUser);
assertEquals(0, current.getGroupNames().length);
return null;
}});
}
/** test constructor */
@Test (timeout = 30000)
public void testConstructor() throws Exception {
// security off, so default should just return simple name
testConstructorSuccess("user1", "user1");
testConstructorSuccess("user2@DEFAULT.REALM", "user2");
testConstructorSuccess("user3/cron@DEFAULT.REALM", "user3");
testConstructorSuccess("user4@OTHER.REALM", "user4");
testConstructorSuccess("user5/cron@OTHER.REALM", "user5");
// failure test
testConstructorFailures(null);
testConstructorFailures("");
}
/** test constructor */
@Test (timeout = 30000)
public void testConstructorWithRules() throws Exception {
// security off, but use rules if explicitly set
conf.set(HADOOP_SECURITY_AUTH_TO_LOCAL,
"RULE:[1:$1@$0](.*@OTHER.REALM)s/(.*)@.*/other-$1/");
conf.set(HADOOP_SECURITY_AUTH_TO_LOCAL_MECHANISM, "hadoop");
UserGroupInformation.setConfiguration(conf);
testConstructorSuccess("user1", "user1");
testConstructorSuccess("user4@OTHER.REALM", "other-user4");
// failure test
testConstructorFailures("user2@DEFAULT.REALM");
testConstructorFailures("user3/cron@DEFAULT.REALM");
testConstructorFailures("user5/cron@OTHER.REALM");
// with MIT
conf.set(HADOOP_SECURITY_AUTH_TO_LOCAL_MECHANISM, "mit");
UserGroupInformation.setConfiguration(conf);
testConstructorSuccess("user2@DEFAULT.REALM", "user2@DEFAULT.REALM");
testConstructorSuccess("user3/cron@DEFAULT.REALM", "user3/cron@DEFAULT.REALM");
testConstructorSuccess("user5/cron@OTHER.REALM", "user5/cron@OTHER.REALM");
// failures
testConstructorFailures("user6@example.com@OTHER.REALM");
testConstructorFailures("user7@example.com@DEFAULT.REALM");
testConstructorFailures(null);
testConstructorFailures("");
conf.set(HADOOP_SECURITY_AUTH_TO_LOCAL_MECHANISM, "hadoop");
}
/** test constructor */
@Test (timeout = 30000)
public void testConstructorWithKerberos() throws Exception {
// security on, default is remove default realm
conf.set(HADOOP_SECURITY_AUTH_TO_LOCAL_MECHANISM, "hadoop");
SecurityUtil.setAuthenticationMethod(AuthenticationMethod.KERBEROS, conf);
UserGroupInformation.setConfiguration(conf);
testConstructorSuccess("user1", "user1");
testConstructorSuccess("user2@DEFAULT.REALM", "user2");
testConstructorSuccess("user3/cron@DEFAULT.REALM", "user3");
// failure test
testConstructorFailures("user4@OTHER.REALM");
testConstructorFailures("user5/cron@OTHER.REALM");
// with MIT
conf.set(HADOOP_SECURITY_AUTH_TO_LOCAL_MECHANISM, "mit");
UserGroupInformation.setConfiguration(conf);
testConstructorSuccess("user4@OTHER.REALM", "user4@OTHER.REALM");
testConstructorSuccess("user5/cron@OTHER.REALM", "user5/cron@OTHER.REALM");
// failures
testConstructorFailures(null);
testConstructorFailures("");
conf.set(HADOOP_SECURITY_AUTH_TO_LOCAL_MECHANISM, "hadoop");
}
/** test constructor */
@Test (timeout = 30000)
public void testConstructorWithKerberosRules() throws Exception {
// security on, explicit rules
SecurityUtil.setAuthenticationMethod(AuthenticationMethod.KERBEROS, conf);
conf.set(HADOOP_SECURITY_AUTH_TO_LOCAL,
"RULE:[2:$1@$0](.*@OTHER.REALM)s/(.*)@.*/other-$1/" +
"RULE:[1:$1@$0](.*@OTHER.REALM)s/(.*)@.*/other-$1/" +
"DEFAULT");
UserGroupInformation.setConfiguration(conf);
testConstructorSuccess("user1", "user1");
testConstructorSuccess("user2@DEFAULT.REALM", "user2");
testConstructorSuccess("user3/cron@DEFAULT.REALM", "user3");
testConstructorSuccess("user4@OTHER.REALM", "other-user4");
testConstructorSuccess("user5/cron@OTHER.REALM", "other-user5");
// failure test
testConstructorFailures(null);
testConstructorFailures("");
}
private void testConstructorSuccess(String principal, String shortName) {
UserGroupInformation ugi =
UserGroupInformation.createUserForTesting(principal, GROUP_NAMES);
// make sure the short and full user names are correct
assertEquals(principal, ugi.getUserName());
assertEquals(shortName, ugi.getShortUserName());
}
private void testConstructorFailures(String userName) {
try {
UserGroupInformation.createRemoteUser(userName);
fail("user:"+userName+" wasn't invalid");
} catch (IllegalArgumentException e) {
String expect = (userName == null || userName.isEmpty())
? "Null user" : "Illegal principal name "+userName;
String expect2 = "Malformed Kerberos name: "+userName;
assertTrue("Did not find "+ expect + " or " + expect2 + " in " + e,
e.toString().contains(expect) || e.toString().contains(expect2));
}
}
@Test (timeout = 30000)
public void testSetConfigWithRules() {
String[] rules = { "RULE:[1:TEST1]", "RULE:[1:TEST2]", "RULE:[1:TEST3]" };
// explicitly set a rule
UserGroupInformation.reset();
assertFalse(KerberosName.hasRulesBeenSet());
KerberosName.setRules(rules[0]);
assertTrue(KerberosName.hasRulesBeenSet());
assertEquals(rules[0], KerberosName.getRules());
// implicit init should honor rules already being set
UserGroupInformation.createUserForTesting("someone", new String[0]);
assertEquals(rules[0], KerberosName.getRules());
// set conf, should override
conf.set(HADOOP_SECURITY_AUTH_TO_LOCAL, rules[1]);
UserGroupInformation.setConfiguration(conf);
assertEquals(rules[1], KerberosName.getRules());
// set conf, should again override
conf.set(HADOOP_SECURITY_AUTH_TO_LOCAL, rules[2]);
UserGroupInformation.setConfiguration(conf);
assertEquals(rules[2], KerberosName.getRules());
// implicit init should honor rules already being set
UserGroupInformation.createUserForTesting("someone", new String[0]);
assertEquals(rules[2], KerberosName.getRules());
}
@Test (timeout = 30000)
public void testEnsureInitWithRules() throws IOException {
String rules = "RULE:[1:RULE1]";
// trigger implicit init, rules should init
UserGroupInformation.reset();
assertFalse(KerberosName.hasRulesBeenSet());
UserGroupInformation.createUserForTesting("someone", new String[0]);
assertTrue(KerberosName.hasRulesBeenSet());
// set a rule, trigger implicit init, rule should not change
UserGroupInformation.reset();
KerberosName.setRules(rules);
assertTrue(KerberosName.hasRulesBeenSet());
assertEquals(rules, KerberosName.getRules());
UserGroupInformation.createUserForTesting("someone", new String[0]);
assertEquals(rules, KerberosName.getRules());
}
@Test (timeout = 30000)
public void testEquals() throws Exception {
UserGroupInformation uugi =
UserGroupInformation.createUserForTesting(USER_NAME, GROUP_NAMES);
assertEquals(uugi, uugi);
// The subjects should be different, so this should fail
UserGroupInformation ugi2 =
UserGroupInformation.createUserForTesting(USER_NAME, GROUP_NAMES);
assertFalse(uugi.equals(ugi2));
assertFalse(uugi.hashCode() == ugi2.hashCode());
// two ugi that have the same subject need to be equal
UserGroupInformation ugi3 = new UserGroupInformation(uugi.getSubject());
assertEquals(uugi, ugi3);
assertEquals(uugi.hashCode(), ugi3.hashCode());
}
@Test (timeout = 30000)
public void testEqualsWithRealUser() throws Exception {
UserGroupInformation realUgi1 = UserGroupInformation.createUserForTesting(
"RealUser", GROUP_NAMES);
UserGroupInformation proxyUgi1 = UserGroupInformation.createProxyUser(
USER_NAME, realUgi1);
UserGroupInformation proxyUgi2 =
new UserGroupInformation( proxyUgi1.getSubject());
UserGroupInformation remoteUgi = UserGroupInformation.createRemoteUser(USER_NAME);
assertEquals(proxyUgi1, proxyUgi2);
assertFalse(remoteUgi.equals(proxyUgi1));
}
@Test (timeout = 30000)
public void testGettingGroups() throws Exception {
UserGroupInformation uugi =
UserGroupInformation.createUserForTesting(USER_NAME, GROUP_NAMES);
assertEquals(USER_NAME, uugi.getUserName());
String[] expected = new String[]{GROUP1_NAME, GROUP2_NAME, GROUP3_NAME};
assertArrayEquals(expected, uugi.getGroupNames());
assertArrayEquals(expected, uugi.getGroups().toArray(new String[0]));
assertEquals(GROUP1_NAME, uugi.getPrimaryGroupName());
}
@SuppressWarnings("unchecked") // from Mockito mocks
@Test (timeout = 30000)
public <T extends TokenIdentifier> void testAddToken() throws Exception {
UserGroupInformation ugi =
UserGroupInformation.createRemoteUser("someone");
Token<T> t1 = mock(Token.class);
Token<T> t2 = mock(Token.class);
Token<T> t3 = mock(Token.class);
// add token to ugi
ugi.addToken(t1);
checkTokens(ugi, t1);
// replace token t1 with t2 - with same key (null)
ugi.addToken(t2);
checkTokens(ugi, t2);
// change t1 service and add token
when(t1.getService()).thenReturn(new Text("t1"));
ugi.addToken(t1);
checkTokens(ugi, t1, t2);
// overwrite t1 token with t3 - same key (!null)
when(t3.getService()).thenReturn(new Text("t1"));
ugi.addToken(t3);
checkTokens(ugi, t2, t3);
// just try to re-add with new name
when(t1.getService()).thenReturn(new Text("t1.1"));
ugi.addToken(t1);
checkTokens(ugi, t1, t2, t3);
// just try to re-add with new name again
ugi.addToken(t1);
checkTokens(ugi, t1, t2, t3);
}
@SuppressWarnings("unchecked") // from Mockito mocks
@Test (timeout = 30000)
public <T extends TokenIdentifier> void testGetCreds() throws Exception {
UserGroupInformation ugi =
UserGroupInformation.createRemoteUser("someone");
Text service = new Text("service");
Token<T> t1 = mock(Token.class);
when(t1.getService()).thenReturn(service);
Token<T> t2 = mock(Token.class);
when(t2.getService()).thenReturn(new Text("service2"));
Token<T> t3 = mock(Token.class);
when(t3.getService()).thenReturn(service);
// add token to ugi
ugi.addToken(t1);
ugi.addToken(t2);
checkTokens(ugi, t1, t2);
Credentials creds = ugi.getCredentials();
creds.addToken(t3.getService(), t3);
assertSame(t3, creds.getToken(service));
// check that ugi wasn't modified
checkTokens(ugi, t1, t2);
}
@SuppressWarnings("unchecked") // from Mockito mocks
@Test (timeout = 30000)
public <T extends TokenIdentifier> void testAddCreds() throws Exception {
UserGroupInformation ugi =
UserGroupInformation.createRemoteUser("someone");
Text service = new Text("service");
Token<T> t1 = mock(Token.class);
when(t1.getService()).thenReturn(service);
Token<T> t2 = mock(Token.class);
when(t2.getService()).thenReturn(new Text("service2"));
byte[] secret = new byte[]{};
Text secretKey = new Text("sshhh");
// fill credentials
Credentials creds = new Credentials();
creds.addToken(t1.getService(), t1);
creds.addToken(t2.getService(), t2);
creds.addSecretKey(secretKey, secret);
// add creds to ugi, and check ugi
ugi.addCredentials(creds);
checkTokens(ugi, t1, t2);
assertSame(secret, ugi.getCredentials().getSecretKey(secretKey));
}
@Test (timeout = 30000)
public <T extends TokenIdentifier> void testGetCredsNotSame()
throws Exception {
UserGroupInformation ugi =
UserGroupInformation.createRemoteUser("someone");
Credentials creds = ugi.getCredentials();
// should always get a new copy
assertNotSame(creds, ugi.getCredentials());
}
private void checkTokens(UserGroupInformation ugi, Token<?> ... tokens) {
// check the ugi's token collection
Collection<Token<?>> ugiTokens = ugi.getTokens();
for (Token<?> t : tokens) {
assertTrue(ugiTokens.contains(t));
}
assertEquals(tokens.length, ugiTokens.size());
// check the ugi's credentials
Credentials ugiCreds = ugi.getCredentials();
for (Token<?> t : tokens) {
assertSame(t, ugiCreds.getToken(t.getService()));
}
assertEquals(tokens.length, ugiCreds.numberOfTokens());
}
@SuppressWarnings("unchecked") // from Mockito mocks
@Test (timeout = 30000)
public <T extends TokenIdentifier> void testAddNamedToken() throws Exception {
UserGroupInformation ugi =
UserGroupInformation.createRemoteUser("someone");
Token<T> t1 = mock(Token.class);
Text service1 = new Text("t1");
Text service2 = new Text("t2");
when(t1.getService()).thenReturn(service1);
// add token
ugi.addToken(service1, t1);
assertSame(t1, ugi.getCredentials().getToken(service1));
// add token with another name
ugi.addToken(service2, t1);
assertSame(t1, ugi.getCredentials().getToken(service1));
assertSame(t1, ugi.getCredentials().getToken(service2));
}
@SuppressWarnings("unchecked") // from Mockito mocks
@Test (timeout = 30000)
public <T extends TokenIdentifier> void testUGITokens() throws Exception {
UserGroupInformation ugi =
UserGroupInformation.createUserForTesting("TheDoctor",
new String [] { "TheTARDIS"});
Token<T> t1 = mock(Token.class);
when(t1.getService()).thenReturn(new Text("t1"));
Token<T> t2 = mock(Token.class);
when(t2.getService()).thenReturn(new Text("t2"));
Credentials creds = new Credentials();
byte[] secretKey = new byte[]{};
Text secretName = new Text("shhh");
creds.addSecretKey(secretName, secretKey);
ugi.addToken(t1);
ugi.addToken(t2);
ugi.addCredentials(creds);
Collection<Token<? extends TokenIdentifier>> z = ugi.getTokens();
assertTrue(z.contains(t1));
assertTrue(z.contains(t2));
assertEquals(2, z.size());
Credentials ugiCreds = ugi.getCredentials();
assertSame(secretKey, ugiCreds.getSecretKey(secretName));
assertEquals(1, ugiCreds.numberOfSecretKeys());
try {
z.remove(t1);
fail("Shouldn't be able to modify token collection from UGI");
} catch(UnsupportedOperationException uoe) {
// Can't modify tokens
}
// ensure that the tokens are passed through doAs
Collection<Token<? extends TokenIdentifier>> otherSet =
ugi.doAs(new PrivilegedExceptionAction<Collection<Token<?>>>(){
@Override
public Collection<Token<?>> run() throws IOException {
return UserGroupInformation.getCurrentUser().getTokens();
}
});
assertTrue(otherSet.contains(t1));
assertTrue(otherSet.contains(t2));
}
@Test (timeout = 30000)
public void testTokenIdentifiers() throws Exception {
UserGroupInformation ugi = UserGroupInformation.createUserForTesting(
"TheDoctor", new String[] { "TheTARDIS" });
TokenIdentifier t1 = mock(TokenIdentifier.class);
TokenIdentifier t2 = mock(TokenIdentifier.class);
ugi.addTokenIdentifier(t1);
ugi.addTokenIdentifier(t2);
Collection<TokenIdentifier> z = ugi.getTokenIdentifiers();
assertTrue(z.contains(t1));
assertTrue(z.contains(t2));
assertEquals(2, z.size());
// ensure that the token identifiers are passed through doAs
Collection<TokenIdentifier> otherSet = ugi
.doAs(new PrivilegedExceptionAction<Collection<TokenIdentifier>>() {
@Override
public Collection<TokenIdentifier> run() throws IOException {
return UserGroupInformation.getCurrentUser().getTokenIdentifiers();
}
});
assertTrue(otherSet.contains(t1));
assertTrue(otherSet.contains(t2));
assertEquals(2, otherSet.size());
}
@Test (timeout = 30000)
public void testTestAuthMethod() throws Exception {
UserGroupInformation ugi = UserGroupInformation.getCurrentUser();
// verify the reverse mappings works
for (AuthenticationMethod am : AuthenticationMethod.values()) {
if (am.getAuthMethod() != null) {
ugi.setAuthenticationMethod(am.getAuthMethod());
assertEquals(am, ugi.getAuthenticationMethod());
}
}
}
@Test (timeout = 30000)
public void testUGIAuthMethod() throws Exception {
final UserGroupInformation ugi = UserGroupInformation.getCurrentUser();
final AuthenticationMethod am = AuthenticationMethod.KERBEROS;
ugi.setAuthenticationMethod(am);
Assert.assertEquals(am, ugi.getAuthenticationMethod());
ugi.doAs(new PrivilegedExceptionAction<Object>() {
@Override
public Object run() throws IOException {
Assert.assertEquals(am, UserGroupInformation.getCurrentUser()
.getAuthenticationMethod());
return null;
}
});
}
@Test (timeout = 30000)
public void testUGIAuthMethodInRealUser() throws Exception {
final UserGroupInformation ugi = UserGroupInformation.getCurrentUser();
UserGroupInformation proxyUgi = UserGroupInformation.createProxyUser(
"proxy", ugi);
final AuthenticationMethod am = AuthenticationMethod.KERBEROS;
ugi.setAuthenticationMethod(am);
Assert.assertEquals(am, ugi.getAuthenticationMethod());
Assert.assertEquals(AuthenticationMethod.PROXY,
proxyUgi.getAuthenticationMethod());
Assert.assertEquals(am, UserGroupInformation
.getRealAuthenticationMethod(proxyUgi));
proxyUgi.doAs(new PrivilegedExceptionAction<Object>() {
@Override
public Object run() throws IOException {
Assert.assertEquals(AuthenticationMethod.PROXY, UserGroupInformation
.getCurrentUser().getAuthenticationMethod());
Assert.assertEquals(am, UserGroupInformation.getCurrentUser()
.getRealUser().getAuthenticationMethod());
return null;
}
});
UserGroupInformation proxyUgi2 =
new UserGroupInformation(proxyUgi.getSubject());
proxyUgi2.setAuthenticationMethod(AuthenticationMethod.PROXY);
Assert.assertEquals(proxyUgi, proxyUgi2);
// Equality should work if authMethod is null
UserGroupInformation realugi = UserGroupInformation.getCurrentUser();
UserGroupInformation proxyUgi3 = UserGroupInformation.createProxyUser(
"proxyAnother", realugi);
UserGroupInformation proxyUgi4 =
new UserGroupInformation(proxyUgi3.getSubject());
Assert.assertEquals(proxyUgi3, proxyUgi4);
}
@Test (timeout = 30000)
public void testLoginObjectInSubject() throws Exception {
UserGroupInformation loginUgi = UserGroupInformation.getLoginUser();
UserGroupInformation anotherUgi = new UserGroupInformation(loginUgi
.getSubject());
LoginContext login1 = loginUgi.getSubject().getPrincipals(User.class)
.iterator().next().getLogin();
LoginContext login2 = anotherUgi.getSubject().getPrincipals(User.class)
.iterator().next().getLogin();
//login1 and login2 must be same instances
Assert.assertTrue(login1 == login2);
}
@Test (timeout = 30000)
public void testLoginModuleCommit() throws Exception {
UserGroupInformation loginUgi = UserGroupInformation.getLoginUser();
User user1 = loginUgi.getSubject().getPrincipals(User.class).iterator()
.next();
LoginContext login = user1.getLogin();
login.logout();
login.login();
User user2 = loginUgi.getSubject().getPrincipals(User.class).iterator()
.next();
// user1 and user2 must be same instances.
Assert.assertTrue(user1 == user2);
}
public static void verifyLoginMetrics(long success, int failure)
throws IOException {
// Ensure metrics related to kerberos login is updated.
MetricsRecordBuilder rb = getMetrics("UgiMetrics");
if (success > 0) {
assertCounter("LoginSuccessNumOps", success, rb);
assertGaugeGt("LoginSuccessAvgTime", 0, rb);
}
if (failure > 0) {
assertCounter("LoginFailureNumPos", failure, rb);
assertGaugeGt("LoginFailureAvgTime", 0, rb);
}
}
private static void verifyGroupMetrics(
long groups) throws InterruptedException {
MetricsRecordBuilder rb = getMetrics("UgiMetrics");
if (groups > 0) {
assertCounterGt("GetGroupsNumOps", groups-1, rb);
double avg = getDoubleGauge("GetGroupsAvgTime", rb);
assertTrue(avg >= 0.0);
// Sleep for an interval+slop to let the percentiles rollover
Thread.sleep((PERCENTILES_INTERVAL+1)*1000);
// Check that the percentiles were updated
assertQuantileGauges("GetGroups1s", rb);
}
}
/**
* Test for the case that UserGroupInformation.getCurrentUser()
* is called when the AccessControlContext has a Subject associated
* with it, but that Subject was not created by Hadoop (ie it has no
* associated User principal)
*/
@Test (timeout = 30000)
public void testUGIUnderNonHadoopContext() throws Exception {
Subject nonHadoopSubject = new Subject();
Subject.doAs(nonHadoopSubject, new PrivilegedExceptionAction<Void>() {
@Override
public Void run() throws IOException {
UserGroupInformation ugi = UserGroupInformation.getCurrentUser();
assertNotNull(ugi);
return null;
}
});
}
@Test (timeout = 30000)
public void testGetUGIFromSubject() throws Exception {
KerberosPrincipal p = new KerberosPrincipal("guest");
Subject subject = new Subject();
subject.getPrincipals().add(p);
UserGroupInformation ugi = UserGroupInformation.getUGIFromSubject(subject);
assertNotNull(ugi);
assertEquals("guest@DEFAULT.REALM", ugi.getUserName());
}
/** Test hasSufficientTimeElapsed method */
@Test (timeout = 30000)
public void testHasSufficientTimeElapsed() throws Exception {
// Make hasSufficientTimeElapsed public
Method method = UserGroupInformation.class
.getDeclaredMethod("hasSufficientTimeElapsed", long.class);
method.setAccessible(true);
UserGroupInformation ugi = UserGroupInformation.getCurrentUser();
User user = ugi.getSubject().getPrincipals(User.class).iterator().next();
long now = System.currentTimeMillis();
// Using default relogin time (1 minute)
user.setLastLogin(now - 2 * 60 * 1000); // 2 minutes before "now"
assertTrue((Boolean)method.invoke(ugi, now));
user.setLastLogin(now - 30 * 1000); // 30 seconds before "now"
assertFalse((Boolean)method.invoke(ugi, now));
// Using relogin time of 10 minutes
Configuration conf2 = new Configuration(conf);
conf2.setLong(
CommonConfigurationKeysPublic.HADOOP_KERBEROS_MIN_SECONDS_BEFORE_RELOGIN,
10 * 60);
UserGroupInformation.setConfiguration(conf2);
user.setLastLogin(now - 15 * 60 * 1000); // 15 minutes before "now"
assertTrue((Boolean)method.invoke(ugi, now));
user.setLastLogin(now - 6 * 60 * 1000); // 6 minutes before "now"
assertFalse((Boolean)method.invoke(ugi, now));
// Restore original conf to UGI
UserGroupInformation.setConfiguration(conf);
// Restore hasSufficientTimElapsed back to private
method.setAccessible(false);
}
@Test(timeout=10000)
public void testSetLoginUser() throws IOException {
UserGroupInformation ugi = UserGroupInformation.createRemoteUser("test-user");
UserGroupInformation.setLoginUser(ugi);
assertEquals(ugi, UserGroupInformation.getLoginUser());
}
/**
* In some scenario, such as HA, delegation tokens are associated with a
* logical name. The tokens are cloned and are associated with the
* physical address of the server where the service is provided.
* This test ensures cloned delegated tokens are locally used
* and are not returned in {@link UserGroupInformation#getCredentials()}
*/
@Test
public void testPrivateTokenExclusion() throws Exception {
UserGroupInformation ugi =
UserGroupInformation.createUserForTesting(
"privateUser", new String[] { "PRIVATEUSERS" });
TestTokenIdentifier tokenId = new TestTokenIdentifier();
Token<TestTokenIdentifier> token = new Token<TestTokenIdentifier>(
tokenId.getBytes(), "password".getBytes(),
tokenId.getKind(), null);
ugi.addToken(new Text("regular-token"), token);
// Now add cloned private token
Text service = new Text("private-token");
ugi.addToken(service, token.privateClone(service));
Text service1 = new Text("private-token1");
ugi.addToken(service1, token.privateClone(service1));
// Ensure only non-private tokens are returned
Collection<Token<? extends TokenIdentifier>> tokens = ugi.getCredentials().getAllTokens();
assertEquals(1, tokens.size());
}
/**
* This test checks a race condition between getting and adding tokens for
* the current user. Calling UserGroupInformation.getCurrentUser() returns
* a new object each time, so simply making these methods synchronized was not
* enough to prevent race conditions and causing a
* ConcurrentModificationException. These methods are synchronized on the
* Subject, which is the same object between UserGroupInformation instances.
* This test tries to cause a CME, by exposing the race condition. Previously
* this test would fail every time; now it does not.
*/
@Test
public void testTokenRaceCondition() throws Exception {
UserGroupInformation userGroupInfo =
UserGroupInformation.createUserForTesting(USER_NAME, GROUP_NAMES);
userGroupInfo.doAs(new PrivilegedExceptionAction<Void>(){
@Override
public Void run() throws Exception {
// make sure it is not the same as the login user because we use the
// same UGI object for every instantiation of the login user and you
// won't run into the race condition otherwise
assertNotEquals(UserGroupInformation.getLoginUser(),
UserGroupInformation.getCurrentUser());
GetTokenThread thread = new GetTokenThread();
try {
thread.start();
for (int i = 0; i < 100; i++) {
@SuppressWarnings("unchecked")
Token<? extends TokenIdentifier> t = mock(Token.class);
when(t.getService()).thenReturn(new Text("t" + i));
UserGroupInformation.getCurrentUser().addToken(t);
assertNull("ConcurrentModificationException encountered",
thread.cme);
}
} catch (ConcurrentModificationException cme) {
cme.printStackTrace();
fail("ConcurrentModificationException encountered");
} finally {
thread.runThread = false;
thread.join(5 * 1000);
}
return null;
}});
}
static class GetTokenThread extends Thread {
boolean runThread = true;
volatile ConcurrentModificationException cme = null;
@Override
public void run() {
while(runThread) {
try {
UserGroupInformation.getCurrentUser().getCredentials();
} catch (ConcurrentModificationException cme) {
this.cme = cme;
cme.printStackTrace();
runThread = false;
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
}
@Test
public void testExternalTokenFiles() throws Exception {
StringBuilder tokenFullPathnames = new StringBuilder();
String tokenFilenames = "token1,token2";
String tokenFiles[] = StringUtils.getTrimmedStrings(tokenFilenames);
final File testDir = new File("target",
TestUserGroupInformation.class.getName() + "-tmpDir").getAbsoluteFile();
String testDirPath = testDir.getAbsolutePath();
// create path for token files
for (String tokenFile: tokenFiles) {
if (tokenFullPathnames.length() > 0) {
tokenFullPathnames.append(",");
}
tokenFullPathnames.append(testDirPath).append("/").append(tokenFile);
}
// create new token and store it
TestTokenIdentifier tokenId = new TestTokenIdentifier();
Credentials cred1 = new Credentials();
Token<TestTokenIdentifier> token1 = new Token<TestTokenIdentifier>(
tokenId.getBytes(), "password".getBytes(),
tokenId.getKind(), new Text("token-service1"));
cred1.addToken(token1.getService(), token1);
cred1.writeTokenStorageFile(new Path(testDirPath, tokenFiles[0]), conf);
Credentials cred2 = new Credentials();
Token<TestTokenIdentifier> token2 = new Token<TestTokenIdentifier>(
tokenId.getBytes(), "password".getBytes(),
tokenId.getKind(), new Text("token-service2"));
cred2.addToken(token2.getService(), token2);
cred2.writeTokenStorageFile(new Path(testDirPath, tokenFiles[1]), conf);
// set property for token external token files
System.setProperty("hadoop.token.files", tokenFullPathnames.toString());
UserGroupInformation.setLoginUser(null);
UserGroupInformation tokenUgi = UserGroupInformation.getLoginUser();
Collection<Token<?>> credsugiTokens = tokenUgi.getTokens();
assertTrue(credsugiTokens.contains(token1));
assertTrue(credsugiTokens.contains(token2));
}
@Test
public void testCheckTGTAfterLoginFromSubject() throws Exception {
// security on, default is remove default realm
SecurityUtil.setAuthenticationMethod(AuthenticationMethod.KERBEROS, conf);
UserGroupInformation.setConfiguration(conf);
// Login from a pre-set subject with a keytab
final Subject subject = new Subject();
KeyTab keytab = KeyTab.getInstance();
subject.getPrivateCredentials().add(keytab);
UserGroupInformation ugi = UserGroupInformation.getCurrentUser();
ugi.doAs(new PrivilegedExceptionAction<Void>() {
@Override
public Void run() throws IOException {
UserGroupInformation.loginUserFromSubject(subject);
// this should not throw.
UserGroupInformation.getLoginUser().checkTGTAndReloginFromKeytab();
return null;
}
});
}
@Test
public void testGetNextRetryTime() throws Exception {
GenericTestUtils.setLogLevel(UserGroupInformation.LOG, Level.DEBUG);
final long reloginInterval = 1;
final long reloginIntervalMs = reloginInterval * 1000;
// Relogin happens every 1 second.
conf.setLong(HADOOP_KERBEROS_MIN_SECONDS_BEFORE_RELOGIN, reloginInterval);
SecurityUtil.setAuthenticationMethod(AuthenticationMethod.KERBEROS, conf);
UserGroupInformation.setConfiguration(conf);
// Suppose tgt start time is now, end time is 20 seconds from now.
final long now = Time.now();
final Date endDate = new Date(now + 20000);
// Explicitly test the exponential back-off logic.
// Suppose some time (10 seconds) passed.
// Verify exponential backoff and max=(login interval before endTime).
final long currentTime = now + 10000;
final long endTime = endDate.getTime();
assertEquals(0, UserGroupInformation.metrics.getRenewalFailures().value());
RetryPolicy rp = RetryPolicies.exponentialBackoffRetry(Long.SIZE - 2,
1000, TimeUnit.MILLISECONDS);
long lastRetry =
UserGroupInformation.getNextTgtRenewalTime(endTime, currentTime, rp);
assertWithinBounds(
UserGroupInformation.metrics.getRenewalFailures().value(),
lastRetry, reloginIntervalMs, currentTime);
UserGroupInformation.metrics.getRenewalFailures().incr();
lastRetry =
UserGroupInformation.getNextTgtRenewalTime(endTime, currentTime, rp);
assertWithinBounds(
UserGroupInformation.metrics.getRenewalFailures().value(),
lastRetry, reloginIntervalMs, currentTime);
UserGroupInformation.metrics.getRenewalFailures().incr();
lastRetry =
UserGroupInformation.getNextTgtRenewalTime(endTime, currentTime, rp);
assertWithinBounds(
UserGroupInformation.metrics.getRenewalFailures().value(),
lastRetry, reloginIntervalMs, currentTime);
UserGroupInformation.metrics.getRenewalFailures().incr();
lastRetry =
UserGroupInformation.getNextTgtRenewalTime(endTime, currentTime, rp);
assertWithinBounds(
UserGroupInformation.metrics.getRenewalFailures().value(),
lastRetry, reloginIntervalMs, currentTime);
// last try should be right before expiry.
UserGroupInformation.metrics.getRenewalFailures().incr();
lastRetry =
UserGroupInformation.getNextTgtRenewalTime(endTime, currentTime, rp);
String str =
"5th retry, now:" + currentTime + ", retry:" + lastRetry;
LOG.info(str);
assertEquals(str, endTime - reloginIntervalMs, lastRetry);
// make sure no more retries after (tgt endTime - login interval).
UserGroupInformation.metrics.getRenewalFailures().incr();
lastRetry =
UserGroupInformation.getNextTgtRenewalTime(endTime, currentTime, rp);
str = "overflow retry, now:" + currentTime + ", retry:" + lastRetry;
LOG.info(str);
assertEquals(str, endTime - reloginIntervalMs, lastRetry);
}
private void assertWithinBounds(final int numFailures, final long lastRetry,
final long reloginIntervalMs, long now) {
// shift is 2 to the power of (numFailure).
int shift = numFailures + 1;
final long lower = now + reloginIntervalMs * (long)((1 << shift) * 0.5);
final long upper = now + reloginIntervalMs * (long)((1 << shift) * 1.5);
final String str = new String("Retry#" + (numFailures + 1) + ", now:" + now
+ ", lower bound:" + lower + ", upper bound:" + upper
+ ", retry:" + lastRetry);
LOG.info(str);
assertTrue(str, lower <= lastRetry && lastRetry < upper);
}
// verify that getCurrentUser on the same and different subjects can be
// concurrent. Ie. no synchronization.
@Test(timeout=8000)
public void testConcurrentGetCurrentUser() throws Exception {
final CyclicBarrier barrier = new CyclicBarrier(2);
final CountDownLatch latch = new CountDownLatch(1);
final UserGroupInformation testUgi1 =
UserGroupInformation.createRemoteUser("testUgi1");
final UserGroupInformation testUgi2 =
UserGroupInformation.createRemoteUser("testUgi2");
// swap the User with a spy to allow getCurrentUser to block when the
// spy is called for the user name.
Set<Principal> principals = testUgi1.getSubject().getPrincipals();
User user =
testUgi1.getSubject().getPrincipals(User.class).iterator().next();
final User spyUser = Mockito.spy(user);
principals.remove(user);
principals.add(spyUser);
when(spyUser.getName()).thenAnswer(new Answer<String>(){
@Override
public String answer(InvocationOnMock invocation) throws Throwable {
latch.countDown();
barrier.await();
return (String)invocation.callRealMethod();
}
});
// wait for the thread to block on the barrier in getCurrentUser.
Future<UserGroupInformation> blockingLookup =
Executors.newSingleThreadExecutor().submit(
new Callable<UserGroupInformation>(){
@Override
public UserGroupInformation call() throws Exception {
return testUgi1.doAs(
new PrivilegedExceptionAction<UserGroupInformation>() {
@Override
public UserGroupInformation run() throws Exception {
return UserGroupInformation.getCurrentUser();
}
});
}
});
latch.await();
// old versions of mockito synchronize on returning mocked answers so
// the blocked getCurrentUser will block all other calls to getName.
// workaround this by swapping out the spy with the original User.
principals.remove(spyUser);
principals.add(user);
// concurrent getCurrentUser on ugi1 should not be blocked.
UserGroupInformation ugi;
ugi = testUgi1.doAs(
new PrivilegedExceptionAction<UserGroupInformation>() {
@Override
public UserGroupInformation run() throws Exception {
return UserGroupInformation.getCurrentUser();
}
});
assertSame(testUgi1.getSubject(), ugi.getSubject());
// concurrent getCurrentUser on ugi2 should not be blocked.
ugi = testUgi2.doAs(
new PrivilegedExceptionAction<UserGroupInformation>() {
@Override
public UserGroupInformation run() throws Exception {
return UserGroupInformation.getCurrentUser();
}
});
assertSame(testUgi2.getSubject(), ugi.getSubject());
// unblock the original call.
barrier.await();
assertSame(testUgi1.getSubject(), blockingLookup.get().getSubject());
}
@Test
public void testKerberosTicketIsDestroyedChecked() throws Exception {
// Create UserGroupInformation
GenericTestUtils.setLogLevel(UserGroupInformation.LOG, Level.DEBUG);
Set<User> users = new HashSet<>();
users.add(new User("Foo"));
Subject subject =
new Subject(true, users, new HashSet<>(), new HashSet<>());
UserGroupInformation ugi = spy(new UserGroupInformation(subject));
// throw IOException in the middle of the autoRenewalForUserCreds
doThrow(new IOException()).when(ugi).reloginFromTicketCache();
// Create and destroy the KerberosTicket, so endTime will be null
Date d = new Date();
KerberosPrincipal kp = new KerberosPrincipal("Foo");
KerberosTicket tgt = spy(new KerberosTicket(new byte[]{}, kp, kp, new
byte[]{}, 0, null, d, d, d, d, null));
tgt.destroy();
// run AutoRenewalForUserCredsRunnable with this
UserGroupInformation.AutoRenewalForUserCredsRunnable userCredsRunnable =
ugi.new AutoRenewalForUserCredsRunnable(tgt,
Boolean.toString(Boolean.TRUE), 100);
// Set the runnable to not to run in a loop
userCredsRunnable.setRunRenewalLoop(false);
// there should be no exception when calling this
userCredsRunnable.run();
// isDestroyed should be called at least once
Mockito.verify(tgt, atLeastOnce()).isDestroyed();
}
}