[java-plugin-shibd] 02/02: External agent resolver support, flat file resolver WIP.

Codeberg noreply at shibboleth.net
Tue Nov 3 20:56:06 UTC 2026


This is an automated email from the git hooks/post-receive script.

codeberg pushed a commit to branch main
in repository java-plugin-shibd.

View the commit online:
https://codeberg.org/Shibboleth/java-plugin-shibd/commit/47b3a5f25f8ebaab1c1205fb2f504db151c0c2bf

commit 47b3a5f25f8ebaab1c1205fb2f504db151c0c2bf
Author: Scott Cantor <scott at restingparrotsoftware.com>
AuthorDate: Tue Nov 3 15:55:48 2026 -0500

    External agent resolver support, flat file resolver WIP.
---
 .../net/shibboleth/sp/conf/agents-system.xml       |   3 +-
 .../sp/impl/AbstractExternalAgentResolver.java     | 966 +++++++++++++++++++++
 .../shibboleth/sp/impl/DefaultAgentResolver.java   |  40 +-
 .../sp/impl/FlatFileExternalAgentResolver.java     | 357 ++++++++
 4 files changed, 1362 insertions(+), 4 deletions(-)

diff --git a/sp-conf-impl/src/main/resources/net/shibboleth/sp/conf/agents-system.xml b/sp-conf-impl/src/main/resources/net/shibboleth/sp/conf/agents-system.xml
index dd240c6..7b06b46 100644
--- a/sp-conf-impl/src/main/resources/net/shibboleth/sp/conf/agents-system.xml
+++ b/sp-conf-impl/src/main/resources/net/shibboleth/sp/conf/agents-system.xml
@@ -16,7 +16,8 @@
     <context:annotation-config />
 
     <!-- Exposes auto-wired resolver of Agents to surrounding service instance. -->
