se.swedenconnect.opensaml.saml2.metadata.provider.AbstractMetadataProvider Maven / Gradle / Ivy
/*
* Copyright 2016-2021 Sweden Connect
*
* 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 se.swedenconnect.opensaml.saml2.metadata.provider;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.xml.namespace.QName;
import org.opensaml.core.criterion.EntityIdCriterion;
import org.opensaml.core.xml.XMLObject;
import org.opensaml.core.xml.io.MarshallingException;
import org.opensaml.core.xml.util.XMLObjectSupport;
import org.opensaml.saml.common.xml.SAMLSchemaBuilder;
import org.opensaml.saml.metadata.resolver.MetadataResolver;
import org.opensaml.saml.metadata.resolver.RefreshableMetadataResolver;
import org.opensaml.saml.metadata.resolver.filter.MetadataFilter;
import org.opensaml.saml.metadata.resolver.filter.MetadataFilterChain;
import org.opensaml.saml.metadata.resolver.filter.impl.PredicateFilter;
import org.opensaml.saml.metadata.resolver.filter.impl.PredicateFilter.Direction;
import org.opensaml.saml.metadata.resolver.filter.impl.SchemaValidationFilter;
import org.opensaml.saml.metadata.resolver.filter.impl.SignatureValidationFilter;
import org.opensaml.saml.metadata.resolver.impl.AbstractMetadataResolver;
import org.opensaml.saml.saml2.metadata.EntitiesDescriptor;
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
import org.opensaml.saml.saml2.metadata.IDPSSODescriptor;
import org.opensaml.saml.saml2.metadata.SPSSODescriptor;
import org.opensaml.security.credential.CredentialResolver;
import org.opensaml.security.credential.impl.StaticCredentialResolver;
import org.opensaml.security.x509.BasicX509Credential;
import org.opensaml.xmlsec.config.impl.DefaultSecurityConfigurationBootstrap;
import org.opensaml.xmlsec.signature.support.impl.ExplicitKeySignatureTrustEngine;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Element;
import net.shibboleth.utilities.java.support.component.AbstractInitializableComponent;
import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
import net.shibboleth.utilities.java.support.component.ComponentSupport;
import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
import net.shibboleth.utilities.java.support.resolver.ResolverException;
/**
* Abstract base class for the {@link MetadataProvider} interface.
*
* @author Martin Lindström ([email protected])
*/
public abstract class AbstractMetadataProvider extends AbstractInitializableComponent implements MetadataProvider {
/** Logging instance. */
private Logger log = LoggerFactory.getLogger(AbstractMetadataProvider.class);
/** Whether the metadata returned by queries must be valid. Default: true. */
private boolean requireValidMetadata = true;
/**
* Whether problems during initialization should cause the provider to fail or go on without metadata. The assumption
* being that in most cases a provider will recover at some point in the future. Default: false.
*/
private boolean failFastInitialization = false;
/**
* The certificate(s) that is/are accepted as signing certificates for the downloaded metadata. If this attribute is
* assigned the provider is configured to expect a valid signature on downloaded metadata.
*/
private List signatureVerificationCertificates = null;
/** Tells whether XML schema validation should be performed on downloaded metadata. Default: false. */
private boolean performSchemaValidation = false;
/** A list of inclusion predicates that will be applied to downloaded metadata. */
private List> inclusionPredicates = null;
/** A list of exclusion predicates that will be applied to downloaded metadata. */
private List> exclusionPredicates = null;
/** The downloaded metadata. */
private XMLObject metadata;
/** The time when the metadata was downloaded. */
private Instant downloadTime;
/** {@inheritDoc} */
@Override
public synchronized XMLObject getMetadata() {
return this.metadata;
}
/** {@inheritDoc} */
@Override
public Element getMetadataDOM() throws MarshallingException {
final XMLObject md = this.getMetadata();
if (md == null) {
return null;
}
if (md.getDOM() != null) {
return md.getDOM();
}
return XMLObjectSupport.marshall(md);
}
/** {@inheritDoc} */
@Override
public Instant getLastUpdate() {
if (RefreshableMetadataResolver.class.isInstance(this.getMetadataResolver())) {
return ((RefreshableMetadataResolver) this.getMetadataResolver()).getLastUpdate();
}
return this.downloadTime;
}
/** {@inheritDoc} */
@Override
public void refresh() throws ResolverException {
if (RefreshableMetadataResolver.class.isInstance(this.getMetadataResolver())) {
((RefreshableMetadataResolver) this.getMetadataResolver()).refresh();
}
else {
log.debug("Refresh of metadata is not supported by {}", this.getClass().getName());
}
}
/** {@inheritDoc} */
@Override
public Iterable iterator() {
return new EntityDescriptorIterator(this.getMetadata());
}
/** {@inheritDoc} */
@Override
public Iterable iterator(final QName role) {
return new EntityDescriptorIterator(this.getMetadata(), role);
}
/** {@inheritDoc} */
@Override
public EntityDescriptor getEntityDescriptor(final String entityID) throws ResolverException {
return this.getEntityDescriptor(entityID, null);
}
/** {@inheritDoc} */
@Override
public EntityDescriptor getEntityDescriptor(final String entityID, final QName role) throws ResolverException {
final CriteriaSet criteria = new CriteriaSet();
criteria.add(new EntityIdCriterion(entityID));
final EntityDescriptor ed = this.getMetadataResolver().resolveSingle(criteria);
if (role != null && ed != null) {
if (ed.getRoleDescriptors(role).isEmpty()) {
return null;
}
}
return ed;
}
/** {@inheritDoc} */
@Override
public List getIdentityProviders() throws ResolverException {
final List list = new ArrayList<>();
final Iterable it = this.iterator(IDPSSODescriptor.DEFAULT_ELEMENT_NAME);
it.forEach(list::add);
return list;
}
/** {@inheritDoc} */
@Override
public List getServiceProviders() throws ResolverException {
final List list = new ArrayList<>();
final Iterable it = this.iterator(SPSSODescriptor.DEFAULT_ELEMENT_NAME);
it.forEach(list::add);
return list;
}
/**
* Assigns the metadata that was downloaded.
*
* @param metadata
* metadata object
*/
private synchronized void setMetadata(final XMLObject metadata) {
this.metadata = metadata;
this.downloadTime = Instant.now();
}
/** {@inheritDoc} */
@Override
protected final void doInitialize() throws ComponentInitializationException {
super.doInitialize();
try {
this.createMetadataResolver(this.requireValidMetadata, this.failFastInitialization, this.createFilter());
}
catch (ResolverException e) {
throw new ComponentInitializationException(e);
}
this.initializeMetadataResolver();
}
/**
* Creates the filter(s) that this instance should be configured with.
*
* @return a metadata filter
*/
protected MetadataFilter createFilter() {
final List filters = new ArrayList<>();
// Verify signature?
if (this.signatureVerificationCertificates != null && !this.signatureVerificationCertificates.isEmpty()) {
final CredentialResolver credentialResolver = new StaticCredentialResolver(
this.signatureVerificationCertificates.stream().map(c -> new BasicX509Credential(c)).collect(Collectors.toList()));
final ExplicitKeySignatureTrustEngine trustEngine = new ExplicitKeySignatureTrustEngine(credentialResolver,
DefaultSecurityConfigurationBootstrap.buildBasicInlineKeyInfoCredentialResolver());
filters.add(new SignatureValidationFilter(trustEngine));
}
// Schema validation?
if (this.performSchemaValidation) {
filters.add(new SchemaValidationFilter(new SAMLSchemaBuilder(SAMLSchemaBuilder.SAML1Version.SAML_11)));
}
// Inclusion predicates?
if (this.inclusionPredicates != null) {
for (Predicate p : this.inclusionPredicates) {
filters.add(new PredicateFilter(Direction.INCLUDE, p));
}
}
// Exclusion predicates?
if (this.exclusionPredicates != null) {
for (Predicate p : this.exclusionPredicates) {
filters.add(new PredicateFilter(Direction.EXCLUDE, p));
}
}
// Install the mandatory filter that saves downloaded metadata.
filters.add((metadata, ctx) -> {
setMetadata(metadata);
return metadata;
});
if (filters.size() == 1) {
return filters.get(0);
}
else {
final MetadataFilterChain chain = new MetadataFilterChain();
chain.setFilters(filters);
return chain;
}
}
/** {@inheritDoc} */
@Override
protected void doDestroy() {
super.doDestroy();
this.destroyMetadataResolver();
}
/**
* Creates the specific {@link MetadataResolver} instance for the provider implementation.
*
* The {@code filter} parameter is a {@link MetadataFilter} that must be installed for the resolver. Any other
* filters that should be installed by the specific instance should be placed last in a filter chain.
*
*
* @param requireValidMetadata
* should be passed into {@link MetadataResolver#setRequireValidMetadata(boolean)}
* @param failFastInitialization
* should be passed into {@link AbstractMetadataResolver#setFailFastInitialization(boolean)} (if applicable)
* @param filter
* filter that must be installed for the resolver
* @throws ResolverException
* for errors creating the resolver
*/
protected abstract void createMetadataResolver(final boolean requireValidMetadata, final boolean failFastInitialization,
final MetadataFilter filter) throws ResolverException;
/**
* Initializes the metadata resolver.
*
* @throws ComponentInitializationException
* for initialization errors
*/
protected abstract void initializeMetadataResolver() throws ComponentInitializationException;
/**
* Destroys the metadata resolver.
*/
protected abstract void destroyMetadataResolver();
/**
* Sets whether the metadata returned by queries must be valid.
*
* @param requireValidMetadata
* whether the metadata returned by queries must be valid
*/
public void setRequireValidMetadata(final boolean requireValidMetadata) {
ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
this.requireValidMetadata = requireValidMetadata;
}
/**
* Sets whether problems during initialization should cause the provider to fail or go on without metadata. The
* assumption being that in most cases a provider will recover at some point in the future.
*
* @param failFast
* whether problems during initialization should cause the provider to fail
*/
public void setFailFastInitialization(final boolean failFast) {
ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
failFastInitialization = failFast;
}
/**
* Assigns the certificate that is to be used when verifying the signature on downloaded metadata. If this attribute
* is assigned the provider is configured to expect a valid signature on downloaded metadata.
*
* @param signatureVerificationCertificate
* the certificate to assign
*/
public void setSignatureVerificationCertificate(final X509Certificate signatureVerificationCertificate) {
ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
this.signatureVerificationCertificates = Collections.singletonList(signatureVerificationCertificate);
}
/**
* Assigns the certificates that are to be used when verifying the signature on downloaded metadata. If this attribute
* is assigned the provider is configured to expect a valid signature on downloaded metadata.
*
* The reason that more than one certificate may be assigned is that we want to be able to handle signing certificate
* updates in a smooth way.
*
*
* @param signatureVerificationCertificates
* the certificates to assign
*/
public void setSignatureVerificationCertificates(final List signatureVerificationCertificates) {
ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
this.signatureVerificationCertificates = signatureVerificationCertificates;
}
/**
* Gets the certificate that is to be used when verifying the signature on downloaded metadata.
*
* @return the certificates or null
*/
public List getSignatureVerificationCertificates() {
return this.signatureVerificationCertificates;
}
/**
* Assigns whether XML schema validation should be performed on downloaded metadata.
*
* @param performSchemaValidation
* whether schema validation should be performed
*/
public void setPerformSchemaValidation(final boolean performSchemaValidation) {
ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
this.performSchemaValidation = performSchemaValidation;
}
/**
* Assigns a list of inclusion predicates that will be applied to downloaded metadata.
*
* @param inclusionPredicates
* predicates
* @see MetadataProviderPredicates
*/
public void setInclusionPredicates(final List> inclusionPredicates) {
ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
this.inclusionPredicates = inclusionPredicates;
}
/**
* Assigns a list of exclusion predicates that will be applied to downloaded metadata.
*
* @param exclusionPredicates
* predicates
* @see MetadataProviderPredicates
*/
public void setExclusionPredicates(final List> exclusionPredicates) {
ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
this.exclusionPredicates = exclusionPredicates;
}
/**
* Iterates over EntitiesDescriptor or EntityDescriptor.
*/
protected static class EntityDescriptorIterator implements Iterator, Iterable {
/** The iterator. */
private Iterator iterator = null;
/**
* Constructor.
*
* @param metadata
* the metadata to iterate
*/
public EntityDescriptorIterator(final XMLObject metadata) {
this(metadata, null);
}
/**
* Constructor.
*
* @param metadata
* the metadata to iterate
* @param role
* role requirements
*/
public EntityDescriptorIterator(final XMLObject metadata, final QName role) {
if (metadata == null) {
return;
}
if (metadata instanceof EntityDescriptor) {
this.iterator = Collections.singletonList((EntityDescriptor) metadata).iterator();
}
else if (metadata instanceof EntitiesDescriptor) {
List edList = setup((EntitiesDescriptor) metadata, role);
this.iterator = edList.iterator();
}
else {
throw new IllegalArgumentException("Expected EntityDescriptor or EntitiesDescriptor");
}
}
private static List setup(final EntitiesDescriptor entitiesDescriptor, final QName role) {
List edList = new ArrayList<>();
entitiesDescriptor.getEntityDescriptors().stream().filter(filterRole(role)).forEach(edList::add);
for (EntitiesDescriptor ed : entitiesDescriptor.getEntitiesDescriptors()) {
edList.addAll(setup(ed, role));
}
return edList;
}
public static Predicate filterRole(final QName role) {
return e -> role == null || !e.getRoleDescriptors(role).isEmpty();
}
@Override
public boolean hasNext() {
return this.iterator != null && this.iterator.hasNext();
}
@Override
public EntityDescriptor next() {
if (this.iterator != null) {
return this.iterator.next();
}
throw new NoSuchElementException();
}
@Nonnull
@Override
public Iterator iterator() {
return this;
}
}
}