/* * Copyright 2017 Graz University of Technology EAAF-Core Components has been developed in a * cooperation between EGIZ, A-SIT Plus, A-SIT, and Graz University of Technology. * * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the European * Commission - subsequent versions of the EUPL (the "Licence"); You may not use this work except in * compliance with the Licence. You may obtain a copy of the Licence at: * https://joinup.ec.europa.eu/news/understanding-eupl-v12 * * Unless required by applicable law or agreed to in writing, software distributed under the Licence * is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the Licence for the specific language governing permissions and limitations under * the Licence. * * This product combines work with different licenses. See the "NOTICE" text file for details on the * various modules and licenses. The "NOTICE" text file is part of the distribution. Any derivative * works that you distribute must include a readable copy of the "NOTICE" text file. */ package at.gv.egiz.eaaf.modules.pvp2.impl.metadata; import java.io.IOException; import java.security.cert.CertificateException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.naming.ConfigurationException; import at.gv.egiz.components.spring.api.IDestroyableObject; import at.gv.egiz.eaaf.core.api.IGarbageCollectorProcessing; import at.gv.egiz.eaaf.core.exceptions.EaafConfigurationException; import at.gv.egiz.eaaf.modules.pvp2.api.metadata.IPvp2MetadataProvider; import at.gv.egiz.eaaf.modules.pvp2.api.metadata.IPvpAddableChainingMetadataProvider; import at.gv.egiz.eaaf.modules.pvp2.api.metadata.IRefreshableMetadataProvider; import org.apache.commons.lang3.StringUtils; import org.joda.time.DateTime; import org.opensaml.core.criterion.EntityIdCriterion; import org.opensaml.saml.metadata.resolver.ClearableMetadataResolver; 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.impl.AbstractMetadataResolver; import org.opensaml.saml.saml2.metadata.EntityDescriptor; import lombok.extern.slf4j.Slf4j; import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements; import net.shibboleth.utilities.java.support.component.IdentifiedComponent; import net.shibboleth.utilities.java.support.resolver.CriteriaSet; import net.shibboleth.utilities.java.support.resolver.ResolverException; @Slf4j public abstract class AbstractChainingMetadataProvider implements IGarbageCollectorProcessing, IRefreshableMetadataProvider, IPvpAddableChainingMetadataProvider, IDestroyableObject, IPvp2MetadataProvider, ClearableMetadataResolver { @Nonnull @NonnullElements private final List internalResolvers; private DateTime lastRefeshTimestamp; private boolean lastRefeshSuccessful; private static Object mutex = new Object(); /** * Build a chaining metadata resolver that requires valid metadata. * */ public AbstractChainingMetadataProvider() { internalResolvers = Collections.synchronizedList(new ArrayList<>()); } /* * (non-Javadoc) * * @see at.gv.egovernment.moa.id.config.auth.IGarbageCollectorProcessing# * runGarbageCollector() */ @Override public void runGarbageCollector() { synchronized (mutex) { /* * add new Metadataprovider or remove Metadataprovider which are not in use any * more. */ try { log.trace("Check consistence of PVP2X metadata"); addAndRemoveMetadataProvider(); } catch (final EaafConfigurationException e) { log.error("Access to MOA-ID configuration FAILED.", e); } } } @Override public void fullyDestroy() { internalDestroy(); } @Override public synchronized boolean refreshMetadataProvider(final String entityId) { try { //if (resolveEntityDescriporForRefesh(entityId)) { // return true; // //} // reload metadata provider final String metadataUrl = getMetadataUrl(entityId); if (StringUtils.isNotEmpty(metadataUrl)) { final Map actuallyLoadedResolver = getAllActuallyLoadedResolvers(); // check if MetadataProvider is actually loaded final MetadataResolver loadedResover = actuallyLoadedResolver.get(metadataUrl); if (loadedResover instanceof RefreshableMetadataResolver) { try { ((RefreshableMetadataResolver) loadedResover).refresh(); log.info("SAML2 metadata for service provider: {} is refreshed.", entityId); return true; } catch (final ResolverException e) { log.info("Can not refresh SAML2 metadata for entityId: {}. Reason: {}", entityId, e.getMessage()); destroyMetadataResolver(loadedResover); internalResolvers.remove(loadedResover); } } else { // load new Metadata Provider internalResolvers.add(createNewMetadataProvider(metadataUrl)); log.info("SAML2 metadata for service provider: {} is added.", entityId); return true; } } else { log.debug( "Can not refresh SAML2 metadata: NO SAML2 metadata URL for SP with Id: {}", entityId); } } catch (final IOException | EaafConfigurationException | CertificateException e) { log.warn("Refresh SAML2 metadata for service provider: " + entityId + " FAILED.", e); } return false; } @Override public final MetadataFilter getMetadataFilter() { log.warn("{} does NOT support {}", AbstractChainingMetadataProvider.class.getName(), MetadataFilter.class.getName()); return null; } @Override public final void setMetadataFilter(final MetadataFilter newFilter) { log.warn("{} does NOT support {}", AbstractChainingMetadataProvider.class.getName(), MetadataFilter.class.getName()); throw new UnsupportedOperationException( "Metadata filters are not supported on AbstractChainingMetadataProvider"); } /* * (non-Javadoc) * * @see at.gv.egovernment.moa.id.protocols.pvp2x.metadata.IEAAFMetadataProvider# * getEntityDescriptor( java.lang.String) */ @Override public final EntityDescriptor getEntityDescriptor(final String entityID) throws ResolverException { EntityDescriptor entityDesc = null; try { entityDesc = resolveEntityDescripor(entityID); if (entityDesc == null) { log.debug("Can not find PVP metadata for entityID: " + entityID + " Start refreshing process ..."); if (refreshMetadataProvider(entityID)) { return resolveEntityDescripor(entityID); } } } catch (final ResolverException e) { log.debug( "Can not find PVP metadata for entityID: " + entityID + " Start refreshing process ..."); if (refreshMetadataProvider(entityID)) { return resolveEntityDescripor(entityID); } } return entityDesc; } @Override @Nullable public final EntityDescriptor resolveSingle(@Nullable final CriteriaSet criteria) throws ResolverException { EntityDescriptor result = internalResolveSingle(criteria); if (result == null && criteria != null) { final EntityIdCriterion entityIdCriteria = criteria.get(EntityIdCriterion.class); if (entityIdCriteria != null && refreshMetadataProvider(entityIdCriteria.getEntityId())) { log.debug("Can not find PVP metadata for entityID: {}. Metadata refreshing was done ... ", entityIdCriteria.getEntityId()); result = internalResolveSingle(criteria); } } return result; } @Override @Nonnull public final Iterable resolve(@Nullable final CriteriaSet criteria) throws ResolverException { Iterable result = internalResolve(criteria); if (criteria != null) { final EntityIdCriterion entityIdCriteria = criteria.get(EntityIdCriterion.class); if (!result.iterator().hasNext() && entityIdCriteria != null && refreshMetadataProvider(entityIdCriteria.getEntityId())) { log.debug("Can not find PVP metadata for entityID: {}. Metadata refreshing was done ... ", entityIdCriteria.getEntityId()); result = internalResolve(criteria); } } return result; } @Override public final void clear() throws ResolverException { for (final MetadataResolver resolver : internalResolvers) { if (resolver instanceof ClearableMetadataResolver) { ((ClearableMetadataResolver) resolver).clear(); } } } @Override public final void clear(String entityID) throws ResolverException { for (final MetadataResolver resolver : internalResolvers) { if (resolver instanceof ClearableMetadataResolver) { ((ClearableMetadataResolver) resolver).clear(entityID); } } } @Override public final void refresh() throws ResolverException { this.lastRefeshSuccessful = false; for (final MetadataResolver resolver : internalResolvers) { if (resolver instanceof RefreshableMetadataResolver) { ((RefreshableMetadataResolver) resolver).refresh(); } } this.lastRefeshTimestamp = DateTime.now(); this.lastRefeshSuccessful = true; } @Override @Nullable public final DateTime getLastUpdate() { DateTime ret = null; for (final MetadataResolver resolver : internalResolvers) { if (resolver instanceof RefreshableMetadataResolver) { final DateTime lastUpdate = ((RefreshableMetadataResolver) resolver).getLastUpdate(); if (ret == null || ret.isBefore(lastUpdate)) { ret = lastUpdate; } } } return ret; } @Override @Nullable public final DateTime getLastRefresh() { DateTime ret = null; for (final MetadataResolver resolver : internalResolvers) { if (resolver instanceof RefreshableMetadataResolver) { final DateTime lastRefresh = ((RefreshableMetadataResolver) resolver).getLastRefresh(); if (ret == null || ret.isBefore(lastRefresh)) { ret = lastRefresh; } } } return ret; } @Override public final DateTime getLastSuccessfulRefresh() { return this.lastRefeshTimestamp; } @Override public final Boolean wasLastRefreshSuccess() { return this.lastRefeshSuccessful; } @Override public final boolean isRequireValidMetadata() { log.warn("Attempt to access unsupported requireValidMetadata property on ChainingMetadataResolver"); return false; } @Override public final void setRequireValidMetadata(final boolean requireValidMetadata) { throw new UnsupportedOperationException( "Setting requireValidMetadata is not supported on chaining resolver"); } @Override public final String getId() { return getMetadataProviderId(); } @Override public final void addMetadataResolverIntoChain(MetadataResolver resolver) { internalResolvers.add(resolver); } /** * Get the URL to metadata for a specific entityID. * * @param entityId EntityId * @return URL to metadata * @throws EaafConfigurationException In case of an error */ protected abstract String getMetadataUrl(String entityId) throws EaafConfigurationException; /** * Creates a new implementation specific SAML2 metadata provider. * * @param entityId EntityId * @return MetadataResolver * @throws EaafConfigurationException In case of an error * @throws IOException In case of an error * @throws CertificateException In case of an error * @throws ConfigurationException In case of an error */ protected abstract MetadataResolver createNewMetadataProvider(String entityId) throws EaafConfigurationException, IOException, CertificateException; /** * Get a List of metadata URLs for all SAML2 SPs from configuration. * * @throws EaafConfigurationException In case of an error */ @Nonnull protected abstract List getAllMetadataUrlsFromConfiguration() throws EaafConfigurationException; /** * Get a Id for this metadata provider. * * @return */ @Nonnull protected abstract String getMetadataProviderId(); protected final MetadataResolver getMetadataResolver() { log.warn("{} does NOT support 'getMetadataResolver'", AbstractChainingMetadataProvider.class.getName()); return null; } private Map getAllActuallyLoadedResolvers() { final Map loadedproviders = new HashMap<>(); // make a Map of all actually loaded HTTPMetadataProvider for (final MetadataResolver resolver : internalResolvers) { loadedproviders.put(((IdentifiedComponent) resolver).getId(), resolver); } return loadedproviders; } private void addAndRemoveMetadataProvider() throws EaafConfigurationException { log.info("EAAF chaining metadata resolver starting internal managment task .... "); // get all actually loaded metadata providers final Map loadedproviders = getAllActuallyLoadedResolvers(); /* * TODO: maybe add metadata provider destroy after timeout. But could be a * problem if one Metadataprovider load an EntitiesDescriptor with more the * multiple EntityDescriptors. If one of this EntityDesciptors are expired the * full EntitiesDescriptor is removed. * * Timeout requires a better solution in this case! */ // load all SAML2 SPs form configuration and // compare actually loaded Providers with configured SAML2 SPs final List allMetadataUrls = getAllMetadataUrlsFromConfiguration(); final Iterator metadataUrlInterator = allMetadataUrls.iterator(); while (metadataUrlInterator.hasNext()) { final String metadataurl = metadataUrlInterator.next(); try { if (StringUtils.isNotEmpty(metadataurl) && loadedproviders.containsKey(metadataurl)) { // SAML2 SP is actually loaded, to nothing loadedproviders.remove(metadataurl); } } catch (final Throwable e) { log.error("Failed to add Metadata (unhandled reason: " + e.getMessage(), e); } } // remove all actually loaded MetadataProviders with are not in ConfigurationDB // any more final Collection notusedproviders = loadedproviders.values(); for (final MetadataResolver resolver : notusedproviders) { log.info("Remove not used MetadataProvider with MetadataURL " + resolver.getId()); destroyMetadataResolver(resolver); internalResolvers.remove(resolver); } } private EntityDescriptor resolveEntityDescripor(String entityId) throws ResolverException { final CriteriaSet criteria = new CriteriaSet(); criteria.add(new EntityIdCriterion(entityId)); return internalResolveSingle(criteria); } private void destroyMetadataResolver(MetadataResolver resolver) { if (resolver instanceof AbstractMetadataResolver) { final AbstractMetadataResolver httpprovider = (AbstractMetadataResolver) resolver; log.debug("Destroy metadata resolver with id: {}", httpprovider.getId()); httpprovider.destroy(); } else { log.warn("Metadata resolver: {} can not be destroyed. Reason: unsupported type: {}", resolver.getId(), resolver.getClass().getName()); } } /** * Close metadata provider and remove all loaded metadata. * */ private void internalDestroy() { log.info("Destroying chained metadata resolvers ..."); for (final MetadataResolver resolver : internalResolvers) { destroyMetadataResolver(resolver); } internalResolvers.clear(); } @Nullable private EntityDescriptor internalResolveSingle(@Nullable final CriteriaSet criteria) throws ResolverException { for (final MetadataResolver resolver : internalResolvers) { try { final EntityDescriptor descriptors = resolver.resolveSingle(criteria); if (descriptors != null) { return descriptors; } } catch (final ResolverException e) { continue; } } return null; } @Nonnull private Iterable internalResolve(@Nullable final CriteriaSet criteria) throws ResolverException { for (final MetadataResolver resolver : internalResolvers) { try { final Iterable descriptors = resolver.resolve(criteria); if (descriptors != null && descriptors.iterator().hasNext()) { return descriptors; } } catch (final ResolverException e) { continue; } } return Collections.emptyList(); } }