Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.elasticsearch.cluster.service.ClusterService;
import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
import org.elasticsearch.env.Environment;
import org.elasticsearch.env.NodeEnvironment;
Expand All @@ -20,9 +21,15 @@
import org.elasticsearch.script.ScriptService;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.watcher.ResourceWatcherService;
import org.elasticsearch.xpack.core.ssl.X509KeyPairSettings;
import org.elasticsearch.xpack.idp.saml.idp.CloudIdp;
import org.elasticsearch.xpack.idp.saml.support.SamlInit;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

/**
Expand All @@ -31,7 +38,38 @@
*/
public class IdentityProviderPlugin extends Plugin implements ActionPlugin {

private final Setting<Boolean> ENABLED_SETTING = Setting.boolSetting("xpack.idp.enabled", false, Setting.Property.NodeScope);
private static final Setting<Boolean> ENABLED_SETTING = Setting.boolSetting("xpack.idp.enabled", false, Setting.Property.NodeScope);
public static final Setting<String> IDP_ENTITY_ID = Setting.simpleString("xpack.idp.entity_id", Setting.Property.NodeScope);
public static final Setting<String> IDP_SSO_REDIRECT_ENDPOINT = Setting.simpleString("xpack.idp.sso_endpoint.redirect", value -> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we make these Setting<URI> instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I went back and forth. I settled on that we care about them being URIs mainly when we validate them while reading the setting and from there on we can treat them as Strings. Since I don't see any other use for these other than:

  • Constructing metadata
  • Validating Authn and Logout Requests where we will use string equality eitherway

it feels that future implementers or anyone reading the code doesn't need to care about the fact that these are URIs so I opted for the "simpler" handling.

try {
new URI(value);
} catch (URISyntaxException e) {
throw new IllegalArgumentException("Invalid value [" + value + "] for [xpack.idp.sso_endpoint.redirect]. Not a valid URI", e);
}
}, Setting.Property.NodeScope);
public static final Setting<String> IDP_SSO_POST_ENDPOINT = Setting.simpleString("xpack.idp.sso_endpoint.post", value -> {
try {
new URI(value);
} catch (URISyntaxException e) {
throw new IllegalArgumentException("Invalid value [" + value + "] for [xpack.idp.sso_endpoint.post]. Not a valid URI", e);
}
}, Setting.Property.NodeScope);
public static final Setting<String> IDP_SLO_REDIRECT_ENDPOINT = Setting.simpleString("xpack.idp.slo_endpoint.redirect", value -> {
try {
new URI(value);
} catch (URISyntaxException e) {
throw new IllegalArgumentException("Invalid value [" + value + "] for [xpack.idp.slo_endpoint.redirect]. Not a valid URI", e);
}
}, Setting.Property.NodeScope);
public static final Setting<String> IDP_SLO_POST_ENDPOINT = Setting.simpleString("xpack.idp.slo_endpoint.post", value -> {
try {
new URI(value);
} catch (URISyntaxException e) {
throw new IllegalArgumentException("Invalid value [" + value + "] for [xpack.idp.slo_endpoint.post]. Not a valid URI", e);
}
}, Setting.Property.NodeScope);
public static final Setting<String> IDP_SIGNING_KEY_ALIAS = Setting.simpleString("xpack.idp.signing.keystore.alias",
Setting.Property.NodeScope);

private final Logger logger = LogManager.getLogger();
private boolean enabled;
Expand All @@ -41,12 +79,27 @@ public Collection<Object> createComponents(Client client, ClusterService cluster
ResourceWatcherService resourceWatcherService, ScriptService scriptService,
NamedXContentRegistry xContentRegistry, Environment environment,
NodeEnvironment nodeEnvironment, NamedWriteableRegistry namedWriteableRegistry) {
enabled = ENABLED_SETTING.get(environment.settings());
final Settings settings = environment.settings();
enabled = ENABLED_SETTING.get(settings);
if (enabled == false) {
return List.of();
}

SamlInit.initialize();
CloudIdp idp = new CloudIdp(environment, settings);
return List.of();
}

