Skip to content

Commit

Permalink
[Kerberos] Add authorization realms support to Kerberos realm (#32392)
Browse files Browse the repository at this point in the history
This commit allows Kerberos realm to delegate `User` creation
to configured authorization realms.
If no authorization realms are configured, then Kerberos realm
uses native role mapper to resolve User.
In the case of delegated realms, users are not cached.
  • Loading branch information
bizybot authored Aug 2, 2018
1 parent d9e5bb9 commit 4e67689
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.elasticsearch.common.settings.Setting.Property;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.common.util.set.Sets;
import org.elasticsearch.xpack.core.security.authc.support.DelegatedAuthorizationSettings;

import java.util.Set;

Expand Down Expand Up @@ -44,7 +45,9 @@ private KerberosRealmSettings() {
* @return the valid set of {@link Setting}s for a {@value #TYPE} realm
*/
public static Set<Setting<?>> getSettings() {
return Sets.newHashSet(HTTP_SERVICE_KEYTAB_PATH, CACHE_TTL_SETTING, CACHE_MAX_USERS_SETTING, SETTING_KRB_DEBUG_ENABLE,
SETTING_REMOVE_REALM_NAME);
final Set<Setting<?>> settings = Sets.newHashSet(HTTP_SERVICE_KEYTAB_PATH, CACHE_TTL_SETTING, CACHE_MAX_USERS_SETTING,
SETTING_KRB_DEBUG_ENABLE, SETTING_REMOVE_REALM_NAME);
settings.addAll(DelegatedAuthorizationSettings.getSettings());
return settings;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import org.elasticsearch.common.cache.CacheBuilder;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.license.XPackLicenseState;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.xpack.core.security.authc.AuthenticationResult;
import org.elasticsearch.xpack.core.security.authc.AuthenticationToken;
Expand All @@ -21,6 +22,7 @@
import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings;
import org.elasticsearch.xpack.core.security.user.User;
import org.elasticsearch.xpack.security.authc.support.CachingRealm;
import org.elasticsearch.xpack.security.authc.support.DelegatedAuthorizationSupport;
import org.elasticsearch.xpack.security.authc.support.UserRoleMapper;
import org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore;
import org.ietf.jgss.GSSException;
Expand Down Expand Up @@ -63,6 +65,7 @@ public final class KerberosRealm extends Realm implements CachingRealm {
private final Path keytabPath;
private final boolean enableKerberosDebug;
private final boolean removeRealmName;
private DelegatedAuthorizationSupport delegatedRealms;

public KerberosRealm(final RealmConfig config, final NativeRoleMappingStore nativeRoleMappingStore, final ThreadPool threadPool) {
this(config, nativeRoleMappingStore, new KerberosTicketValidator(), threadPool, null);
Expand Down Expand Up @@ -100,6 +103,15 @@ public KerberosRealm(final RealmConfig config, final NativeRoleMappingStore nati
}
this.enableKerberosDebug = KerberosRealmSettings.SETTING_KRB_DEBUG_ENABLE.get(config.settings());
this.removeRealmName = KerberosRealmSettings.SETTING_REMOVE_REALM_NAME.get(config.settings());
this.delegatedRealms = null;
}

@Override
public void initialize(Iterable<Realm> realms, XPackLicenseState licenseState) {
if (delegatedRealms != null) {
throw new IllegalStateException("Realm has already been initialized");
}
delegatedRealms = new DelegatedAuthorizationSupport(realms, config, licenseState);
}

@Override
Expand Down Expand Up @@ -133,13 +145,14 @@ public AuthenticationToken token(final ThreadContext context) {

@Override
public void authenticate(final AuthenticationToken token, final ActionListener<AuthenticationResult> listener) {
assert delegatedRealms != null : "Realm has not been initialized correctly";
assert token instanceof KerberosAuthenticationToken;
final KerberosAuthenticationToken kerbAuthnToken = (KerberosAuthenticationToken) token;
kerberosTicketValidator.validateTicket((byte[]) kerbAuthnToken.credentials(), keytabPath, enableKerberosDebug,
ActionListener.wrap(userPrincipalNameOutToken -> {
if (userPrincipalNameOutToken.v1() != null) {
final String username = maybeRemoveRealmName(userPrincipalNameOutToken.v1());
buildUser(username, userPrincipalNameOutToken.v2(), listener);
resolveUser(username, userPrincipalNameOutToken.v2(), listener);
} else {
/**
* This is when security context could not be established may be due to ongoing
Expand Down Expand Up @@ -189,35 +202,36 @@ private void handleException(Exception e, final ActionListener<AuthenticationRes
}
}

private void buildUser(final String username, final String outToken, final ActionListener<AuthenticationResult> listener) {
private void resolveUser(final String username, final String outToken, final ActionListener<AuthenticationResult> listener) {
// if outToken is present then it needs to be communicated with peer, add it to
// response header in thread context.
if (Strings.hasText(outToken)) {
threadPool.getThreadContext().addResponseHeader(WWW_AUTHENTICATE, NEGOTIATE_AUTH_HEADER_PREFIX + outToken);
}
final User user = (userPrincipalNameToUserCache != null) ? userPrincipalNameToUserCache.get(username) : null;
if (user != null) {
/**
* TODO: bizybot If authorizing realms configured, resolve user from those
* realms and then return.
*/
listener.onResponse(AuthenticationResult.success(user));

if (delegatedRealms.hasDelegation()) {
delegatedRealms.resolve(username, listener);
} else {
/**
* TODO: bizybot If authorizing realms configured, resolve user from those
* realms, cache it and then return.
*/
final UserRoleMapper.UserData userData = new UserRoleMapper.UserData(username, null, Collections.emptySet(), null, this.config);
userRoleMapper.resolveRoles(userData, ActionListener.wrap(roles -> {
final User computedUser = new User(username, roles.toArray(new String[roles.size()]), null, null, null, true);
if (userPrincipalNameToUserCache != null) {
userPrincipalNameToUserCache.put(username, computedUser);
}
listener.onResponse(AuthenticationResult.success(computedUser));
}, listener::onFailure));
final User user = (userPrincipalNameToUserCache != null) ? userPrincipalNameToUserCache.get(username) : null;
if (user != null) {
listener.onResponse(AuthenticationResult.success(user));
} else {
buildUser(username, listener);
}
}
}

private void buildUser(final String username, final ActionListener<AuthenticationResult> listener) {
final UserRoleMapper.UserData userData = new UserRoleMapper.UserData(username, null, Collections.emptySet(), null, this.config);
userRoleMapper.resolveRoles(userData, ActionListener.wrap(roles -> {
final User computedUser = new User(username, roles.toArray(new String[roles.size()]), null, null, null, true);
if (userPrincipalNameToUserCache != null) {
userPrincipalNameToUserCache.put(username, computedUser);
}
listener.onResponse(AuthenticationResult.success(computedUser));
}, listener::onFailure));
}

@Override
public void lookupUser(final String username, final ActionListener<User> listener) {
listener.onResponse(null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,20 @@
import org.elasticsearch.action.support.PlainActionFuture;
import org.elasticsearch.common.collect.Tuple;
import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.env.TestEnvironment;
import org.elasticsearch.xpack.core.security.authc.AuthenticationResult;
import org.elasticsearch.xpack.core.security.authc.RealmConfig;
import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings;
import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken;
import org.elasticsearch.xpack.core.security.user.User;
import org.elasticsearch.xpack.security.authc.support.MockLookupRealm;
import org.ietf.jgss.GSSException;

import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;

import javax.security.auth.login.LoginException;
Expand All @@ -29,7 +36,9 @@
import static org.mockito.AdditionalMatchers.aryEq;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;

public class KerberosRealmAuthenticateFailedTests extends KerberosRealmTestCase {

Expand Down Expand Up @@ -105,4 +114,30 @@ public void testAuthenticateDifferentFailureScenarios() throws LoginException, G
any(ActionListener.class));
}
}

public void testDelegatedAuthorizationFailedToResolve() throws Exception {
final String username = randomPrincipalName();
final MockLookupRealm otherRealm = new MockLookupRealm(new RealmConfig("other_realm", Settings.EMPTY, globalSettings,
TestEnvironment.newEnvironment(globalSettings), new ThreadContext(globalSettings)));
final User lookupUser = new User(randomAlphaOfLength(5));
otherRealm.registerUser(lookupUser);

settings = Settings.builder().put(settings).putList("authorization_realms", "other_realm").build();
final KerberosRealm kerberosRealm = createKerberosRealm(Collections.singletonList(otherRealm), username);
final byte[] decodedTicket = "base64encodedticket".getBytes(StandardCharsets.UTF_8);
final Path keytabPath = config.env().configFile().resolve(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(config.settings()));
final boolean krbDebug = KerberosRealmSettings.SETTING_KRB_DEBUG_ENABLE.get(config.settings());
mockKerberosTicketValidator(decodedTicket, keytabPath, krbDebug, new Tuple<>(username, "out-token"), null);
final KerberosAuthenticationToken kerberosAuthenticationToken = new KerberosAuthenticationToken(decodedTicket);

final PlainActionFuture<AuthenticationResult> future = new PlainActionFuture<>();
kerberosRealm.authenticate(kerberosAuthenticationToken, future);

AuthenticationResult result = future.actionGet();
assertThat(result.getStatus(), is(equalTo(AuthenticationResult.Status.CONTINUE)));
verify(mockKerberosTicketValidator, times(1)).validateTicket(aryEq(decodedTicket), eq(keytabPath), eq(krbDebug),
any(ActionListener.class));
verify(mockNativeRoleMappingStore).refreshRealmOnChange(kerberosRealm);
verifyNoMoreInteractions(mockKerberosTicketValidator, mockNativeRoleMappingStore);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.common.util.set.Sets;
import org.elasticsearch.env.TestEnvironment;
import org.elasticsearch.license.XPackLicenseState;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.threadpool.TestThreadPool;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.watcher.ResourceWatcherService;
import org.elasticsearch.xpack.core.security.authc.AuthenticationResult;
import org.elasticsearch.xpack.core.security.authc.Realm;
import org.elasticsearch.xpack.core.security.authc.RealmConfig;
import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings;
import org.elasticsearch.xpack.core.security.support.Exceptions;
Expand All @@ -30,6 +32,7 @@

import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
Expand Down Expand Up @@ -58,6 +61,7 @@ public abstract class KerberosRealmTestCase extends ESTestCase {

protected KerberosTicketValidator mockKerberosTicketValidator;
protected NativeRoleMappingStore mockNativeRoleMappingStore;
protected XPackLicenseState licenseState;

protected static final Set<String> roles = Sets.newHashSet("admin", "kibana_user");

Expand All @@ -69,6 +73,8 @@ public void setup() throws Exception {
globalSettings = Settings.builder().put("path.home", dir).build();
settings = KerberosTestCase.buildKerberosRealmSettings(KerberosTestCase.writeKeyTab(dir.resolve("key.keytab"), "asa").toString(),
100, "10m", true, randomBoolean());
licenseState = mock(XPackLicenseState.class);
when(licenseState.isAuthorizationRealmAllowed()).thenReturn(true);
}

@After
Expand Down Expand Up @@ -102,12 +108,18 @@ protected void assertSuccessAuthenticationResult(final User expectedUser, final
}

protected KerberosRealm createKerberosRealm(final String... userForRoleMapping) {
return createKerberosRealm(Collections.emptyList(), userForRoleMapping);
}

protected KerberosRealm createKerberosRealm(final List<Realm> delegatedRealms, final String... userForRoleMapping) {
config = new RealmConfig("test-kerb-realm", settings, globalSettings, TestEnvironment.newEnvironment(globalSettings),
new ThreadContext(globalSettings));
mockNativeRoleMappingStore = roleMappingStore(Arrays.asList(userForRoleMapping));
mockKerberosTicketValidator = mock(KerberosTicketValidator.class);
final KerberosRealm kerberosRealm =
new KerberosRealm(config, mockNativeRoleMappingStore, mockKerberosTicketValidator, threadPool, null);
Collections.shuffle(delegatedRealms, random());
kerberosRealm.initialize(delegatedRealms, licenseState);
return kerberosRealm;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import org.elasticsearch.action.support.PlainActionFuture;
import org.elasticsearch.common.collect.Tuple;
import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.env.TestEnvironment;
import org.elasticsearch.rest.RestStatus;
Expand All @@ -19,6 +20,7 @@
import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings;
import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken;
import org.elasticsearch.xpack.core.security.user.User;
import org.elasticsearch.xpack.security.authc.support.MockLookupRealm;
import org.elasticsearch.xpack.security.authc.support.UserRoleMapper.UserData;
import org.ietf.jgss.GSSException;

Expand All @@ -33,6 +35,7 @@
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Set;

Expand All @@ -45,6 +48,7 @@
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
Expand Down Expand Up @@ -148,4 +152,37 @@ public void testKerberosRealmWithInvalidKeytabPathConfigurations() throws IOExce
() -> new KerberosRealm(config, mockNativeRoleMappingStore, mockKerberosTicketValidator, threadPool, null));
assertThat(iae.getMessage(), is(equalTo(expectedErrorMessage)));
}

public void testDelegatedAuthorization() throws Exception {
final String username = randomPrincipalName();
final String expectedUsername = maybeRemoveRealmName(username);
final MockLookupRealm otherRealm = spy(new MockLookupRealm(new RealmConfig("other_realm", Settings.EMPTY, globalSettings,
TestEnvironment.newEnvironment(globalSettings), new ThreadContext(globalSettings))));
final User lookupUser = new User(expectedUsername, new String[] { "admin-role" }, expectedUsername,
expectedUsername + "@example.com", Collections.singletonMap("k1", "v1"), true);
otherRealm.registerUser(lookupUser);

settings = Settings.builder().put(settings).putList("authorization_realms", "other_realm").build();
final KerberosRealm kerberosRealm = createKerberosRealm(Collections.singletonList(otherRealm), username);
final User expectedUser = lookupUser;
final byte[] decodedTicket = "base64encodedticket".getBytes(StandardCharsets.UTF_8);
final Path keytabPath = config.env().configFile().resolve(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(config.settings()));
final boolean krbDebug = KerberosRealmSettings.SETTING_KRB_DEBUG_ENABLE.get(config.settings());
mockKerberosTicketValidator(decodedTicket, keytabPath, krbDebug, new Tuple<>(username, "out-token"), null);
final KerberosAuthenticationToken kerberosAuthenticationToken = new KerberosAuthenticationToken(decodedTicket);

PlainActionFuture<AuthenticationResult> future = new PlainActionFuture<>();
kerberosRealm.authenticate(kerberosAuthenticationToken, future);
assertSuccessAuthenticationResult(expectedUser, "out-token", future.actionGet());

future = new PlainActionFuture<>();
kerberosRealm.authenticate(kerberosAuthenticationToken, future);
assertSuccessAuthenticationResult(expectedUser, "out-token", future.actionGet());

verify(mockKerberosTicketValidator, times(2)).validateTicket(aryEq(decodedTicket), eq(keytabPath), eq(krbDebug),
any(ActionListener.class));
verify(mockNativeRoleMappingStore).refreshRealmOnChange(kerberosRealm);
verifyNoMoreInteractions(mockKerberosTicketValidator, mockNativeRoleMappingStore);
verify(otherRealm, times(2)).lookupUser(eq(expectedUsername), any(ActionListener.class));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ public abstract class KerberosTestCase extends ESTestCase {
unsupportedLocaleLanguages.add("uz");
unsupportedLocaleLanguages.add("fa");
unsupportedLocaleLanguages.add("ks");
unsupportedLocaleLanguages.add("ckb");
}

@BeforeClass
Expand Down

0 comments on commit 4e67689

Please sign in to comment.