Skip to content

Commit 09458ac

Browse files
committed
Merge remote-tracking branch 'origin/main'
2 parents 91d487c + 0c6100d commit 09458ac

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)