Skip to content

Commit b10bf0a

Browse files
slobodanadamovicpabloem
authored andcommitted
Allow configuring SAML private attributes (elastic#133154)
This PR is twofold: - adds a new `private_attributes` setting to the SAML realm, and - introduces extension point that allows providing a custom `SamlAuthenticateResponseHandler` The `private_attributes` setting can be used to define which SAML attributes should be treated as private. This implies that these attributes will not be logged or returned as part of user's metadata when `populate_user_metadata` is set to `true`.
1 parent 965aca3 commit b10bf0a

File tree

13 files changed

+499
-61
lines changed

13 files changed

+499
-61
lines changed

docs/changelog/133154.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 133154
2+
summary: Allow configuring SAML private attributes
3+
area: Authentication
4+
type: enhancement
5+
issues: []

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

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77
package org.elasticsearch.xpack.core.security.authc.saml;
88

9+
import org.elasticsearch.common.Strings;
910
import org.elasticsearch.common.settings.Setting;
1011
import org.elasticsearch.common.settings.SettingsException;
1112
import org.elasticsearch.common.util.set.Sets;
@@ -139,6 +140,56 @@ public class SamlRealmSettings {
139140
key -> Setting.positiveTimeSetting(key, TimeValue.timeValueMinutes(3), Setting.Property.NodeScope)
140141
);
141142

143+
/**
144+
* The names of attributes that should be treated as private and never populated as part of the user's metadata
145+
* (even when {@code #POPULATE_USER_METADATA} is configured).
146+
*/
147+
public static final Function<String, Setting.AffixSetting<List<String>>> PRIVATE_ATTRIBUTES = (type) -> Setting.affixKeySetting(
148+
RealmSettings.realmSettingPrefix(type),
149+
"private_attributes",
150+
(namespace, key) -> Setting.stringListSetting(key, new Setting.Validator<>() {
151+
152+
@Override
153+
public Iterator<Setting<?>> settings() {
154+
final List<Setting<?>> settings = List.of(
155+
PRINCIPAL_ATTRIBUTE.apply(type).getAttribute().getConcreteSettingForNamespace(namespace),
156+
GROUPS_ATTRIBUTE.apply(type).getAttributeSetting().getAttribute().getConcreteSettingForNamespace(namespace),
157+
DN_ATTRIBUTE.apply(type).getAttribute().getConcreteSettingForNamespace(namespace),
158+
NAME_ATTRIBUTE.apply(type).getAttribute().getConcreteSettingForNamespace(namespace),
159+
MAIL_ATTRIBUTE.apply(type).getAttribute().getConcreteSettingForNamespace(namespace)
160+
);
161+
return settings.iterator();
162+
}
163+
164+
@Override
165+
public void validate(List<String> attributes) {
166+
verifyNonNullNotEmpty(key, attributes);
167+
}
168+
169+
@Override
170+
public void validate(List<String> privateAttributes, Map<Setting<?>, Object> settings) {
171+
if (false == privateAttributes.isEmpty()) {
172+
final Set<String> privateAttributesSet = Set.copyOf(privateAttributes);
173+
this.settings().forEachRemaining(attributeSetting -> {
174+
String attributeName = (String) settings.get(attributeSetting);
175+
176+
if (false == Strings.isNullOrBlank(attributeName) && privateAttributesSet.contains(attributeName)) {
177+
throw new SettingsException(
178+
"SAML Attribute ["
179+
+ attributeName
180+
+ "] cannot be both configured for ["
181+
+ key
182+
+ "] and ["
183+
+ attributeSetting.getKey()
184+
+ "] settings."
185+
);
186+
}
187+
});
188+
}
189+
}
190+
}, Setting.Property.NodeScope)
191+
);
192+
142193
public static final Function<String, Setting.AffixSetting<List<String>>> EXCLUDE_ROLES = (type) -> Setting.affixKeySetting(
143194
RealmSettings.realmSettingPrefix(type),
144195
"exclude_roles",
@@ -201,7 +252,8 @@ public static Set<Setting.AffixSetting<?>> getSettings(String type) {
201252
ENCRYPTION_KEY_ALIAS.apply(type),
202253
SIGNING_KEY_ALIAS.apply(type),
203254
SIGNING_MESSAGE_TYPES.apply(type),
204-
REQUESTED_AUTHN_CONTEXT_CLASS_REF.apply(type)
255+
REQUESTED_AUTHN_CONTEXT_CLASS_REF.apply(type),
256+
PRIVATE_ATTRIBUTES.apply(type)
205257
);
206258

207259
set.addAll(X509KeyPairSettings.affix(RealmSettings.realmSettingPrefix(type), ENCRYPTION_SETTING_KEY, false));

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,7 @@
304304
import org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore;
305305
import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm;
306306
import org.elasticsearch.xpack.security.authc.jwt.JwtRealm;
307+
import org.elasticsearch.xpack.security.authc.saml.SamlAuthenticateResponseHandler;
307308
import org.elasticsearch.xpack.security.authc.service.CachingServiceAccountTokenStore;
308309
import org.elasticsearch.xpack.security.authc.service.CompositeServiceAccountTokenStore;
309310
import org.elasticsearch.xpack.security.authc.service.FileServiceAccountTokenStore;
@@ -628,6 +629,7 @@ public class Security extends Plugin
628629
private final SetOnce<FileRoleValidator> fileRoleValidator = new SetOnce<>();
629630
private final SetOnce<SecondaryAuthActions> secondaryAuthActions = new SetOnce<>();
630631
private final SetOnce<QueryableBuiltInRolesProviderFactory> queryableRolesProviderFactory = new SetOnce<>();
632+
private final SetOnce<SamlAuthenticateResponseHandler.Factory> samlAuthenticateResponseHandlerFactory = new SetOnce<>();
631633

632634
private final SetOnce<SecurityMigrations.Manager> migrationManager = new SetOnce<>();
633635
private final SetOnce<List<Closeable>> closableComponents = new SetOnce<>();
@@ -957,6 +959,15 @@ Collection<Object> createComponents(
957959
if (fileRoleValidator.get() == null) {
958960
fileRoleValidator.set(new FileRoleValidator.Default());
959961
}
962+
if (samlAuthenticateResponseHandlerFactory.get() == null) {
963+
samlAuthenticateResponseHandlerFactory.set(new SamlAuthenticateResponseHandler.DefaultFactory());
964+
}
965+
components.add(
966+
new PluginComponentBinding<>(
967+
SamlAuthenticateResponseHandler.class,
968+
samlAuthenticateResponseHandlerFactory.get().create(settings, tokenService, getClock())
969+
)
970+
);
960971
this.fileRolesStore.set(
961972
new FileRolesStore(settings, environment, resourceWatcherService, getLicenseState(), xContentRegistry, fileRoleValidator.get())
962973
);
@@ -2419,6 +2430,7 @@ public void loadExtensions(ExtensionLoader loader) {
24192430
loadSingletonExtensionAndSetOnce(loader, fileRoleValidator, FileRoleValidator.class);
24202431
loadSingletonExtensionAndSetOnce(loader, secondaryAuthActions, SecondaryAuthActions.class);
24212432
loadSingletonExtensionAndSetOnce(loader, queryableRolesProviderFactory, QueryableBuiltInRolesProviderFactory.class);
2433+
loadSingletonExtensionAndSetOnce(loader, samlAuthenticateResponseHandlerFactory, SamlAuthenticateResponseHandler.Factory.class);
24222434
}
24232435

24242436
private <T> void loadSingletonExtensionAndSetOnce(ExtensionLoader loader, SetOnce<T> setOnce, Class<T> clazz) {

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlAuthenticateAction.java

Lines changed: 7 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
import org.elasticsearch.action.support.HandledTransportAction;
1313
import org.elasticsearch.common.util.concurrent.EsExecutors;
1414
import org.elasticsearch.common.util.concurrent.ThreadContext;
15-
import org.elasticsearch.core.TimeValue;
1615
import org.elasticsearch.injection.guice.Inject;
1716
import org.elasticsearch.tasks.Task;
1817
import org.elasticsearch.threadpool.ThreadPool;
@@ -25,11 +24,9 @@
2524
import org.elasticsearch.xpack.core.security.authc.AuthenticationResult;
2625
import org.elasticsearch.xpack.core.security.user.User;
2726
import org.elasticsearch.xpack.security.authc.AuthenticationService;
28-
import org.elasticsearch.xpack.security.authc.TokenService;
29-
import org.elasticsearch.xpack.security.authc.saml.SamlRealm;
27+
import org.elasticsearch.xpack.security.authc.saml.SamlAuthenticateResponseHandler;
3028
import org.elasticsearch.xpack.security.authc.saml.SamlToken;
3129

32-
import java.util.Map;
3330
import java.util.concurrent.Executor;
3431

3532
/**
@@ -39,7 +36,7 @@ public final class TransportSamlAuthenticateAction extends HandledTransportActio
3936

4037
private final ThreadPool threadPool;
4138
private final AuthenticationService authenticationService;
42-
private final TokenService tokenService;
39+
private final SamlAuthenticateResponseHandler tokenHandler;
4340
private final SecurityContext securityContext;
4441
private final Executor genericExecutor;
4542

@@ -49,7 +46,7 @@ public TransportSamlAuthenticateAction(
4946
TransportService transportService,
5047
ActionFilters actionFilters,
5148
AuthenticationService authenticationService,
52-
TokenService tokenService,
49+
SamlAuthenticateResponseHandler tokenHandler,
5350
SecurityContext securityContext
5451
) {
5552
// TODO replace DIRECT_EXECUTOR_SERVICE when removing workaround for https://github.com/elastic/elasticsearch/issues/97916
@@ -62,7 +59,7 @@ public TransportSamlAuthenticateAction(
6259
);
6360
this.threadPool = threadPool;
6461
this.authenticationService = authenticationService;
65-
this.tokenService = tokenService;
62+
this.tokenHandler = tokenHandler;
6663
this.securityContext = securityContext;
6764
this.genericExecutor = threadPool.generic();
6865
}
@@ -88,25 +85,9 @@ private void doExecuteForked(Task task, SamlAuthenticateRequest request, ActionL
8885
}
8986
assert authentication != null : "authentication should never be null at this point";
9087
assert false == authentication.isRunAs() : "saml realm authentication cannot have run-as";
91-
@SuppressWarnings("unchecked")
92-
final Map<String, Object> tokenMeta = (Map<String, Object>) result.getMetadata().get(SamlRealm.CONTEXT_TOKEN_DATA);
93-
tokenService.createOAuth2Tokens(
94-
authentication,
95-
originatingAuthentication,
96-
tokenMeta,
97-
true,
98-
ActionListener.wrap(tokenResult -> {
99-
final TimeValue expiresIn = tokenService.getExpirationDelay();
100-
listener.onResponse(
101-
new SamlAuthenticateResponse(
102-
authentication,
103-
tokenResult.getAccessToken(),
104-
tokenResult.getRefreshToken(),
105-
expiresIn
106-
)
107-
);
108-
}, listener::onFailure)
109-
);
88+
assert result.isAuthenticated();
89+
tokenHandler.handleTokenResponse(authentication, originatingAuthentication, result, listener);
90+
11091
}, e -> {
11192
logger.debug(() -> "SamlToken [" + saml + "] could not be authenticated", e);
11293
listener.onFailure(e);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
package org.elasticsearch.xpack.security.authc.saml;
8+
9+
import org.elasticsearch.action.ActionListener;
10+
import org.elasticsearch.core.TimeValue;
11+
import org.elasticsearch.xpack.core.security.action.saml.SamlAuthenticateResponse;
12+
import org.elasticsearch.xpack.core.security.authc.Authentication;
13+
import org.elasticsearch.xpack.core.security.authc.AuthenticationResult;
14+
import org.elasticsearch.xpack.core.security.user.User;
15+
import org.elasticsearch.xpack.security.authc.TokenService;
16+
17+
import java.util.Map;
18+
19+
/**
20+
* Default implementation of {@link SamlAuthenticateResponseHandler} that returns tokens crested using the {@link TokenService}.
21+
*/
22+
public final class DefaultSamlAuthenticateResponseHandler implements SamlAuthenticateResponseHandler {
23+
24+
private final TokenService tokenService;
25+
26+
public DefaultSamlAuthenticateResponseHandler(TokenService tokenService) {
27+
this.tokenService = tokenService;
28+
}
29+
30+
@Override
31+
public void handleTokenResponse(
32+
Authentication authentication,
33+
Authentication originatingAuthentication,
34+
AuthenticationResult<User> authenticationResult,
35+
ActionListener<SamlAuthenticateResponse> listener
36+
) {
37+
@SuppressWarnings("unchecked")
38+
final Map<String, Object> tokenMeta = (Map<String, Object>) authenticationResult.getMetadata().get(SamlRealm.CONTEXT_TOKEN_DATA);
39+
tokenService.createOAuth2Tokens(authentication, originatingAuthentication, tokenMeta, true, ActionListener.wrap(tokenResult -> {
40+
final TimeValue expiresIn = tokenService.getExpirationDelay();
41+
listener.onResponse(
42+
new SamlAuthenticateResponse(authentication, tokenResult.getAccessToken(), tokenResult.getRefreshToken(), expiresIn)
43+
);
44+
}, listener::onFailure));
45+
}
46+
}

0 commit comments

Comments
 (0)