Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add IDP Configuration settings and related tests #51682

Merged
merged 7 commits into from
Feb 3, 2020
Merged
Show file tree
Hide file tree
Changes from 5 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
Member 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,125 @@
/*
* 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));
this.ssoEndpoints.computeIfAbsent("post", v -> settings.get(IDP_SSO_POST_ENDPOINT.getKey()));
this.sloEndpoints.computeIfAbsent("post", v -> settings.get(IDP_SLO_POST_ENDPOINT.getKey()));
this.sloEndpoints.computeIfAbsent("redirect", v -> settings.get(IDP_SLO_REDIRECT_ENDPOINT.getKey()));
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't understand why we have 1 put and 3 computeIfAbsent
I think you'd get the same bevahiour with 4 put wouldn't you? None of the keys are going to be present.

Copy link
Member Author

Choose a reason for hiding this comment

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

The "added benefit" of computeIfAbsent is that if the setting is not defined and settings.get returns null, then the null value won't be added to the hashmap.
I don't feel strongly about it, it seemed as a nice thing to do but I could also do with null checks here or when consuming the map values.

Copy link
Contributor

Choose a reason for hiding this comment

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

It seems like it's depending on a side effect (a documented one, but still a side effect) and makes the code less clear because the reader is left trying to understand why we're using different methods.
I'm not particular fussed though.

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() + "]");
jkakavas marked this conversation as resolved.
Show resolved Hide resolved
}
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
Member 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