Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Kerberos] Add authorization realms support to Kerberos realm #32392

Merged
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 @@ -62,6 +64,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 @@ -89,6 +92,15 @@ public KerberosRealm(final RealmConfig config, final NativeRoleMappingStore nati
this.keytabPath = config.env().configFile().resolve(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(config.settings()));
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 @@ -122,13 +134,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 @@ -178,35 +191,41 @@ 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));
} 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);

if (delegatedRealms.hasDelegation()) {
delegatedRealms.resolve(username, ActionListener.wrap(result -> {
if (result.isAuthenticated() && userPrincipalNameToUserCache != null) {
userPrincipalNameToUserCache.put(username, result.getUser());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to cache here. The delegated realm resolved the user and it should be caching it. The kerberos realm will never use the cache entry that we add

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, I too did not want to add caching here but thought it would be consistent with others. I will remove it. Thank you.

}
listener.onResponse(AuthenticationResult.success(computedUser));
listener.onResponse(result);
}, listener::onFailure));
} else {
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("authorizing_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.isAuthorizingRealmAllowed()).thenReturn(true);
}

@After
Expand Down Expand Up @@ -101,13 +107,20 @@ protected void assertSuccessAuthenticationResult(final User expectedUser, final
is(equalTo(KerberosAuthenticationToken.NEGOTIATE_AUTH_HEADER_PREFIX + outToken)));
}


Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: drop this extra line

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,17 +11,23 @@
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;
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.elasticsearch.xpack.security.authc.support.UserRoleMapper.UserData;
import org.ietf.jgss.GSSException;

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

import javax.security.auth.login.LoginException;

Expand All @@ -31,6 +37,7 @@
import static org.mockito.AdditionalMatchers.aryEq;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
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 @@ -94,4 +101,36 @@ public void testLookupUser() {
assertThat(future.actionGet(), is(nullValue()));
}

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("authorizing_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));
}
}