blob: 933ed086281b816e33298aac0db15df460c08949 [file] [log] [blame]
// Copyright 2009 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.enterprise.secmgr.saml;
import static com.google.enterprise.secmgr.common.XmlUtil.countChildElements;
import static com.google.enterprise.secmgr.common.XmlUtil.findChildComments;
import static com.google.enterprise.secmgr.common.XmlUtil.findChildElement;
import static com.google.enterprise.secmgr.common.XmlUtil.getElementsByQname;
import static com.google.enterprise.secmgr.common.XmlUtil.makeCommentChild;
import static com.google.enterprise.secmgr.common.XmlUtil.makeElement;
import static com.google.enterprise.secmgr.common.XmlUtil.makeElementChild;
import static com.google.enterprise.secmgr.common.XmlUtil.makeTextChild;
import static com.google.enterprise.secmgr.common.XmlUtil.makeTextElementChild;
import static com.google.enterprise.secmgr.common.XmlUtil.setConfigParams;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.enterprise.secmgr.common.XmlUtil;
import org.w3c.dom.Comment;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.io.Writer;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.concurrent.Immutable;
import javax.xml.crypto.dsig.XMLSignature;
import javax.xml.namespace.QName;
/**
* Utilities for editing SAML metadata files.
*/
@Immutable
public class MetadataEditor {
private static final Logger LOGGER = Logger.getLogger(MetadataEditor.class.getName());
// SAML metadata elements.
public static final String SAML_MD_NS = "urn:oasis:names:tc:SAML:2.0:metadata";
private static QName mdName(String localPart) {
return new QName(SAML_MD_NS, localPart);
}
public static final QName SAML_DESCRIPTOR_ENTITIES = mdName("EntitiesDescriptor");
public static final QName SAML_DESCRIPTOR_ENTITY = mdName("EntityDescriptor");
public static final QName SAML_DESCRIPTOR_IDP_SSO = mdName("IDPSSODescriptor");
public static final QName SAML_DESCRIPTOR_KEY = mdName("KeyDescriptor");
public static final QName SAML_DESCRIPTOR_PDP = mdName("PDPDescriptor");
public static final QName SAML_DESCRIPTOR_SP_SSO = mdName("SPSSODescriptor");
public static final QName SAML_ORGANIZATION = mdName("Organization");
public static final QName SAML_ORGANIZATION_DISPLAY_NAME = mdName("OrganizationDisplayName");
public static final QName SAML_ORGANIZATION_NAME = mdName("OrganizationName");
public static final QName SAML_ORGANIZATION_URL = mdName("OrganizationURL");
public static final QName SAML_SERVICE_ARTIFACT_RESOLUTION = mdName("ArtifactResolutionService");
public static final QName SAML_SERVICE_ASSERTION_CONSUMER = mdName("AssertionConsumerService");
public static final QName SAML_SERVICE_AUTHZ = mdName("AuthzService");
public static final QName SAML_SERVICE_SINGLE_SIGN_ON = mdName("SingleSignOnService");
// SAML metadata attributes.
public static final String SAML_ATTR_BINDING = "Binding";
public static final String SAML_ATTR_CACHE_DURATION = "cacheDuration";
public static final String SAML_ATTR_ENTITY_ID = "entityID";
public static final String SAML_ATTR_INDEX = "index";
public static final String SAML_ATTR_IS_DEFAULT = "isDefault";
public static final String SAML_ATTR_LOCATION = "Location";
public static final String SAML_ATTR_NAME = "Name";
public static final String SAML_ATTR_PROTOCOL_SUPPORT_ENUMERATION = "protocolSupportEnumeration";
public static final String SAML_ATTR_USE = "use";
public static final String XML_ATTR_ID = "ID";
// SAML metadata attribute values.
public static final String SAML_BINDING_HTTP_ARTIFACT =
"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact";
public static final String SAML_BINDING_HTTP_POST =
"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST";
public static final String SAML_BINDING_HTTP_REDIRECT =
"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect";
public static final String SAML_BINDING_SOAP = "urn:oasis:names:tc:SAML:2.0:bindings:SOAP";
public static final String SAML_PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol";
public static final String SAML_USAGE_SIGNING = "signing";
private static final String SAML_CACHE_DURATION = "PT1H"; // one hour
// Entity group for SAML clients.
public static final String SECMGR_CLIENTS_ENTITIES_NAME = "security-manager-clients";
public static final String SECMGR_CLIENTS_ENTITIES_COMMENT = "SAML client IdPs";
// XML Digital Signature elements.
private static QName dsName(String localPart) {
return new QName(XMLSignature.XMLNS, localPart, "ds");
}
public static final QName XMLDSIG_KEY_INFO = dsName("KeyInfo");
public static final QName XMLDSIG_X509_DATA = dsName("X509Data");
public static final QName XMLDSIG_X509_CERTIFICATE = dsName("X509Certificate");
// Entity for GSA.
public static final String GSA_ENTITY_ID_ROOT = "http://google.com/enterprise/gsa/";
public static final String GSA_ENTITY_COMMENT = "Description of the GSA";
public static final String GSA_ENT_CONFIG_NAME_MARKER = "${ENT_CONFIG_NAME}";
// Entity for security manager.
public static final String SECMGR_ENTITY_ID_SUFFIX = "/security-manager";
public static final String SECMGR_ENTITY_COMMENT = "Description of the Security Manager";
static final String SECMGR_ID_FOR_ENTITY = "security-manager";
// Google organization element.
public static final String GOOGLE_ORGANIZATION_NAME = "google.com";
public static final String GOOGLE_ORGANIZATION_DISPLAY_NAME = "Google Inc.";
public static final String GOOGLE_ORGANIZATION_URL = "http://www.google.com/";
// Endpoint URLs.
private static final String HTTP_PROTOCOL = "http";
private static final String HTTPS_PROTOCOL = "https";
static final String GSA_HOST_MARKER = "$$GSA$$";
private static final String SECMGR_WEBAPP_PATH = "/security-manager";
private static final String LOCALHOST = "localhost";
public static final String GSA_ASSERTION_CONSUMER_PATH = "SamlArtifactConsumer";
public static final String SECMGR_SSO_PATH = "samlauthn";
public static final String SECMGR_ARTIFACT_RESOLVER_PATH = "samlartifact";
public static final String SECMGR_ASSERTION_CONSUMER_PATH = "samlassertionconsumer";
public static final String SECMGR_AUTHZ_PATH = "samlauthz";
public static final String X509_HEADER = "-----BEGIN CERTIFICATE-----";
public static final String X509_FOOTER = "-----END CERTIFICATE-----";
// Don't instantiate this class.
private MetadataEditor() {}
/**
* A datatype to hold the parameters of a SAML client IdP.
*/
@Immutable
public static class SamlClientIdp {
private final String id;
private final String loginUrl;
private final String artifactUrl;
private final String certificate;
private final String authzServiceUrl;
private SamlClientIdp(String id, String loginUrl, String artifactUrl, String certificate,
String authzServiceUrl) {
Preconditions.checkNotNull(id);
this.id = id;
this.loginUrl = loginUrl;
this.artifactUrl = artifactUrl;
this.certificate = normalizeCertificate(certificate);
this.authzServiceUrl = authzServiceUrl;
}
/**
* Make a client with only IDP SSO components.
*
* @param id The entity ID for the IdP.
* @param loginUrl The single sign-on service URL for the IdP.
* @param artifactUrl The artifact resolution service URL for the IdP, or null if none.
* @param certificate A PEM encoded X509 certificate, or null if none.
* @return A new IDP SSO client.
*/
public static SamlClientIdp makeSso(String id, String loginUrl, String artifactUrl,
String certificate) {
Preconditions.checkNotNull(loginUrl);
Preconditions.checkArgument(artifactUrl != null ^ certificate != null);
return new SamlClientIdp(id, loginUrl, artifactUrl, certificate, null);
}
/**
* Make a client with only PDP components.
*
* @param id The entity ID for the PDP.
* @param authzServiceUrl The URL for the PDP's AuthzService.
* @return A new PDP client.
*/
public static SamlClientIdp makePdp(String id, String authzServiceUrl) {
Preconditions.checkNotNull(authzServiceUrl);
return new SamlClientIdp(id, null, null, null, authzServiceUrl);
}
/**
* @return True if this client has IdP SSO components.
*/
public boolean hasSso() {
return loginUrl != null && (artifactUrl != null || certificate != null);
}
/**
* @return True if this client has PDP components.
*/
public boolean hasPdp() {
return authzServiceUrl != null;
}
/**
* @return True if this client has components other than IDP SSO.
*/
public boolean hasNonSso() {
return hasPdp();
}
/**
* @return True if this client has components other than PDP.
*/
public boolean hasNonPdp() {
return hasSso();
}
/**
* Replace the IDP SSO components of this client with those of another.
*
* @param client The client to get the IDP SSO components from.
* @return A copy of this client with its IDP SSO components replaced.
*/
public SamlClientIdp replaceSso(SamlClientIdp client) {
return new SamlClientIdp(id,
client.getUrl(),
client.getArtifactUrl(),
client.getCertificate(),
authzServiceUrl);
}
/**
* @return A copy of this client with its IDP SSO components removed.
*/
public SamlClientIdp removeSso() {
return new SamlClientIdp(id, null, null, null, authzServiceUrl);
}
/**
* Replace the PDP components of this client with those of another.
*
* @param client The client to get the PDP components from.
* @return A copy of this client with its PDP components replaced.
*/
public SamlClientIdp replacePdp(SamlClientIdp client) {
return new SamlClientIdp(id,
loginUrl,
artifactUrl,
certificate,
client.getAuthzServiceUrl());
}
/**
* @return A copy of this client with its PDP components removed.
*/
public SamlClientIdp removePdp() {
return new SamlClientIdp(id, loginUrl, artifactUrl, certificate, null);
}
/**
* @return The entity ID of the SAML client.
*/
public String getId() {
return id;
}
/**
* @return The single-sign-on entry URL for the SAML client.
*/
public String getUrl() {
return loginUrl;
}
/**
* @return The artifact-resolution entry URL for the SAML client, or null if none.
*/
public String getArtifactUrl() {
return artifactUrl;
}
/**
* @return The signing certificate for the SAML client, or null if none.
*/
public String getCertificate() {
return certificate;
}
/**
* @return The AuthzService URL for the SAML client.
*/
public String getAuthzServiceUrl() {
return authzServiceUrl;
}
@Override
public int hashCode() {
return Objects.hashCode(id, loginUrl, artifactUrl, certificate, authzServiceUrl);
}
@Override
public boolean equals(Object obj) {
if (obj == this) { return true; }
if (!(obj instanceof SamlClientIdp)) { return false; }
SamlClientIdp other = (SamlClientIdp) obj;
return Objects.equal(id, other.id)
&& Objects.equal(loginUrl, other.loginUrl)
&& Objects.equal(artifactUrl, other.artifactUrl)
&& Objects.equal(certificate, other.certificate)
&& Objects.equal(authzServiceUrl, other.authzServiceUrl);
}
}
/**
* Get the SAML client IdPs from metadata.
*
* @param document The metadata as a {@link Document}.
* @return A list of SAML client IdP descriptors.
* @throws IllegalArgumentException if metadata can't be parsed.
*/
public static List<SamlClientIdp> getSamlClientsInMetadata(Document document) {
List<SamlClientIdp> clients = Lists.newArrayList();
Set<String> idsSeen = Sets.newHashSet();
Element entities = getSamlClients(document);
if (entities != null) {
NodeList nodes = getElementsByQname(entities, SAML_DESCRIPTOR_ENTITY);
for (int index = 0; index < nodes.getLength(); index++) {
Element element = Element.class.cast(nodes.item(index));
SamlClientIdp client;
try {
client = parseClient(element, idsSeen);
} catch (IllegalArgumentException e) {
LOGGER.log(Level.WARNING, "Unable to parse SAML client spec: ", e);
client = null;
}
if (client != null) {
clients.add(client);
}
}
}
return clients;
}
private static SamlClientIdp parseClient(Element element, Set<String> idsSeen) {
String id = element.getAttribute(SAML_ATTR_ENTITY_ID);
Preconditions.checkArgument(!idsSeen.contains(id), "Duplicate entity ID in metadata: %s", id);
idsSeen.add(id);
SamlClientIdp sso = parseSso(id, findChildElement(element, SAML_DESCRIPTOR_IDP_SSO, false));
SamlClientIdp pdp = parsePdp(id, findChildElement(element, SAML_DESCRIPTOR_PDP, false));
return
(sso == null) ? pdp
: (pdp == null) ? sso
: sso.replacePdp(pdp);
}
private static SamlClientIdp parseSso(String id, Element ssoRole) {
if (ssoRole == null) {
return null;
}
Element sso = findChildElement(ssoRole, SAML_SERVICE_SINGLE_SIGN_ON, true);
Element ars = findChildElement(ssoRole, SAML_SERVICE_ARTIFACT_RESOLUTION, false);
Element kd = findChildElement(ssoRole, SAML_DESCRIPTOR_KEY, false);
return SamlClientIdp.makeSso(id,
sso.getAttribute(SAML_ATTR_LOCATION),
(ars == null) ? null : ars.getAttribute(SAML_ATTR_LOCATION),
(kd == null) ? null : keyDescriptorCertificate(kd));
}
private static SamlClientIdp parsePdp(String id, Element pdp) {
if (pdp == null) {
return null;
}
Element authz = findChildElement(pdp, SAML_SERVICE_AUTHZ, true);
return SamlClientIdp.makePdp(id,
(authz == null) ? null : authz.getAttribute(SAML_ATTR_LOCATION));
}
/**
* Set the SAML client IdPs in given metadata to the given set. Removes any
* clients already in the metadata, then inserts the given clients.
*
* @param document The metadata as a {@link Document}. This document is
* modified to contain the new clients.
* @param clients A set of SAML client IdP descriptors; null means delete all.
* @throws IOException if metadata can't be parsed or serialized.
*/
public static void setSamlClientsInMetadata(Document document, Collection<SamlClientIdp> clients)
throws IOException {
if (clients == null) {
deleteSamlClients(document);
return;
}
Set<String> idsSeen = Sets.newHashSet();
checkSamlClients(idsSeen, clients);
deleteSamlClients(document);
for (SamlClientIdp client : clients) {
addIdpEntity(document, client);
}
}
/**
* Add the given SAML client IdPs to the given metadata.
*
* @param document The metadata as a {@link Document}. This document is
* modified to contain the new clients.
* @param clients A set of SAML client IdP descriptors; never null.
* @throws IOException if metadata can't be parsed or serialized.
*/
public static void addSamlClientsToMetadata(Document document, Collection<SamlClientIdp> clients)
throws IOException {
Set<String> idsSeen = Sets.newHashSet();
for (SamlClientIdp client : getSamlClientsInMetadata(document)) {
idsSeen.add(client.getId());
}
checkSamlClients(idsSeen, clients);
for (SamlClientIdp client : clients) {
addIdpEntity(document, client);
}
}
private static void checkSamlClients(Set<String> idsSeen, Collection<SamlClientIdp> clients) {
for (SamlClientIdp client : clients) {
Preconditions.checkArgument(!idsSeen.contains(client.getId()),
"Duplicate entity ID in clients: %s", client.getId());
idsSeen.add(client.getId());
}
}
/**
* Find a SAML client with a given entity ID.
*
* @param entityId The entity ID to look for.
* @param clients The clients to look through.
* @return The client with that ID, or null if none.
*/
public static SamlClientIdp findSamlClient(String entityId, Iterable<SamlClientIdp> clients) {
for (SamlClientIdp client : clients) {
if (entityId.equals(client.getId())) {
return client;
}
}
return null;
}
// **************** Versions of the above using XML strings ****************
/**
* Get the SAML client IdPs from metadata.
*
* @param metadata The metadata as XML String.
* @return A list of SAML client IdP descriptors.
* @throws IOException if metadata can't be parsed.
*/
public static List<SamlClientIdp> getSamlClientsInMetadata(String metadata)
throws IOException {
return getSamlClientsInMetadata(stringToMetadataDocument(metadata));
}
/**
* Set the SAML client IdPs in given metadata to the given set. Removes any
* clients already in the metadata, then inserts the given clients.
*
* @param origMetadata The original metadata as an XML String.
* @param clients The SAML client IdP descriptors; null means delete all.
* @return The new metadata as XML String.
* @throws IOException if metadata can't be parsed or serialized.
*/
public static String setSamlClientsInMetadata(String origMetadata,
Collection<SamlClientIdp> clients)
throws IOException {
Document document = stringToMetadataDocument(origMetadata);
setSamlClientsInMetadata(document, clients);
return metadataDocumentToString(document);
}
/**
* Add the given SAML client IdPs to the given metadata.
*
* @param origMetadata The original metadata as an XML String.
* @param clients The SAML client IdP descriptors; never null.
* @return The new metadata as an XML String.
* @throws IOException if metadata can't be parsed or serialized.
*/
public static String addSamlClientsToMetadata(String origMetadata,
Collection<SamlClientIdp> clients)
throws IOException {
Document document = stringToMetadataDocument(origMetadata);
addSamlClientsToMetadata(document, clients);
return metadataDocumentToString(document);
}
/**
* Parse a metadata string into an XML document.
*
* @param metadata The metadata as an XML string.
* @return The SAML metadata document.
* @throws IOException if the metadata can't be parsed.
*/
public static Document stringToMetadataDocument(String metadata)
throws IOException {
return readMetadataDocument(new StringReader(metadata));
}
/**
* Convert a metadata XML document into a string.
*
* @param metadata The SAML metadata document.
* @return The metadata as an XML string.
* @throws IOException if the metadata can't be converted.
*/
public static String metadataDocumentToString(Document metadata)
throws IOException {
return XmlUtil.getInstance().buildXmlString(metadata);
}
/**
* Read SAML metadata and return it as a DOM document.
*
* @param input The Reader to read from.
* @return The SAML metadata document.
* @throws IOException if the document can't be parsed.
*/
public static Document readMetadataDocument(Reader input)
throws IOException {
Document document = XmlUtil.getInstance().readXmlDocument(input);
setConfigParams(document);
return document;
}
/**
* Write a SAML metadata document as XML.
*
* @param document The SAML metadata document to write.
* @param writer The writer to write it to.
* @throws IOException if the document can't be serialized.
*/
public static void writeMetadataDocument(Document document, Writer writer)
throws IOException {
XmlUtil.getInstance().writeXmlDocument(document, writer);
}
/**
* Read SAML metadata and return it as a DOM document.
*
* @param file The file to read from.
* @return The SAML metadata document.
* @throws IOException if the document can't be parsed.
*/
public static Document readMetadataDocument(File file)
throws IOException {
Reader reader = new FileReader(file);
try {
return readMetadataDocument(reader);
} finally {
reader.close();
}
}
/**
* Write a SAML metadata document as XML.
*
* @param document The SAML metadata document to write.
* @param file The file to write it to.
* @throws IOException if the document can't be serialized.
*/
public static void writeMetadataDocument(Document document, File file)
throws IOException {
Writer writer = new FileWriter(file);
try {
writeMetadataDocument(document, writer);
} finally {
writer.close();
}
}
/**
* Make a default metadata document for the security manager, when it is installed
* onboard the GSA. This metadata uses the token "$$GSA$$" to represent the hostname of
* the GSA; this token will be dynamically replaced with the real hostname on each
* request. URLs that identify endpoints for connections between the GSA and the
* security manager use "localhost".
*
* @return An XML document representing the metadata.
*/
public static Document makeOnboardSecurityManagerMetadata() {
Document document = XmlUtil.getInstance()
.makeDocument(SAML_DESCRIPTOR_ENTITIES.getNamespaceURI(),
SAML_DESCRIPTOR_ENTITIES.getLocalPart(),
null);
setConfigParams(document);
Element entities = document.getDocumentElement();
entities.setAttribute(SAML_ATTR_CACHE_DURATION, SAML_CACHE_DURATION);
String gsaEntityId = GSA_ENTITY_ID_ROOT + GSA_ENT_CONFIG_NAME_MARKER;
makeGsaEntity(entities, gsaEntityId);
makeSmEntity(entities, gsaEntityId + SECMGR_ENTITY_ID_SUFFIX);
return document;
}
/**
* Make a SAML EntityDescriptor element for the GSA.
*
* @param entities The SAML EntitiesDescriptor element to put the element in.
* @param id The SAML entity ID for the new element.
* @return The EntityDescriptor as a DOM element.
*/
private static Element makeGsaEntity(Element entities, String id) {
Element entity = makeTopLevelEntity(entities, id, GSA_ENTITY_COMMENT);
makeRole(entity, SAML_DESCRIPTOR_SP_SSO);
makeAssertionConsumer(entity,
SAML_BINDING_HTTP_ARTIFACT,
makeGsaUrl(GSA_HOST_MARKER, GSA_ASSERTION_CONSUMER_PATH));
makeGoogleOrganization(entity);
return entity;
}
/**
* Make a URL for the GSA.
*
* @param host The hostname to use.
* @param path The URL path.
* @return The URL as a string.
*/
private static String makeGsaUrl(String host, String path) {
URL url;
try {
url = new URL(HTTPS_PROTOCOL, host, -1, path);
} catch (MalformedURLException e) {
throw new IllegalStateException(e);
}
return url.toString();
}
/**
* Make a SAML EntityDescriptor element for the security manager.
*
* @param entities The SAML EntitiesDescriptor element to put the new element in.
* @param id The SAML entity ID for the new element.
* @return The EntityDescriptor as a DOM element.
*/
private static Element makeSmEntity(Element entities, String id) {
Element entity = makeTopLevelEntity(entities, id, SECMGR_ENTITY_COMMENT);
entity.setAttribute(XML_ATTR_ID, SECMGR_ID_FOR_ENTITY);
makeRole(entity, SAML_DESCRIPTOR_IDP_SSO);
makeSingleSignOn(entity, makeSecurityManagerUrl(GSA_HOST_MARKER, SECMGR_SSO_PATH));
makeArtifactResolver(entity, makeSecurityManagerUrl(LOCALHOST, SECMGR_ARTIFACT_RESOLVER_PATH));
makeRole(entity, SAML_DESCRIPTOR_SP_SSO);
makeAssertionConsumer(entity,
SAML_BINDING_HTTP_POST,
makeSecurityManagerUrl(GSA_HOST_MARKER, SECMGR_ASSERTION_CONSUMER_PATH));
makeAssertionConsumer(entity,
SAML_BINDING_HTTP_ARTIFACT,
makeSecurityManagerUrl(GSA_HOST_MARKER, SECMGR_ASSERTION_CONSUMER_PATH));
makePdp(entity, makeSecurityManagerUrl(LOCALHOST, SECMGR_AUTHZ_PATH));
makeGoogleOrganization(entity);
return entity;
}
/**
* Make a URL for a security manager servlet.
*
* @param host The hostname to use.
* @param servletPath The part of the path that's specific to the servlet.
* @return The URL as a string.
*/
private static String makeSecurityManagerUrl(String host, String servletPath) {
String path = SECMGR_WEBAPP_PATH + "/" + servletPath;
URL url;
try {
if (LOCALHOST.equals(host)) {
url = new URL(HTTP_PROTOCOL, host, path);
} else {
url = new URL(HTTPS_PROTOCOL, host, path);
}
} catch (MalformedURLException e) {
throw new IllegalStateException(e);
}
return url.toString();
}
/**
* Make a SAML Organization element for Google.
*
* @param entity The SAML Entity element to put the new element in.
* @return The Organization as a DOM element.
*/
private static Element makeGoogleOrganization(Element entity) {
Element organization = makeElement(entity, SAML_ORGANIZATION);
makeTextElementChild(organization, SAML_ORGANIZATION_NAME, GOOGLE_ORGANIZATION_NAME);
makeTextElementChild(organization, SAML_ORGANIZATION_DISPLAY_NAME,
GOOGLE_ORGANIZATION_DISPLAY_NAME);
makeTextElementChild(organization, SAML_ORGANIZATION_URL, GOOGLE_ORGANIZATION_URL);
return organization;
}
/**
* Add an external SAML client IdP to a security manager metadata document. If there is
* already an entity with the same ID, it is replaced.
*
* @param document A security manager metadata document.
* @param client A SAML client IdP descriptor.
*/
public static void addIdpEntity(Document document, SamlClientIdp client) {
Element clients = getSamlClients(document);
if (clients == null) {
clients = makeSamlClients(document);
}
Element entity = makeEntity(clients, client.getId());
if (client.hasSso()) {
Element role = makeRole(entity, SAML_DESCRIPTOR_IDP_SSO);
makeSingleSignOn(entity, client.getUrl());
String artifactUrl = normalizeString(client.getArtifactUrl());
if (artifactUrl != null) {
makeArtifactResolver(entity, artifactUrl);
}
if (client.getCertificate() != null) {
makeKeyDescriptor(role, client.getCertificate());
}
}
if (client.hasPdp()) {
makePdp(entity, client.getAuthzServiceUrl());
}
}
private static String normalizeString(String s) {
if (s == null) {
return s;
}
s = s.trim();
return s.isEmpty() ? null : s;
}
/**
* Get the SAML EntitiesDescriptor that holds the client entities.
*
* @param document The SAML metadata document to look in.
* @return The EntitiesDescriptor element, or null if no such.
*/
public static Element getSamlClients(Document document) {
NodeList nodes =
getElementsByQname(document.getDocumentElement(), SAML_DESCRIPTOR_ENTITIES);
for (int index = 0; index < nodes.getLength(); index++) {
Element element = Element.class.cast(nodes.item(index));
if (SECMGR_CLIENTS_ENTITIES_NAME.equals(element.getAttribute(SAML_ATTR_NAME))) {
return element;
}
}
return null;
}
/**
* Make an empty SAML EntitiesDescriptor to hold client entities.
*
* @param document The SAML metadata document to look in.
* @return The new EntitiesDescriptor as a DOM element.
*/
private static Element makeSamlClients(Document document) {
return makeTopLevelEntities(document.getDocumentElement(),
SECMGR_CLIENTS_ENTITIES_NAME, SECMGR_CLIENTS_ENTITIES_COMMENT);
}
/**
* Remove the SAML client entities descriptor, if there is one.
*
* @param document The SAML metadata document to look in.
*/
private static void deleteSamlClients(Document document) {
Element oldClients = getSamlClients(document);
if (oldClients != null) {
oldClients.getParentNode().removeChild(oldClients);
}
for (Comment comment :
findChildComments(
document.getDocumentElement(),
SECMGR_CLIENTS_ENTITIES_COMMENT)) {
comment.getParentNode().removeChild(comment);
}
}
// Endpoint constructors.
/**
* Make a SAML SingleSignOn element.
*
* @param entity The SAML EntityDescriptor to put the new element in.
* @param url The endpoint URL for the new element.
* @return The new SingleSignOn as a DOM element.
*/
private static Element makeSingleSignOn(Element entity, String url) {
return makeEndpoint(findRole(entity, SAML_DESCRIPTOR_IDP_SSO),
SAML_SERVICE_SINGLE_SIGN_ON, SAML_BINDING_HTTP_REDIRECT, url);
}
/**
* Make a SAML ArtifactResolver element.
*
* @param entity The SAML EntityDescriptor to put the new element in.
* @param url The endpoint URL for the new element.
* @return The new ArtifactResolver as a DOM element.
*/
private static Element makeArtifactResolver(Element entity, String url) {
return makeIndexedEndpoint(findRole(entity, SAML_DESCRIPTOR_IDP_SSO),
SAML_SERVICE_ARTIFACT_RESOLUTION, SAML_BINDING_SOAP, url);
}
/**
* Make a SAML AssertionConsumer element.
*
* @param entity The SAML EntityDescriptor to put the new element in.
* @param url The endpoint URL for the new element.
* @return The new AssertionConsumer as a DOM element.
*/
private static Element makeAssertionConsumer(Element entity, String binding, String url) {
return makeIndexedEndpoint(findRole(entity, SAML_DESCRIPTOR_SP_SSO),
SAML_SERVICE_ASSERTION_CONSUMER, binding, url);
}
/**
* Make a SAML PDPDescriptor element.
*
* @param entity The SAML EntityDescriptor to put the new element in.
* @param url The endpoint URL for the new element.
* @return The new PDPDescriptor as a DOM element.
*/
private static Element makePdp(Element entity, String url) {
Element role = makeRole(entity, SAML_DESCRIPTOR_PDP);
makeEndpoint(role, SAML_SERVICE_AUTHZ, SAML_BINDING_SOAP, url);
return role;
}
/**
* Find a role element with a given name.
*
* @param entity The SAML EntityDescriptor to look in.
* @param qname The qname of the role element to look for.
* @return The specified role element.
* @throws IllegalArgumentException if there's no such child.
*/
private static Element findRole(Element entity, QName qname) {
return findChildElement(entity, qname, true);
}
// Element constructors.
/**
* Make a new top-level SAML EntitiesDescriptor element.
*
* @param entities The SAML EntitiesDescriptor to put the new element in.
* @param name The name of the new element -- will be put in the name attribute.
* @param comment A descriptive comment that will be added as an XML comment.
* @return The new EntitiesDescriptor as a DOM element.
*/
private static Element makeTopLevelEntities(Element entities, String name, String comment) {
makeTextChild(entities, "\n");
makeCommentChild(entities, " " + comment + " ");
makeTextChild(entities, "\n");
Element result = makeEntities(entities, name);
makeTextChild(entities, "\n");
return result;
}
/**
* Make a new SAML EntitiesDescriptor element.
*
* @param entities The SAML EntitiesDescriptor to put the new element in.
* @param name The name of the new element -- will be put in the name attribute.
* @return The new EntitiesDescriptor as a DOM element.
*/
private static Element makeEntities(Element entities, String name) {
Element child = makeElementChild(entities, SAML_DESCRIPTOR_ENTITIES);
child.setAttribute(SAML_ATTR_NAME, name);
return child;
}
/**
* Make a new top-level SAML EntityDescriptor element.
*
* @param entities The SAML EntitiesDescriptor to put the new element in.
* @param id The id of the new element -- will be put in the ID attribute.
* @param comment A descriptive comment that will be added as an XML comment.
* @return The new EntityDescriptor as a DOM element.
*/
private static Element makeTopLevelEntity(Element entities, String id, String comment) {
makeTextChild(entities, "\n");
makeCommentChild(entities, " " + comment + " ");
makeTextChild(entities, "\n");
Element entity = makeEntity(entities, id);
makeTextChild(entities, "\n");
return entity;
}
/**
* Make a new SAML EntityDescriptor element.
*
* @param entities The SAML EntitiesDescriptor to put the new element in.
* @param id The id of the new element -- will be put in the ID attribute.
* @return The new EntityDescriptor as a DOM element.
*/
private static Element makeEntity(Element entities, String id) {
Element old = findEntity(entities, id);
Element entity = makeElement(entities, SAML_DESCRIPTOR_ENTITY);
entity.setAttribute(SAML_ATTR_ENTITY_ID, id);
if (old == null) {
entities.appendChild(entity);
} else {
entities.replaceChild(entity, old);
}
return entity;
}
/**
* Find a SAML EntityDescriptor element.
*
* @param entities The SAML EntitiesDescriptor to look in.
* @param id The id of the element to look for.
* @return The specified EntityDescriptor, or null if no such.
*/
private static Element findEntity(Element entities, String id) {
NodeList nodes = getElementsByQname(entities, SAML_DESCRIPTOR_ENTITY);
for (int index = 0; index < nodes.getLength(); index++) {
Element element = Element.class.cast(nodes.item(index));
if (id.equals(element.getAttribute(SAML_ATTR_ENTITY_ID))) {
return element;
}
}
return null;
}
/**
* Make a new SAML role.
*
* @param entity the SAML EntityDescriptor to put the new role in.
* @param qname The qname of the new role.
* @return The new role as a DOM element.
*/
private static Element makeRole(Element entity, QName qname) {
Element role = makeElementChild(entity, qname);
role.setAttribute(SAML_ATTR_PROTOCOL_SUPPORT_ENUMERATION, SAML_PROTOCOL);
return role;
}
/**
* Make a new SAML endpoint.
*
* @param role the SAML role to put the new endpoint in.
* @param qname The qname of the new endpoint.
* @param binding The SAML binding specifier.
* @param url The endpoing URL.
* @return The new endpoint as a DOM element.
*/
private static Element makeEndpoint(Element role, QName qname, String binding, String url) {
Element endpoint = makeElementChild(role, qname);
endpoint.setAttribute(SAML_ATTR_BINDING, binding);
endpoint.setAttribute(SAML_ATTR_LOCATION, url);
return endpoint;
}
/**
* Make a new SAML indexed endpoint.
*
* @param role the SAML role to put the new endpoint in.
* @param qname The qname of the new endpoint.
* @param binding The SAML binding specifier.
* @param url The endpoing URL.
* @return The new endpoint as a DOM element.
*/
private static Element makeIndexedEndpoint(
Element role, QName qname, String binding, String url) {
int nChildren = countChildElements(role, qname);
Element endpoint = makeEndpoint(role, qname, binding, url);
endpoint.setAttribute(SAML_ATTR_INDEX, Integer.toString(nChildren));
endpoint.setAttribute(SAML_ATTR_IS_DEFAULT, (nChildren == 0) ? "true" : "false");
return endpoint;
}
/**
* Make a new SAML key descriptor.
*
* @param role the SAML role to put the new descriptor in.
* @param certificate The X509 certificate that the descriptor will contain.
* @return The new key descriptor as a DOM element.
*/
private static Element makeKeyDescriptor(Element role, String certificate) {
Element keyDescriptor = makeElementChild(role, SAML_DESCRIPTOR_KEY);
keyDescriptor.setAttribute(SAML_ATTR_USE, SAML_USAGE_SIGNING);
Element keyInfo = makeElementChild(keyDescriptor, XMLDSIG_KEY_INFO);
Element x509Data = makeElementChild(keyInfo, XMLDSIG_X509_DATA);
makeTextElementChild(x509Data, XMLDSIG_X509_CERTIFICATE, normalizeCertificate(certificate));
return keyDescriptor;
}
/**
* Get the certificate out of a SAML key descriptor. Works only with key descriptors
* created by #makeKeyDescriptor().
*
* @param keyDescriptor The SAML key descriptor to extract from.
* @return The extracted certificate, or null if none such.
*/
private static String keyDescriptorCertificate(Element keyDescriptor) {
Element keyInfo = findChildElement(keyDescriptor, XMLDSIG_KEY_INFO, false);
if (keyInfo == null) { return null; }
Element x509Data = findChildElement(keyInfo, XMLDSIG_X509_DATA, false);
if (x509Data == null) { return null; }
Element x509Certificate = findChildElement(x509Data, XMLDSIG_X509_CERTIFICATE, false);
if (x509Certificate == null) { return null; }
NodeList nodes = x509Certificate.getChildNodes();
if (nodes.getLength() == 1 && nodes.item(0).getNodeType() == Node.TEXT_NODE) {
return normalizeCertificate(nodes.item(0).getNodeValue());
}
return null;
}
/**
* Convert a PEM certificate to normalized form.
*
* @param s The certificate to normalize.
* @return The normalized certificate.
*/
public static String normalizeCertificate(String s) {
if (s == null) {
return s;
}
List<String> lines = Lists.newArrayList();
for (String line : s.trim().split("\n+")) {
lines.add(line.trim());
}
if (lines.size() == 0) {
return null;
}
// Trim off X509 header/footer if present.
if (X509_HEADER.equals(lines.get(0))
&& X509_FOOTER.equals(lines.get(lines.size() - 1))) {
lines.remove(lines.size() - 1);
lines.remove(0);
}
StringBuilder builder = new StringBuilder();
for (String line : lines) {
builder.append(line);
builder.append("\n");
}
return builder.toString();
}
}