-    <bean class="net.shibboleth.sp.impl.DefaultAgentResolver" />
+    <bean class="net.shibboleth.sp.impl.DefaultAgentResolver"
+        p:externalResolvers="#{getObject('shibboleth.sp.ExternalAgentResolvers')}" />
 
     <!-- Exposes auto-wired protocol support information from protocol plugins. -->
     <bean id="ProtocolSupportServiceManager" class="net.shibboleth.sp.impl.ProtocolSupportServiceManager" />
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/impl/AbstractExternalAgentResolver.java b/sp-server-impl/src/main/java/net/shibboleth/sp/impl/AbstractExternalAgentResolver.java
new file mode 100644
index 0000000..3a58d0a
--- /dev/null
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/impl/AbstractExternalAgentResolver.java
@@ -0,0 +1,966 @@
+/*
+ * 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 net.shibboleth.sp.impl;
+
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.security.config.SecurityConfiguration;
+import org.opensaml.security.credential.CredentialResolver;
+import org.slf4j.Logger;
+
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+
+import net.shibboleth.idp.attribute.filter.AttributeFilter;
+import net.shibboleth.idp.attribute.resolver.AttributeResolver;
+import net.shibboleth.idp.attribute.transcoding.AttributeTranscoderRegistry;
+import net.shibboleth.profile.config.ProfileConfiguration;
+import net.shibboleth.profile.relyingparty.RelyingPartyConfiguration;
+import net.shibboleth.shared.annotation.ParameterName;
+import net.shibboleth.shared.annotation.constraint.NonNegative;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.annotation.constraint.Positive;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.FunctionSupport;
+import net.shibboleth.shared.logic.PredicateSupport;
+import net.shibboleth.shared.net.IPRange;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+import net.shibboleth.shared.resolver.CriteriaSet;
+import net.shibboleth.shared.resolver.Resolver;
+import net.shibboleth.shared.resolver.ResolverException;
+import net.shibboleth.shared.service.ReloadableService;
+import net.shibboleth.sp.Agent;
+import net.shibboleth.sp.Application;
+import net.shibboleth.sp.criteria.AgentCredentialCriterion;
+import net.shibboleth.sp.criteria.AgentIDCriterion;
+import net.shibboleth.sp.state.StateManager;
+
+/**
+ * Abstract component that builds {@link Agent} objects by loading information from
+ * an external source in response to queries and applying locally configured settings.
+ * 
+ * <p>The required criteria supported are {@link AgentIDCriterion} and {@link AgentCredentialCriterion}.</p>
+ * 
+ * <p>Includes optional caching.</p>
+ * 
+ * <p>The resolver is configured with any of the possible settings that must be settable on the
+ * resulting object, which span the {@link Agent}, {@link Application}, and {@link RelyingPartyConfiguration}
+ * interfaces.</p>
+ */
+public abstract class AbstractExternalAgentResolver extends AbstractIdentifiableInitializableComponent
+        implements Resolver<Agent,CriteriaSet> {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(AbstractExternalAgentResolver.class);
+    
+    /** Protocol manager to inject into agents. */
+    @Nonnull private ProtocolSupportServiceManager protocolSupportManager;
+
+    /** Whether to enable caching. */
+    private boolean cacheEnabled;
+    
+    // Agent fields.
+    
+    /** Size of cache. */
+    @Nonnull @Positive private Integer maximumCachedElements;
+    
+    /** Sets cache expiration based on last access time. */
+    @Nullable @Positive private Duration expireAfterAccess;
+
+    /** Sets cache expiration based on entry creation. */
+    @Nullable @Positive private Duration expireAfterWrite;
+    
+    /** Agent resolution cache. */
+    @Nullable private Cache<CriteriaSet,Agent> resultsCache;
+
+    /** Allowed address ranges. */
+    @Nonnull private Set<IPRange> allowedAddressRanges;
+    
+    /** Whether cached authentication is supported. */
+    private boolean supportsCachedAuthentication;
+    
+    /** Authentication method. */
+    @Nullable @NotEmpty String authenticationMethod;
+
+    /** Whether to support form submission preservation and recovery. */
+    private boolean supportsPostPreservation;
+    
+    /** Limit on size of form data to preserve. */
+    private long postLimit;
+    
+    /** Character set encoding for POST recovery. */
+    @Nonnull private Charset encoding;
+    
+    /** Authorized utility flows. */
+    @Nonnull private Set<String> authorizedOperations;
+ 
+    // Default Application fields.
+
+    /** Lookup strategy for authenticating authority. */
+    @Nonnull private Function<ProfileRequestContext,String> authenticatingAuthorityLookupStrategy;
+    
+    /** Session initiator list lookup strategy. */
+    @Nullable private Function<ProfileRequestContext,List<String>> sessionInitiatorLookupStrategy;
+
+    /** Discovery service lookup strategy. */
+    @Nonnull private Function<ProfileRequestContext,String> discoveryServiceLookupStrategy;
+    
+    /** Token consumer list lookup strategy. */
+    @Nullable private Function<ProfileRequestContext,List<String>> tokenConsumerLookupStrategy;
+
+    /** Logout initiator list lookup strategy. */
+    @Nullable private Function<ProfileRequestContext,List<String>> logoutInitiatorLookupStrategy;
+
+    /** Logout consumer list lookup strategy. */
+    @Nullable private Function<ProfileRequestContext,List<String>> logoutConsumerLookupStrategy;
+
+    /** Logout admin list lookup strategy. */
+    @Nullable private Function<ProfileRequestContext,List<String>> logoutAdminLookupStrategy;
+    
+    /** State management. */
+    @NonnullAfterInit private StateManager stateManager;
+    
+    /** Transcoding registry. */
+    @NonnullAfterInit private ReloadableService<AttributeTranscoderRegistry> transcodingRegistry;
+    
+    /** Attribute resolver. */
+    @NonnullAfterInit private ReloadableService<AttributeResolver> attributeResolver;
+    
+    /** Attribute filter. */
+    @NonnullAfterInit private ReloadableService<AttributeFilter> attributeFilter;
+    
+    /** Access to credentials indirectly. */
+    @Nullable private CredentialResolver credentialResolver;
+    
+    // Default RelyingPartyConfiguration fields.
+    
+    /** Lookup function to supply issuer. */
+    @Nonnull private Function<ProfileRequestContext,String> issuerLookupStrategy;
+
+    /** Controls whether detailed information about errors should be exposed. */
+    @Nonnull private Predicate<ProfileRequestContext> detailedErrorsPredicate;
+
+    /** Lookup function to supply securityConfiguration property. */
+    @Nonnull private Function<ProfileRequestContext,SecurityConfiguration> securityConfigurationLookupStrategy;
+    
+    /** Lookup function to supply <code>profileConfigurations</code> property. */
+    @Nonnull
+    private Function<ProfileRequestContext,Map<String,ProfileConfiguration>> profileConfigurationsLookupStrategy;
+    
+    /**
+     * Constructor.
+     * 
+     * @param protocolManager  injected plugin capability objects
+     */
+    public AbstractExternalAgentResolver(
+            @ParameterName(name="protocolManager") @Nonnull final ProtocolSupportServiceManager protocolManager) {
+        protocolSupportManager = Constraint.isNotNull(protocolManager, "Protocol manager cannot be null");
+        
+        maximumCachedElements = 100;
+        expireAfterAccess = Duration.ofHours(4);
+        
+        // Agent fields.
+        allowedAddressRanges = CollectionSupport.emptySet();
+        supportsCachedAuthentication = true;
+        authenticationMethod  = null;
+        supportsPostPreservation = false;
+        postLimit = 1024 * 1024;
+        encoding = StandardCharsets.UTF_8;
+        authorizedOperations = CollectionSupport.emptySet();
+        
+        // Default Application fields.
+        authenticatingAuthorityLookupStrategy = FunctionSupport.constant(null);
+        discoveryServiceLookupStrategy = FunctionSupport.constant(null);
+
+        // Default RelyingPartyConfiguration fields.
+        issuerLookupStrategy = FunctionSupport.constant(null);
+        detailedErrorsPredicate = PredicateSupport.alwaysFalse();
+        securityConfigurationLookupStrategy = FunctionSupport.constant(null);
+        profileConfigurationsLookupStrategy = FunctionSupport.constant(null);
+        
+    }
+    
+    /**
+     * Sets whether to enable caching.
+     * 
+     * <p>Defaults to false.</p>
+     * 
+     * @param flag flag to set
+     */
+    public void setCacheEnabled(final boolean flag) {
+        cacheEnabled = flag;
+    }
+    
+    /**
+     * Sets the maximum cache size.
+     * 
+     * <p>Defaults to 100.</p>
+     * 
+     * @param max maximum size
+     */
+    public void setMaximumCachedElements(@Nonnull @Positive final Integer max) {
+        checkSetterPreconditions();
+        
+        maximumCachedElements = Constraint.isGreaterThan(0, max, "Maximum cache size must be greater than zero");
+    }
+    
+    /**
+     * Sets the cache policy to expire entries after access, with the time reset on each access.
+     * 
+     * <p>Defaults to 4 hours.</p>
+     * 
+     * <p>Mutually exclusive with {@link #setExpireAfterWrite(Duration)}.</p>
+     * 
+     * @param exp threshold
+     */
+    public void setExpireAfterAccess(@Nullable @Positive final Duration exp) {
+        checkSetterPreconditions();
+        
+        if (exp != null) {
+            Constraint.isFalse(exp.isNegative() || exp.isZero(), "Thresold must be greater than zero");
+            expireAfterAccess = exp;
+        } else {
+            expireAfterAccess = null;
+        }
+    }
+
+    /**
+     * Sets the cache policy to expire entries after creation.
+     * 
+     * <p>Defaults to null.</p>
+     * 
+     * <p>Mutually exclusive with {@link #setExpireAfterAccess(Duration)}.</p>
+     * 
+     * @param exp threshold
+     */
+    public void setExpireAfterWrite(@Nullable @Positive final Duration exp) {
+        checkSetterPreconditions();
+        
+        if (exp != null) {
+            Constraint.isFalse(exp.isNegative() || exp.isZero(), "Thresold must be greater than zero");
+            expireAfterWrite = exp;
+        } else {
+            expireAfterWrite = null;
+        }
+    }
+    
+    /**
+     * Sets the network ranges from which this loader's agents may make requests.
+     * 
+     * @param ranges allowed ranges
+     */
+    public void setAllowedAddressRanges(@Nullable final Set<IPRange> ranges) {
+        checkSetterPreconditions();
+        
+        allowedAddressRanges = ranges != null ? CollectionSupport.copyToSet(ranges) : CollectionSupport.emptySet();
+    }
+    
+    /**
+     * Sets the authentication method to require for this loader's agents.
+     * 
+     * <p>Defaults to {@link Agent#AUTH_METHOD_BASIC}.</p>
+     * 
+     * @param method method identifier
+     */
+    public void setAuthenticationMethod(@Nullable @NotEmpty final String method) {
+        checkSetterPreconditions();
+        
+        final String trimmed = StringSupport.trimOrNull(method);
+        Constraint.isTrue(trimmed == null || Agent.AUTH_METHOD_BASIC.equals(trimmed),
+                "Unsupported authentication method");
+        authenticationMethod = trimmed;
+    }
+    
+    /**
+     * Sets whether this loader's agents support cached authentication via cookie.
+     * 
+     * <p>Defaults to true.</p>
+     * 
+     * @param flag flag to set
+     */
+    public void setSupportsCachedAuthentication(final boolean flag) {
+        checkSetterPreconditions();
+        
+        supportsCachedAuthentication = flag;
+    }
+    
+    /**
+     * Sets the set of "utility" operations authorized for use by this loader's agents.
+     * 
+     * <p>Defaults to empty.</p>
+     * 
+     * @param ops authorized operation names
+     */
+    public void setAuthorizedUtilityOperations(@Nullable final Collection<String> ops) {
+        checkSetterPreconditions();
+        
+        if (ops != null) {
+            authorizedOperations = CollectionSupport.copyToSet(StringSupport.normalizeStringCollection(ops));
+        } else {
+            authorizedOperations = CollectionSupport.emptySet();
+        }
+    }
+    
+    /**
+     * Sets whether this loader's agents are permitted to submit form data for preservation during SSO.
+     * 
+     * @param flag flag to set
+     */
+    public void setSupportsPostPreservation(final boolean flag) {
+        checkSetterPreconditions();
+        
+        supportsPostPreservation = flag;
+    }
+    
+    /**
+     * Sets limit on size of form data to preserve.
+     * 
+     * @param limit size limit or 0 for none
+     */
+    public void setPostLimit(@NonNegative final long limit) {
+        checkSetterPreconditions();
+        
+        postLimit = Constraint.isGreaterThanOrEqual(0, limit, "Post limit cannot be negative.");
+    }
+    
+    /**
+     * Sets the character encoding to apply for POST recovery.
+     * 
+     * @param name name of encoding
+     */
+    public void setCharacterEncoding(@Nonnull final String name) {
+        encoding = Charset.forName(name);
+    }
+
+    /**
+     * Sets the identifier of the authenticating authority (IdP, OP, etc.) to use for this loader's agents.
+     * 
+     * @param authority authority name
+     */
+    public void setAuthenticatingAuthority(@Nullable @NotEmpty final String authority) {
+        checkSetterPreconditions();
+        
+        authenticatingAuthorityLookupStrategy = FunctionSupport.constant(StringSupport.trimOrNull(authority));
+    }
+    
+    /**
+     * Sets the discovery service URL to use in the event a session initiator cannot be executed.
+     * 
+     * @param url discovery service URL
+     */
+    public void setDiscoveryService(@Nullable final String url) {
+        checkSetterPreconditions();
+        
+        discoveryServiceLookupStrategy = FunctionSupport.constant(url);
+    }
+    
+    /**
+     * Sets the lookup strategy for the discovery service URL to use in the event a session initiator cannot be executed.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setDiscoveryServiceLookupStrategy(@Nonnull final Function<ProfileRequestContext,String> strategy) {
+        checkSetterPreconditions();
+        
+        discoveryServiceLookupStrategy =
+                Constraint.isNotNull(strategy, "Discovery service lookup strategy cannot be null");
+    }
+    
+    /**
+     * Sets the ordered collection of unprefixed session initiator flow IDs to use for this loader's agents.
+     * 
+     * @param initiators ordered collection of unprefixed session initiator flow IDs
+     */
+    public void setSessionInitiators(@Nullable final Collection<String> initiators) {
+        checkSetterPreconditions();
+        
+        if (initiators != null) {
+            sessionInitiatorLookupStrategy = FunctionSupport.constant(
+                    CollectionSupport.copyToList(StringSupport.normalizeStringCollection(initiators)));
+        } else {
+            sessionInitiatorLookupStrategy = FunctionSupport.constant(CollectionSupport.emptyList());
+        }
+    }
+    
+    /**
+     * Sets the lookup strategy for the ordered collection of unprefixed session initiator flow IDs
+     * to use for this loader's agents.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setSessionInitiatorsLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,List<String>> strategy) {
+        checkSetterPreconditions();
+        
+        sessionInitiatorLookupStrategy = Constraint.isNotNull(strategy,
+                "Session initiators lookup strategy cannot be null");
+    }
+    
+    /**
+     * Sets the ordered collection of unprefixed token consumer flow IDs to use for this loader's agents.
+     * 
+     * @param consumers ordered collection of unprefixed token consumer flow IDs
+     */
+    public void setTokenConsumers(@Nullable final Collection<String> consumers) {
+        checkSetterPreconditions();
+        
+        if (consumers != null) {
+            tokenConsumerLookupStrategy = FunctionSupport.constant(
+                    CollectionSupport.copyToList(StringSupport.normalizeStringCollection(consumers)));
+        } else {
+            tokenConsumerLookupStrategy = FunctionSupport.constant(CollectionSupport.emptyList());
+        }
+    }
+    
+    /**
+     * Sets the lookup strategy for the ordered collection of unprefixed token consumer flow IDs
+     * to use for this loader's agents.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setTokenConsumersLookupStrategy(@Nonnull final Function<ProfileRequestContext,List<String>> strategy) {
+        checkSetterPreconditions();
+        
+        tokenConsumerLookupStrategy = Constraint.isNotNull(strategy, "Token consumers lookup strategy cannot be null");
+    }
+
+    /**
+     * Sets the ordered collection of unprefixed logout initiator flow IDs to use for this loader's agents.
+     * 
+     * @param initiators ordered collection of unprefixed logout initiator flow IDs
+     */
+    public void setLogoutInitiators(@Nullable final Collection<String> initiators) {
+        checkSetterPreconditions();
+        
+        if (initiators != null) {
+            logoutInitiatorLookupStrategy = FunctionSupport.constant(
+                    CollectionSupport.copyToList(StringSupport.normalizeStringCollection(initiators)));
+        } else {
+            logoutInitiatorLookupStrategy = FunctionSupport.constant(CollectionSupport.emptyList());
+        }
+    }
+    
+    /**
+     * Sets the lookup strategy for the ordered collection of unprefixed logout initiator flow IDs
+     * to use for this loader's agents.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setLogoutInitiatorsLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,List<String>> strategy) {
+        checkSetterPreconditions();
+        
+        logoutInitiatorLookupStrategy = Constraint.isNotNull(strategy,
+                "Logout initiators lookup strategy cannot be null");
+    }
+
+    /**
+     * Sets the ordered collection of unprefixed logout consumer flow IDs to use for this loader's agents.
+     * 
+     * @param consumers ordered collection of unprefixed logout consumer flow IDs
+     */
+    public void setLogoutConsumers(@Nullable final Collection<String> consumers) {
+        checkSetterPreconditions();
+        
+        if (consumers != null) {
+            logoutConsumerLookupStrategy = FunctionSupport.constant(
+                    CollectionSupport.copyToList(StringSupport.normalizeStringCollection(consumers)));
+        } else {
+            logoutConsumerLookupStrategy = FunctionSupport.constant(CollectionSupport.emptyList());
+        }
+    }
+    
+    /**
+     * Sets the lookup strategy for the ordered collection of unprefixed logout consumer flow IDs
+     * to use for this loader's agents.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setLogoutConsumersLookupStrategy(@Nonnull final Function<ProfileRequestContext,List<String>> strategy) {
+        checkSetterPreconditions();
+        
+        logoutConsumerLookupStrategy = Constraint.isNotNull(strategy, "Logout consumers lookup strategy cannot be null");
+    }
+    
+    /**
+     * Sets the ordered collection of unprefixed admin logout flow IDs to use for this loader's agents.
+     * 
+     * @param admins ordered collection of unprefixed admin logout flow IDs
+     */
+    public void setLogoutAdmins(@Nullable final Collection<String> admins) {
+        checkSetterPreconditions();
+        
+        if (admins != null) {
+            logoutAdminLookupStrategy = FunctionSupport.constant(
+                    CollectionSupport.copyToList(StringSupport.normalizeStringCollection(admins)));
+        } else {
+            logoutAdminLookupStrategy = FunctionSupport.constant(CollectionSupport.emptyList());
+        }
+    }
+    
+    /**
+     * Sets the lookup strategy for the ordered collection of unprefixed admin logout flow IDs
+     * to use for this loader's agents.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setLogoutAdminsLookupStrategy(@Nonnull final Function<ProfileRequestContext,List<String>> strategy) {
+        checkSetterPreconditions();
+        
+        logoutAdminLookupStrategy = Constraint.isNotNull(strategy, "Logout admins lookup strategy cannot be null");
+    }
+    
+    /**
+     * Set the {@link StateManager} to use.
+     * 
+     * @param manager state manager
+     */
+    public void setStateManager(@Nonnull final StateManager manager) {
+        checkSetterPreconditions();
+        
+        stateManager = Constraint.isNotNull(manager, "StateManager cannot be null");
+    }
+
+    /**
+     * Set the {@link AttributeTranscoderRegistry} to use.
+     * 
+     * @param service registry service
+     */
+    public void setAttributeTranscoderRegistry(@Nonnull final ReloadableService<AttributeTranscoderRegistry> service) {
+        checkSetterPreconditions();
+        
+        transcodingRegistry = Constraint.isNotNull(service, "AttributeTranscoderRegistry service cannot be null");
+    }
+
+    /**
+     * Set the {@link AttributeResolver} to use.
+     * 
+     * @param service resolver service
+     */
+    public void setAttributeResolver(@Nonnull final ReloadableService<AttributeResolver> service) {
+        checkSetterPreconditions();
+        
+        attributeResolver = Constraint.isNotNull(service, "AttributeTranscoderRegistry service cannot be null");
+    }
+
+    /**
+     * Set the {@link AttributeFilter} to use.
+     * 
+     * @param service filter service
+     */
+    public void setAttributeFilter(@Nonnull final ReloadableService<AttributeFilter> service) {
+        checkSetterPreconditions();
+        
+        attributeFilter = Constraint.isNotNull(service, "AttributeFilter service cannot be null");
+    }
+    
+    /**
+     * Sets the lookup strategy for the identifier of the authenticating authority (IdP, OP, etc.) to use
+     * for this loader's agents.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setAuthenticatingAuthorityLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,String> strategy) {
+        checkSetterPreconditions();
+        
+        authenticatingAuthorityLookupStrategy =
+                Constraint.isNotNull(strategy, "Authority lookup strategy cannot be null");
+    }
+    
+    /**
+     * Sets a {@link CredentialResolver} to use for resolving keys and certificates "indirectly"
+     * in support of the deprecated older methods.
+     * 
+     * <p>This method should not be relied on "publically", and is solely used internally for
+     * backward compatibility.</p>
+     * 
+     * @param resolver credential resolver
+     * 
+     * @deprecated
+     */
+    @Deprecated(since="1.0.0", forRemoval=true)
+    public void setCredentialResolver(@Nullable final CredentialResolver resolver) {
+        checkSetterPreconditions();
+        
+        credentialResolver = resolver;
+    }
+    
+    /**
+     * Set the name to use when issuing messages to peers.
+     * 
+     * @param issuer name to set
+     */
+    public void setIssuer(@Nullable @NotEmpty final String issuer) {
+        checkSetterPreconditions();
+        
+        issuerLookupStrategy = FunctionSupport.constant(issuer);
+    }
+
+    /**
+     * Set a lookup strategy for the name to use when issuing messages to peers.
+     * 
+     * @param strategy  lookup strategy
+     */
+    public void setIssuerLookupStrategy(@Nonnull final Function<ProfileRequestContext,String> strategy) {
+        checkSetterPreconditions();
+        
+        issuerLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+    }
+    
+    /**
+     * Set whether detailed information about errors should be exposed.
+     * 
+     * @param flag  flag to set
+     */
+    public void setDetailedErrors(final boolean flag) {
+        checkSetterPreconditions();
+        
+        detailedErrorsPredicate = PredicateSupport.constant(flag);
+    }
+    
+    /**
+     * Set a condition to determine whether detailed information about errors should be exposed.
+     * 
+     * @param condition  condition to set
+     */
+    public void setDetailedErrorsPredicate(@Nonnull final Predicate<ProfileRequestContext> condition) {
+        checkSetterPreconditions();
+        
+        detailedErrorsPredicate = Constraint.isNotNull(condition, "Condition cannot be null");
+    }
+
+    /**
+     * Sets the security configuration for this profile.
+     * 
+     * @param configuration security configuration for this profile
+     */
+    public void setSecurityConfiguration(@Nullable final SecurityConfiguration configuration) {
+        checkSetterPreconditions();
+        
+        securityConfigurationLookupStrategy = FunctionSupport.constant(configuration);
+    }
+
+    /**
+     * Set a lookup strategy for the security configuration.
+     *
+     * @param strategy  lookup strategy
+     */
+    public void setSecurityConfigurationLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,SecurityConfiguration> strategy) {
+        securityConfigurationLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+    }
+
+    /**
+     * Set the profile configurations for this relying party.
+     * 
+     * @param configs the configurations to set
+     */
+    public void setProfileConfigurations(@Nullable final Collection<ProfileConfiguration> configs) {
+        checkSetterPreconditions();
+        
+        if (configs == null) {
+            profileConfigurationsLookupStrategy = FunctionSupport.constant(null);
+        } else {
+            final HashMap<String,ProfileConfiguration> map = new HashMap<>();
+            for (final ProfileConfiguration config : configs) {
+                final String trimmedId =
+                        Constraint.isNotNull(StringSupport.trimOrNull(config.getId()),
+                                "ID of profile configuration class " + config.getClass().getName() + " cannot be null");
+                map.put(trimmedId, config);
+            }
+            profileConfigurationsLookupStrategy = FunctionSupport.constant(map);
+        }
+    }
+
+    /**
+     * Set a lookup strategy for the <code>profileConfigurations</code> property.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setProfileConfigurationsLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,Map<String,ProfileConfiguration>> strategy) {
+        checkSetterPreconditions();
+        
+        profileConfigurationsLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+    }    
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (stateManager == null) {
+            throw new ComponentInitializationException("StateManager cannot be null");
+        } else if (transcodingRegistry == null) {
+            throw new ComponentInitializationException("AttributeTranscoderRegistry cannot be null");
+        } else if (attributeResolver == null) {
+            throw new ComponentInitializationException("AttributeResolver cannot be null");
+        } else if (attributeFilter == null) {
+            throw new ComponentInitializationException("AttributeFilter cannot be null");
+        }
+        
+        if (cacheEnabled) {
+            final CacheBuilder<Object,Object> builder = CacheBuilder.newBuilder();
+            builder.maximumSize(maximumCachedElements);
+            if (expireAfterAccess != null) {
+                if (expireAfterWrite != null) {
+                    log.warn("AgentResolver {}: ignoring expireAfterWrite, expireAfterAccess is set", getId());
+                }
+                builder.expireAfterAccess(expireAfterAccess);
+            } else if (expireAfterWrite != null) {
+                builder.expireAfterWrite(expireAfterWrite);
+            } else {
+                throw new ComponentInitializationException("Cache enabled, but expireAfterAccess/expireAfterWrite both null");
+            }
+            resultsCache = builder.build();
+        }
+    }
+    
+    /** {@inheritDoc} */
+    @Nullable public Agent resolveSingle(@Nullable final CriteriaSet criteria) throws ResolverException {
+        checkComponentActive();
+        
+        if (criteria == null) {
+            return null;
+        }
+        
+        final Cache<CriteriaSet,Agent> localCache = resultsCache;
+        
+        // Check cache first if it exists.
+        if (localCache != null) {
+            final Agent cached = localCache.getIfPresent(criteria);
+            if (cached != null) {
+                log.debug("AgentResolver {}: Resolved Agent from cache", getId());
+                return cached;
+            }
+        }
+        
+        // Resolve from subclass and build new Agent.
+        
+        final AgentTemplate template = doResolve(criteria);
+        if (template == null) {
+            return null;
+        }
+
+        try {
+            final BasicAgent agent = new BasicAgent(protocolSupportManager);
+            
+            // Copy in template-only settings.
+            agent.setId(template.getId());
+            agent.setUsernames(template.getUsernames());
+            
+            // Copy over loader's configuration.
+
+            if (!template.getAddressRanges().isEmpty()) {
+                agent.setAllowedAddressRanges(template.getAddressRanges());
+            } else {
+                agent.setAllowedAddressRanges(allowedAddressRanges);
+            }
+            
+            agent.setAuthenticationMethod(authenticationMethod);
+            agent.setSupportsCachedAuthentication(supportsCachedAuthentication);
+            agent.setAuthorizedUtilityOperations(authorizedOperations);
+            agent.setSupportsPostPreservation(supportsPostPreservation);
+            agent.setPostLimit(postLimit);
+            agent.setCharacterEncoding(encoding.name());
+            
+            if (template.getAuthority() != null) {
+                agent.setAuthenticatingAuthority(template.getAuthority());
+            } else {
+                agent.setAuthenticatingAuthorityLookupStrategy(authenticatingAuthorityLookupStrategy);
+            }
+            
+            agent.setDiscoveryServiceLookupStrategy(discoveryServiceLookupStrategy);
+            agent.setStateManager(stateManager);
+            agent.setAttributeTranscoderRegistry(transcodingRegistry);
+            agent.setAttributeResolver(attributeResolver);
+            agent.setAttributeFilter(attributeFilter);
+            agent.setCredentialResolver(credentialResolver);            
+            if (sessionInitiatorLookupStrategy != null) {
+                agent.setSessionInitiatorsLookupStrategy(sessionInitiatorLookupStrategy);
+            }
+            if (tokenConsumerLookupStrategy != null) {
+                agent.setTokenConsumersLookupStrategy(tokenConsumerLookupStrategy);
+            }
+            if (logoutInitiatorLookupStrategy != null) {
+                agent.setLogoutInitiatorsLookupStrategy(logoutInitiatorLookupStrategy);
+            }
+            if (logoutConsumerLookupStrategy != null) {
+                agent.setLogoutConsumersLookupStrategy(logoutConsumerLookupStrategy);
+            }
+            if (logoutAdminLookupStrategy != null) {
+                agent.setLogoutAdminsLookupStrategy(logoutAdminLookupStrategy);
+            }
+            
+            if (template.getIssuer() != null) {
+                agent.setIssuer(template.getIssuer());
+            } else {
+                agent.setIssuerLookupStrategy(issuerLookupStrategy);
+            }
+            agent.setDetailedErrorsPredicate(detailedErrorsPredicate);
+            agent.setSecurityConfigurationLookupStrategy(securityConfigurationLookupStrategy);
+            agent.setProfileConfigurationsLookupStrategy(profileConfigurationsLookupStrategy);
+            
+            agent.initialize();
+            return agent;
+            
+        } catch (final Exception e) {
+            throw new ResolverException(e);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Nonnull public Iterable<Agent> resolve(@Nullable final CriteriaSet criteria) throws ResolverException {
+        
+        final Agent agent = resolveSingle(criteria);
+        return agent != null ? CollectionSupport.singletonList(agent) : CollectionSupport.emptyList();
+    }
+    
+    /**
+     * Performs resolution of a specific named agent record in the form of a template object
+     * which the base class will fully configure and resolve.
+     * 
+     * @param criteria resolution criteria
+     * 
+     * @return agent template record or null if absent
+     * 
+     * @throws ResolverException if an error occurs
+     */
+    @Nullable public abstract AgentTemplate doResolve(@Nonnull final CriteriaSet criteria) throws ResolverException;
+
+    /**
+     * Stub record representing a "partial" {@link Agent} definition that the loader
+     * base class will "wrap" with the settings configured on the loader. 
+     */
+    public static class AgentTemplate {
+
+        /** Agent ID. */
+        @Nonnull @NotEmpty private String agentID;
+        
+        /** Issuer. */
+        @Nullable @NotEmpty private String issuer;
+        
+        /** Authority. */
+        @Nullable @NotEmpty private String authority;
+        
+        /** Address ranges. */
+        @Nonnull private Set<IPRange> addressRanges;
+        
+        /** Usernames. */
+        @Nonnull private Collection<String> usernames;
+        
+        /**
+         * Constructor.
+         * 
+         * @param id        agent ID 
+         * @param iss       issuer
+         * @param auth      authority
+         * @param ranges    address ranges
+         * @param unames    usernames
+         * 
+         */
+        public AgentTemplate(@Nonnull @NotEmpty final String id, @Nullable @NotEmpty final String iss,
+                @Nullable @NotEmpty final String auth, @Nullable final Set<String> ranges,
+                @Nullable final Collection<String> unames) {
+            
+            agentID = Constraint.isNotNull(StringSupport.trimOrNull(id), "ID cannot be null or empty");
+            
+            issuer = StringSupport.trimOrNull(iss);
+            authority = StringSupport.trimOrNull(auth);
+            
+            if (ranges != null) {
+                addressRanges = new HashSet<>();
+                ranges.stream()
+                    .map(IPRange::parseCIDRBlock)
+                    .collect(CollectionSupport.nonnullCollector(Collectors.toUnmodifiableSet())).get();
+            } else {
+                addressRanges = CollectionSupport.emptySet();
+            }
+            
+            if (unames != null) {
+                usernames = StringSupport.normalizeStringCollection(unames);
+            } else {
+                usernames = CollectionSupport.emptySet();
+            }
+        }
+        
+        /**
+         * Get the agent ID.
+         * 
+         * @return agent ID
+         */
+        @Nonnull @NotEmpty public String getId() {
+            return agentID;
+        }
+        
+        /**
+         * Get the issuer, if set.
+         * 
+         * @return issuer or null
+         */
+        @Nullable @NotEmpty public String getIssuer() {
+            return issuer;
+        }
+
+        /**
+         * Get the authenticating authority to use, if set.
+         * 
+         * @return authority or null
+         */
+        @Nullable @NotEmpty public String getAuthority() {
+            return authority;
+        }
+        
+        /**
+         * Get the allowed address ranges.
+         * 
+         * @return allowed address ranges
+         */
+        @Nonnull public Set<IPRange> getAddressRanges() {
+            return addressRanges;
+        }
+        
+        /**
+         * Get the alternative usernames.
+         * 
+         * @return usernames
+         */
+        @Nonnull public Collection<String> getUsernames() {
+            return usernames;
+        }
+    }
+    
+}
\ No newline at end of file
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/impl/DefaultAgentResolver.java b/sp-server-impl/src/main/java/net/shibboleth/sp/impl/DefaultAgentResolver.java
index ef6244c..b271332 100644
--- a/sp-server-impl/src/main/java/net/shibboleth/sp/impl/DefaultAgentResolver.java
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/impl/DefaultAgentResolver.java
@@ -16,6 +16,7 @@ package net.shibboleth.sp.impl;
 
 import java.util.Collection;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 import javax.annotation.Nonnull;
@@ -40,9 +41,12 @@ import net.shibboleth.sp.criteria.AgentCredentialCriterion;
  */
 public class DefaultAgentResolver extends IdentifiedComponentManager<Agent> implements AgentResolver {
     
-    /** Agent set. */
+    /** Agent map. */
     @NonnullAfterInit private Map<String,Agent> agentMap;
     
+    /** External resolvers to run. */
+    @Nonnull private List<AgentResolver> externalResolvers;
+    
     /** Class logger. */
     @Nonnull private final Logger log = LoggerFactory.getLogger(DefaultAgentResolver.class);
 
@@ -54,6 +58,24 @@ public class DefaultAgentResolver extends IdentifiedComponentManager<Agent> impl
     @Autowired
     public DefaultAgentResolver(@Nullable final Collection<Agent> agents) {
         super(agents);
+        
+        externalResolvers = CollectionSupport.emptyList();
+    }
+    
+    /**
+     * Sets a list of external resolvers to query if the in-memory map does not contain
+     * a matching {@link Agent}.
+     * 
+     * @param resolvers
+     */
+    public void setExternalResolvers(@Nullable final List<AgentResolver> resolvers) {
+        checkSetterPreconditions();
+        
+        if (resolvers != null) {
+            externalResolvers = CollectionSupport.copyToList(resolvers);
+        } else {
+            externalResolvers = CollectionSupport.emptyList();
+        }
     }
         
     /** {@inheritDoc} */
@@ -92,11 +114,23 @@ public class DefaultAgentResolver extends IdentifiedComponentManager<Agent> impl
         
         if (criteria != null) {
             final AgentCredentialCriterion c_agent = criteria.get(AgentCredentialCriterion.class);
-            if (c_agent != null) {
-                log.trace("Looking up agent via username: {}", c_agent.getId());
+            if (c_agent != null && agentMap.containsKey(c_agent.getId())) {
+                log.trace("Located internally defined agent via username: {}", c_agent.getId());
                 return agentMap.get(c_agent.getId());
             }
         }
+        
+        for (final AgentResolver resolver : externalResolvers) {
+            assert resolver != null;
+            try {
+                final Agent agent = resolver.resolveSingle(criteria);
+                if (agent != null) {
+                    return agent;
+                }
+            } catch (final ResolverException e) {
+                log.error("External resolver raised exceptoon", e);
+            }
+        }
 
         return null;
     }
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/impl/FlatFileExternalAgentResolver.java b/sp-server-impl/src/main/java/net/shibboleth/sp/impl/FlatFileExternalAgentResolver.java
new file mode 100644
index 0000000..a961c59
--- /dev/null
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/impl/FlatFileExternalAgentResolver.java
@@ -0,0 +1,357 @@
+/*
+ * 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 net.shibboleth.sp.impl;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.Timer;
+import java.util.TimerTask;
+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 org.slf4j.Logger;
+
+import net.shibboleth.shared.annotation.ParameterName;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.annotation.constraint.NotLive;
+import net.shibboleth.shared.annotation.constraint.Unmodifiable;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.TimerSupport;
+import net.shibboleth.shared.resolver.CriteriaSet;
+import net.shibboleth.shared.resolver.ResolverException;
+import net.shibboleth.shared.resource.Resource;
+import net.shibboleth.sp.criteria.AgentCredentialCriterion;
+import net.shibboleth.sp.criteria.AgentIDCriterion;
+
+/**
+ * External agent source that relies on a flat file.
+ * 
+ * <p>The format of this file is similar to an LDIF file due to the variability of fields supported
+ * and the need for multiple values in some of the fields. Entries are separated by empty lines and
+ * lines that begin with a '#' are comments and ignored.
+ * 
+ * Each valid line consists of a field specifier followed by a colon, followed by a single field value.
+ * All values are trimmed of leading and trailing whitespace.
+ * 
+ * Supported fields:</p>
+ * 
+ * <dl>
+ *  <dt>id</dt>
+ *  <dd>Agent ID</dd>
+ *  
+ *  <dt>allowedRange</dt>
+ *  <dd>Allowed address range</dd>
+ *  
+ *  <dt>username</dt>
+ *  <dd>An alternative agent username</dd>
+ *  
+ *  <dt>issuer</dt>
+ *  <dd>Issuer value for Agent protocol messages</dd>
+ *  
+ *  <dt>authority</dt>
+ *  <dd>Authenticating authority</dd>
+ * </dl>
+ * 
+ * <p>Only the "id" field is required. Both "allowedRange" and "username" may appear any number of times.</p> 
+ */
+public class FlatFileExternalAgentResolver extends AbstractExternalAgentResolver {
+
+    /** Agent ID field */
+    @Nonnull @NotEmpty public static final String ID_FIELD = "id:";
+
+    /** IP Range field */
+    @Nonnull @NotEmpty public static final String ALLOWED_RANGE_FIELD = "allowedRange:";
+
+    /** Username field */
+    @Nonnull @NotEmpty public static final String USERNAME_FIELD = "username:";
+
+    /** Issuer field */
+    @Nonnull @NotEmpty public static final String ISSUER_FIELD = "issuer:";
+
+    /** Authority field */
+    @Nonnull @NotEmpty public static final String AUTHORITY_FIELD = "authority:";
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(FlatFileExternalAgentResolver.class);
+    
+    /** Source of information. */
+    @Nullable private Resource storageResource;
+    
+    /** Optional reload interval. */
+    @Nullable private Duration reloadInterval;
+    
+    /** File timestamp. */
+    private long lastModified;
+
+    /** Task that monitors resource for reload. */
+    @Nullable private TimerTask reloadTask;
+
+    /** Timer used to schedule reload task. */
+    @Nullable private Timer reloadTaskTimer;
+    
+    /** A shared lock to synchronize access. */
+    @NonnullAfterInit private ReadWriteLock lock;
+    
+    
+    /** Agent map. */
+    @NonnullAfterInit private Map<String,AgentTemplate> agentMap;
+    
+    /**
+     * Constructor.
+     *
+     * @param protocolManager protocol support manager
+     */
+    public FlatFileExternalAgentResolver(
+            @ParameterName(name="protocolManager") @Nonnull final ProtocolSupportServiceManager protocolManager) {
+        super(protocolManager);
+        
+        lastModified = 0;
+        reloadInterval = Duration.ofMinutes(5);
+    }
+    
+    /**
+     * Set the resource to use.
+     * 
+     * @param resource resource to use
+     */
+    public void setResource(@Nonnull final Resource resource) {
+        checkSetterPreconditions();
+        storageResource = Constraint.isNotNull(resource, "Resource cannot be null");
+    }
+    
+    /**
+     * Sets the time between checks for changes to the resource and reload of the records.
+     * 
+     * <p>A value of 0 (the default) indicates that no reloading will be performed. The default is
+     * 5 minutes.</p>
+     * 
+     * <p>Non-file resources are detected and reload is not supported.</p>
+     * 
+     * @param interval time between reload checking
+     */
+    public void setReloadInterval(@Nonnull final Duration interval) {
+        checkSetterPreconditions();
+        
+        Constraint.isNotNull(interval, "Interval cannot be null");
+        Constraint.isFalse(interval.isNegative(), "Interval cannot be negative");
+
+        reloadInterval = interval;
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        try {
+            final Resource resource = storageResource;
+            if (resource == null) {
+                throw new ComponentInitializationException("Resource cannot be null");
+            }
+            
+            try (final InputStream is = resource.getInputStream()) {
+                agentMap = load(is);
+            }
+
+            if (reloadInterval != null && !reloadInterval.isZero() && resource.isFile()) {
+                lastModified = resource.lastModified();
+
+                assert reloadInterval != null;
+                final long interval = reloadInterval.toMillis();
+                
+                lock = new ReentrantReadWriteLock(true);
+                reloadTask = getReloadTask();
+                reloadTaskTimer = new Timer(TimerSupport.getTimerName(this), true);
+                assert reloadTaskTimer != null;
+                reloadTaskTimer.schedule(reloadTask, interval, interval);
+            } else {
+                storageResource = null;
+            }
+            
+        } catch (final IOException e) {
+            throw new ComponentInitializationException("Error reading backing resource", e);
+        }
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doDestroy() {
+        if (reloadTask != null) {
+            reloadTask.cancel();
+            reloadTask = null;
+            if (reloadTaskTimer != null) {
+                reloadTaskTimer.cancel();
+            }
+            reloadTaskTimer = null;
+        }
+        super.doDestroy();
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    @Nullable public AgentTemplate doResolve(@Nonnull final CriteriaSet criteria) throws ResolverException {
+
+        AgentTemplate template = null;
+
+        final Lock readLock = lock.readLock();
+        try {
+            readLock.lock();
+        
+            final AgentCredentialCriterion credentialCrit = criteria.get(AgentCredentialCriterion.class);
+            if (credentialCrit != null) {
+                template = agentMap.get(credentialCrit.getId());
+            }
+            
+            if (template == null) {
+                final AgentIDCriterion agentCrit = criteria.get(AgentIDCriterion.class);
+                if (agentCrit != null) {
+                    template = agentMap.get(agentCrit.getId());
+                }
+            }
+        } finally {
+            readLock.unlock();
+        }
+        
+        return template;
+    }
+
+ // Checkstyle: AnonInnerLength OFF
+    @Nonnull protected TimerTask getReloadTask() {
+        return new TimerTask() {
+            
+            /** {@inheritDoc} */
+            @Override
+            public void run() {
+                log.debug("Running reload task");
+                
+                final Lock writeLock = lock.writeLock();
+                
+                try {
+                    writeLock.lock();
+
+                    final Resource resource = storageResource;
+
+                    try {
+                        if (resource != null && resource.isFile() && resource.exists()) {
+                            final long modTime = resource.lastModified();
+                            if (modTime > lastModified) {
+                                log.info("Resource {} modified, attempting reload", resource);
+                                try (final InputStream is = resource.getInputStream()) {
+                                    agentMap = load(is);
+                                }
+                                lastModified = modTime;
+                            } else {
+                                log.info("Resource {} not modified, skipping reload", resource);
+                            }
+                        } else {
+                            log.warn("Non-file resource, reload monitoring not supported");
+                        }
+                    } catch (final IOException e) {
+                        log.error("Error reloading records", e);
+                    }
+                    
+                } finally {
+                    writeLock.unlock();
+                }
+            }
+        };
+    }
+// Checkstyle: AnonInnerLength ON
+    
+    /**
+     * Loads data from the supplied input stream.
+     * 
+     * <p>Must be called while holding the write lock.</p>
+     * 
+     * @param is
+     * 
+     * @return the constructed map of records
+     * 
+     * @throws IOException on error
+     */
+    @Nonnull @Unmodifiable @NotLive private Map<String,AgentTemplate> load(
+            @Nonnull final InputStream is) throws IOException {
+
+        final Map<String,AgentTemplate> theMap = new HashMap<>(); 
+        
+        try (final BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
+            
+            int lineno = 0;
+            String line;
+
+            String id = null;
+            String issuer = null;
+            String authority = null;
+            final Set<String> ranges = new HashSet<>();
+            final Collection<String> usernames = new HashSet<>();
+            
+            
+            while ((line = reader.readLine()) != null) {
+                ++lineno;
+
+                if (line.isEmpty() || line.isBlank()) {
+                    if (id != null) {
+                        theMap.put(id, new AgentTemplate(id, issuer, authority, ranges, usernames));
+                    }
+                    
+                    id = null;
+                    issuer = null;
+                    authority = null;
+                    ranges.clear();
+                    usernames.clear();
+                    continue;
+                } else if (line.startsWith("#")) {
+                    continue;
+                } else if (line.startsWith(ID_FIELD)) {
+                    id = line.substring(ID_FIELD.length());
+                } else if (line.startsWith(ALLOWED_RANGE_FIELD)) {
+                    ranges.add(line.substring(ALLOWED_RANGE_FIELD.length()));
+                } else if (line.startsWith(USERNAME_FIELD)) {
+                    usernames.add(line.substring(USERNAME_FIELD.length()));
+                } else if (line.startsWith(ISSUER_FIELD)) {
+                    issuer = line.substring(ISSUER_FIELD.length());
+                } else if (line.startsWith(AUTHORITY_FIELD)) {
+                    authority = line.substring(AUTHORITY_FIELD.length());
+                } else {
+                    log.warn("Invalid line ({}) found", lineno);
+                }
+            }
+            
+            if (id != null) {
+                theMap.put(id, new AgentTemplate(id, issuer, authority, ranges, usernames));
+            }
+            
+        }
+        
+        return theMap;
+    }
+    
+}

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list