@Override
public List<Setting<?>> getSettings() {
List<Setting<?>> settings = new ArrayList<>();
settings.add(ENABLED_SETTING);
settings.add(IDP_ENTITY_ID);
settings.add(IDP_SLO_REDIRECT_ENDPOINT);
settings.add(IDP_SLO_POST_ENDPOINT);
settings.add(IDP_SSO_REDIRECT_ENDPOINT);
settings.add(IDP_SSO_POST_ENDPOINT);
settings.addAll(X509KeyPairSettings.withPrefix("xpack.idp.signing.", false).getAllSettings());
return Collections.unmodifiableList(settings);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

package org.elasticsearch.xpack.idp.saml.idp;

import org.elasticsearch.common.Strings;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.settings.SettingsException;
import org.elasticsearch.common.util.iterable.Iterables;
import org.elasticsearch.env.Environment;
import org.elasticsearch.xpack.core.ssl.CertParsingUtils;
import org.elasticsearch.xpack.core.ssl.X509KeyPairSettings;
import org.opensaml.security.x509.X509Credential;
import org.opensaml.security.x509.impl.X509KeyManagerX509CredentialAdapter;

import javax.net.ssl.X509KeyManager;
import java.security.PrivateKey;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;

import static org.elasticsearch.xpack.idp.IdentityProviderPlugin.IDP_ENTITY_ID;
import static org.elasticsearch.xpack.idp.IdentityProviderPlugin.IDP_SIGNING_KEY_ALIAS;
import static org.elasticsearch.xpack.idp.IdentityProviderPlugin.IDP_SLO_POST_ENDPOINT;
import static org.elasticsearch.xpack.idp.IdentityProviderPlugin.IDP_SLO_REDIRECT_ENDPOINT;
import static org.elasticsearch.xpack.idp.IdentityProviderPlugin.IDP_SSO_POST_ENDPOINT;
import static org.elasticsearch.xpack.idp.IdentityProviderPlugin.IDP_SSO_REDIRECT_ENDPOINT;

public class CloudIdp implements SamlIdentityProvider {

private final String entityId;
private final HashMap<String, String> ssoEndpoints = new HashMap<>();
private final HashMap<String, String> sloEndpoints = new HashMap<>();
private final X509Credential signingCredential;

public CloudIdp(Environment env, Settings settings) {
this.entityId = require(settings, IDP_ENTITY_ID);
this.ssoEndpoints.put("redirect", require(settings, IDP_SSO_REDIRECT_ENDPOINT));
if (settings.hasValue(IDP_SSO_POST_ENDPOINT.getKey())) {
this.ssoEndpoints.put("post", settings.get(IDP_SSO_POST_ENDPOINT.getKey()));
}
if (settings.hasValue(IDP_SLO_POST_ENDPOINT.getKey())) {
this.sloEndpoints.put("post", settings.get(IDP_SLO_POST_ENDPOINT.getKey()));
}
if (settings.hasValue(IDP_SLO_REDIRECT_ENDPOINT.getKey())) {
this.sloEndpoints.put("redirect", settings.get(IDP_SLO_REDIRECT_ENDPOINT.getKey()));
}
this.signingCredential = buildSigningCredential(env, settings);
}

@Override
public String getEntityId() {
return entityId;
}

@Override
public String getSingleSignOnEndpoint(String binding) {
return ssoEndpoints.get(binding);
}

@Override
public String getSingleLogoutEndpoint(String binding) {
return sloEndpoints.get(binding);
}

@Override
public X509Credential getSigningCredential() {
return signingCredential;
}

private static String require(Settings settings, Setting<String> setting) {
if (settings.hasValue(setting.getKey())) {
return setting.get(settings);
} else {
throw new SettingsException("The configuration setting [" + setting.getKey() + "] is required");
}
}

static X509Credential buildSigningCredential(Environment env, Settings settings) {
final X509KeyPairSettings keyPairSettings = X509KeyPairSettings.withPrefix("xpack.idp.signing.", false);
final X509KeyManager keyManager = CertParsingUtils.getKeyManager(keyPairSettings, settings, null, env);
if (keyManager == null) {
return null;
}

final String selectedAlias;
final String configAlias = IDP_SIGNING_KEY_ALIAS.get(settings);
if (Strings.isNullOrEmpty(configAlias)) {
final Set<String> aliases = new HashSet<>();
final String[] rsaAliases = keyManager.getServerAliases("RSA", null);
if (null != rsaAliases) {
aliases.addAll(Arrays.asList(rsaAliases));
}
final String[] ecAliases = keyManager.getServerAliases("EC", null);
if (null != ecAliases) {
aliases.addAll(Arrays.asList(ecAliases));
}
if (aliases.isEmpty()) {
throw new IllegalArgumentException(
"The configured keystore for xpack.idp.signing.keystore does not contain any RSA or EC key pairs");
}
if (aliases.size() > 1) {
throw new IllegalArgumentException("The configured keystore for xpack.idp.signing.keystore contains multiple key pairs" +
" but no alias has been configured with [" + IDP_SIGNING_KEY_ALIAS.getKey() + "]");
}
selectedAlias = Iterables.get(aliases, 0);
} else {
selectedAlias = configAlias;
}
final PrivateKey signingKey = keyManager.getPrivateKey(selectedAlias);
if (signingKey == null) {
throw new IllegalArgumentException("The configured keystore for xpack.idp.signing.keystore does not have a private key" +
" associated with alias [" + selectedAlias + "]");
}

final String keyType = signingKey.getAlgorithm();
if (keyType.equals("RSA") == false && keyType.equals("EC") == false) {
throw new IllegalArgumentException("The key associated with alias [" + selectedAlias + "] " + "that has been configured with ["
+ IDP_SIGNING_KEY_ALIAS.getKey() + "] uses unsupported key algorithm type [" + keyType
+ "], only RSA and EC are supported");
}
return new X509KeyManagerX509CredentialAdapter(keyManager, selectedAlias);
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,18 @@
package org.elasticsearch.xpack.idp.saml.idp;


import org.opensaml.security.x509.X509Credential;

/**
* SAML 2.0 configuration information about this IdP
*/
public interface SamlIdentityProvider {

String getEntityId();

String getSingleSignOnEndpoint(String binding);
Copy link
Contributor

Choose a reason for hiding this comment

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

Could these return URI instead?

Also where do we expect to need these? Is it just for metadata? (which is a real need - I just want to check how we expect to consume them).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

see #51682 (comment)

Also where do we expect to need these? Is it just for metadata?

Metadata creation and incoming SAML message validation


String getSingleLogoutEndpoint(String binding);

X509Credential getSigningCredential();
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ grant {
// needed because of SAML (cf. o.e.x.c.s.s.RestorableContextClassLoader)
permission java.lang.RuntimePermission "getClassLoader";
permission java.lang.RuntimePermission "setContextClassLoader";

// needed during initialization of OpenSAML library where xml security algorithms are registered
// see https://github.com/apache/santuario-java/blob/e79f1fe4192de73a975bc7246aee58ed0703343d/src/main/java/org/apache/xml/security/utils/JavaUtils.java#L205-L220
// and https://git.shibboleth.net/view/?p=java-opensaml.git;a=blob;f=opensaml-xmlsec-impl/src/main/java/org/opensaml/xmlsec/signature/impl/SignatureMarshaller.java;hb=db0eaa64210f0e32d359cd6c57bedd57902bf811#l52
Expand Down
Loading