All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.opensaml.saml.metadata.resolver.impl.AbstractDynamicMetadataResolver Maven / Gradle / Ivy

There is a newer version: 4.0.1
Show newest version
/*
 * Licensed to the University Corporation for Advanced Internet Development, 
 * Inc. (UCAID) under one or more contributor license agreements.  See the 
 * NOTICE file distributed with this work for additional information regarding
 * copyright ownership. The UCAID licenses this file to You 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 org.opensaml.saml.metadata.resolver.impl;

import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import net.shibboleth.utilities.java.support.annotation.Duration;
import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
import net.shibboleth.utilities.java.support.annotation.constraint.Positive;
import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
import net.shibboleth.utilities.java.support.component.ComponentSupport;
import net.shibboleth.utilities.java.support.logic.Constraint;
import net.shibboleth.utilities.java.support.primitive.StringSupport;
import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
import net.shibboleth.utilities.java.support.resolver.ResolverException;

import org.joda.time.DateTime;
import org.joda.time.chrono.ISOChronology;
import org.opensaml.core.criterion.EntityIdCriterion;
import org.opensaml.core.xml.XMLObject;
import org.opensaml.saml.metadata.resolver.DynamicMetadataResolver;
import org.opensaml.saml.metadata.resolver.filter.FilterException;
import org.opensaml.saml.saml2.common.SAML2Support;
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Strings;

/**
 * Abstract subclass for metadata resolvers that resolve metadata dynamically, as needed and on demand.
 */
