From 860fd30017de5c43af0da1888640c491eab5e0f0 Mon Sep 17 00:00:00 2001 From: BigPandaToo Date: Tue, 3 Nov 2020 10:23:43 +0100 Subject: [PATCH 1/7] Adding API for generating SAML SP metadata Resolve #49018 --- .../action/saml/SamlSPMetadataAction.java | 18 +++++ .../action/saml/SamlSPMetadataRequest.java | 60 ++++++++++++++ .../action/saml/SamlSPMetadataResponse.java | 38 +++++++++ .../saml/SamlSPMetadataRequestTests.java | 43 ++++++++++ .../xpack/security/Security.java | 5 ++ .../saml/TransportSamlSPMetadataAction.java | 78 +++++++++++++++++++ .../authc/saml/SamlMetadataCommand.java | 12 +++ .../action/saml/RestSamlSPMetadataAction.java | 78 +++++++++++++++++++ 8 files changed, 332 insertions(+) create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlSPMetadataAction.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlSPMetadataRequest.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlSPMetadataResponse.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/saml/SamlSPMetadataRequestTests.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlSPMetadataAction.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/saml/RestSamlSPMetadataAction.java diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlSPMetadataAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlSPMetadataAction.java new file mode 100644 index 0000000000000..8e620e0c9301c --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlSPMetadataAction.java @@ -0,0 +1,18 @@ +/* + * 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.core.security.action.saml; + +import org.elasticsearch.action.ActionType; + +public class SamlSPMetadataAction extends ActionType { + public static final String NAME = "cluster:admin/xpack/security/saml/metadata"; + public static final SamlSPMetadataAction INSTANCE = new SamlSPMetadataAction(); + + private SamlSPMetadataAction() { + super(NAME, SamlSPMetadataResponse::new); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlSPMetadataRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlSPMetadataRequest.java new file mode 100644 index 0000000000000..9db28864a36fe --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlSPMetadataRequest.java @@ -0,0 +1,60 @@ +/* + * 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.core.security.action.saml; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; + +import java.io.IOException; + +import static org.elasticsearch.action.ValidateActions.addValidationError; + +public class SamlSPMetadataRequest extends ActionRequest { + + String realmName; + + public SamlSPMetadataRequest(StreamInput in) throws IOException { + super(in); + realmName = in.readOptionalString(); + } + + public SamlSPMetadataRequest() { + } + + @Override + public ActionRequestValidationException validate() { + ActionRequestValidationException validationException = null; + if (Strings.hasText(realmName) == false) { + validationException = addValidationError("realm may not be empty", validationException); + } + return validationException; + } + + public String getRealmName() { + return realmName; + } + + public void setRealmName(String realmName) { + this.realmName = realmName; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "{" + + "realmName=" + realmName + + '}'; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeOptionalString(realmName); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlSPMetadataResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlSPMetadataResponse.java new file mode 100644 index 0000000000000..804430d2bfb86 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlSPMetadataResponse.java @@ -0,0 +1,38 @@ +/* + * 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.core.security.action.saml; + +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; + +import java.io.IOException; + +/** + * Response containing a SAML SP metadata for a specific realm as XML. + */ +public class SamlSPMetadataResponse extends ActionResponse { + public String getXMLString() { + return XMLString; + } + + private String XMLString; + + public SamlSPMetadataResponse(StreamInput in) throws IOException { + super(in); + XMLString = in.readString(); + } + + public SamlSPMetadataResponse(String XMLString) { + this.XMLString = XMLString; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(XMLString); + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/saml/SamlSPMetadataRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/saml/SamlSPMetadataRequestTests.java new file mode 100644 index 0000000000000..b2f9abe644d3a --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/saml/SamlSPMetadataRequestTests.java @@ -0,0 +1,43 @@ +/* + * 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.core.security.action.saml; + +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.test.ESTestCase; + +import java.io.IOException; + +import static org.hamcrest.Matchers.containsString; + +public class SamlSPMetadataRequestTests extends ESTestCase { + + public void testValidateFailsWhenRealmNotSet() { + final SamlSPMetadataRequest samlSPMetadataRequest = new SamlSPMetadataRequest(); + final ActionRequestValidationException validationException = samlSPMetadataRequest.validate(); + assertThat(validationException.getMessage(), containsString("realm may not be empty")); + } + + public void testValidateSerialization() throws IOException { + final SamlSPMetadataRequest samlSPMetadataRequest = new SamlSPMetadataRequest(); + samlSPMetadataRequest.setRealmName("saml1"); + try (BytesStreamOutput out = new BytesStreamOutput()) { + samlSPMetadataRequest.writeTo(out); + try (StreamInput in = out.bytes().streamInput()) { + final SamlSPMetadataRequest serialized = new SamlSPMetadataRequest(in); + assertEquals(samlSPMetadataRequest.getRealmName(), serialized.getRealmName()); + } + } + } + + public void testValidateToString() { + final SamlSPMetadataRequest samlSPMetadataRequest = new SamlSPMetadataRequest(); + samlSPMetadataRequest.setRealmName("saml1"); + assertThat(samlSPMetadataRequest.toString(), containsString("{realmName=saml1}")); + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index 4474e401ecbaf..4466bce80071f 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -108,6 +108,7 @@ import org.elasticsearch.xpack.core.security.action.saml.SamlLogoutAction; import org.elasticsearch.xpack.core.security.action.saml.SamlCompleteLogoutAction; import org.elasticsearch.xpack.core.security.action.saml.SamlPrepareAuthenticationAction; +import org.elasticsearch.xpack.core.security.action.saml.SamlSPMetadataAction; import org.elasticsearch.xpack.core.security.action.token.CreateTokenAction; import org.elasticsearch.xpack.core.security.action.token.InvalidateTokenAction; import org.elasticsearch.xpack.core.security.action.token.RefreshTokenAction; @@ -174,6 +175,7 @@ import org.elasticsearch.xpack.security.action.saml.TransportSamlLogoutAction; import org.elasticsearch.xpack.security.action.saml.TransportSamlCompleteLogoutAction; import org.elasticsearch.xpack.security.action.saml.TransportSamlPrepareAuthenticationAction; +import org.elasticsearch.xpack.security.action.saml.TransportSamlSPMetadataAction; import org.elasticsearch.xpack.security.action.token.TransportCreateTokenAction; import org.elasticsearch.xpack.security.action.token.TransportInvalidateTokenAction; import org.elasticsearch.xpack.security.action.token.TransportRefreshTokenAction; @@ -243,6 +245,7 @@ import org.elasticsearch.xpack.security.rest.action.saml.RestSamlLogoutAction; import org.elasticsearch.xpack.security.rest.action.saml.RestSamlCompleteLogoutAction; import org.elasticsearch.xpack.security.rest.action.saml.RestSamlPrepareAuthenticationAction; +import org.elasticsearch.xpack.security.rest.action.saml.RestSamlSPMetadataAction; import org.elasticsearch.xpack.security.rest.action.user.RestChangePasswordAction; import org.elasticsearch.xpack.security.rest.action.user.RestDeleteUserAction; import org.elasticsearch.xpack.security.rest.action.user.RestGetUserPrivilegesAction; @@ -781,6 +784,7 @@ public void onIndexModule(IndexModule module) { new ActionHandler<>(SamlLogoutAction.INSTANCE, TransportSamlLogoutAction.class), new ActionHandler<>(SamlInvalidateSessionAction.INSTANCE, TransportSamlInvalidateSessionAction.class), new ActionHandler<>(SamlCompleteLogoutAction.INSTANCE, TransportSamlCompleteLogoutAction.class), + new ActionHandler<>(SamlSPMetadataAction.INSTANCE, TransportSamlSPMetadataAction.class), new ActionHandler<>(OpenIdConnectPrepareAuthenticationAction.INSTANCE, TransportOpenIdConnectPrepareAuthenticationAction.class), new ActionHandler<>(OpenIdConnectAuthenticateAction.INSTANCE, TransportOpenIdConnectAuthenticateAction.class), @@ -841,6 +845,7 @@ public List getRestHandlers(Settings settings, RestController restC new RestSamlLogoutAction(settings, getLicenseState()), new RestSamlInvalidateSessionAction(settings, getLicenseState()), new RestSamlCompleteLogoutAction(settings, getLicenseState()), + new RestSamlSPMetadataAction(settings, getLicenseState()), new RestOpenIdConnectPrepareAuthenticationAction(settings, getLicenseState()), new RestOpenIdConnectAuthenticateAction(settings, getLicenseState()), new RestOpenIdConnectLogoutAction(settings, getLicenseState()), diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlSPMetadataAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlSPMetadataAction.java new file mode 100644 index 0000000000000..edbcb22cbd2ef --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlSPMetadataAction.java @@ -0,0 +1,78 @@ +/* + * 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.security.action.saml; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.HandledTransportAction; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.core.security.action.saml.SamlSPMetadataAction; +import org.elasticsearch.xpack.core.security.action.saml.SamlSPMetadataRequest; +import org.elasticsearch.xpack.core.security.action.saml.SamlSPMetadataResponse; +import org.elasticsearch.xpack.security.authc.Realms; +import org.elasticsearch.xpack.security.authc.saml.SamlMetadataCommand; +import org.elasticsearch.xpack.security.authc.saml.SamlRealm; +import org.elasticsearch.xpack.security.authc.saml.SamlUtils; +import org.opensaml.saml.saml2.metadata.EntityDescriptor; +import org.opensaml.saml.saml2.metadata.impl.EntityDescriptorMarshaller; +import org.w3c.dom.Element; + +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import java.io.StringWriter; +import java.util.List; + +import static org.elasticsearch.xpack.security.authc.saml.SamlRealm.findSamlRealms; + +/** + * Transport action responsible for generating a SAML SP Metadata. + */ +public class TransportSamlSPMetadataAction + extends HandledTransportAction { + + private final Realms realms; + + @Inject + public TransportSamlSPMetadataAction(TransportService transportService, ActionFilters actionFilters, Realms realms) { + super(SamlSPMetadataAction.NAME, transportService, actionFilters, SamlSPMetadataRequest::new + ); + this.realms = realms; + } + + @Override + protected void doExecute(Task task, SamlSPMetadataRequest request, + ActionListener listener) { + List realms = findSamlRealms(this.realms, request.getRealmName(), null); + if (realms.isEmpty()) { + listener.onFailure(SamlUtils.samlException("Cannot find any matching realm for [{}]", request)); + } else if (realms.size() > 1) { + listener.onFailure(SamlUtils.samlException("Found multiple matching realms [{}] for [{}]", realms, request)); + } else { + prepareMetadata(realms.get(0), listener); + } + } + + private void prepareMetadata(SamlRealm realm, ActionListener listener) { + try { + final EntityDescriptorMarshaller marshaller = new EntityDescriptorMarshaller(); + final EntityDescriptor descriptor = SamlMetadataCommand.buildEntityDescriptorFromSamlRealm(realm); + final Element element = marshaller.marshall(descriptor); + final StringWriter writer = new StringWriter(); + final Transformer serializer = SamlUtils.getHardenedXMLTransformer(); + serializer.setOutputProperty(OutputKeys.INDENT, "yes"); + serializer.transform(new DOMSource(element), new StreamResult(writer)); + listener.onResponse(new SamlSPMetadataResponse(writer.toString())); + } catch (Exception e) { + logger.debug("Internal exception during SAML SP metadata generation", e); + listener.onFailure(e); + } + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlMetadataCommand.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlMetadataCommand.java index 3a2b87afe1fa6..2d6a1c62e099b 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlMetadataCommand.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlMetadataCommand.java @@ -93,6 +93,18 @@ public static void main(String[] args) throws Exception { exit(new SamlMetadataCommand().main(args, Terminal.DEFAULT)); } + public static EntityDescriptor buildEntityDescriptorFromSamlRealm(SamlRealm samlRealm) throws Exception { + final SpConfiguration spConfig = samlRealm.getLogoutHandler().getSpConfiguration(); + final Locale locale = Locale.getDefault(); + final SamlSpMetadataBuilder builder = new SamlSpMetadataBuilder(locale, spConfig.getEntityId()) + .assertionConsumerServiceUrl(spConfig.getAscUrl()) + .singleLogoutServiceUrl(spConfig.getLogoutUrl()) + .encryptionCredentials(spConfig.getEncryptionCredentials()) + .signingCredential(spConfig.getSigningConfiguration().getCredential()) + .authnRequestsSigned(spConfig.getSigningConfiguration().shouldSign(AuthnRequest.DEFAULT_ELEMENT_LOCAL_NAME)); + return builder.build(); + } + public SamlMetadataCommand() { this((environment) -> { KeyStoreWrapper ksWrapper = KeyStoreWrapper.load(environment.configFile()); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/saml/RestSamlSPMetadataAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/saml/RestSamlSPMetadataAction.java new file mode 100644 index 0000000000000..dc58b41d74ed2 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/saml/RestSamlSPMetadataAction.java @@ -0,0 +1,78 @@ +/* + * 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.security.rest.action.saml; + +import org.elasticsearch.client.node.NodeClient; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.ObjectParser; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.rest.BytesRestResponse; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.RestResponse; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.rest.action.RestBuilderListener; +import org.elasticsearch.xpack.core.security.action.saml.SamlSPMetadataAction; +import org.elasticsearch.xpack.core.security.action.saml.SamlSPMetadataRequest; +import org.elasticsearch.xpack.core.security.action.saml.SamlSPMetadataResponse; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +import static org.elasticsearch.rest.RestRequest.Method.GET; + +public class RestSamlSPMetadataAction extends SamlBaseRestHandler { + + static class Input { + String realm; + void setRealm(String realm) { + this.realm = realm; + } + } + + static final ObjectParser PARSER = new ObjectParser<>("security_saml_metadata", + SamlSPMetadataRequest::new); + + static { + PARSER.declareStringOrNull(SamlSPMetadataRequest::setRealmName, new ParseField("realm")); + } + + public RestSamlSPMetadataAction(Settings settings, XPackLicenseState licenseState) { + super(settings, licenseState); + } + + @Override + public List routes() { + return Collections.singletonList( + new Route(GET, "/_security/saml/metadata")); + } + + @Override + public String getName() { + return "security_saml_metadata_action"; + } + + @Override + public RestChannelConsumer innerPrepareRequest(RestRequest request, NodeClient client) throws IOException { + try (XContentParser parser = request.contentParser()) { + final SamlSPMetadataRequest SamlSPRequest = PARSER.parse(parser, null); + return channel -> client.execute(SamlSPMetadataAction.INSTANCE, SamlSPRequest, + new RestBuilderListener(channel) { + @Override + public RestResponse buildResponse(SamlSPMetadataResponse response, XContentBuilder builder) throws Exception { + builder.startObject(); + builder.field("xml_metadata", response.getXMLString()); + builder.endObject(); + return new BytesRestResponse(RestStatus.OK, builder); + } + }); + } + } +} From 1180e645b6b156be2af38bed936c6a3da95d7386 Mon Sep 17 00:00:00 2001 From: BigPandaToo Date: Tue, 3 Nov 2020 23:18:12 +0100 Subject: [PATCH 2/7] Adding API for generating SAML SP metadata Resolves #49018 --- ...aAction.java => SamlSpMetadataAction.java} | 8 +- ...equest.java => SamlSpMetadataRequest.java} | 8 +- ...ponse.java => SamlSpMetadataResponse.java} | 6 +- ...s.java => SamlSpMetadataRequestTests.java} | 12 +- .../xpack/security/Security.java | 10 +- ...ava => TransportSamlSpMetadataAction.java} | 36 ++--- .../saml/SamlEntityDescriptorBuilder.java | 123 ++++++++++++++++++ .../authc/saml/SamlMetadataCommand.java | 99 +------------- .../authc/saml/SamlSpMetadataBuilder.java | 16 +++ ...ion.java => RestSamlSpMetadataAction.java} | 39 ++---- 10 files changed, 200 insertions(+), 157 deletions(-) rename x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/{SamlSPMetadataAction.java => SamlSpMetadataAction.java} (65%) rename x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/{SamlSPMetadataRequest.java => SamlSpMetadataRequest.java} (84%) rename x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/{SamlSPMetadataResponse.java => SamlSpMetadataResponse.java} (83%) rename x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/saml/{SamlSPMetadataRequestTests.java => SamlSpMetadataRequestTests.java} (76%) rename x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/{TransportSamlSPMetadataAction.java => TransportSamlSpMetadataAction.java} (65%) create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlEntityDescriptorBuilder.java rename x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/saml/{RestSamlSPMetadataAction.java => RestSamlSpMetadataAction.java} (62%) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlSPMetadataAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlSpMetadataAction.java similarity index 65% rename from x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlSPMetadataAction.java rename to x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlSpMetadataAction.java index 8e620e0c9301c..f1313de650058 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlSPMetadataAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlSpMetadataAction.java @@ -8,11 +8,11 @@ import org.elasticsearch.action.ActionType; -public class SamlSPMetadataAction extends ActionType { +public class SamlSpMetadataAction extends ActionType { public static final String NAME = "cluster:admin/xpack/security/saml/metadata"; - public static final SamlSPMetadataAction INSTANCE = new SamlSPMetadataAction(); + public static final SamlSpMetadataAction INSTANCE = new SamlSpMetadataAction(); - private SamlSPMetadataAction() { - super(NAME, SamlSPMetadataResponse::new); + private SamlSpMetadataAction() { + super(NAME, SamlSpMetadataResponse::new); } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlSPMetadataRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlSpMetadataRequest.java similarity index 84% rename from x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlSPMetadataRequest.java rename to x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlSpMetadataRequest.java index 9db28864a36fe..106b16d43766a 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlSPMetadataRequest.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlSpMetadataRequest.java @@ -16,23 +16,23 @@ import static org.elasticsearch.action.ValidateActions.addValidationError; -public class SamlSPMetadataRequest extends ActionRequest { +public class SamlSpMetadataRequest extends ActionRequest { String realmName; - public SamlSPMetadataRequest(StreamInput in) throws IOException { + public SamlSpMetadataRequest(StreamInput in) throws IOException { super(in); realmName = in.readOptionalString(); } - public SamlSPMetadataRequest() { + public SamlSpMetadataRequest() { } @Override public ActionRequestValidationException validate() { ActionRequestValidationException validationException = null; if (Strings.hasText(realmName) == false) { - validationException = addValidationError("realm may not be empty", validationException); + validationException = addValidationError("Realm name may not be empty", validationException); } return validationException; } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlSPMetadataResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlSpMetadataResponse.java similarity index 83% rename from x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlSPMetadataResponse.java rename to x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlSpMetadataResponse.java index 804430d2bfb86..6afa597e40f1d 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlSPMetadataResponse.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlSpMetadataResponse.java @@ -15,19 +15,19 @@ /** * Response containing a SAML SP metadata for a specific realm as XML. */ -public class SamlSPMetadataResponse extends ActionResponse { +public class SamlSpMetadataResponse extends ActionResponse { public String getXMLString() { return XMLString; } private String XMLString; - public SamlSPMetadataResponse(StreamInput in) throws IOException { + public SamlSpMetadataResponse(StreamInput in) throws IOException { super(in); XMLString = in.readString(); } - public SamlSPMetadataResponse(String XMLString) { + public SamlSpMetadataResponse(String XMLString) { this.XMLString = XMLString; } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/saml/SamlSPMetadataRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/saml/SamlSpMetadataRequestTests.java similarity index 76% rename from x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/saml/SamlSPMetadataRequestTests.java rename to x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/saml/SamlSpMetadataRequestTests.java index b2f9abe644d3a..5e0da8b2be6d5 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/saml/SamlSPMetadataRequestTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/saml/SamlSpMetadataRequestTests.java @@ -15,28 +15,28 @@ import static org.hamcrest.Matchers.containsString; -public class SamlSPMetadataRequestTests extends ESTestCase { +public class SamlSpMetadataRequestTests extends ESTestCase { public void testValidateFailsWhenRealmNotSet() { - final SamlSPMetadataRequest samlSPMetadataRequest = new SamlSPMetadataRequest(); + final SamlSpMetadataRequest samlSPMetadataRequest = new SamlSpMetadataRequest(); final ActionRequestValidationException validationException = samlSPMetadataRequest.validate(); - assertThat(validationException.getMessage(), containsString("realm may not be empty")); + assertThat(validationException.getMessage(), containsString("Realm name may not be empty")); } public void testValidateSerialization() throws IOException { - final SamlSPMetadataRequest samlSPMetadataRequest = new SamlSPMetadataRequest(); + final SamlSpMetadataRequest samlSPMetadataRequest = new SamlSpMetadataRequest(); samlSPMetadataRequest.setRealmName("saml1"); try (BytesStreamOutput out = new BytesStreamOutput()) { samlSPMetadataRequest.writeTo(out); try (StreamInput in = out.bytes().streamInput()) { - final SamlSPMetadataRequest serialized = new SamlSPMetadataRequest(in); + final SamlSpMetadataRequest serialized = new SamlSpMetadataRequest(in); assertEquals(samlSPMetadataRequest.getRealmName(), serialized.getRealmName()); } } } public void testValidateToString() { - final SamlSPMetadataRequest samlSPMetadataRequest = new SamlSPMetadataRequest(); + final SamlSpMetadataRequest samlSPMetadataRequest = new SamlSpMetadataRequest(); samlSPMetadataRequest.setRealmName("saml1"); assertThat(samlSPMetadataRequest.toString(), containsString("{realmName=saml1}")); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index 4466bce80071f..870fb9faf3ed8 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -108,7 +108,7 @@ import org.elasticsearch.xpack.core.security.action.saml.SamlLogoutAction; import org.elasticsearch.xpack.core.security.action.saml.SamlCompleteLogoutAction; import org.elasticsearch.xpack.core.security.action.saml.SamlPrepareAuthenticationAction; -import org.elasticsearch.xpack.core.security.action.saml.SamlSPMetadataAction; +import org.elasticsearch.xpack.core.security.action.saml.SamlSpMetadataAction; import org.elasticsearch.xpack.core.security.action.token.CreateTokenAction; import org.elasticsearch.xpack.core.security.action.token.InvalidateTokenAction; import org.elasticsearch.xpack.core.security.action.token.RefreshTokenAction; @@ -175,7 +175,7 @@ import org.elasticsearch.xpack.security.action.saml.TransportSamlLogoutAction; import org.elasticsearch.xpack.security.action.saml.TransportSamlCompleteLogoutAction; import org.elasticsearch.xpack.security.action.saml.TransportSamlPrepareAuthenticationAction; -import org.elasticsearch.xpack.security.action.saml.TransportSamlSPMetadataAction; +import org.elasticsearch.xpack.security.action.saml.TransportSamlSpMetadataAction; import org.elasticsearch.xpack.security.action.token.TransportCreateTokenAction; import org.elasticsearch.xpack.security.action.token.TransportInvalidateTokenAction; import org.elasticsearch.xpack.security.action.token.TransportRefreshTokenAction; @@ -245,7 +245,7 @@ import org.elasticsearch.xpack.security.rest.action.saml.RestSamlLogoutAction; import org.elasticsearch.xpack.security.rest.action.saml.RestSamlCompleteLogoutAction; import org.elasticsearch.xpack.security.rest.action.saml.RestSamlPrepareAuthenticationAction; -import org.elasticsearch.xpack.security.rest.action.saml.RestSamlSPMetadataAction; +import org.elasticsearch.xpack.security.rest.action.saml.RestSamlSpMetadataAction; import org.elasticsearch.xpack.security.rest.action.user.RestChangePasswordAction; import org.elasticsearch.xpack.security.rest.action.user.RestDeleteUserAction; import org.elasticsearch.xpack.security.rest.action.user.RestGetUserPrivilegesAction; @@ -784,7 +784,7 @@ public void onIndexModule(IndexModule module) { new ActionHandler<>(SamlLogoutAction.INSTANCE, TransportSamlLogoutAction.class), new ActionHandler<>(SamlInvalidateSessionAction.INSTANCE, TransportSamlInvalidateSessionAction.class), new ActionHandler<>(SamlCompleteLogoutAction.INSTANCE, TransportSamlCompleteLogoutAction.class), - new ActionHandler<>(SamlSPMetadataAction.INSTANCE, TransportSamlSPMetadataAction.class), + new ActionHandler<>(SamlSpMetadataAction.INSTANCE, TransportSamlSpMetadataAction.class), new ActionHandler<>(OpenIdConnectPrepareAuthenticationAction.INSTANCE, TransportOpenIdConnectPrepareAuthenticationAction.class), new ActionHandler<>(OpenIdConnectAuthenticateAction.INSTANCE, TransportOpenIdConnectAuthenticateAction.class), @@ -845,7 +845,7 @@ public List getRestHandlers(Settings settings, RestController restC new RestSamlLogoutAction(settings, getLicenseState()), new RestSamlInvalidateSessionAction(settings, getLicenseState()), new RestSamlCompleteLogoutAction(settings, getLicenseState()), - new RestSamlSPMetadataAction(settings, getLicenseState()), + new RestSamlSpMetadataAction(settings, getLicenseState()), new RestOpenIdConnectPrepareAuthenticationAction(settings, getLicenseState()), new RestOpenIdConnectAuthenticateAction(settings, getLicenseState()), new RestOpenIdConnectLogoutAction(settings, getLicenseState()), diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlSPMetadataAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlSpMetadataAction.java similarity index 65% rename from x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlSPMetadataAction.java rename to x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlSpMetadataAction.java index edbcb22cbd2ef..00c1d073c8229 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlSPMetadataAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlSpMetadataAction.java @@ -6,24 +6,24 @@ package org.elasticsearch.xpack.security.action.saml; +import org.apache.logging.log4j.message.ParameterizedMessage; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.HandledTransportAction; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.tasks.Task; import org.elasticsearch.transport.TransportService; -import org.elasticsearch.xpack.core.security.action.saml.SamlSPMetadataAction; -import org.elasticsearch.xpack.core.security.action.saml.SamlSPMetadataRequest; -import org.elasticsearch.xpack.core.security.action.saml.SamlSPMetadataResponse; +import org.elasticsearch.xpack.core.security.action.saml.SamlSpMetadataAction; +import org.elasticsearch.xpack.core.security.action.saml.SamlSpMetadataRequest; +import org.elasticsearch.xpack.core.security.action.saml.SamlSpMetadataResponse; import org.elasticsearch.xpack.security.authc.Realms; -import org.elasticsearch.xpack.security.authc.saml.SamlMetadataCommand; +import org.elasticsearch.xpack.security.authc.saml.SamlEntityDescriptorBuilder; import org.elasticsearch.xpack.security.authc.saml.SamlRealm; import org.elasticsearch.xpack.security.authc.saml.SamlUtils; import org.opensaml.saml.saml2.metadata.EntityDescriptor; import org.opensaml.saml.saml2.metadata.impl.EntityDescriptorMarshaller; import org.w3c.dom.Element; -import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; @@ -35,43 +35,43 @@ /** * Transport action responsible for generating a SAML SP Metadata. */ -public class TransportSamlSPMetadataAction - extends HandledTransportAction { +public class TransportSamlSpMetadataAction + extends HandledTransportAction { private final Realms realms; @Inject - public TransportSamlSPMetadataAction(TransportService transportService, ActionFilters actionFilters, Realms realms) { - super(SamlSPMetadataAction.NAME, transportService, actionFilters, SamlSPMetadataRequest::new + public TransportSamlSpMetadataAction(TransportService transportService, ActionFilters actionFilters, Realms realms) { + super(SamlSpMetadataAction.NAME, transportService, actionFilters, SamlSpMetadataRequest::new ); this.realms = realms; } @Override - protected void doExecute(Task task, SamlSPMetadataRequest request, - ActionListener listener) { + protected void doExecute(Task task, SamlSpMetadataRequest request, + ActionListener listener) { List realms = findSamlRealms(this.realms, request.getRealmName(), null); if (realms.isEmpty()) { - listener.onFailure(SamlUtils.samlException("Cannot find any matching realm for [{}]", request)); + listener.onFailure(SamlUtils.samlException("Cannot find any matching realm for [{}]", request.getRealmName())); } else if (realms.size() > 1) { - listener.onFailure(SamlUtils.samlException("Found multiple matching realms [{}] for [{}]", realms, request)); + listener.onFailure(SamlUtils.samlException("Found multiple matching realms [{}] for [{}]", realms, request.getRealmName())); } else { prepareMetadata(realms.get(0), listener); } } - private void prepareMetadata(SamlRealm realm, ActionListener listener) { + private void prepareMetadata(SamlRealm realm, ActionListener listener) { try { final EntityDescriptorMarshaller marshaller = new EntityDescriptorMarshaller(); - final EntityDescriptor descriptor = SamlMetadataCommand.buildEntityDescriptorFromSamlRealm(realm); + final SamlEntityDescriptorBuilder samlEntityDescriptorBuilder = new SamlEntityDescriptorBuilder(realm); + final EntityDescriptor descriptor = samlEntityDescriptorBuilder.getEntityDescriptor(); final Element element = marshaller.marshall(descriptor); final StringWriter writer = new StringWriter(); final Transformer serializer = SamlUtils.getHardenedXMLTransformer(); - serializer.setOutputProperty(OutputKeys.INDENT, "yes"); serializer.transform(new DOMSource(element), new StreamResult(writer)); - listener.onResponse(new SamlSPMetadataResponse(writer.toString())); + listener.onResponse(new SamlSpMetadataResponse(writer.toString())); } catch (Exception e) { - logger.debug("Internal exception during SAML SP metadata generation", e); + logger.error(new ParameterizedMessage("Error during SAML SP metadata generation for real [{}]", realm.name()), e); listener.onFailure(e); } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlEntityDescriptorBuilder.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlEntityDescriptorBuilder.java new file mode 100644 index 0000000000000..59d3aae1bddb7 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlEntityDescriptorBuilder.java @@ -0,0 +1,123 @@ +/* + * 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.security.authc.saml; + +import org.elasticsearch.cli.ExitCodes; +import org.elasticsearch.cli.Terminal; +import org.elasticsearch.cli.UserException; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.xpack.core.security.authc.RealmConfig; +import org.elasticsearch.xpack.core.security.authc.RealmSettings; +import org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings; +import org.opensaml.saml.saml2.core.AuthnRequest; +import org.opensaml.saml.saml2.metadata.EntityDescriptor; + +import java.util.Locale; +import java.util.Map; + +public class SamlEntityDescriptorBuilder { + private final EntityDescriptor entityDescriptor; + + public EntityDescriptor getEntityDescriptor() { + return entityDescriptor; + } + + public SamlEntityDescriptorBuilder(SamlRealm samlRealm) throws Exception{ + final SpConfiguration spConfig = samlRealm.getLogoutHandler().getSpConfiguration(); + final SamlSpMetadataBuilder builder = new SamlSpMetadataBuilder(samlRealm) + .encryptionCredentials(spConfig.getEncryptionCredentials()) + .signingCredential(spConfig.getSigningConfiguration().getCredential()) + .authnRequestsSigned(spConfig.getSigningConfiguration().shouldSign(AuthnRequest.DEFAULT_ELEMENT_LOCAL_NAME)); + entityDescriptor = builder.build(); + } + + public SamlEntityDescriptorBuilder(RealmConfig realm, boolean batch, String serviceName, String orgName, String orgUrl, + String orgDisplayName, boolean contacts, Locale locale, Map attributes, + Terminal terminal) throws Exception { + final Settings realmSettings = realm.settings().getByPrefix(RealmSettings.realmSettingPrefix(realm.identifier())); + terminal.println(Terminal.Verbosity.VERBOSE, + "Using realm configuration\n=====\n" + realmSettings.toDelimitedString('\n') + "====="); + terminal.println(Terminal.Verbosity.VERBOSE, "Using locale: " + locale.toLanguageTag()); + + final SpConfiguration spConfig = SamlRealm.getSpConfiguration(realm); + final SamlSpMetadataBuilder builder = new SamlSpMetadataBuilder(locale, spConfig.getEntityId()) + .assertionConsumerServiceUrl(spConfig.getAscUrl()) + .singleLogoutServiceUrl(spConfig.getLogoutUrl()) + .encryptionCredentials(spConfig.getEncryptionCredentials()) + .signingCredential(spConfig.getSigningConfiguration().getCredential()) + .authnRequestsSigned(spConfig.getSigningConfiguration().shouldSign(AuthnRequest.DEFAULT_ELEMENT_LOCAL_NAME)) + .nameIdFormat(realm.getSetting(SamlRealmSettings.NAMEID_FORMAT)) + .serviceName(serviceName); + + for (String attr : attributes.keySet()) { + final String name; + String friendlyName; + final String settingName = attributes.get(attr); + final String attributeSource = settingName == null ? "command line" : '"' + settingName + '"'; + if (attr.contains(":")) { + name = attr; + if (batch) { + friendlyName = settingName; + } else { + friendlyName = terminal.readText("What is the friendly name for " + + attributeSource + + " attribute \"" + attr + "\" [default: " + + (settingName == null ? "none" : settingName) + + "] "); + if (Strings.isNullOrEmpty(friendlyName)) { + friendlyName = settingName; + } + } + } else { + if (batch) { + throw new UserException(ExitCodes.CONFIG, "Option batch is specified, but attribute " + + attr + " appears to be a FriendlyName value"); + } + friendlyName = attr; + name = requireText(terminal, + "What is the standard (urn) name for " + attributeSource + " attribute \"" + attr + "\" (required): "); + } + terminal.println(Terminal.Verbosity.VERBOSE, "Requesting attribute '" + name + "' (FriendlyName: '" + friendlyName + "')"); + builder.withAttribute(friendlyName, name); + } + + if (orgName != null && orgUrl != null) { + builder.organization(orgName, orgDisplayName, orgUrl); + } + + if (contacts) { + terminal.println("\nPlease enter the personal details for each contact to be included in the metadata"); + do { + final String givenName = requireText(terminal, "What is the given name for the contact: "); + final String surName = requireText(terminal, "What is the surname for the contact: "); + final String displayName = givenName + ' ' + surName; + final String email = requireText(terminal, "What is the email address for " + displayName + ": "); + String type; + while (true) { + type = requireText(terminal, "What is the contact type for " + displayName + ": "); + if (SamlSpMetadataBuilder.ContactInfo.TYPES.containsKey(type)) { + break; + } else { + terminal.errorPrintln("Type '" + type + "' is not valid. Valid values are " + + Strings.collectionToCommaDelimitedString(SamlSpMetadataBuilder.ContactInfo.TYPES.keySet())); + } + } + builder.withContact(type, givenName, surName, email); + } while (terminal.promptYesNo("Enter details for another contact", true)); + } + entityDescriptor = builder.build(); + } + + private String requireText(Terminal terminal, String prompt) { + String value = null; + while (Strings.isNullOrEmpty(value)) { + value = terminal.readText(prompt); + } + return value; + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlMetadataCommand.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlMetadataCommand.java index 2d6a1c62e099b..3aef30a67cac9 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlMetadataCommand.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlMetadataCommand.java @@ -38,7 +38,6 @@ import org.elasticsearch.cli.Terminal; import org.elasticsearch.cli.UserException; import org.elasticsearch.common.CheckedFunction; -import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.PathUtils; import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.common.settings.KeyStoreWrapper; @@ -51,10 +50,8 @@ import org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings; import org.elasticsearch.xpack.core.ssl.CertParsingUtils; import org.elasticsearch.xpack.core.ssl.PemUtils; -import org.elasticsearch.xpack.security.authc.saml.SamlSpMetadataBuilder.ContactInfo; import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; import org.opensaml.core.xml.io.MarshallingException; -import org.opensaml.saml.saml2.core.AuthnRequest; import org.opensaml.saml.saml2.metadata.EntityDescriptor; import org.opensaml.saml.saml2.metadata.impl.EntityDescriptorMarshaller; import org.opensaml.security.credential.Credential; @@ -93,18 +90,6 @@ public static void main(String[] args) throws Exception { exit(new SamlMetadataCommand().main(args, Terminal.DEFAULT)); } - public static EntityDescriptor buildEntityDescriptorFromSamlRealm(SamlRealm samlRealm) throws Exception { - final SpConfiguration spConfig = samlRealm.getLogoutHandler().getSpConfiguration(); - final Locale locale = Locale.getDefault(); - final SamlSpMetadataBuilder builder = new SamlSpMetadataBuilder(locale, spConfig.getEntityId()) - .assertionConsumerServiceUrl(spConfig.getAscUrl()) - .singleLogoutServiceUrl(spConfig.getLogoutUrl()) - .encryptionCredentials(spConfig.getEncryptionCredentials()) - .signingCredential(spConfig.getSigningConfiguration().getCredential()) - .authnRequestsSigned(spConfig.getSigningConfiguration().shouldSign(AuthnRequest.DEFAULT_ELEMENT_LOCAL_NAME)); - return builder.build(); - } - public SamlMetadataCommand() { this((environment) -> { KeyStoreWrapper ksWrapper = KeyStoreWrapper.load(environment.configFile()); @@ -170,81 +155,19 @@ EntityDescriptor buildEntityDescriptor(Terminal terminal, OptionSet options, Env final RealmConfig realm = findRealm(terminal, options, env); final Settings realmSettings = realm.settings().getByPrefix(RealmSettings.realmSettingPrefix(realm.identifier())); + final String serviceName = option(serviceNameSpec, options, env.settings().get("cluster.name")); + final String orgName = options.has(orgNameSpec)? orgNameSpec.value(options): null; + final String orgUrl = options.has(orgUrlSpec)? orgUrlSpec.value(options): null; + final String orgDisplayName = option(orgDisplayNameSpec, options, orgName); terminal.println(Terminal.Verbosity.VERBOSE, "Using realm configuration\n=====\n" + realmSettings.toDelimitedString('\n') + "====="); final Locale locale = findLocale(options); terminal.println(Terminal.Verbosity.VERBOSE, "Using locale: " + locale.toLanguageTag()); - final SpConfiguration spConfig = SamlRealm.getSpConfiguration(realm); - final SamlSpMetadataBuilder builder = new SamlSpMetadataBuilder(locale, spConfig.getEntityId()) - .assertionConsumerServiceUrl(spConfig.getAscUrl()) - .singleLogoutServiceUrl(spConfig.getLogoutUrl()) - .encryptionCredentials(spConfig.getEncryptionCredentials()) - .signingCredential(spConfig.getSigningConfiguration().getCredential()) - .authnRequestsSigned(spConfig.getSigningConfiguration().shouldSign(AuthnRequest.DEFAULT_ELEMENT_LOCAL_NAME)) - .nameIdFormat(realm.getSetting(SamlRealmSettings.NAMEID_FORMAT)) - .serviceName(option(serviceNameSpec, options, env.settings().get("cluster.name"))); - - Map attributes = getAttributeNames(options, realm); - for (String attr : attributes.keySet()) { - final String name; - String friendlyName; - final String settingName = attributes.get(attr); - final String attributeSource = settingName == null ? "command line" : '"' + settingName + '"'; - if (attr.contains(":")) { - name = attr; - if (batch) { - friendlyName = settingName; - } else { - friendlyName = terminal.readText("What is the friendly name for " + - attributeSource - + " attribute \"" + attr + "\" [default: " + - (settingName == null ? "none" : settingName) + - "] "); - if (Strings.isNullOrEmpty(friendlyName)) { - friendlyName = settingName; - } - } - } else { - if (batch) { - throw new UserException(ExitCodes.CONFIG, "Option " + batchSpec.toString() + " is specified, but attribute " - + attr + " appears to be a FriendlyName value"); - } - friendlyName = attr; - name = requireText(terminal, - "What is the standard (urn) name for " + attributeSource + " attribute \"" + attr + "\" (required): "); - } - terminal.println(Terminal.Verbosity.VERBOSE, "Requesting attribute '" + name + "' (FriendlyName: '" + friendlyName + "')"); - builder.withAttribute(friendlyName, name); - } + final SamlEntityDescriptorBuilder samlEntityDescriptorBuilder = new SamlEntityDescriptorBuilder(realm, batch, serviceName, orgName, + orgUrl, orgDisplayName, options.has(contactsSpec), locale, getAttributeNames(options, realm), terminal); - if (options.has(orgNameSpec) && options.has(orgUrlSpec)) { - String name = orgNameSpec.value(options); - builder.organization(name, option(orgDisplayNameSpec, options, name), orgUrlSpec.value(options)); - } - - if (options.has(contactsSpec)) { - terminal.println("\nPlease enter the personal details for each contact to be included in the metadata"); - do { - final String givenName = requireText(terminal, "What is the given name for the contact: "); - final String surName = requireText(terminal, "What is the surname for the contact: "); - final String displayName = givenName + ' ' + surName; - final String email = requireText(terminal, "What is the email address for " + displayName + ": "); - String type; - while (true) { - type = requireText(terminal, "What is the contact type for " + displayName + ": "); - if (ContactInfo.TYPES.containsKey(type)) { - break; - } else { - terminal.errorPrintln("Type '" + type + "' is not valid. Valid values are " - + Strings.collectionToCommaDelimitedString(ContactInfo.TYPES.keySet())); - } - } - builder.withContact(type, givenName, surName, email); - } while (terminal.promptYesNo("Enter details for another contact", true)); - } - - return builder.build(); + return samlEntityDescriptorBuilder.getEntityDescriptor(); } // package-protected for testing @@ -386,14 +309,6 @@ private Path resolvePath(String name) { return PathUtils.get(name).normalize(); } - private String requireText(Terminal terminal, String prompt) { - String value = null; - while (Strings.isNullOrEmpty(value)) { - value = terminal.readText(prompt); - } - return value; - } - private T option(OptionSpec spec, OptionSet options, T defaultValue) { if (options.has(spec)) { return spec.value(options); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlSpMetadataBuilder.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlSpMetadataBuilder.java index 0993878e76a87..9e83ea1807899 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlSpMetadataBuilder.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlSpMetadataBuilder.java @@ -95,6 +95,22 @@ public SamlSpMetadataBuilder(Locale locale, String entityId) { this.authnRequestsSigned = Boolean.FALSE; } + /** + * @param samlRealm SamlRealm for which SP Metadata is built + */ + public SamlSpMetadataBuilder(SamlRealm samlRealm) { + final SpConfiguration spConfig = samlRealm.getLogoutHandler().getSpConfiguration(); + this.locale = Locale.getDefault(); + this.entityId = spConfig.getEntityId(); + this.attributeNames = null; + this.contacts = null; + this.serviceName = "Elasticsearch"; + this.nameIdFormat = null; + this.authnRequestsSigned = Boolean.FALSE; + this.assertionConsumerServiceUrl = spConfig.getAscUrl(); + this.singleLogoutServiceUrl = spConfig.getLogoutUrl(); + } + /** * The format that the service provider expects for incoming NameID element. */ diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/saml/RestSamlSPMetadataAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/saml/RestSamlSpMetadataAction.java similarity index 62% rename from x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/saml/RestSamlSPMetadataAction.java rename to x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/saml/RestSamlSpMetadataAction.java index dc58b41d74ed2..2fe8227e8ce8d 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/saml/RestSamlSPMetadataAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/saml/RestSamlSpMetadataAction.java @@ -7,7 +7,6 @@ package org.elasticsearch.xpack.security.rest.action.saml; import org.elasticsearch.client.node.NodeClient; -import org.elasticsearch.common.ParseField; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.ObjectParser; import org.elasticsearch.common.xcontent.XContentBuilder; @@ -18,9 +17,9 @@ import org.elasticsearch.rest.RestResponse; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.rest.action.RestBuilderListener; -import org.elasticsearch.xpack.core.security.action.saml.SamlSPMetadataAction; -import org.elasticsearch.xpack.core.security.action.saml.SamlSPMetadataRequest; -import org.elasticsearch.xpack.core.security.action.saml.SamlSPMetadataResponse; +import org.elasticsearch.xpack.core.security.action.saml.SamlSpMetadataAction; +import org.elasticsearch.xpack.core.security.action.saml.SamlSpMetadataRequest; +import org.elasticsearch.xpack.core.security.action.saml.SamlSpMetadataResponse; import java.io.IOException; import java.util.Collections; @@ -28,30 +27,19 @@ import static org.elasticsearch.rest.RestRequest.Method.GET; -public class RestSamlSPMetadataAction extends SamlBaseRestHandler { +public class RestSamlSpMetadataAction extends SamlBaseRestHandler { - static class Input { - String realm; - void setRealm(String realm) { - this.realm = realm; - } - } - - static final ObjectParser PARSER = new ObjectParser<>("security_saml_metadata", - SamlSPMetadataRequest::new); - - static { - PARSER.declareStringOrNull(SamlSPMetadataRequest::setRealmName, new ParseField("realm")); - } + static final ObjectParser PARSER = new ObjectParser<>("security_saml_metadata", + SamlSpMetadataRequest::new); - public RestSamlSPMetadataAction(Settings settings, XPackLicenseState licenseState) { + public RestSamlSpMetadataAction(Settings settings, XPackLicenseState licenseState) { super(settings, licenseState); } @Override public List routes() { return Collections.singletonList( - new Route(GET, "/_security/saml/metadata")); + new Route(GET, "/_security/saml/metadata/{realm}")); } @Override @@ -62,13 +50,14 @@ public String getName() { @Override public RestChannelConsumer innerPrepareRequest(RestRequest request, NodeClient client) throws IOException { try (XContentParser parser = request.contentParser()) { - final SamlSPMetadataRequest SamlSPRequest = PARSER.parse(parser, null); - return channel -> client.execute(SamlSPMetadataAction.INSTANCE, SamlSPRequest, - new RestBuilderListener(channel) { + final SamlSpMetadataRequest SamlSpRequest = PARSER.parse(parser, null); + SamlSpRequest.setRealmName(request.param("realm")); + return channel -> client.execute(SamlSpMetadataAction.INSTANCE, SamlSpRequest, + new RestBuilderListener(channel) { @Override - public RestResponse buildResponse(SamlSPMetadataResponse response, XContentBuilder builder) throws Exception { + public RestResponse buildResponse(SamlSpMetadataResponse response, XContentBuilder builder) throws Exception { builder.startObject(); - builder.field("xml_metadata", response.getXMLString()); + builder.field("metadata", response.getXMLString()); builder.endObject(); return new BytesRestResponse(RestStatus.OK, builder); } From 1b18d9f85d73afa7404d9924d396f993400e89ef Mon Sep 17 00:00:00 2001 From: BigPandaToo Date: Wed, 4 Nov 2020 15:42:50 +0100 Subject: [PATCH 3/7] Adding API for generating SAML SP metadata Resolves #49018 --- .../action/saml/SamlSpMetadataAction.java | 2 +- .../action/saml/SamlSpMetadataRequest.java | 5 +- .../saml/SamlSpMetadataRequestTests.java | 10 +- .../saml/TransportSamlSpMetadataAction.java | 6 +- .../saml/SamlEntityDescriptorBuilder.java | 123 ---------------- .../authc/saml/SamlMetadataCommand.java | 133 ++++++++++++++---- .../authc/saml/SamlSpMetadataBuilder.java | 15 +- .../action/saml/RestSamlSpMetadataAction.java | 30 ++-- 8 files changed, 138 insertions(+), 186 deletions(-) delete mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlEntityDescriptorBuilder.java diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlSpMetadataAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlSpMetadataAction.java index f1313de650058..17588ec8c6eaf 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlSpMetadataAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlSpMetadataAction.java @@ -9,7 +9,7 @@ import org.elasticsearch.action.ActionType; public class SamlSpMetadataAction extends ActionType { - public static final String NAME = "cluster:admin/xpack/security/saml/metadata"; + public static final String NAME = "cluster:monitor/xpack/security/saml/metadata"; public static final SamlSpMetadataAction INSTANCE = new SamlSpMetadataAction(); private SamlSpMetadataAction() { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlSpMetadataRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlSpMetadataRequest.java index 106b16d43766a..4b302ecc5173c 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlSpMetadataRequest.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlSpMetadataRequest.java @@ -18,14 +18,15 @@ public class SamlSpMetadataRequest extends ActionRequest { - String realmName; + private String realmName; public SamlSpMetadataRequest(StreamInput in) throws IOException { super(in); realmName = in.readOptionalString(); } - public SamlSpMetadataRequest() { + public SamlSpMetadataRequest(String realmName) { + this.realmName = realmName; } @Override diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/saml/SamlSpMetadataRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/saml/SamlSpMetadataRequestTests.java index 5e0da8b2be6d5..4a9e935c1d50f 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/saml/SamlSpMetadataRequestTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/saml/SamlSpMetadataRequestTests.java @@ -17,15 +17,14 @@ public class SamlSpMetadataRequestTests extends ESTestCase { - public void testValidateFailsWhenRealmNotSet() { - final SamlSpMetadataRequest samlSPMetadataRequest = new SamlSpMetadataRequest(); + public void testValidateFailsWhenRealmEmpty() { + final SamlSpMetadataRequest samlSPMetadataRequest = new SamlSpMetadataRequest(""); final ActionRequestValidationException validationException = samlSPMetadataRequest.validate(); assertThat(validationException.getMessage(), containsString("Realm name may not be empty")); } public void testValidateSerialization() throws IOException { - final SamlSpMetadataRequest samlSPMetadataRequest = new SamlSpMetadataRequest(); - samlSPMetadataRequest.setRealmName("saml1"); + final SamlSpMetadataRequest samlSPMetadataRequest = new SamlSpMetadataRequest("saml1"); try (BytesStreamOutput out = new BytesStreamOutput()) { samlSPMetadataRequest.writeTo(out); try (StreamInput in = out.bytes().streamInput()) { @@ -36,8 +35,7 @@ public void testValidateSerialization() throws IOException { } public void testValidateToString() { - final SamlSpMetadataRequest samlSPMetadataRequest = new SamlSpMetadataRequest(); - samlSPMetadataRequest.setRealmName("saml1"); + final SamlSpMetadataRequest samlSPMetadataRequest = new SamlSpMetadataRequest("saml1"); assertThat(samlSPMetadataRequest.toString(), containsString("{realmName=saml1}")); } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlSpMetadataAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlSpMetadataAction.java index 00c1d073c8229..e83e594b02ae5 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlSpMetadataAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlSpMetadataAction.java @@ -17,8 +17,8 @@ import org.elasticsearch.xpack.core.security.action.saml.SamlSpMetadataRequest; import org.elasticsearch.xpack.core.security.action.saml.SamlSpMetadataResponse; import org.elasticsearch.xpack.security.authc.Realms; -import org.elasticsearch.xpack.security.authc.saml.SamlEntityDescriptorBuilder; import org.elasticsearch.xpack.security.authc.saml.SamlRealm; +import org.elasticsearch.xpack.security.authc.saml.SamlSpMetadataBuilder; import org.elasticsearch.xpack.security.authc.saml.SamlUtils; import org.opensaml.saml.saml2.metadata.EntityDescriptor; import org.opensaml.saml.saml2.metadata.impl.EntityDescriptorMarshaller; @@ -63,8 +63,8 @@ protected void doExecute(Task task, SamlSpMetadataRequest request, private void prepareMetadata(SamlRealm realm, ActionListener listener) { try { final EntityDescriptorMarshaller marshaller = new EntityDescriptorMarshaller(); - final SamlEntityDescriptorBuilder samlEntityDescriptorBuilder = new SamlEntityDescriptorBuilder(realm); - final EntityDescriptor descriptor = samlEntityDescriptorBuilder.getEntityDescriptor(); + final SamlSpMetadataBuilder builder = new SamlSpMetadataBuilder(realm); + final EntityDescriptor descriptor = builder.build(); final Element element = marshaller.marshall(descriptor); final StringWriter writer = new StringWriter(); final Transformer serializer = SamlUtils.getHardenedXMLTransformer(); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlEntityDescriptorBuilder.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlEntityDescriptorBuilder.java deleted file mode 100644 index 59d3aae1bddb7..0000000000000 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlEntityDescriptorBuilder.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * 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.security.authc.saml; - -import org.elasticsearch.cli.ExitCodes; -import org.elasticsearch.cli.Terminal; -import org.elasticsearch.cli.UserException; -import org.elasticsearch.common.Strings; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.xpack.core.security.authc.RealmConfig; -import org.elasticsearch.xpack.core.security.authc.RealmSettings; -import org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings; -import org.opensaml.saml.saml2.core.AuthnRequest; -import org.opensaml.saml.saml2.metadata.EntityDescriptor; - -import java.util.Locale; -import java.util.Map; - -public class SamlEntityDescriptorBuilder { - private final EntityDescriptor entityDescriptor; - - public EntityDescriptor getEntityDescriptor() { - return entityDescriptor; - } - - public SamlEntityDescriptorBuilder(SamlRealm samlRealm) throws Exception{ - final SpConfiguration spConfig = samlRealm.getLogoutHandler().getSpConfiguration(); - final SamlSpMetadataBuilder builder = new SamlSpMetadataBuilder(samlRealm) - .encryptionCredentials(spConfig.getEncryptionCredentials()) - .signingCredential(spConfig.getSigningConfiguration().getCredential()) - .authnRequestsSigned(spConfig.getSigningConfiguration().shouldSign(AuthnRequest.DEFAULT_ELEMENT_LOCAL_NAME)); - entityDescriptor = builder.build(); - } - - public SamlEntityDescriptorBuilder(RealmConfig realm, boolean batch, String serviceName, String orgName, String orgUrl, - String orgDisplayName, boolean contacts, Locale locale, Map attributes, - Terminal terminal) throws Exception { - final Settings realmSettings = realm.settings().getByPrefix(RealmSettings.realmSettingPrefix(realm.identifier())); - terminal.println(Terminal.Verbosity.VERBOSE, - "Using realm configuration\n=====\n" + realmSettings.toDelimitedString('\n') + "====="); - terminal.println(Terminal.Verbosity.VERBOSE, "Using locale: " + locale.toLanguageTag()); - - final SpConfiguration spConfig = SamlRealm.getSpConfiguration(realm); - final SamlSpMetadataBuilder builder = new SamlSpMetadataBuilder(locale, spConfig.getEntityId()) - .assertionConsumerServiceUrl(spConfig.getAscUrl()) - .singleLogoutServiceUrl(spConfig.getLogoutUrl()) - .encryptionCredentials(spConfig.getEncryptionCredentials()) - .signingCredential(spConfig.getSigningConfiguration().getCredential()) - .authnRequestsSigned(spConfig.getSigningConfiguration().shouldSign(AuthnRequest.DEFAULT_ELEMENT_LOCAL_NAME)) - .nameIdFormat(realm.getSetting(SamlRealmSettings.NAMEID_FORMAT)) - .serviceName(serviceName); - - for (String attr : attributes.keySet()) { - final String name; - String friendlyName; - final String settingName = attributes.get(attr); - final String attributeSource = settingName == null ? "command line" : '"' + settingName + '"'; - if (attr.contains(":")) { - name = attr; - if (batch) { - friendlyName = settingName; - } else { - friendlyName = terminal.readText("What is the friendly name for " + - attributeSource - + " attribute \"" + attr + "\" [default: " + - (settingName == null ? "none" : settingName) + - "] "); - if (Strings.isNullOrEmpty(friendlyName)) { - friendlyName = settingName; - } - } - } else { - if (batch) { - throw new UserException(ExitCodes.CONFIG, "Option batch is specified, but attribute " - + attr + " appears to be a FriendlyName value"); - } - friendlyName = attr; - name = requireText(terminal, - "What is the standard (urn) name for " + attributeSource + " attribute \"" + attr + "\" (required): "); - } - terminal.println(Terminal.Verbosity.VERBOSE, "Requesting attribute '" + name + "' (FriendlyName: '" + friendlyName + "')"); - builder.withAttribute(friendlyName, name); - } - - if (orgName != null && orgUrl != null) { - builder.organization(orgName, orgDisplayName, orgUrl); - } - - if (contacts) { - terminal.println("\nPlease enter the personal details for each contact to be included in the metadata"); - do { - final String givenName = requireText(terminal, "What is the given name for the contact: "); - final String surName = requireText(terminal, "What is the surname for the contact: "); - final String displayName = givenName + ' ' + surName; - final String email = requireText(terminal, "What is the email address for " + displayName + ": "); - String type; - while (true) { - type = requireText(terminal, "What is the contact type for " + displayName + ": "); - if (SamlSpMetadataBuilder.ContactInfo.TYPES.containsKey(type)) { - break; - } else { - terminal.errorPrintln("Type '" + type + "' is not valid. Valid values are " - + Strings.collectionToCommaDelimitedString(SamlSpMetadataBuilder.ContactInfo.TYPES.keySet())); - } - } - builder.withContact(type, givenName, surName, email); - } while (terminal.promptYesNo("Enter details for another contact", true)); - } - entityDescriptor = builder.build(); - } - - private String requireText(Terminal terminal, String prompt) { - String value = null; - while (Strings.isNullOrEmpty(value)) { - value = terminal.readText(prompt); - } - return value; - } -} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlMetadataCommand.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlMetadataCommand.java index 3aef30a67cac9..b4110424672c8 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlMetadataCommand.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlMetadataCommand.java @@ -38,6 +38,7 @@ import org.elasticsearch.cli.Terminal; import org.elasticsearch.cli.UserException; import org.elasticsearch.common.CheckedFunction; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.PathUtils; import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.common.settings.KeyStoreWrapper; @@ -50,8 +51,10 @@ import org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings; import org.elasticsearch.xpack.core.ssl.CertParsingUtils; import org.elasticsearch.xpack.core.ssl.PemUtils; +import org.elasticsearch.xpack.security.authc.saml.SamlSpMetadataBuilder.ContactInfo; import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; import org.opensaml.core.xml.io.MarshallingException; +import org.opensaml.saml.saml2.core.AuthnRequest; import org.opensaml.saml.saml2.metadata.EntityDescriptor; import org.opensaml.saml.saml2.metadata.impl.EntityDescriptorMarshaller; import org.opensaml.security.credential.Credential; @@ -107,22 +110,22 @@ public SamlMetadataCommand(CheckedFunction attributes = getAttributeNames(options, realm); + for (String attr : attributes.keySet()) { + final String name; + String friendlyName; + final String settingName = attributes.get(attr); + final String attributeSource = settingName == null ? "command line" : '"' + settingName + '"'; + if (attr.contains(":")) { + name = attr; + if (batch) { + friendlyName = settingName; + } else { + friendlyName = terminal.readText("What is the friendly name for " + + attributeSource + + " attribute \"" + attr + "\" [default: " + + (settingName == null ? "none" : settingName) + + "] "); + if (Strings.isNullOrEmpty(friendlyName)) { + friendlyName = settingName; + } + } + } else { + if (batch) { + throw new UserException(ExitCodes.CONFIG, "Option " + batchSpec.toString() + " is specified, but attribute " + + attr + " appears to be a FriendlyName value"); + } + friendlyName = attr; + name = requireText(terminal, + "What is the standard (urn) name for " + attributeSource + " attribute \"" + attr + "\" (required): "); + } + terminal.println(Terminal.Verbosity.VERBOSE, "Requesting attribute '" + name + "' (FriendlyName: '" + friendlyName + "')"); + builder.withAttribute(friendlyName, name); + } + + if (options.has(orgNameSpec) && options.has(orgUrlSpec)) { + String name = orgNameSpec.value(options); + builder.organization(name, option(orgDisplayNameSpec, options, name), orgUrlSpec.value(options)); + } - return samlEntityDescriptorBuilder.getEntityDescriptor(); + if (options.has(contactsSpec)) { + terminal.println("\nPlease enter the personal details for each contact to be included in the metadata"); + do { + final String givenName = requireText(terminal, "What is the given name for the contact: "); + final String surName = requireText(terminal, "What is the surname for the contact: "); + final String displayName = givenName + ' ' + surName; + final String email = requireText(terminal, "What is the email address for " + displayName + ": "); + String type; + while (true) { + type = requireText(terminal, "What is the contact type for " + displayName + ": "); + if (ContactInfo.TYPES.containsKey(type)) { + break; + } else { + terminal.errorPrintln("Type '" + type + "' is not valid. Valid values are " + + Strings.collectionToCommaDelimitedString(ContactInfo.TYPES.keySet())); + } + } + builder.withContact(type, givenName, surName, email); + } while (terminal.promptYesNo("Enter details for another contact", true)); + } + + return builder.build(); } // package-protected for testing Element possiblySignDescriptor(Terminal terminal, OptionSet options, EntityDescriptor descriptor, Environment env) - throws UserException { + throws UserException { try { final EntityDescriptorMarshaller marshaller = new EntityDescriptorMarshaller(); if (options.has(signingPkcs12PathSpec) || (options.has(signingCertPathSpec) && options.has(signingKeyPathSpec))) { Signature signature = (Signature) XMLObjectProviderRegistrySupport.getBuilderFactory() - .getBuilder(Signature.DEFAULT_ELEMENT_NAME) - .buildObject(Signature.DEFAULT_ELEMENT_NAME); + .getBuilder(Signature.DEFAULT_ELEMENT_NAME) + .buildObject(Signature.DEFAULT_ELEMENT_NAME); signature.setSigningCredential(buildSigningCredential(terminal, options, env)); signature.setSignatureAlgorithm(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256); signature.setCanonicalizationAlgorithm(SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS); @@ -214,18 +279,18 @@ private Path writeOutput(Terminal terminal, OptionSet options, Element element) } private Credential buildSigningCredential(Terminal terminal, OptionSet options, Environment env) throws - Exception { + Exception { X509Certificate signingCertificate; PrivateKey signingKey; char[] password = getChars(keyPasswordSpec.value(options)); if (options.has(signingPkcs12PathSpec)) { Path p12Path = resolvePath(signingPkcs12PathSpec.value(options)); Map keys = withPassword("certificate bundle (" + p12Path + ")", password, - terminal, keyPassword -> CertParsingUtils.readPkcs12KeyPairs(p12Path, keyPassword, a -> keyPassword)); + terminal, keyPassword -> CertParsingUtils.readPkcs12KeyPairs(p12Path, keyPassword, a -> keyPassword)); if (keys.size() != 1) { throw new IllegalArgumentException("expected a single key in file [" + p12Path.toAbsolutePath() + "] but found [" + - keys.size() + "]"); + keys.size() + "]"); } final Map.Entry pair = keys.entrySet().iterator().next(); signingCertificate = (X509Certificate) pair.getKey(); @@ -237,7 +302,7 @@ private Credential buildSigningCredential(Terminal terminal, OptionSet options, Certificate[] certificates = CertParsingUtils.readCertificates(Collections.singletonList(resolvedSigningCertPath), env); if (certificates.length != 1) { throw new IllegalArgumentException("expected a single certificate in file [" + resolvedSigningCertPath + "] but found [" + - certificates.length + "]"); + certificates.length + "]"); } signingCertificate = (X509Certificate) certificates[0]; signingKey = readSigningKey(key, password, terminal); @@ -264,7 +329,7 @@ private static char[] getChars(String password) { } private static PrivateKey readSigningKey(Path path, char[] password, Terminal terminal) - throws Exception { + throws Exception { AtomicReference passwordReference = new AtomicReference<>(password); try { return PemUtils.readPrivateKey(path, () -> { @@ -309,6 +374,14 @@ private Path resolvePath(String name) { return PathUtils.get(name).normalize(); } + private String requireText(Terminal terminal, String prompt) { + String value = null; + while (Strings.isNullOrEmpty(value)) { + value = terminal.readText(prompt); + } + return value; + } + private T option(OptionSpec spec, OptionSet options, T defaultValue) { if (options.has(spec)) { return spec.value(options); @@ -374,18 +447,18 @@ private RealmConfig findRealm(Terminal terminal, OptionSet options, Environment } } else { final List> saml = realms.entrySet().stream() - .filter(entry -> isSamlRealm(entry.getKey())) - .collect(Collectors.toList()); + .filter(entry -> isSamlRealm(entry.getKey())) + .collect(Collectors.toList()); if (saml.isEmpty()) { throw new UserException(ExitCodes.CONFIG, "There is no SAML realm configured in " + env.configFile()); } if (saml.size() > 1) { terminal.errorPrintln("Using configuration in " + env.configFile()); terminal.errorPrintln("Found multiple SAML realms: " - + saml.stream().map(Map.Entry::getKey).map(Object::toString).collect(Collectors.joining(", "))); + + saml.stream().map(Map.Entry::getKey).map(Object::toString).collect(Collectors.joining(", "))); terminal.errorPrintln("Use the -" + optionName(realmSpec) + " option to specify an explicit realm"); throw new UserException(ExitCodes.CONFIG, - "Found multiple SAML realms, please specify one with '-" + optionName(realmSpec) + "'"); + "Found multiple SAML realms, please specify one with '-" + optionName(realmSpec) + "'"); } final Map.Entry entry = saml.get(0); terminal.println("Building metadata for SAML realm " + entry.getKey()); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlSpMetadataBuilder.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlSpMetadataBuilder.java index 9e83ea1807899..0c01c4d39160d 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlSpMetadataBuilder.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlSpMetadataBuilder.java @@ -8,6 +8,7 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.collect.MapBuilder; import org.opensaml.saml.common.xml.SAMLConstants; +import org.opensaml.saml.saml2.core.AuthnRequest; import org.opensaml.saml.saml2.metadata.AssertionConsumerService; import org.opensaml.saml.saml2.metadata.AttributeConsumingService; import org.opensaml.saml.saml2.metadata.ContactPerson; @@ -109,6 +110,14 @@ public SamlSpMetadataBuilder(SamlRealm samlRealm) { this.authnRequestsSigned = Boolean.FALSE; this.assertionConsumerServiceUrl = spConfig.getAscUrl(); this.singleLogoutServiceUrl = spConfig.getLogoutUrl(); + if (spConfig.getEncryptionCredentials() != null) { + this.encryptionCertificates.addAll(spConfig.getEncryptionCredentials() + .stream().map(credential -> credential.getEntityCertificate()).collect(Collectors.toList())); + } + if(spConfig.getSigningConfiguration() != null && spConfig.getSigningConfiguration().getCredential() != null) { + this.signingCertificate = spConfig.getSigningConfiguration().getCredential().getEntityCertificate(); + } + this.authnRequestsSigned = spConfig.getSigningConfiguration().shouldSign(AuthnRequest.DEFAULT_ELEMENT_LOCAL_NAME); } /** @@ -241,7 +250,7 @@ public EntityDescriptor build() throws Exception { spRoleDescriptor.getNameIDFormats().add(buildNameIdFormat()); } spRoleDescriptor.getAssertionConsumerServices().add(buildAssertionConsumerService()); - if (attributeNames.size() > 0) { + if (attributeNames != null && attributeNames.size() > 0) { spRoleDescriptor.getAttributeConsumingServices().add(buildAttributeConsumerService()); } if (Strings.hasText(singleLogoutServiceUrl)) { @@ -256,7 +265,9 @@ public EntityDescriptor build() throws Exception { if (organization != null) { descriptor.setOrganization(buildOrganization()); } - contacts.forEach(c -> descriptor.getContactPersons().add(buildContact(c))); + if(contacts != null && contacts.size() > 0) { + contacts.forEach(c -> descriptor.getContactPersons().add(buildContact(c))); + } return descriptor; } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/saml/RestSamlSpMetadataAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/saml/RestSamlSpMetadataAction.java index 2fe8227e8ce8d..2102e0388ce22 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/saml/RestSamlSpMetadataAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/saml/RestSamlSpMetadataAction.java @@ -8,9 +8,7 @@ import org.elasticsearch.client.node.NodeClient; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.xcontent.ObjectParser; import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.rest.BytesRestResponse; import org.elasticsearch.rest.RestRequest; @@ -29,9 +27,6 @@ public class RestSamlSpMetadataAction extends SamlBaseRestHandler { - static final ObjectParser PARSER = new ObjectParser<>("security_saml_metadata", - SamlSpMetadataRequest::new); - public RestSamlSpMetadataAction(Settings settings, XPackLicenseState licenseState) { super(settings, licenseState); } @@ -49,19 +44,16 @@ public String getName() { @Override public RestChannelConsumer innerPrepareRequest(RestRequest request, NodeClient client) throws IOException { - try (XContentParser parser = request.contentParser()) { - final SamlSpMetadataRequest SamlSpRequest = PARSER.parse(parser, null); - SamlSpRequest.setRealmName(request.param("realm")); - return channel -> client.execute(SamlSpMetadataAction.INSTANCE, SamlSpRequest, - new RestBuilderListener(channel) { - @Override - public RestResponse buildResponse(SamlSpMetadataResponse response, XContentBuilder builder) throws Exception { - builder.startObject(); - builder.field("metadata", response.getXMLString()); - builder.endObject(); - return new BytesRestResponse(RestStatus.OK, builder); - } - }); - } + final SamlSpMetadataRequest SamlSpRequest = new SamlSpMetadataRequest(request.param("realm")); + return channel -> client.execute(SamlSpMetadataAction.INSTANCE, SamlSpRequest, + new RestBuilderListener(channel) { + @Override + public RestResponse buildResponse(SamlSpMetadataResponse response, XContentBuilder builder) throws Exception { + builder.startObject(); + builder.field("metadata", response.getXMLString()); + builder.endObject(); + return new BytesRestResponse(RestStatus.OK, builder); + } + }); } } From b076e6c7ee10e94232c66429f007728e580fbd14 Mon Sep 17 00:00:00 2001 From: BigPandaToo Date: Wed, 4 Nov 2020 19:12:37 +0100 Subject: [PATCH 4/7] Adding API for generating SAML SP metadata Resolves #49018 --- .../privilege/ClusterPrivilegeResolver.java | 3 +- .../authc/saml/SamlMetadataCommand.java | 74 +++++++++---------- .../xpack/security/authc/saml/SamlRealm.java | 4 + .../authc/saml/SamlSpMetadataBuilder.java | 2 +- .../action/saml/RestSamlSpMetadataAction.java | 4 +- 5 files changed, 46 insertions(+), 41 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilegeResolver.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilegeResolver.java index b4515e0511f39..4d2b254f3d90d 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilegeResolver.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilegeResolver.java @@ -22,6 +22,7 @@ import org.elasticsearch.xpack.core.ilm.action.StopILMAction; import org.elasticsearch.xpack.core.security.action.DelegatePkiAuthenticationAction; import org.elasticsearch.xpack.core.security.action.GrantApiKeyAction; +import org.elasticsearch.xpack.core.security.action.saml.SamlSpMetadataAction; import org.elasticsearch.xpack.core.security.action.token.InvalidateTokenAction; import org.elasticsearch.xpack.core.security.action.token.RefreshTokenAction; import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesAction; @@ -45,7 +46,7 @@ public class ClusterPrivilegeResolver { // shared automatons private static final Set ALL_SECURITY_PATTERN = Set.of("cluster:admin/xpack/security/*"); private static final Set MANAGE_SAML_PATTERN = Set.of("cluster:admin/xpack/security/saml/*", - InvalidateTokenAction.NAME, RefreshTokenAction.NAME); + InvalidateTokenAction.NAME, RefreshTokenAction.NAME, SamlSpMetadataAction.NAME); private static final Set MANAGE_OIDC_PATTERN = Set.of("cluster:admin/xpack/security/oidc/*"); private static final Set MANAGE_TOKEN_PATTERN = Set.of("cluster:admin/xpack/security/token/*"); private static final Set MANAGE_API_KEY_PATTERN = Set.of("cluster:admin/xpack/security/api_key/*"); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlMetadataCommand.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlMetadataCommand.java index b4110424672c8..3a2b87afe1fa6 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlMetadataCommand.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlMetadataCommand.java @@ -110,22 +110,22 @@ public SamlMetadataCommand(CheckedFunction attributes = getAttributeNames(options, realm); for (String attr : attributes.keySet()) { @@ -185,10 +185,10 @@ EntityDescriptor buildEntityDescriptor(Terminal terminal, OptionSet options, Env friendlyName = settingName; } else { friendlyName = terminal.readText("What is the friendly name for " + - attributeSource - + " attribute \"" + attr + "\" [default: " + - (settingName == null ? "none" : settingName) + - "] "); + attributeSource + + " attribute \"" + attr + "\" [default: " + + (settingName == null ? "none" : settingName) + + "] "); if (Strings.isNullOrEmpty(friendlyName)) { friendlyName = settingName; } @@ -196,11 +196,11 @@ EntityDescriptor buildEntityDescriptor(Terminal terminal, OptionSet options, Env } else { if (batch) { throw new UserException(ExitCodes.CONFIG, "Option " + batchSpec.toString() + " is specified, but attribute " - + attr + " appears to be a FriendlyName value"); + + attr + " appears to be a FriendlyName value"); } friendlyName = attr; name = requireText(terminal, - "What is the standard (urn) name for " + attributeSource + " attribute \"" + attr + "\" (required): "); + "What is the standard (urn) name for " + attributeSource + " attribute \"" + attr + "\" (required): "); } terminal.println(Terminal.Verbosity.VERBOSE, "Requesting attribute '" + name + "' (FriendlyName: '" + friendlyName + "')"); builder.withAttribute(friendlyName, name); @@ -225,7 +225,7 @@ EntityDescriptor buildEntityDescriptor(Terminal terminal, OptionSet options, Env break; } else { terminal.errorPrintln("Type '" + type + "' is not valid. Valid values are " - + Strings.collectionToCommaDelimitedString(ContactInfo.TYPES.keySet())); + + Strings.collectionToCommaDelimitedString(ContactInfo.TYPES.keySet())); } } builder.withContact(type, givenName, surName, email); @@ -237,13 +237,13 @@ EntityDescriptor buildEntityDescriptor(Terminal terminal, OptionSet options, Env // package-protected for testing Element possiblySignDescriptor(Terminal terminal, OptionSet options, EntityDescriptor descriptor, Environment env) - throws UserException { + throws UserException { try { final EntityDescriptorMarshaller marshaller = new EntityDescriptorMarshaller(); if (options.has(signingPkcs12PathSpec) || (options.has(signingCertPathSpec) && options.has(signingKeyPathSpec))) { Signature signature = (Signature) XMLObjectProviderRegistrySupport.getBuilderFactory() - .getBuilder(Signature.DEFAULT_ELEMENT_NAME) - .buildObject(Signature.DEFAULT_ELEMENT_NAME); + .getBuilder(Signature.DEFAULT_ELEMENT_NAME) + .buildObject(Signature.DEFAULT_ELEMENT_NAME); signature.setSigningCredential(buildSigningCredential(terminal, options, env)); signature.setSignatureAlgorithm(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256); signature.setCanonicalizationAlgorithm(SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS); @@ -279,18 +279,18 @@ private Path writeOutput(Terminal terminal, OptionSet options, Element element) } private Credential buildSigningCredential(Terminal terminal, OptionSet options, Environment env) throws - Exception { + Exception { X509Certificate signingCertificate; PrivateKey signingKey; char[] password = getChars(keyPasswordSpec.value(options)); if (options.has(signingPkcs12PathSpec)) { Path p12Path = resolvePath(signingPkcs12PathSpec.value(options)); Map keys = withPassword("certificate bundle (" + p12Path + ")", password, - terminal, keyPassword -> CertParsingUtils.readPkcs12KeyPairs(p12Path, keyPassword, a -> keyPassword)); + terminal, keyPassword -> CertParsingUtils.readPkcs12KeyPairs(p12Path, keyPassword, a -> keyPassword)); if (keys.size() != 1) { throw new IllegalArgumentException("expected a single key in file [" + p12Path.toAbsolutePath() + "] but found [" + - keys.size() + "]"); + keys.size() + "]"); } final Map.Entry pair = keys.entrySet().iterator().next(); signingCertificate = (X509Certificate) pair.getKey(); @@ -302,7 +302,7 @@ private Credential buildSigningCredential(Terminal terminal, OptionSet options, Certificate[] certificates = CertParsingUtils.readCertificates(Collections.singletonList(resolvedSigningCertPath), env); if (certificates.length != 1) { throw new IllegalArgumentException("expected a single certificate in file [" + resolvedSigningCertPath + "] but found [" + - certificates.length + "]"); + certificates.length + "]"); } signingCertificate = (X509Certificate) certificates[0]; signingKey = readSigningKey(key, password, terminal); @@ -329,7 +329,7 @@ private static char[] getChars(String password) { } private static PrivateKey readSigningKey(Path path, char[] password, Terminal terminal) - throws Exception { + throws Exception { AtomicReference passwordReference = new AtomicReference<>(password); try { return PemUtils.readPrivateKey(path, () -> { @@ -447,18 +447,18 @@ private RealmConfig findRealm(Terminal terminal, OptionSet options, Environment } } else { final List> saml = realms.entrySet().stream() - .filter(entry -> isSamlRealm(entry.getKey())) - .collect(Collectors.toList()); + .filter(entry -> isSamlRealm(entry.getKey())) + .collect(Collectors.toList()); if (saml.isEmpty()) { throw new UserException(ExitCodes.CONFIG, "There is no SAML realm configured in " + env.configFile()); } if (saml.size() > 1) { terminal.errorPrintln("Using configuration in " + env.configFile()); terminal.errorPrintln("Found multiple SAML realms: " - + saml.stream().map(Map.Entry::getKey).map(Object::toString).collect(Collectors.joining(", "))); + + saml.stream().map(Map.Entry::getKey).map(Object::toString).collect(Collectors.joining(", "))); terminal.errorPrintln("Use the -" + optionName(realmSpec) + " option to specify an explicit realm"); throw new UserException(ExitCodes.CONFIG, - "Found multiple SAML realms, please specify one with '-" + optionName(realmSpec) + "'"); + "Found multiple SAML realms, please specify one with '-" + optionName(realmSpec) + "'"); } final Map.Entry entry = saml.get(0); terminal.println("Building metadata for SAML realm " + entry.getKey()); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlRealm.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlRealm.java index e5e5e9edde663..2390d820d5fce 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlRealm.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlRealm.java @@ -208,6 +208,10 @@ public static SamlRealm create(RealmConfig config, SSLService sslService, Resour return realm; } + public SpConfiguration getServiceProvider() { + return serviceProvider; + } + // For testing SamlRealm( RealmConfig config, diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlSpMetadataBuilder.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlSpMetadataBuilder.java index 0c01c4d39160d..95f47c0cc09fc 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlSpMetadataBuilder.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlSpMetadataBuilder.java @@ -100,7 +100,7 @@ public SamlSpMetadataBuilder(Locale locale, String entityId) { * @param samlRealm SamlRealm for which SP Metadata is built */ public SamlSpMetadataBuilder(SamlRealm samlRealm) { - final SpConfiguration spConfig = samlRealm.getLogoutHandler().getSpConfiguration(); + final SpConfiguration spConfig = samlRealm.getServiceProvider(); this.locale = Locale.getDefault(); this.entityId = spConfig.getEntityId(); this.attributeNames = null; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/saml/RestSamlSpMetadataAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/saml/RestSamlSpMetadataAction.java index 2102e0388ce22..2f6d3357740c8 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/saml/RestSamlSpMetadataAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/saml/RestSamlSpMetadataAction.java @@ -44,8 +44,8 @@ public String getName() { @Override public RestChannelConsumer innerPrepareRequest(RestRequest request, NodeClient client) throws IOException { - final SamlSpMetadataRequest SamlSpRequest = new SamlSpMetadataRequest(request.param("realm")); - return channel -> client.execute(SamlSpMetadataAction.INSTANCE, SamlSpRequest, + final SamlSpMetadataRequest SamlSpMetadataRequest = new SamlSpMetadataRequest(request.param("realm")); + return channel -> client.execute(SamlSpMetadataAction.INSTANCE, SamlSpMetadataRequest, new RestBuilderListener(channel) { @Override public RestResponse buildResponse(SamlSpMetadataResponse response, XContentBuilder builder) throws Exception { From 9d1a098e865e36bbfaed54a2f6491885f314c49d Mon Sep 17 00:00:00 2001 From: BigPandaToo Date: Thu, 5 Nov 2020 12:50:48 +0100 Subject: [PATCH 5/7] Adding API for generating SAML SP metadata Resolves #49018 --- .../saml/TransportSamlSpMetadataAction.java | 11 ++++++++- .../authc/saml/SamlSpMetadataBuilder.java | 24 ------------------- .../authc/saml/SigningConfiguration.java | 6 ++--- .../security/authc/saml/SpConfiguration.java | 10 ++++---- 4 files changed, 18 insertions(+), 33 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlSpMetadataAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlSpMetadataAction.java index e83e594b02ae5..04af44f1aade8 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlSpMetadataAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlSpMetadataAction.java @@ -20,6 +20,8 @@ import org.elasticsearch.xpack.security.authc.saml.SamlRealm; import org.elasticsearch.xpack.security.authc.saml.SamlSpMetadataBuilder; import org.elasticsearch.xpack.security.authc.saml.SamlUtils; +import org.elasticsearch.xpack.security.authc.saml.SpConfiguration; +import org.opensaml.saml.saml2.core.AuthnRequest; import org.opensaml.saml.saml2.metadata.EntityDescriptor; import org.opensaml.saml.saml2.metadata.impl.EntityDescriptorMarshaller; import org.w3c.dom.Element; @@ -29,6 +31,7 @@ import javax.xml.transform.stream.StreamResult; import java.io.StringWriter; import java.util.List; +import java.util.Locale; import static org.elasticsearch.xpack.security.authc.saml.SamlRealm.findSamlRealms; @@ -63,7 +66,13 @@ protected void doExecute(Task task, SamlSpMetadataRequest request, private void prepareMetadata(SamlRealm realm, ActionListener listener) { try { final EntityDescriptorMarshaller marshaller = new EntityDescriptorMarshaller(); - final SamlSpMetadataBuilder builder = new SamlSpMetadataBuilder(realm); + final SpConfiguration spConfig = realm.getServiceProvider(); + final SamlSpMetadataBuilder builder = new SamlSpMetadataBuilder(Locale.getDefault(), spConfig.getEntityId()) + .assertionConsumerServiceUrl(spConfig.getAscUrl()) + .singleLogoutServiceUrl(spConfig.getLogoutUrl()) + .encryptionCredentials(spConfig.getEncryptionCredentials()) + .signingCredential(spConfig.getSigningConfiguration().getCredential()) + .authnRequestsSigned(spConfig.getSigningConfiguration().shouldSign(AuthnRequest.DEFAULT_ELEMENT_LOCAL_NAME)); final EntityDescriptor descriptor = builder.build(); final Element element = marshaller.marshall(descriptor); final StringWriter writer = new StringWriter(); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlSpMetadataBuilder.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlSpMetadataBuilder.java index 95f47c0cc09fc..292a64c797a98 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlSpMetadataBuilder.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlSpMetadataBuilder.java @@ -96,30 +96,6 @@ public SamlSpMetadataBuilder(Locale locale, String entityId) { this.authnRequestsSigned = Boolean.FALSE; } - /** - * @param samlRealm SamlRealm for which SP Metadata is built - */ - public SamlSpMetadataBuilder(SamlRealm samlRealm) { - final SpConfiguration spConfig = samlRealm.getServiceProvider(); - this.locale = Locale.getDefault(); - this.entityId = spConfig.getEntityId(); - this.attributeNames = null; - this.contacts = null; - this.serviceName = "Elasticsearch"; - this.nameIdFormat = null; - this.authnRequestsSigned = Boolean.FALSE; - this.assertionConsumerServiceUrl = spConfig.getAscUrl(); - this.singleLogoutServiceUrl = spConfig.getLogoutUrl(); - if (spConfig.getEncryptionCredentials() != null) { - this.encryptionCertificates.addAll(spConfig.getEncryptionCredentials() - .stream().map(credential -> credential.getEntityCertificate()).collect(Collectors.toList())); - } - if(spConfig.getSigningConfiguration() != null && spConfig.getSigningConfiguration().getCredential() != null) { - this.signingCertificate = spConfig.getSigningConfiguration().getCredential().getEntityCertificate(); - } - this.authnRequestsSigned = spConfig.getSigningConfiguration().shouldSign(AuthnRequest.DEFAULT_ELEMENT_LOCAL_NAME); - } - /** * The format that the service provider expects for incoming NameID element. */ diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SigningConfiguration.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SigningConfiguration.java index 349d72d5369e8..dd4fa5a06a865 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SigningConfiguration.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SigningConfiguration.java @@ -16,7 +16,7 @@ /** * Encapsulates the rules and credentials for how and when Elasticsearch should sign outgoing SAML messages. */ -class SigningConfiguration { +public class SigningConfiguration { private final Set messageTypes; private final X509Credential credential; @@ -30,7 +30,7 @@ boolean shouldSign(SAMLObject object) { return shouldSign(object.getElementQName().getLocalPart()); } - boolean shouldSign(String elementName) { + public boolean shouldSign(String elementName) { if (credential == null) { return false; } @@ -45,7 +45,7 @@ byte[] sign(byte[] content, String algo) throws SecurityException { return XMLSigningUtil.signWithURI(this.credential, algo, content); } - X509Credential getCredential() { + public X509Credential getCredential() { return credential; } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SpConfiguration.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SpConfiguration.java index bc1ac3999211b..95d819dac7fe7 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SpConfiguration.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SpConfiguration.java @@ -41,23 +41,23 @@ public SpConfiguration(final String entityId, final String ascUrl, final String /** * The SAML identifier (as a URI) for the Sp */ - String getEntityId() { + public String getEntityId() { return entityId; } - String getAscUrl() { + public String getAscUrl() { return ascUrl; } - String getLogoutUrl() { + public String getLogoutUrl() { return logoutUrl; } - List getEncryptionCredentials() { + public List getEncryptionCredentials() { return encryptionCredentials; } - SigningConfiguration getSigningConfiguration() { + public SigningConfiguration getSigningConfiguration() { return signingConfiguration; } From 337f23c3fc2111992b206ea9218e8295db4afb44 Mon Sep 17 00:00:00 2001 From: BigPandaToo Date: Thu, 5 Nov 2020 14:21:16 +0100 Subject: [PATCH 6/7] Adding API for generating SAML SP metadata Resolves #49018 --- .../xpack/security/authc/saml/SamlSpMetadataBuilder.java | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlSpMetadataBuilder.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlSpMetadataBuilder.java index 292a64c797a98..042fd4e73c5f6 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlSpMetadataBuilder.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlSpMetadataBuilder.java @@ -8,7 +8,6 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.collect.MapBuilder; import org.opensaml.saml.common.xml.SAMLConstants; -import org.opensaml.saml.saml2.core.AuthnRequest; import org.opensaml.saml.saml2.metadata.AssertionConsumerService; import org.opensaml.saml.saml2.metadata.AttributeConsumingService; import org.opensaml.saml.saml2.metadata.ContactPerson; From 6567e0f25c293bf099f6fa8da3c32eb906914f34 Mon Sep 17 00:00:00 2001 From: BigPandaToo Date: Thu, 5 Nov 2020 16:00:24 +0100 Subject: [PATCH 7/7] Adding API for generating SAML SP metadata Resolves #49018 --- .../security/action/saml/TransportSamlSpMetadataAction.java | 3 ++- .../xpack/security/authc/saml/SamlSpMetadataBuilder.java | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlSpMetadataAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlSpMetadataAction.java index 04af44f1aade8..ba12571d5215c 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlSpMetadataAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlSpMetadataAction.java @@ -80,7 +80,8 @@ private void prepareMetadata(SamlRealm realm, ActionListener 0) { + if (attributeNames.size() > 0) { spRoleDescriptor.getAttributeConsumingServices().add(buildAttributeConsumerService()); } if (Strings.hasText(singleLogoutServiceUrl)) { @@ -240,7 +240,7 @@ public EntityDescriptor build() throws Exception { if (organization != null) { descriptor.setOrganization(buildOrganization()); } - if(contacts != null && contacts.size() > 0) { + if(contacts.size() > 0) { contacts.forEach(c -> descriptor.getContactPersons().add(buildContact(c))); }