blob: df06562cd1251a238aec984bf7fbe84eed79f4ae [file] [log] [blame]
// Copyright 2008 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 com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Maps;
import com.google.enterprise.secmgr.common.FileUtil;
import com.google.enterprise.secmgr.common.SecurityManagerUtil;
import com.google.enterprise.secmgr.common.XmlUtil;
import com.google.enterprise.secmgr.config.ConfigSingleton;
import org.opensaml.saml2.metadata.EntitiesDescriptor;
import org.opensaml.saml2.metadata.EntityDescriptor;
import org.opensaml.saml2.metadata.provider.AbstractObservableMetadataProvider;
import org.opensaml.saml2.metadata.provider.MetadataProvider;
import org.opensaml.saml2.metadata.provider.MetadataProviderException;
import org.opensaml.saml2.metadata.provider.ObservableMetadataProvider;
import org.opensaml.xml.XMLObject;
import org.opensaml.xml.io.MarshallingException;
import org.opensaml.xml.io.UnmarshallingException;
import org.w3c.dom.CDATASection;
import org.w3c.dom.Comment;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.EntityReference;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.ProcessingInstruction;
import org.w3c.dom.Text;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.Map;
import java.util.logging.Logger;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
/**
* An abstract interface to the SAML metadata configuration. Tracks a given
* metadata file and keeps it up to date. Also rewrites the metadata so it uses
* the correct hostname.
*/
@ThreadSafe
public class Metadata {
private static final Logger LOGGER = Logger.getLogger(Metadata.class.getName());
@GuardedBy("itself")
private static final Map<String, Metadata> perHostMap = Maps.newHashMap();
private static File metadataFile = null;
private final MetadataProvider provider;
private Metadata(String urlPrefix)
throws IOException {
try {
this.provider = new MyProvider(
OpenSamlUtil.getMetadataFromFile(
(metadataFile != null)
? metadataFile
: FileUtil.getContextFile(ConfigSingleton.getSamlMetadataFilename())),
urlPrefix, SecurityManagerUtil.getGsaEntConfigName());
} catch (MetadataProviderException e) {
throw new IOException(e);
}
}
@VisibleForTesting
static void setMetadataFile(File metadataFile) {
Metadata.metadataFile = metadataFile;
}
@VisibleForTesting
public static Metadata getInstance(String host)
throws IOException {
return getInstance("http", host);
}
public static Metadata getInstance(URL url)
throws IOException {
return getInstance(url.getProtocol(), url.getHost());
}
public static Metadata getInstance(String protocol, String host)
throws IOException {
String urlPrefix = protocol + "://" + host;
Metadata result;
synchronized (perHostMap) {
result = perHostMap.get(urlPrefix);
if (result == null) {
result = new Metadata(urlPrefix);
perHostMap.put(urlPrefix, result);
}
}
return result;
}
public MetadataProvider getProvider() {
return provider;
}
public EntitiesDescriptor getMetadata() throws IOException {
XMLObject root;
try {
root = provider.getMetadata();
} catch (MetadataProviderException e) {
throw new IOException(e);
}
if (root instanceof EntitiesDescriptor) {
return (EntitiesDescriptor) root;
}
throw new IOException("Malformed SAML metadata");
}
public EntityDescriptor getEntity(String id) throws IOException {
EntityDescriptor entity;
try {
entity = provider.getEntityDescriptor(id);
} catch (MetadataProviderException e) {
throw new IOException(e);
}
if (entity == null) {
throw new IllegalArgumentException("Unknown entity ID: " + id);
}
return entity;
}
public EntityDescriptor getSmEntity() throws IOException {
for (EntityDescriptor e : getMetadata().getEntityDescriptors()) {
if (MetadataEditor.SECMGR_ID_FOR_ENTITY.equals(e.getID())) {
return e;
}
}
throw new IllegalStateException("Can't find security manager's entity descriptor");
}
public String getSmEntityId() throws IOException {
return getSmEntity().getEntityID();
}
/**
* This class implements a wrapper around an OpenSAML
* ObservableMetadataProvider that customizes the metadata for a particular
* host. When the metadata is updated, as when the configuration file is
* changed, this provider notices that, gets the updated metadata, and
* customizes it. To speed things up a bit, the customized metadata is
* cached, so it need not be customized every time.
*/
private static class MyProvider
extends AbstractObservableMetadataProvider
implements ObservableMetadataProvider.Observer {
private final ObservableMetadataProvider wrappedProvider;
private final String urlPrefix;
private final String gsaEntConfigName;
private XMLObject savedMetadata;
public MyProvider(ObservableMetadataProvider wrappedProvider,
String urlPrefix, String gsaEntConfigName) {
super();
this.wrappedProvider = wrappedProvider;
this.urlPrefix = urlPrefix;
this.gsaEntConfigName = gsaEntConfigName;
savedMetadata = null;
wrappedProvider.getObservers().add(this);
}
public synchronized void onEvent(MetadataProvider provider) {
LOGGER.info("Clearing cached metadata");
savedMetadata = null;
emitChangeEvent();
}
public synchronized XMLObject getMetadata() throws MetadataProviderException {
// This will call onEvent if the file has changed:
XMLObject rawMetadata = wrappedProvider.getMetadata();
if (savedMetadata == null) {
try {
savedMetadata = OpenSamlUtil.unmarshallXmlObject(
substituteTopLevel(
OpenSamlUtil.marshallXmlObject(rawMetadata)));
} catch (MarshallingException e) {
throw new MetadataProviderException(e);
} catch (UnmarshallingException e) {
throw new MetadataProviderException(e);
}
}
return savedMetadata;
}
private Element substituteTopLevel(Element element) {
Document doc = XmlUtil.getInstance()
.makeDocument(element.getNamespaceURI(), element.getTagName(), null);
Element newElement = doc.getDocumentElement();
substituteInNodeChildren(element, newElement, doc);
return newElement;
}
private void substituteInNodeChildren(Node node, Node newNode, Document doc) {
if (node instanceof Element) {
NamedNodeMap attrs = node.getAttributes();
NamedNodeMap newAttrs = newNode.getAttributes();
for (int i = 0; i < attrs.getLength(); i++) {
Node attr = attrs.item(i);
Node newAttr = doc.createAttributeNS(attr.getNamespaceURI(), attr.getNodeName());
newAttr.setNodeValue(substituteInString(attr.getNodeValue()));
newAttrs.setNamedItemNS(newAttr);
}
}
for (Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) {
Node newChild = substituteInNode(child, doc);
substituteInNodeChildren(child, newChild, doc);
newNode.appendChild(newChild);
}
}
private Node substituteInNode(Node node, Document doc) {
if (node instanceof Element) {
return doc.createElementNS(node.getNamespaceURI(), node.getNodeName());
} else if (node instanceof Text) {
return doc.createTextNode(substituteInString(node.getNodeValue()));
} else if (node instanceof CDATASection) {
return doc.createCDATASection(node.getNodeValue());
} else if (node instanceof Comment) {
return doc.createComment(node.getNodeValue());
} else if (node instanceof EntityReference) {
return doc.createEntityReference(node.getNodeName());
} else if (node instanceof ProcessingInstruction) {
return doc.createProcessingInstruction(node.getNodeName(), node.getNodeValue());
} else {
throw new IllegalArgumentException("Unknown node type: " + node.getNodeType());
}
}
private String substituteInString(String original) {
if (original == null) { return original; }
String pattern = "https://" + MetadataEditor.GSA_HOST_MARKER;
if (original.startsWith(pattern)) {
return original.replace(pattern, urlPrefix);
}
pattern = "http://" + MetadataEditor.GSA_HOST_MARKER;
if (original.startsWith(pattern)) {
return original.replace(pattern, urlPrefix);
}
if (original.contains(MetadataEditor.GSA_ENT_CONFIG_NAME_MARKER)) {
return original.replace(MetadataEditor.GSA_ENT_CONFIG_NAME_MARKER, gsaEntConfigName);
}
return original;
}
}
}