Skip to content

Commit c363a84

Browse files
authored
Add authorizing_realms support to PKI realm (#31643)
Authorizing Realms allow an authenticating realm to delegate the task of constructing a User object (with name, roles, etc) to one or more other realms. This commit allows the PKI realm to delegate authorization to any other configured realm
1 parent dc633e0 commit c363a84

File tree

12 files changed

+669
-66
lines changed

12 files changed

+669
-66
lines changed

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import org.apache.logging.log4j.Logger;
99
import org.elasticsearch.action.ActionListener;
1010
import org.elasticsearch.common.util.concurrent.ThreadContext;
11+
import org.elasticsearch.xpack.core.security.authc.support.DelegatedAuthorizationSettings;
1112
import org.elasticsearch.xpack.core.security.user.User;
1213

1314
import java.util.HashMap;
@@ -131,6 +132,14 @@ public String toString() {
131132
return type + "/" + config.name;
132133
}
133134

135+
/**
136+
* This is no-op in the base class, but allows realms to be aware of what other realms are configured
137+
*
138+
* @see DelegatedAuthorizationSettings
139+
*/
140+
public void initialize(Iterable<Realm> realms) {
141+
}
142+
134143
/**
135144
* A factory interface to construct a security realm.
136145
*/

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/pki/PkiRealmSettings.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import org.elasticsearch.common.settings.Setting;
99
import org.elasticsearch.common.unit.TimeValue;
10+
import org.elasticsearch.xpack.core.security.authc.support.DelegatedAuthorizationSettings;
1011
import org.elasticsearch.xpack.core.security.authc.support.mapper.CompositeRoleMapperSettings;
1112
import org.elasticsearch.xpack.core.ssl.SSLConfigurationSettings;
1213

@@ -43,6 +44,7 @@ public static Set<Setting<?>> getSettings() {
4344
settings.add(SSL_SETTINGS.truststoreAlgorithm);
4445
settings.add(SSL_SETTINGS.caPaths);
4546

47+
settings.addAll(DelegatedAuthorizationSettings.getSettings());
4648
settings.addAll(CompositeRoleMapperSettings.getSettings());
4749

4850
return settings;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
package org.elasticsearch.xpack.core.security.authc.support;
8+
9+
import org.elasticsearch.common.settings.Setting;
10+
11+
import java.util.Collection;
12+
import java.util.Collections;
13+
import java.util.List;
14+
import java.util.function.Function;
15+
16+
/**
17+
* Settings related to "Delegated Authorization" (aka Lookup Realms)
18+
*/
19+
public class DelegatedAuthorizationSettings {
20+
21+
public static final Setting<List<String>> AUTHZ_REALMS = Setting.listSetting("authorizing_realms",
22+
Collections.emptyList(), Function.identity(), Setting.Property.NodeScope);
23+
24+
public static Collection<Setting<?>> getSettings() {
25+
return Collections.singleton(AUTHZ_REALMS);
26+
}
27+
}

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

Lines changed: 14 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,12 @@
3434
import org.elasticsearch.xpack.core.security.user.User;
3535
import org.elasticsearch.xpack.security.audit.AuditTrail;
3636
import org.elasticsearch.xpack.security.audit.AuditTrailService;
37+
import org.elasticsearch.xpack.security.authc.support.RealmUserLookup;
3738

3839
import java.util.LinkedHashMap;
3940
import java.util.List;
4041
import java.util.Map;
42+
import java.util.Objects;
4143
import java.util.function.BiConsumer;
4244
import java.util.function.Consumer;
4345

@@ -379,33 +381,18 @@ private void consumeUser(User user, Map<Realm, Tuple<String, Exception>> message
379381
* names of users that exist using a timing attack
380382
*/
381383
private void lookupRunAsUser(final User user, String runAsUsername, Consumer<User> userConsumer) {
382-
final List<Realm> realmsList = realms.asList();
383-
final BiConsumer<Realm, ActionListener<User>> realmLookupConsumer = (realm, lookupUserListener) ->
384-
realm.lookupUser(runAsUsername, ActionListener.wrap((lookedupUser) -> {
385-
if (lookedupUser != null) {
386-
lookedupBy = new RealmRef(realm.name(), realm.type(), nodeName);
387-
lookupUserListener.onResponse(lookedupUser);
388-
} else {
389-
lookupUserListener.onResponse(null);
390-
}
391-
}, lookupUserListener::onFailure));
392-
393-
final IteratingActionListener<User, Realm> userLookupListener =
394-
new IteratingActionListener<>(ActionListener.wrap((lookupUser) -> {
395-
if (lookupUser == null) {
396-
// the user does not exist, but we still create a User object, which will later be rejected by authz
397-
userConsumer.accept(new User(runAsUsername, null, user));
398-
} else {
399-
userConsumer.accept(new User(lookupUser, user));
400-
}
401-
},
402-
(e) -> listener.onFailure(request.exceptionProcessingRequest(e, authenticationToken))),
403-
realmLookupConsumer, realmsList, threadContext);
404-
try {
405-
userLookupListener.run();
406-
} catch (Exception e) {
407-
listener.onFailure(request.exceptionProcessingRequest(e, authenticationToken));
408-
}
384+
final RealmUserLookup lookup = new RealmUserLookup(realms.asList(), threadContext);
385+
lookup.lookup(runAsUsername, ActionListener.wrap(tuple -> {
386+
if (tuple == null) {
387+
// the user does not exist, but we still create a User object, which will later be rejected by authz
388+
userConsumer.accept(new User(runAsUsername, null, user));
389+
} else {
390+
User foundUser = Objects.requireNonNull(tuple.v1());
391+
Realm realm = Objects.requireNonNull(tuple.v2());
392+
lookedupBy = new RealmRef(realm.name(), realm.type(), nodeName);
393+
userConsumer.accept(new User(foundUser, user));
394+
}
395+
}, exception -> listener.onFailure(request.exceptionProcessingRequest(exception, authenticationToken))));
409396
}
410397

411398
/**

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ public Realms(Settings settings, Environment env, Map<String, Realm.Factory> fac
9292

9393
this.standardRealmsOnly = Collections.unmodifiableList(standardRealms);
9494
this.nativeRealmsOnly = Collections.unmodifiableList(nativeRealms);
95+
realms.forEach(r -> r.initialize(this));
9596
}
9697

9798
@Override

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/PkiRealm.java

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,12 @@
3131
import org.elasticsearch.xpack.core.ssl.SSLConfigurationSettings;
3232
import org.elasticsearch.xpack.security.authc.BytesKey;
3333
import org.elasticsearch.xpack.security.authc.support.CachingRealm;
34+
import org.elasticsearch.xpack.security.authc.support.DelegatedAuthorizationSupport;
3435
import org.elasticsearch.xpack.security.authc.support.UserRoleMapper;
3536
import org.elasticsearch.xpack.security.authc.support.mapper.CompositeRoleMapper;
3637
import org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore;
3738

3839
import javax.net.ssl.X509TrustManager;
39-
4040
import java.security.MessageDigest;
4141
import java.security.cert.Certificate;
4242
import java.security.cert.CertificateEncodingException;
@@ -75,6 +75,7 @@ public class PkiRealm extends Realm implements CachingRealm {
7575
private final Pattern principalPattern;
7676
private final UserRoleMapper roleMapper;
7777
private final Cache<BytesKey, User> cache;
78+
private DelegatedAuthorizationSupport delegatedRealms;
7879

7980
public PkiRealm(RealmConfig config, ResourceWatcherService watcherService, NativeRoleMappingStore nativeRoleMappingStore) {
8081
this(config, new CompositeRoleMapper(PkiRealmSettings.TYPE, config, watcherService, nativeRoleMappingStore));
@@ -91,6 +92,15 @@ public PkiRealm(RealmConfig config, ResourceWatcherService watcherService, Nativ
9192
.setExpireAfterWrite(PkiRealmSettings.CACHE_TTL_SETTING.get(config.settings()))
9293
.setMaximumWeight(PkiRealmSettings.CACHE_MAX_USERS_SETTING.get(config.settings()))
9394
.build();
95+
this.delegatedRealms = null;
96+
}
97+
98+
@Override
99+
public void initialize(Iterable<Realm> realms) {
100+
if (delegatedRealms != null) {
101+
throw new IllegalStateException("Realm has already been initialized");
102+
}
103+
delegatedRealms = new DelegatedAuthorizationSupport(realms, config);
94104
}
95105

96106
@Override
@@ -105,32 +115,50 @@ public X509AuthenticationToken token(ThreadContext context) {
105115

106116
@Override
107117
public void authenticate(AuthenticationToken authToken, ActionListener<AuthenticationResult> listener) {
118+
assert delegatedRealms != null : "Realm has not been initialized correctly";
108119
X509AuthenticationToken token = (X509AuthenticationToken)authToken;
109120
try {
110121
final BytesKey fingerprint = computeFingerprint(token.credentials()[0]);
111122
User user = cache.get(fingerprint);
112123
if (user != null) {
113-
listener.onResponse(AuthenticationResult.success(user));
124+
if (delegatedRealms.hasDelegation()) {
125+
delegatedRealms.resolve(token.principal(), listener);
126+
} else {
127+
listener.onResponse(AuthenticationResult.success(user));
128+
}
114129
} else if (isCertificateChainTrusted(trustManager, token, logger) == false) {
115130
listener.onResponse(AuthenticationResult.unsuccessful("Certificate for " + token.dn() + " is not trusted", null));
116131
} else {
117-
final Map<String, Object> metadata = Collections.singletonMap("pki_dn", token.dn());
118-
final UserRoleMapper.UserData userData = new UserRoleMapper.UserData(token.principal(),
119-
token.dn(), Collections.emptySet(), metadata, this.config);
120-
roleMapper.resolveRoles(userData, ActionListener.wrap(roles -> {
121-
final User computedUser =
122-
new User(token.principal(), roles.toArray(new String[roles.size()]), null, null, metadata, true);
123-
try (ReleasableLock ignored = readLock.acquire()) {
124-
cache.put(fingerprint, computedUser);
132+
final ActionListener<AuthenticationResult> cachingListener = ActionListener.wrap(result -> {
133+
if (result.isAuthenticated()) {
134+
try (ReleasableLock ignored = readLock.acquire()) {
135+
cache.put(fingerprint, result.getUser());
136+
}
125137
}
126-
listener.onResponse(AuthenticationResult.success(computedUser));
127-
}, listener::onFailure));
138+
listener.onResponse(result);
139+
}, listener::onFailure);
140+
if (delegatedRealms.hasDelegation()) {
141+
delegatedRealms.resolve(token.principal(), cachingListener);
142+
} else {
143+
this.buildUser(token, cachingListener);
144+
}
128145
}
129146
} catch (CertificateEncodingException e) {
130147
listener.onResponse(AuthenticationResult.unsuccessful("Certificate for " + token.dn() + " has encoding issues", e));
131148
}
132149
}
133150

151+
private void buildUser(X509AuthenticationToken token, ActionListener<AuthenticationResult> listener) {
152+
final Map<String, Object> metadata = Collections.singletonMap("pki_dn", token.dn());
153+
final UserRoleMapper.UserData userData = new UserRoleMapper.UserData(token.principal(),
154+
token.dn(), Collections.emptySet(), metadata, this.config);
155+
roleMapper.resolveRoles(userData, ActionListener.wrap(roles -> {
156+
final User computedUser =
157+
new User(token.principal(), roles.toArray(new String[roles.size()]), null, null, metadata, true);
158+
listener.onResponse(AuthenticationResult.success(computedUser));
159+
}, listener::onFailure));
160+
}
161+
134162
@Override
135163
public void lookupUser(String username, ActionListener<User> listener) {
136164
listener.onResponse(null);
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
package org.elasticsearch.xpack.security.authc.support;
8+
9+
import org.apache.logging.log4j.Logger;
10+
import org.elasticsearch.action.ActionListener;
11+
import org.elasticsearch.common.collect.Tuple;
12+
import org.elasticsearch.common.logging.Loggers;
13+
import org.elasticsearch.common.util.concurrent.ThreadContext;
14+
import org.elasticsearch.xpack.core.security.authc.AuthenticationResult;
15+
import org.elasticsearch.xpack.core.security.authc.Realm;
16+
import org.elasticsearch.xpack.core.security.authc.RealmConfig;
17+
import org.elasticsearch.xpack.core.security.authc.support.DelegatedAuthorizationSettings;
18+
import org.elasticsearch.xpack.core.security.user.User;
19+
20+
import java.util.ArrayList;
21+
import java.util.List;
22+
23+
import static org.elasticsearch.common.Strings.collectionToDelimitedString;
24+
25+
/**
26+
* Utility class for supporting "delegated authorization" (aka "authorizing_realms", aka "lookup realms").
27+
* A {@link Realm} may support delegating authorization to another realm. It does this by registering a
28+
* setting for {@link DelegatedAuthorizationSettings#AUTHZ_REALMS}, and constructing an instance of this
29+
* class. Then, after the realm has performed any authentication steps, if {@link #hasDelegation()} is
30+
* {@code true}, it delegates the construction of the {@link User} object and {@link AuthenticationResult}
31+
* to {@link #resolve(String, ActionListener)}.
32+
*/
33+
public class DelegatedAuthorizationSupport {
34+
35+
private final RealmUserLookup lookup;
36+
private final Logger logger;
37+
38+
/**
39+
* Resolves the {@link DelegatedAuthorizationSettings#AUTHZ_REALMS} setting from {@code config} and calls
40+
* {@link #DelegatedAuthorizationSupport(Iterable, List, ThreadContext)}
41+
*/
42+
public DelegatedAuthorizationSupport(Iterable<? extends Realm> allRealms, RealmConfig config) {
43+
this(allRealms, DelegatedAuthorizationSettings.AUTHZ_REALMS.get(config.settings()), config.threadContext());
44+
}
45+
46+
/**
47+
* Constructs a new object that delegates to the named realms ({@code lookupRealms}), which must exist within
48+
* {@code allRealms}.
49+
* @throws IllegalArgumentException if one of the specified realms does not exist
50+
*/
51+
protected DelegatedAuthorizationSupport(Iterable<? extends Realm> allRealms, List<String> lookupRealms, ThreadContext threadContext) {
52+
this.lookup = new RealmUserLookup(resolveRealms(allRealms, lookupRealms), threadContext);
53+
this.logger = Loggers.getLogger(getClass());
54+
}
55+
56+
/**
57+
* Are there any realms configured for delegated lookup
58+
*/
59+
public boolean hasDelegation() {
60+
return this.lookup.hasRealms();
61+
}
62+
63+
/**
64+
* Attempts to find the user specified by {@code username} in one of the delegated realms.
65+
* The realms are searched in the order specified during construction.
66+
* Returns a {@link AuthenticationResult#success(User) successful result} if a {@link User}
67+
* was found, otherwise returns an
68+
* {@link AuthenticationResult#unsuccessful(String, Exception) unsuccessful result}
69+
* with a meaningful diagnostic message.
70+
*/
71+
public void resolve(String username, ActionListener<AuthenticationResult> resultListener) {
72+
if (hasDelegation() == false) {
73+
resultListener.onResponse(AuthenticationResult.unsuccessful(
74+
"No [" + DelegatedAuthorizationSettings.AUTHZ_REALMS.getKey() + "] have been configured", null));
75+
return;
76+
}
77+
ActionListener<Tuple<User, Realm>> userListener = ActionListener.wrap(tuple -> {
78+
if (tuple != null) {
79+
logger.trace("Found user " + tuple.v1() + " in realm " + tuple.v2());
80+
resultListener.onResponse(AuthenticationResult.success(tuple.v1()));
81+
} else {
82+
resultListener.onResponse(AuthenticationResult.unsuccessful("the principal [" + username
83+
+ "] was authenticated, but no user could be found in realms [" + collectionToDelimitedString(lookup.getRealms(), ",")
84+
+ "]", null));
85+
}
86+
}, resultListener::onFailure);
87+
lookup.lookup(username, userListener);
88+
}
89+
90+
private List<Realm> resolveRealms(Iterable<? extends Realm> allRealms, List<String> lookupRealms) {
91+
final List<Realm> result = new ArrayList<>(lookupRealms.size());
92+
for (String name : lookupRealms) {
93+
result.add(findRealm(name, allRealms));
94+
}
95+
assert result.size() == lookupRealms.size();
96+
return result;
97+
}
98+
99+
private Realm findRealm(String name, Iterable<? extends Realm> allRealms) {
100+
for (Realm realm : allRealms) {
101+
if (name.equals(realm.name())) {
102+
return realm;
103+
}
104+
}
105+
throw new IllegalArgumentException("configured authorizing realm [" + name + "] does not exist (or is not enabled)");
106+
}
107+
108+
}

0 commit comments

Comments
 (0)