Skip to content

Commit 4e67689

Browse files
authored
[Kerberos] Add authorization realms support to Kerberos realm (#32392)
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.
1 parent d9e5bb9 commit 4e67689

File tree

6 files changed

+125
-23
lines changed

6 files changed

+125
-23
lines changed

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/kerberos/KerberosRealmSettings.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import org.elasticsearch.common.settings.Setting.Property;
1111
import org.elasticsearch.common.unit.TimeValue;
1212
import org.elasticsearch.common.util.set.Sets;
13+
import org.elasticsearch.xpack.core.security.authc.support.DelegatedAuthorizationSettings;
1314

1415
import java.util.Set;
1516

@@ -44,7 +45,9 @@ private KerberosRealmSettings() {
4445
* @return the valid set of {@link Setting}s for a {@value #TYPE} realm
4546
*/
4647
public static Set<Setting<?>> getSettings() {
47-
return Sets.newHashSet(HTTP_SERVICE_KEYTAB_PATH, CACHE_TTL_SETTING, CACHE_MAX_USERS_SETTING, SETTING_KRB_DEBUG_ENABLE,
48-
SETTING_REMOVE_REALM_NAME);
48+
final Set<Setting<?>> settings = Sets.newHashSet(HTTP_SERVICE_KEYTAB_PATH, CACHE_TTL_SETTING, CACHE_MAX_USERS_SETTING,
49+
SETTING_KRB_DEBUG_ENABLE, SETTING_REMOVE_REALM_NAME);
50+
settings.addAll(DelegatedAuthorizationSettings.getSettings());
51+
return settings;
4952
}
5053
}

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealm.java

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import org.elasticsearch.common.cache.CacheBuilder;
1414
import org.elasticsearch.common.unit.TimeValue;
1515
import org.elasticsearch.common.util.concurrent.ThreadContext;
16+
import org.elasticsearch.license.XPackLicenseState;
1617
import org.elasticsearch.threadpool.ThreadPool;
1718
import org.elasticsearch.xpack.core.security.authc.AuthenticationResult;
1819
import org.elasticsearch.xpack.core.security.authc.AuthenticationToken;
@@ -21,6 +22,7 @@
2122
import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings;
2223
import org.elasticsearch.xpack.core.security.user.User;
2324
import org.elasticsearch.xpack.security.authc.support.CachingRealm;
25+
import org.elasticsearch.xpack.security.authc.support.DelegatedAuthorizationSupport;
2426
import org.elasticsearch.xpack.security.authc.support.UserRoleMapper;
2527
import org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore;
2628
import org.ietf.jgss.GSSException;
@@ -63,6 +65,7 @@ public final class KerberosRealm extends Realm implements CachingRealm {
6365
private final Path keytabPath;
6466
private final boolean enableKerberosDebug;
6567
private final boolean removeRealmName;
68+
private DelegatedAuthorizationSupport delegatedRealms;
6669

6770
public KerberosRealm(final RealmConfig config, final NativeRoleMappingStore nativeRoleMappingStore, final ThreadPool threadPool) {
6871
this(config, nativeRoleMappingStore, new KerberosTicketValidator(), threadPool, null);
@@ -100,6 +103,15 @@ public KerberosRealm(final RealmConfig config, final NativeRoleMappingStore nati
100103
}
101104
this.enableKerberosDebug = KerberosRealmSettings.SETTING_KRB_DEBUG_ENABLE.get(config.settings());
102105
this.removeRealmName = KerberosRealmSettings.SETTING_REMOVE_REALM_NAME.get(config.settings());
106+
this.delegatedRealms = null;
107+
}
108+
109+
@Override
110+
public void initialize(Iterable<Realm> realms, XPackLicenseState licenseState) {
111+
if (delegatedRealms != null) {
112+
throw new IllegalStateException("Realm has already been initialized");
113+
}
114+
delegatedRealms = new DelegatedAuthorizationSupport(realms, config, licenseState);
103115
}
104116

105117
@Override
@@ -133,13 +145,14 @@ public AuthenticationToken token(final ThreadContext context) {
133145

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

192-
private void buildUser(final String username, final String outToken, final ActionListener<AuthenticationResult> listener) {
205+
private void resolveUser(final String username, final String outToken, final ActionListener<AuthenticationResult> listener) {
193206
// if outToken is present then it needs to be communicated with peer, add it to
194207
// response header in thread context.
195208
if (Strings.hasText(outToken)) {
196209
threadPool.getThreadContext().addResponseHeader(WWW_AUTHENTICATE, NEGOTIATE_AUTH_HEADER_PREFIX + outToken);
197210
}
198-
final User user = (userPrincipalNameToUserCache != null) ? userPrincipalNameToUserCache.get(username) : null;
199-
if (user != null) {
200-
/**
201-
* TODO: bizybot If authorizing realms configured, resolve user from those
202-
* realms and then return.
203-
*/
204-
listener.onResponse(AuthenticationResult.success(user));
211+
212+
if (delegatedRealms.hasDelegation()) {
213+
delegatedRealms.resolve(username, listener);
205214
} else {
206-
/**
207-
* TODO: bizybot If authorizing realms configured, resolve user from those
208-
* realms, cache it and then return.
209-
*/
210-
final UserRoleMapper.UserData userData = new UserRoleMapper.UserData(username, null, Collections.emptySet(), null, this.config);
211-
userRoleMapper.resolveRoles(userData, ActionListener.wrap(roles -> {
212-
final User computedUser = new User(username, roles.toArray(new String[roles.size()]), null, null, null, true);
213-
if (userPrincipalNameToUserCache != null) {
214-
userPrincipalNameToUserCache.put(username, computedUser);
215-
}
216-
listener.onResponse(AuthenticationResult.success(computedUser));
217-
}, listener::onFailure));
215+
final User user = (userPrincipalNameToUserCache != null) ? userPrincipalNameToUserCache.get(username) : null;
216+
if (user != null) {
217+
listener.onResponse(AuthenticationResult.success(user));
218+
} else {
219+
buildUser(username, listener);
220+
}
218221
}
219222
}
220223

224+
private void buildUser(final String username, final ActionListener<AuthenticationResult> listener) {
225+
final UserRoleMapper.UserData userData = new UserRoleMapper.UserData(username, null, Collections.emptySet(), null, this.config);
226+
userRoleMapper.resolveRoles(userData, ActionListener.wrap(roles -> {
227+
final User computedUser = new User(username, roles.toArray(new String[roles.size()]), null, null, null, true);
228+
if (userPrincipalNameToUserCache != null) {
229+
userPrincipalNameToUserCache.put(username, computedUser);
230+
}
231+
listener.onResponse(AuthenticationResult.success(computedUser));
232+
}, listener::onFailure));
233+
}
234+
221235
@Override
222236
public void lookupUser(final String username, final ActionListener<User> listener) {
223237
listener.onResponse(null);

x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmAuthenticateFailedTests.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,20 @@
1111
import org.elasticsearch.action.support.PlainActionFuture;
1212
import org.elasticsearch.common.collect.Tuple;
1313
import org.elasticsearch.common.settings.SecureString;
14+
import org.elasticsearch.common.settings.Settings;
15+
import org.elasticsearch.common.util.concurrent.ThreadContext;
16+
import org.elasticsearch.env.TestEnvironment;
1417
import org.elasticsearch.xpack.core.security.authc.AuthenticationResult;
18+
import org.elasticsearch.xpack.core.security.authc.RealmConfig;
1519
import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings;
1620
import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken;
1721
import org.elasticsearch.xpack.core.security.user.User;
22+
import org.elasticsearch.xpack.security.authc.support.MockLookupRealm;
1823
import org.ietf.jgss.GSSException;
1924

25+
import java.nio.charset.StandardCharsets;
2026
import java.nio.file.Path;
27+
import java.util.Collections;
2128
import java.util.List;
2229

2330
import javax.security.auth.login.LoginException;
@@ -29,7 +36,9 @@
2936
import static org.mockito.AdditionalMatchers.aryEq;
3037
import static org.mockito.Matchers.any;
3138
import static org.mockito.Matchers.eq;
39+
import static org.mockito.Mockito.times;
3240
import static org.mockito.Mockito.verify;
41+
import static org.mockito.Mockito.verifyNoMoreInteractions;
3342

3443
public class KerberosRealmAuthenticateFailedTests extends KerberosRealmTestCase {
3544

@@ -105,4 +114,30 @@ public void testAuthenticateDifferentFailureScenarios() throws LoginException, G
105114
any(ActionListener.class));
106115
}
107116
}
117+
118+
public void testDelegatedAuthorizationFailedToResolve() throws Exception {
119+
final String username = randomPrincipalName();
120+
final MockLookupRealm otherRealm = new MockLookupRealm(new RealmConfig("other_realm", Settings.EMPTY, globalSettings,
121+
TestEnvironment.newEnvironment(globalSettings), new ThreadContext(globalSettings)));
122+
final User lookupUser = new User(randomAlphaOfLength(5));
123+
otherRealm.registerUser(lookupUser);
124+
125+
settings = Settings.builder().put(settings).putList("authorization_realms", "other_realm").build();
126+
final KerberosRealm kerberosRealm = createKerberosRealm(Collections.singletonList(otherRealm), username);
127+
final byte[] decodedTicket = "base64encodedticket".getBytes(StandardCharsets.UTF_8);
128+
final Path keytabPath = config.env().configFile().resolve(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(config.settings()));
129+
final boolean krbDebug = KerberosRealmSettings.SETTING_KRB_DEBUG_ENABLE.get(config.settings());
130+
mockKerberosTicketValidator(decodedTicket, keytabPath, krbDebug, new Tuple<>(username, "out-token"), null);
131+
final KerberosAuthenticationToken kerberosAuthenticationToken = new KerberosAuthenticationToken(decodedTicket);
132+
133+
final PlainActionFuture<AuthenticationResult> future = new PlainActionFuture<>();
134+
kerberosRealm.authenticate(kerberosAuthenticationToken, future);
135+
136+
AuthenticationResult result = future.actionGet();
137+
assertThat(result.getStatus(), is(equalTo(AuthenticationResult.Status.CONTINUE)));
138+
verify(mockKerberosTicketValidator, times(1)).validateTicket(aryEq(decodedTicket), eq(keytabPath), eq(krbDebug),
139+
any(ActionListener.class));
140+
verify(mockNativeRoleMappingStore).refreshRealmOnChange(kerberosRealm);
141+
verifyNoMoreInteractions(mockKerberosTicketValidator, mockNativeRoleMappingStore);
142+
}
108143
}

x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTestCase.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@
1313
import org.elasticsearch.common.util.concurrent.ThreadContext;
1414
import org.elasticsearch.common.util.set.Sets;
1515
import org.elasticsearch.env.TestEnvironment;
16+
import org.elasticsearch.license.XPackLicenseState;
1617
import org.elasticsearch.test.ESTestCase;
1718
import org.elasticsearch.threadpool.TestThreadPool;
1819
import org.elasticsearch.threadpool.ThreadPool;
1920
import org.elasticsearch.watcher.ResourceWatcherService;
2021
import org.elasticsearch.xpack.core.security.authc.AuthenticationResult;
22+
import org.elasticsearch.xpack.core.security.authc.Realm;
2123
import org.elasticsearch.xpack.core.security.authc.RealmConfig;
2224
import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings;
2325
import org.elasticsearch.xpack.core.security.support.Exceptions;
@@ -30,6 +32,7 @@
3032

3133
import java.nio.file.Path;
3234
import java.util.Arrays;
35+
import java.util.Collections;
3336
import java.util.List;
3437
import java.util.Locale;
3538
import java.util.Map;
@@ -58,6 +61,7 @@ public abstract class KerberosRealmTestCase extends ESTestCase {
5861

5962
protected KerberosTicketValidator mockKerberosTicketValidator;
6063
protected NativeRoleMappingStore mockNativeRoleMappingStore;
64+
protected XPackLicenseState licenseState;
6165

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

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

7480
@After
@@ -102,12 +108,18 @@ protected void assertSuccessAuthenticationResult(final User expectedUser, final
102108
}
103109

104110
protected KerberosRealm createKerberosRealm(final String... userForRoleMapping) {
111+
return createKerberosRealm(Collections.emptyList(), userForRoleMapping);
112+
}
113+
114+
protected KerberosRealm createKerberosRealm(final List<Realm> delegatedRealms, final String... userForRoleMapping) {
105115
config = new RealmConfig("test-kerb-realm", settings, globalSettings, TestEnvironment.newEnvironment(globalSettings),
106116
new ThreadContext(globalSettings));
107117
mockNativeRoleMappingStore = roleMappingStore(Arrays.asList(userForRoleMapping));
108118
mockKerberosTicketValidator = mock(KerberosTicketValidator.class);
109119
final KerberosRealm kerberosRealm =
110120
new KerberosRealm(config, mockNativeRoleMappingStore, mockKerberosTicketValidator, threadPool, null);
121+
Collections.shuffle(delegatedRealms, random());
122+
kerberosRealm.initialize(delegatedRealms, licenseState);
111123
return kerberosRealm;
112124
}
113125

x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTests.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import org.elasticsearch.action.support.PlainActionFuture;
1212
import org.elasticsearch.common.collect.Tuple;
1313
import org.elasticsearch.common.settings.SecureString;
14+
import org.elasticsearch.common.settings.Settings;
1415
import org.elasticsearch.common.util.concurrent.ThreadContext;
1516
import org.elasticsearch.env.TestEnvironment;
1617
import org.elasticsearch.rest.RestStatus;
@@ -19,6 +20,7 @@
1920
import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings;
2021
import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken;
2122
import org.elasticsearch.xpack.core.security.user.User;
23+
import org.elasticsearch.xpack.security.authc.support.MockLookupRealm;
2224
import org.elasticsearch.xpack.security.authc.support.UserRoleMapper.UserData;
2325
import org.ietf.jgss.GSSException;
2426

@@ -33,6 +35,7 @@
3335
import java.nio.file.attribute.PosixFilePermission;
3436
import java.nio.file.attribute.PosixFilePermissions;
3537
import java.util.Arrays;
38+
import java.util.Collections;
3639
import java.util.EnumSet;
3740
import java.util.Set;
3841

@@ -45,6 +48,7 @@
4548
import static org.mockito.Matchers.any;
4649
import static org.mockito.Matchers.eq;
4750
import static org.mockito.Mockito.mock;
51+
import static org.mockito.Mockito.spy;
4852
import static org.mockito.Mockito.times;
4953
import static org.mockito.Mockito.verify;
5054
import static org.mockito.Mockito.verifyNoMoreInteractions;
@@ -148,4 +152,37 @@ public void testKerberosRealmWithInvalidKeytabPathConfigurations() throws IOExce
148152
() -> new KerberosRealm(config, mockNativeRoleMappingStore, mockKerberosTicketValidator, threadPool, null));
149153
assertThat(iae.getMessage(), is(equalTo(expectedErrorMessage)));
150154
}
155+
156+
public void testDelegatedAuthorization() throws Exception {
157+
final String username = randomPrincipalName();
158+
final String expectedUsername = maybeRemoveRealmName(username);
159+
final MockLookupRealm otherRealm = spy(new MockLookupRealm(new RealmConfig("other_realm", Settings.EMPTY, globalSettings,
160+
TestEnvironment.newEnvironment(globalSettings), new ThreadContext(globalSettings))));
161+
final User lookupUser = new User(expectedUsername, new String[] { "admin-role" }, expectedUsername,
162+
expectedUsername + "@example.com", Collections.singletonMap("k1", "v1"), true);
163+
otherRealm.registerUser(lookupUser);
164+
165+
settings = Settings.builder().put(settings).putList("authorization_realms", "other_realm").build();
166+
final KerberosRealm kerberosRealm = createKerberosRealm(Collections.singletonList(otherRealm), username);
167+
final User expectedUser = lookupUser;
168+
final byte[] decodedTicket = "base64encodedticket".getBytes(StandardCharsets.UTF_8);
169+
final Path keytabPath = config.env().configFile().resolve(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(config.settings()));
170+
final boolean krbDebug = KerberosRealmSettings.SETTING_KRB_DEBUG_ENABLE.get(config.settings());
171+
mockKerberosTicketValidator(decodedTicket, keytabPath, krbDebug, new Tuple<>(username, "out-token"), null);
172+
final KerberosAuthenticationToken kerberosAuthenticationToken = new KerberosAuthenticationToken(decodedTicket);
173+
174+
PlainActionFuture<AuthenticationResult> future = new PlainActionFuture<>();
175+
kerberosRealm.authenticate(kerberosAuthenticationToken, future);
176+
assertSuccessAuthenticationResult(expectedUser, "out-token", future.actionGet());
177+
178+
future = new PlainActionFuture<>();
179+
kerberosRealm.authenticate(kerberosAuthenticationToken, future);
180+
assertSuccessAuthenticationResult(expectedUser, "out-token", future.actionGet());
181+
182+
verify(mockKerberosTicketValidator, times(2)).validateTicket(aryEq(decodedTicket), eq(keytabPath), eq(krbDebug),
183+
any(ActionListener.class));
184+
verify(mockNativeRoleMappingStore).refreshRealmOnChange(kerberosRealm);
185+
verifyNoMoreInteractions(mockKerberosTicketValidator, mockNativeRoleMappingStore);
186+
verify(otherRealm, times(2)).lookupUser(eq(expectedUsername), any(ActionListener.class));
187+
}
151188
}

x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTestCase.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ public abstract class KerberosTestCase extends ESTestCase {
7070
unsupportedLocaleLanguages.add("uz");
7171
unsupportedLocaleLanguages.add("fa");
7272
unsupportedLocaleLanguages.add("ks");
73+
unsupportedLocaleLanguages.add("ckb");
7374
}
7475

7576
@BeforeClass

0 commit comments

Comments
 (0)