public abstract class AbstractDynamicMetadataResolver extends AbstractMetadataResolver 
        implements DynamicMetadataResolver {
    
    /** Class logger. */
    private final Logger log = LoggerFactory.getLogger(AbstractDynamicMetadataResolver.class);
    
    /** Timer used to schedule background metadata update tasks. */
    private Timer taskTimer;
    
    /** Whether we created our own task timer during object construction. */
    private boolean createdOwnTaskTimer;
    
    /** Minimum cache duration. */
    @Duration @Positive private Long minCacheDuration;
    
    /** Maximum cache duration. */
    @Duration @Positive private Long maxCacheDuration;
    
    /** Factor used to compute when the next refresh interval will occur. Default value: 0.75 */
    @Positive private Float refreshDelayFactor;
    
    /** The maximum idle time in milliseconds for which the resolver will keep data for a given entityID, 
     * before it is removed. */
    @Duration @Positive private Long maxIdleEntityData;
    
    /** Flag indicating whether idle entity data should be removed. */
    private boolean removeIdleEntityData;
    
    /** The interval in milliseconds at which the cleanup task should run. */
    @Duration @Positive private Long cleanupTaskInterval;
    
    /** The backing store cleanup sweeper background task. */
    private BackingStoreCleanupSweeper cleanupTask;
    
    /**
     * Constructor.
     *
     * @param backgroundTaskTimer the {@link Timer} instance used to run resolver background managment tasks
     */
    public AbstractDynamicMetadataResolver(@Nullable final Timer backgroundTaskTimer) {
        super();
        
        if (backgroundTaskTimer == null) {
            taskTimer = new Timer(true);
            createdOwnTaskTimer = true;
        } else {
            taskTimer = backgroundTaskTimer;
        }
        
        // Default to 10 minutes.
        minCacheDuration = 10*60*1000L;
        
        // Default to 8 hours.
        maxCacheDuration = 8*60*60*1000L;
        
        refreshDelayFactor = 0.75f;
        
        // Default to 30 minutes.
        cleanupTaskInterval = 30*60*1000L;
        
        // Default to 8 hours.
        maxIdleEntityData = 8*60*60*1000L;
        
        // Default to removing idle metadata
        removeIdleEntityData = true;
    }
    
    /**
     *  Get the minimum cache duration for metadata.
     *  
     *  

Defaults to: 10 minutes.

* * @return the minimum cache duration, in milliseconds */ @Nonnull public Long getMinCacheDuration() { return minCacheDuration; } /** * Set the minimum cache duration for metadata. * *

Defaults to: 10 minutes.

* * @param duration the minimum cache duration, in milliseconds */ public void setMinCacheDuration(@Nonnull final Long duration) { ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this); ComponentSupport.ifDestroyedThrowDestroyedComponentException(this); minCacheDuration = Constraint.isNotNull(duration, "Minimum cache duration may not be null"); } /** * Get the maximum cache duration for metadata. * *

Defaults to: 8 hours.

* * @return the maximum cache duration, in milliseconds */ @Nonnull public Long getMaxCacheDuration() { return maxCacheDuration; } /** * Set the maximum cache duration for metadata. * *

Defaults to: 8 hours.

* * @param duration the maximum cache duration, in milliseconds */ public void setMaxCacheDuration(@Nonnull final Long duration) { ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this); ComponentSupport.ifDestroyedThrowDestroyedComponentException(this); maxCacheDuration = Constraint.isNotNull(duration, "Maximum cache duration may not be null"); } /** * Gets the delay factor used to compute the next refresh time. * *

Defaults to: 0.75.

* * @return delay factor used to compute the next refresh time */ public Float getRefreshDelayFactor() { return refreshDelayFactor; } /** * Sets the delay factor used to compute the next refresh time. The delay must be between 0.0 and 1.0, exclusive. * *

Defaults to: 0.75.

* * @param factor delay factor used to compute the next refresh time */ public void setRefreshDelayFactor(Float factor) { ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this); ComponentSupport.ifDestroyedThrowDestroyedComponentException(this); if (factor <= 0 || factor >= 1) { throw new IllegalArgumentException("Refresh delay factor must be a number between 0.0 and 1.0, exclusive"); } refreshDelayFactor = factor; } /** * Get the flag indicating whether idle entity data should be removed. * * @return true if idle entity data should be removed, false otherwise */ public boolean isRemoveIdleEntityData() { return removeIdleEntityData; } /** * Set the flag indicating whether idle entity data should be removed. * * @param flag true if idle entity data should be removed, false otherwise */ public void setRemoveIdleEntityData(boolean flag) { ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this); ComponentSupport.ifDestroyedThrowDestroyedComponentException(this); removeIdleEntityData = flag; } /** * Get the maximum idle time in milliseconds for which the resolver will keep data for a given entityID, * before it is removed. * *

Defaults to: 8 hours.

* * @return return the maximum idle time in milliseconds */ @Nonnull public Long getMaxIdleEntityData() { return maxIdleEntityData; } /** * Set the maximum idle time in milliseconds for which the resolver will keep data for a given entityID, * before it is removed. * *

Defaults to: 8 hours.

* * @param max the maximum entity data idle time, in milliseconds */ public void setMaxIdleEntityData(@Nonnull final Long max) { ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this); ComponentSupport.ifDestroyedThrowDestroyedComponentException(this); maxIdleEntityData = Constraint.isNotNull(max, "Max idle entity data may not be null"); } /** * Get the interval in milliseconds at which the cleanup task should run. * *

Defaults to: 30 minutes.

* * @return return the interval, in milliseconds */ @Nonnull public Long getCleanupTaskInterval() { return cleanupTaskInterval; } /** * Set the interval in milliseconds at which the cleanup task should run. * *

Defaults to: 30 minutes.

* * @param interval the interval to set, in milliseconds */ public void setCleanupTaskInterval(@Nonnull final Long interval) { ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this); ComponentSupport.ifDestroyedThrowDestroyedComponentException(this); cleanupTaskInterval = Constraint.isNotNull(interval, "Cleanup task interval may not be null"); } /** {@inheritDoc} */ @Nonnull public Iterable resolve(@Nonnull final CriteriaSet criteria) throws ResolverException { ComponentSupport.ifNotInitializedThrowUninitializedComponentException(this); ComponentSupport.ifDestroyedThrowDestroyedComponentException(this); EntityIdCriterion entityIdCriterion = criteria.get(EntityIdCriterion.class); if (entityIdCriterion == null || Strings.isNullOrEmpty(entityIdCriterion.getEntityId())) { //TODO throw or just log? throw new ResolverException("Entity Id was not supplied in criteria set"); } String entityID = StringSupport.trimOrNull(criteria.get(EntityIdCriterion.class).getEntityId()); log.debug("Attempting to resolve metadata for entityID: {}", entityID); EntityManagementData mgmtData = getBackingStore().getManagementData(entityID); Lock readLock = mgmtData.getReadWriteLock().readLock(); try { readLock.lock(); if (!shouldAttemptRefresh(mgmtData)) { List descriptors = lookupEntityID(entityID); if (!descriptors.isEmpty()) { log.debug("Found requested metadata in backing store, returning"); return descriptors; } else { log.debug("Did not find requested metadata in backing store, will attempt to resolve dynamically"); } } else { log.debug("Metadata was indicated to be refreshed based on refresh trigger time"); } } finally { readLock.unlock(); } return resolveFromOriginSource(criteria); } /** * Fetch metadata from an origin source based on the input criteria, store it in the backing store * and then return it. * * @param criteria the input criteria set * @return the resolved metadata * @throws ResolverException if there is a fatal error attempting to resolve the metadata */ @Nonnull @NonnullElements protected Iterable resolveFromOriginSource( @Nonnull final CriteriaSet criteria) throws ResolverException { String entityID = StringSupport.trimOrNull(criteria.get(EntityIdCriterion.class).getEntityId()); EntityManagementData mgmtData = getBackingStore().getManagementData(entityID); Lock writeLock = mgmtData.getReadWriteLock().writeLock(); try { writeLock.lock(); // It's possible that multiple threads fall into here and attempt to preemptively refresh. // This check should ensure that only 1 actually successfully does it, b/c the refresh // trigger time will be updated as seen by the subsequent ones. if (!shouldAttemptRefresh(mgmtData)) { List descriptors = lookupEntityID(entityID); if (!descriptors.isEmpty()) { log.debug("Metadata was resolved and stored by another thread " + "while this thread was waiting on the write lock"); return descriptors; } } log.debug("Resolving metadata dynamically for entity ID: {}", entityID); XMLObject root = fetchFromOriginSource(criteria); if (root == null) { log.debug("No metadata was fetched from the origin source"); } else { try { processNewMetadata(root, entityID); } catch (FilterException e) { log.error("Metadata filtering problem processing new metadata", e); } } return lookupEntityID(entityID); } catch (IOException e) { log.error("Error fetching metadata from origin source", e); return lookupEntityID(entityID); } finally { writeLock.unlock(); } } /** * Fetch the metadata from the origin source. * * @param criteria the input criteria set * @return the resolved metadata root XMLObject, or null if metadata could not be fetched * @throws IOException if there is a fatal error fetching metadata from the origin source */ @Nullable protected abstract XMLObject fetchFromOriginSource(@Nonnull final CriteriaSet criteria) throws IOException; /** {@inheritDoc} */ @Nonnull @NonnullElements protected List lookupEntityID(@Nonnull String entityID) throws ResolverException { getBackingStore().getManagementData(entityID).recordEntityAccess(); return super.lookupEntityID(entityID); } /** * Process the specified new metadata document, including metadata filtering, and store the * processed metadata in the backing store. * *

* In order to be processed successfully, the metadata (after filtering) must be an instance of * {@link EntityDescriptor} and its entityID value must match the value supplied * as the required expectedEntityID argument. *

* * @param root the root of the new metadata document being processed * @param expectedEntityID the expected entityID of the resolved metadata * * @throws FilterException if there is a problem filtering the metadata */ @Nonnull protected void processNewMetadata(@Nonnull final XMLObject root, @Nonnull final String expectedEntityID) throws FilterException { XMLObject filteredMetadata = filterMetadata(root); if (filteredMetadata == null) { log.info("Metadata filtering process produced a null document, resulting in an empty data set"); return; } if (filteredMetadata instanceof EntityDescriptor) { EntityDescriptor entityDescriptor = (EntityDescriptor) filteredMetadata; if (!Objects.equals(entityDescriptor.getEntityID(), expectedEntityID)) { log.warn("New metadata's entityID '{}' does not match expected entityID '{}', will not process", entityDescriptor.getEntityID(), expectedEntityID); return; } preProcessEntityDescriptor(entityDescriptor, getBackingStore()); } else { log.warn("Document root was not an EntityDescriptor: {}", root.getClass().getName()); } } /** {@inheritDoc} */ protected void preProcessEntityDescriptor(@Nonnull EntityDescriptor entityDescriptor, @Nonnull EntityBackingStore backingStore) { String entityID = StringSupport.trimOrNull(entityDescriptor.getEntityID()); removeByEntityID(entityID, backingStore); super.preProcessEntityDescriptor(entityDescriptor, backingStore); DynamicEntityBackingStore dynamicBackingStore = (DynamicEntityBackingStore) backingStore; EntityManagementData mgmtData = dynamicBackingStore.getManagementData(entityID); DateTime now = new DateTime(ISOChronology.getInstanceUTC()); log.debug("For metadata expiration and refresh computation, 'now' is : {}", now); mgmtData.setLastUpdateTime(now); mgmtData.setExpirationTime(computeExpirationTime(entityDescriptor, now)); log.debug("Computed metadata expiration time: {}", mgmtData.getExpirationTime()); mgmtData.setRefreshTriggerTime(computeRefreshTriggerTime(mgmtData.getExpirationTime(), now)); log.debug("Computed refresh trigger time: {}", mgmtData.getRefreshTriggerTime()); } /** * Compute the effective expiration time for the specified metadata. * * @param entityDescriptor the EntityDescriptor instance to evaluate * @param now the current date time instant * @return the effective expiration time for the metadata */ @Nonnull protected DateTime computeExpirationTime(@Nonnull final EntityDescriptor entityDescriptor, @Nonnull final DateTime now) { DateTime lowerBound = now.toDateTime(ISOChronology.getInstanceUTC()).plus(getMinCacheDuration()); DateTime expiration = SAML2Support.getEarliestExpiration(entityDescriptor, now.plus(getMaxCacheDuration()), now); if (expiration.isBefore(lowerBound)) { expiration = lowerBound; } return expiration; } /** * Compute the refresh trigger time. * * @param expirationTime the time at which the metadata effectively expires * @param nowDateTime the current date time instant * * @return the time after which refresh attempt(s) should be made */ @Nonnull protected DateTime computeRefreshTriggerTime(@Nullable final DateTime expirationTime, @Nonnull final DateTime nowDateTime) { DateTime nowDateTimeUTC = nowDateTime.toDateTime(ISOChronology.getInstanceUTC()); long now = nowDateTimeUTC.getMillis(); long expireInstant = 0; if (expirationTime != null) { expireInstant = expirationTime.toDateTime(ISOChronology.getInstanceUTC()).getMillis(); } long refreshDelay = (long) ((expireInstant - now) * getRefreshDelayFactor()); // if the expiration time was null or the calculated refresh delay was less than the floor // use the floor if (refreshDelay < getMinCacheDuration()) { refreshDelay = getMinCacheDuration(); } return nowDateTimeUTC.plus(refreshDelay); } /** * Determine whether should attempt to refresh the metadata, based on stored refresh trigger time. * * @param mgmtData the entity'd management data * @return true if should attempt refresh, false otherwise */ protected boolean shouldAttemptRefresh(@Nonnull final EntityManagementData mgmtData) { DateTime now = new DateTime(ISOChronology.getInstanceUTC()); return now.isAfter(mgmtData.getRefreshTriggerTime()); } /** {@inheritDoc} */ @Nonnull protected DynamicEntityBackingStore createNewBackingStore() { return new DynamicEntityBackingStore(); } /** {@inheritDoc} */ @NonnullAfterInit protected DynamicEntityBackingStore getBackingStore() { return (DynamicEntityBackingStore) super.getBackingStore(); } /** {@inheritDoc} */ protected void initMetadataResolver() throws ComponentInitializationException { super.initMetadataResolver(); setBackingStore(createNewBackingStore()); cleanupTask = new BackingStoreCleanupSweeper(); // Start with a delay of 1 minute, run at the user-specified interval taskTimer.schedule(cleanupTask, 1*60*1000, getCleanupTaskInterval()); } /** {@inheritDoc} */ protected void doDestroy() { cleanupTask.cancel(); if (createdOwnTaskTimer) { taskTimer.cancel(); } cleanupTask = null; taskTimer = null; super.doDestroy(); } /** * Specialized entity backing store implementation for dynamic metadata resolvers. */ protected class DynamicEntityBackingStore extends EntityBackingStore { /** Map holding management data for each entityID. */ private Map mgmtDataMap; /** Constructor. */ protected DynamicEntityBackingStore() { super(); mgmtDataMap = new ConcurrentHashMap<>(); } /** * Get the management data for the specified entityID. * * @param entityID the input entityID * @return the corresponding management data */ @Nonnull public EntityManagementData getManagementData(@Nonnull final String entityID) { Constraint.isNotNull(entityID, "EntityID may not be null"); EntityManagementData entityData = mgmtDataMap.get(entityID); if (entityData != null) { return entityData; } // TODO use intern-ed String here for monitor target? synchronized (this) { // Check again in case another thread beat us into the monitor entityData = mgmtDataMap.get(entityID); if (entityData != null) { return entityData; } else { entityData = new EntityManagementData(entityID); mgmtDataMap.put(entityID, entityData); return entityData; } } } /** * Remove the management data for the specified entityID. * * @param entityID the input entityID */ public void removeManagementData(@Nonnull final String entityID) { Constraint.isNotNull(entityID, "EntityID may not be null"); // TODO use intern-ed String here for monitor target? synchronized (this) { mgmtDataMap.remove(entityID); } } } /** * Class holding per-entity management data. */ protected class EntityManagementData { /** The entity ID managed by this instance. */ private String entityID; /** Last update time of the associated metadata. */ private DateTime lastUpdateTime; /** Expiration time of the associated metadata. */ private DateTime expirationTime; /** Time at which should start attempting to refresh the metadata. */ private DateTime refreshTriggerTime; /** The last time in milliseconds at which the entity's backing store data was accessed. */ private DateTime lastAccessedTime; /** Read-write lock instance which governs access to the entity's backing store data. */ private ReadWriteLock readWriteLock; /** Constructor. * * @param id the entity ID managed by this instance */ protected EntityManagementData(@Nonnull final String id) { entityID = Constraint.isNotNull(id, "Entity ID was null"); expirationTime = new DateTime(ISOChronology.getInstanceUTC()).plus(getMaxCacheDuration()); refreshTriggerTime = new DateTime(ISOChronology.getInstanceUTC()).plus(getMaxCacheDuration()); lastAccessedTime = new DateTime(ISOChronology.getInstanceUTC()); readWriteLock = new ReentrantReadWriteLock(true); } /** * Get the entity ID managed by this instance. * * @return the entity ID */ @Nonnull public String getEntityID() { return entityID; } /** * Get the last update time of the metadata. * * @return the last update time, or null if no metadata is yet loaded for the entity */ @Nullable public DateTime getLastUpdateTime() { return lastUpdateTime; } /** * Set the last update time of the metadata. * * @param dateTime the last update time */ public void setLastUpdateTime(@Nonnull final DateTime dateTime) { lastUpdateTime = dateTime; } /** * Get the expiration time of the metadata. * * @return the expiration time */ @Nonnull public DateTime getExpirationTime() { return expirationTime; } /** * Set the expiration time of the metadata. * * @param dateTime the new expiration time */ public void setExpirationTime(@Nonnull final DateTime dateTime) { expirationTime = Constraint.isNotNull(dateTime, "Expiration time may not be null"); } /** * Get the refresh trigger time of the metadata. * * @return the refresh trigger time */ @Nonnull public DateTime getRefreshTriggerTime() { return refreshTriggerTime; } /** * Set the refresh trigger time of the metadata. * * @param dateTime the new refresh trigger time */ public void setRefreshTriggerTime(@Nonnull final DateTime dateTime) { refreshTriggerTime = Constraint.isNotNull(dateTime, "Refresh trigger time may not be null"); } /** * Get the last time at which the entity's backing store data was accessed. * * @return the time in milliseconds since the epoch */ @Nonnull public DateTime getLastAccessedTime() { return lastAccessedTime; } /** * Record access of the entity's backing store data. */ public void recordEntityAccess() { lastAccessedTime = new DateTime(ISOChronology.getInstanceUTC()); } /** * Get the read-write lock instance which governs access to the entity's backing store data. * * @return the lock instance */ @Nonnull public ReadWriteLock getReadWriteLock() { return readWriteLock; } } /** * Background maintenance task which cleans expired and idle metadata from the backing store, and removes * orphaned entity management data. */ protected class BackingStoreCleanupSweeper extends TimerTask { /** Logger. */ private final Logger log = LoggerFactory.getLogger(BackingStoreCleanupSweeper.class); /** {@inheritDoc} */ public void run() { if (isDestroyed() || !isInitialized()) { // just in case the metadata resolver was destroyed before this task runs, // or if it somehow is being called on a non-successfully-inited resolver instance. log.debug("BackingStoreCleanupSweeper will not run because: inited: {}, destroyed: {}", isInitialized(), isDestroyed()); return; } removeExpiredAndIdleMetadata(); } /** * Purge metadata which is either 1) expired or 2) (if {@link #isRemoveIdleEntityData()} is true) * which hasn't been accessed within the last {@link #getMaxIdleEntityData()} milliseconds. */ private void removeExpiredAndIdleMetadata() { DateTime now = new DateTime(ISOChronology.getInstanceUTC()); DateTime earliestValidLastAccessed = now.minus(getMaxIdleEntityData()); DynamicEntityBackingStore backingStore = getBackingStore(); Map> indexedDescriptors = backingStore.getIndexedDescriptors(); for (String entityID : indexedDescriptors.keySet()) { EntityManagementData mgmtData = backingStore.getManagementData(entityID); Lock writeLock = mgmtData.getReadWriteLock().writeLock(); try { writeLock.lock(); if (isRemoveData(mgmtData, now, earliestValidLastAccessed)) { removeByEntityID(entityID, backingStore); backingStore.removeManagementData(entityID); } } finally { writeLock.unlock(); } } } /** * Determine whether metadata should be removed based on expiration and idle time data. * * @param mgmtData the management data instance for the entity * @param now the current time * @param earliestValidLastAccessed the earliest last accessed time which would be valid * * @return true if the entity is expired or exceeds the max idle time, false otherwise */ private boolean isRemoveData(EntityManagementData mgmtData, DateTime now, DateTime earliestValidLastAccessed) { if (isRemoveIdleEntityData() && mgmtData.getLastAccessedTime().isBefore(earliestValidLastAccessed)) { log.debug("Entity metadata exceeds maximum idle time, removing: {}", mgmtData.getEntityID()); return true; } else if (now.isAfter(mgmtData.getExpirationTime())) { log.debug("Entity metadata is expired, removing: {}", mgmtData.getEntityID()); return true; } else { return false; } } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy