[java-plugin-shibd] branch main updated: WIP on storage-based credential resolvers.

Codeberg noreply at shibboleth.net
Mon Aug 24 18:54:40 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/ccd8e0434f9dbc8a397d247307d96ff332cc95c1

The following commit(s) were added to refs/heads/main by this push:
     new ccd8e04  WIP on storage-based credential resolvers.
ccd8e04 is described below

commit ccd8e0434f9dbc8a397d247307d96ff332cc95c1
Author: Scott Cantor <scott at restingparrotsoftware.com>
AuthorDate: Mon Aug 24 14:54:24 2026 -0400

    WIP on storage-based credential resolvers.
---
 sp-server-api/pom.xml                              |   6 +
 .../AbstractOrderedCredentialResolver.java         |   2 +-
 .../AbstractStorageServiceCredentialResolver.java  | 373 +++++++++++++++++++++
 sp-server-impl/pom.xml                             |   1 -
 .../credential/impl/StaticCredentialResolver.java  |   2 +-
 .../impl/StaticMapCredentialResolver.java          |   2 +-
 .../impl/X509CredentialStorageServiceResolver.java | 195 +++++++++++
 .../sp/storage/impl/FilesystemStorageService.java  |   7 +-
 .../storage/impl/FilesystemStorageServiceTest.java |   2 +-
 9 files changed, 584 insertions(+), 6 deletions(-)

diff --git a/sp-server-api/pom.xml b/sp-server-api/pom.xml
index b97fefc..afaddf3 100644
--- a/sp-server-api/pom.xml
+++ b/sp-server-api/pom.xml
@@ -84,6 +84,12 @@
             <scope>provided</scope>
         </dependency>
 
+        <dependency>
+            <groupId>org.apache.velocity</groupId>
+            <artifactId>velocity-engine-core</artifactId>
+            <scope>provided</scope>
+        </dependency>
+
         <dependency>
             <groupId>${slf4j.groupId}</groupId>
             <artifactId>jcl-over-slf4j</artifactId>
diff --git a/sp-server-api/src/main/java/net/shibboleth/sp/credential/AbstractOrderedCredentialResolver.java b/sp-server-api/src/main/java/net/shibboleth/sp/credential/AbstractOrderedCredentialResolver.java
index 91f1034..f66a4fc 100644
--- a/sp-server-api/src/main/java/net/shibboleth/sp/credential/AbstractOrderedCredentialResolver.java
+++ b/sp-server-api/src/main/java/net/shibboleth/sp/credential/AbstractOrderedCredentialResolver.java
@@ -185,7 +185,7 @@ public abstract class AbstractOrderedCredentialResolver extends AbstractIdentifi
      * 
      * @throws ResolverException if an error occurs
      */
-    @Nonnull public abstract Iterable<Credential> doResolve(@Nullable final CriteriaSet criteria)
+    @Nonnull protected abstract Iterable<Credential> doResolve(@Nullable final CriteriaSet criteria)
             throws ResolverException;
 
     /**
diff --git a/sp-server-api/src/main/java/net/shibboleth/sp/credential/AbstractStorageServiceCredentialResolver.java b/sp-server-api/src/main/java/net/shibboleth/sp/credential/AbstractStorageServiceCredentialResolver.java
new file mode 100644
index 0000000..0845e58
--- /dev/null
+++ b/sp-server-api/src/main/java/net/shibboleth/sp/credential/AbstractStorageServiceCredentialResolver.java
@@ -0,0 +1,373 @@
+/*
+ * 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.credential;
+
+import java.nio.charset.StandardCharsets;
+import java.security.NoSuchAlgorithmException;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.apache.velocity.VelocityContext;
+import org.apache.velocity.app.VelocityEngine;
+import org.opensaml.core.criterion.EntityIdCriterion;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.criterion.ProfileRequestContextCriterion;
+import org.opensaml.security.credential.Credential;
+import org.opensaml.security.credential.UsageType;
+import org.opensaml.security.criteria.UsageCriterion;
+import org.opensaml.storage.StorageService;
+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.codec.StringDigester;
+import net.shibboleth.shared.codec.StringDigester.OutputFormat;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.ClassCriterion;
+import net.shibboleth.shared.resolver.CriteriaSet;
+import net.shibboleth.shared.resolver.Criterion;
+import net.shibboleth.shared.resolver.ResolverException;
+import net.shibboleth.shared.velocity.Template;
+import net.shibboleth.sp.Agent;
+import net.shibboleth.sp.AgentIDCriterion;
+import net.shibboleth.sp.Application;
+import net.shibboleth.sp.ApplicationIDCriterion;
+
+/**
+ * A dymamic resolver that relies on a {@link StorageService} to retrieve blobs
+ * of information that its subclasses will understand how to turn into a
+ * specifically known type of {@link Credential}.
+ * 
+ * <p>Typically more than one object from storage will be required to construct
+ * the expected type of {@link Credential}, and subclasses are expected to
+ * govern exactly how that occurs.</p>
+ * 
+ * <p>The resolver functions by evaluating a Velocity Template with information
+ * pulled from the supplied {@link CriteriaSet} to allow dynamic construction
+ * of the storage context and key to retrieve. The resulting values are
+ * passed to the subclass to further process into specific lookup values
+ * to retrieve.</p> 
+ * 
+ * <p>The resolver recognizes these {@link Criterion} types natively and
+ * supplies them as input to the template:</p>
+ * 
+ * <ul>
+ *  <li>{@link ProfileRequestContextCriterion} - generic extension point</li> 
+ *  <li>{@link AgentIDCriterion} - used for {@link Agent}-based lookup 
+ *  <li>{@link ApplicationIDCriterion} - used for {@link Application}-based lookup 
+ *  <li>{@link EntityIdCriterion} - used for relying party-based lookup</li> 
+ *  <li>{@link UsageCriterion} - used for usage-based lookup</li> 
+ * </ul>
+ * 
+ * <p>In addition, the entire {@link CriteriaSet} is also supplied as an extension point, and
+ * if supplied, the entityID is transformed by a pluggable function, by default into a SHA-1
+ * hash that is hex-encoded in lower case.</p>
+ * 
+ * <p>Note that subclasses are required to perform evaluation of intended usage based on
+ * the {@link UsageCriterion} such that any resolved credentials are assumed to either
+ * match the caller's expectations or not carry any usage designation at all.</p>
+ * 
+ * TODO: implement caching layer here or in subclasses somehow
+ * 
+ * @param <T> the type of credential supported 
+ */
+public abstract class AbstractStorageServiceCredentialResolver<T extends Credential> extends AbstractOrderedCredentialResolver {
+
+    /** Default prefix for default context value. */
+    @Nonnull @NotEmpty public static final String DEFAULT_CONTEXT_PREFIX = "net.shibboleth.sp.credential.";
+
+    /** The Velocity context variable name for the Agent ID. */
+    @Nonnull @NotEmpty public static final String CONTEXT_KEY_CRITERIA = "criteria";
+    
+    /** The Velocity context variable name for the Agent ID. */
+    @Nonnull @NotEmpty public static final String CONTEXT_KEY_AGENT_ID = "agentID";
+
+    /** The Velocity context variable name for the Agent ID. */
+    @Nonnull @NotEmpty public static final String CONTEXT_KEY_APPLICATION_ID = "applicationID";
+    
+    /** The Velocity context variable name for the entity ID. */
+    @Nonnull @NotEmpty public static final String CONTEXT_KEY_ENTITY_ID = "entityID";
+
+    /** The Velocity context variable name for the transformed entity ID. */
+    @Nonnull @NotEmpty public static final String CONTEXT_KEY_TX_ENTITY_ID = "transformedEntityID";
+    
+    /** The Velocity context variable name for the {@link ProfileRequestContext}. */
+    @Nonnull @NotEmpty public static final String CONTEXT_KEY_PRC = "profileRequestContext";
+
+    /** The Velocity context variable name for the {@link UsageType}. */
+    @Nonnull @NotEmpty public static final String CONTEXT_KEY_USAGE = "usage";
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(AbstractStorageServiceCredentialResolver.class);
+
+    /** Credential type. */
+    @Nonnull private final Class<T> credentialType;
+    
+    /** Storage service to use. */
+    @NonnullAfterInit private StorageService storageService;
+    
+    /** Velocity engine to use. */
+    @NonnullAfterInit private VelocityEngine velocityEngine;
+
+    /** Function to transform the entityID. */
+    @NonnullAfterInit private Function<String,String> entityIDTransformStrategy;
+        
+    /** Velocity template source for context. */
+    @NonnullAfterInit private String contextTemplateString;
+
+    /** Velocity template source for key. */
+    @NonnullAfterInit private String keyTemplateString;
+
+    /** Velocity template for context. */
+    @NonnullAfterInit private Template contextTemplate;
+
+    /** Velocity template for key. */
+    @NonnullAfterInit private Template keyTemplate;
+    
+    /**
+     * Constructor.
+     * 
+     * @param type class type of generic parameter
+     */
+    public AbstractStorageServiceCredentialResolver(@Nonnull @ParameterName(name="type") final Class<T> type) {
+        credentialType = Constraint.isNotNull(type, "Credential type cannot be null");
+        
+    }
+    
+    /**
+     * Gets the {@link StorageService} to use.
+     * 
+     * @return storage service
+     */
+    @NonnullAfterInit public StorageService getStorageService() {
+        return storageService;
+    }
+    
+    /**
+     * Sets the {@link StorageService} to use.
+     * 
+     * @param storage storage service
+     */
+    public void setStorageService(@Nonnull final StorageService storage) {
+        checkSetterPreconditions();
+        
+        storageService = Constraint.isNotNull(storage, "StorageService cannot be null");
+    }
+    
+    /**
+     * Sets the {@link VelocityEngine} to use.
+     * 
+     * @param engine Velocity engine
+     */
+    public void setVelocityEngine(@Nonnull final VelocityEngine engine) {
+        checkSetterPreconditions();
+        
+        velocityEngine = Constraint.isNotNull(engine, "VelocityEngine cannot be null");
+    }
+    
+    /**
+     * Sets the transform to apply to the entityID if supplied.
+     * 
+     * <p>Defaults to a SHA-1 hash as a hex-encoded lower-case string.</p>
+     * 
+     * @param strategy transform strategy
+     */
+    public void setEntityIDTransformStrategy(@Nonnull final Function<String,String> strategy) {
+        checkSetterPreconditions();
+        
+        entityIDTransformStrategy = Constraint.isNotNull(strategy, "EntityID transform strategy cannot be null");
+    }
+    
+    /**
+     * Sets the Velocity template to use to construct the storage context.
+     * 
+     * <p>Defaults to "net.shibboleth.sp.credential.resolver.id" where "id" is the component ID.</p> 
+     * 
+     * @param template template for storage context
+     */
+    public void setContextTemplate(@Nonnull final String template) {
+        checkSetterPreconditions();
+        
+        contextTemplateString = Constraint.isNotNull(template, "Storage context template cannot be null");
+    }
+
+    /**
+     * Sets the Velocity template to use to construct the storage key.
+     * 
+     * @param template template for storage key
+     */
+    public void setKeyTemplate(@Nonnull final String template) {
+        checkSetterPreconditions();
+        
+        contextTemplateString = Constraint.isNotNull(template, "Storage key template cannot be null");
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (storageService == null) {
+            throw new ComponentInitializationException("StorageService cannot be null");
+        } else if (velocityEngine == null) {
+            throw new ComponentInitializationException("VelocityEngine cannot be null");
+        } else if (keyTemplateString == null) {
+            throw new ComponentInitializationException("Storage key template cannot be null");
+        }
+        
+        keyTemplate = Template.fromTemplate(velocityEngine, keyTemplateString, StandardCharsets.UTF_8);
+        
+        if (contextTemplateString == null) {
+            contextTemplateString = DEFAULT_CONTEXT_PREFIX + ensureId();
+        }
+        
+        contextTemplate = Template.fromTemplate(velocityEngine, contextTemplateString, StandardCharsets.UTF_8);
+        
+        if (entityIDTransformStrategy == null) {
+            entityIDTransformStrategy = new DefaultTransformStrategy();
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nonnull @NotLive @Unmodifiable protected Iterable<Credential> doResolve(@Nullable final CriteriaSet criteria)
+            throws ResolverException {
+
+        if (criteria != null) {
+            // Check type.
+            final ClassCriterion<?> classCriterion = criteria.get(ClassCriterion.class);
+            if (classCriterion != null && !classCriterion.getClass().isAssignableFrom(credentialType)) {
+                log.debug("{}: ClassCriterion specified {}, not compatible with this resolver", getId(),
+                        classCriterion.getClass());
+                return CollectionSupport.emptyList();
+            }
+        }
+        
+        final VelocityContext ctx = populateVelocityContext(criteria);
+        
+        final String storageContext = contextTemplate.merge(ctx);
+        final String storageKey = keyTemplate.merge(ctx);
+        
+        log.debug("{}: Resolved storage context ({}), key ({})", getId(), storageContext, storageKey);
+        
+        return doResolve(criteria, storageContext, storageKey);
+    }
+    
+    /**
+     * Populate and return a {@link VelocityContext} with the necessary criteria information.
+     * 
+     * @param criteria input to resolver
+     * 
+     * @return the populated context
+     */
+    @Nonnull protected VelocityContext populateVelocityContext(@Nullable final CriteriaSet criteria) {
+        final VelocityContext ctx = new VelocityContext();
+        
+        if (criteria == null) {
+            return ctx;
+        }
+        ctx.put(CONTEXT_KEY_CRITERIA, criteria);
+        
+        
+        final ProfileRequestContextCriterion prcCrit = criteria.get(ProfileRequestContextCriterion.class);
+        if (prcCrit != null) {
+            ctx.put(CONTEXT_KEY_PRC, prcCrit.getProfileRequestContext());
+        }
+        
+        final AgentIDCriterion agentCrit = criteria.get(AgentIDCriterion.class);
+        if (agentCrit != null) {
+            ctx.put(CONTEXT_KEY_AGENT_ID, agentCrit.getId());
+        }
+
+        final ApplicationIDCriterion appCrit = criteria.get(ApplicationIDCriterion.class);
+        if (appCrit != null) {
+            ctx.put(CONTEXT_KEY_APPLICATION_ID, appCrit.getId());
+        }
+        
+        final EntityIdCriterion entityIDCrit = criteria.get(EntityIdCriterion.class);
+        if (entityIDCrit != null) {
+            ctx.put(CONTEXT_KEY_ENTITY_ID, entityIDCrit.getEntityId());
+            ctx.put(CONTEXT_KEY_TX_ENTITY_ID, entityIDTransformStrategy.apply(entityIDCrit.getEntityId()));
+        }
+        
+        final UsageCriterion usageCrit = criteria.get(UsageCriterion.class);
+        if (usageCrit != null) {
+            ctx.put(CONTEXT_KEY_USAGE, usageCrit.getUsage());
+        } else {
+            ctx.put(CONTEXT_KEY_USAGE, UsageType.UNSPECIFIED);
+        }
+        
+        return ctx;
+    }
+
+    /**
+     * Perform the resolution as required for the specific type of {@link Credential}.
+     * 
+     * <p>Subclasses implement this by taking the supplied information and adjusting it if required to
+     * perform one or more storage lookups for information needed to construct the resolved credential(s).</p>
+     * 
+     * <p>Most often, a single {@link Credential} (at most) will be returned as it is complex to store
+     * the information needed for multiple objects without some fairly exotic tricks, but this is left
+     * to the subclass to decide.</p>
+     * 
+     * @param criteria input to resolver
+     * @param storageContext evaluated storage context
+     * @param storageKey evaluated storage key
+     * 
+     * @return zero or more resolved credentials
+     * 
+     * @throws ResolverException on errors, generally wrapped from storage service 
+     */
+    @Nonnull @NotLive @Unmodifiable protected abstract Iterable<Credential> doResolve(
+            @Nullable final CriteriaSet criteria, @Nonnull final String storageContext,
+            @Nonnull final String storageKey) throws ResolverException;
+
+    /**
+     * Defaults to SHA-1 hash.
+     */
+    private static class DefaultTransformStrategy implements Function<String,String> {
+
+        /** String digester. */
+        @Nonnull private final StringDigester digester;
+        
+        /**
+         * Constructor.
+         *
+         * @throws ComponentInitializationException on failure
+         */
+        public DefaultTransformStrategy() throws ComponentInitializationException {
+            try {
+                digester = new StringDigester("SHA1", OutputFormat.HEX_LOWER);
+            } catch (final NoSuchAlgorithmException e) {
+                throw new ComponentInitializationException(e);
+            }
+        }
+        
+        /** {@inheritDoc} */
+        @Nullable public String apply(@Nullable final String t) {
+            return digester.apply(t);
+        }
+        
+    }
+    
+}
\ No newline at end of file
diff --git a/sp-server-impl/pom.xml b/sp-server-impl/pom.xml
index 05cd9b2..e3767ad 100644
--- a/sp-server-impl/pom.xml
+++ b/sp-server-impl/pom.xml
@@ -147,7 +147,6 @@
             <scope>provided</scope>
         </dependency>
 
-
         <!-- Test Dependencies -->
         <dependency>
             <groupId>${project.groupId}</groupId>
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/credential/impl/StaticCredentialResolver.java b/sp-server-impl/src/main/java/net/shibboleth/sp/credential/impl/StaticCredentialResolver.java
index 95643af..1150627 100644
--- a/sp-server-impl/src/main/java/net/shibboleth/sp/credential/impl/StaticCredentialResolver.java
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/credential/impl/StaticCredentialResolver.java
@@ -51,7 +51,7 @@ public class StaticCredentialResolver extends AbstractOrderedCredentialResolver
 
     /** {@inheritDoc} */
     @Override
-    @Nonnull @NotLive @Unmodifiable public Iterable<Credential> doResolve(@Nullable final CriteriaSet criteria) throws ResolverException {
+    @Nonnull @NotLive @Unmodifiable protected Iterable<Credential> doResolve(@Nullable final CriteriaSet criteria) throws ResolverException {
         return creds;
     }
     
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/credential/impl/StaticMapCredentialResolver.java b/sp-server-impl/src/main/java/net/shibboleth/sp/credential/impl/StaticMapCredentialResolver.java
index 509190b..6490fc3 100644
--- a/sp-server-impl/src/main/java/net/shibboleth/sp/credential/impl/StaticMapCredentialResolver.java
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/credential/impl/StaticMapCredentialResolver.java
@@ -155,7 +155,7 @@ public class StaticMapCredentialResolver extends AbstractOrderedCredentialResolv
 
     /** {@inheritDoc} */
     @Override
-    @Nonnull @NotLive @Unmodifiable public Iterable<Credential> doResolve(@Nullable final CriteriaSet criteria)
+    @Nonnull @NotLive @Unmodifiable protected Iterable<Credential> doResolve(@Nullable final CriteriaSet criteria)
             throws ResolverException {
 
         // Extract known criteria.
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/credential/impl/X509CredentialStorageServiceResolver.java b/sp-server-impl/src/main/java/net/shibboleth/sp/credential/impl/X509CredentialStorageServiceResolver.java
new file mode 100644
index 0000000..ae86610
--- /dev/null
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/credential/impl/X509CredentialStorageServiceResolver.java
@@ -0,0 +1,195 @@
+/*
+ * 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.credential.impl;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.security.credential.Credential;
+import org.opensaml.security.x509.X509Credential;
+import org.opensaml.spring.credential.BasicX509CredentialFactoryBean;
+import org.opensaml.storage.StorageRecord;
+import org.slf4j.Logger;
+import org.springframework.core.io.ByteArrayResource;
+
+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.collection.CollectionSupport;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+import net.shibboleth.shared.resolver.CriteriaSet;
+import net.shibboleth.shared.resolver.ResolverException;
+import net.shibboleth.shared.spring.resource.ResourceHelper;
+import net.shibboleth.sp.credential.AbstractStorageServiceCredentialResolver;
+
+/**
+ * A specialized storage-based resolver that can resolve the {@link X509Credential} type.
+ * 
+ * <p>Additional settings are required to specify how to locate the various components such as
+ * the private key and certificate(s). A key password may be configured but must be usable for any
+ * resolved keys.</p>
+ * 
+ * <p>The private key and certificate(s) must be stored separately for this implementation in DER or PEM
+ * fornat but cannot be binary-encoded.</p>
+ */
+public class X509CredentialStorageServiceResolver extends AbstractStorageServiceCredentialResolver<X509Credential> {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(X509CredentialStorageServiceResolver.class);
+    
+    /** Suffix added to storage key when searching for the private key. */
+    @Nonnull private String privateKeySuffix;
+
+    /** Suffix added to storage key when searching for the public key certificate(s). */
+    @Nonnull private String certificateSuffix;
+
+    /** Suffix added to storage key when searching for the CRL(s). */
+    @Nullable private String crlSuffix;
+    
+    /** Password for private key(s). */
+    @Nullable private char[] privateKeyPassword;
+    
+    /**
+     * Constructor.
+     */
+    public X509CredentialStorageServiceResolver() {
+        super(X509Credential.class);
+        
+        privateKeySuffix = ".key";
+        certificateSuffix = ".crt";
+    }
+
+    /**
+     * Sets the storage key suffix to add when resolving the private key.
+     * 
+     * <p>Defaults to ".key"</p>
+     * 
+     * @param suffix suffix to add
+     */
+    public void setPrivateKeySuffix(@Nonnull @NotEmpty final String suffix) {
+        checkSetterPreconditions();
+        
+        privateKeySuffix = Constraint.isNotNull(StringSupport.trimOrNull(suffix),
+                "Private key suffix cannot be null or empty");
+    }
+    
+    /**
+     * Sets the storage key suffix to add when resolving the certificate(s).
+     * 
+     * <p>Defaults to ".crt"</p>
+     * 
+     * @param suffix suffix to add
+     */
+    public void setCertificateSuffix(@Nonnull @NotEmpty final String suffix) {
+        checkSetterPreconditions();
+        
+        certificateSuffix = Constraint.isNotNull(StringSupport.trimOrNull(suffix),
+                "Certificate suffix cannot be null or empty");
+    }
+
+    /**
+     * Sets the storage key suffix to add when resolving the CRL(s).
+     * 
+     * <p>Defaults to null, which skips attempted resolution of CRLs.</p>
+     * 
+     * @param suffix suffix to add
+     */
+    public void setCRLSuffix(@Nullable @NotEmpty final String suffix) {
+        checkSetterPreconditions();
+        
+        crlSuffix = StringSupport.trimOrNull(suffix);
+    }
+    
+    /**
+     * Sets the private key password to apply to any resolved keys.
+     * 
+     * @param password key password
+     */
+    public void setPrivateKeyPassword(@Nullable final String password) {
+        checkSetterPreconditions();
+        
+        privateKeyPassword = password != null ? password.toCharArray() : null;
+    }
+
+    /** {@inheritDoc} 
+     * @throws ResolverException */
+    @Override
+    @Nonnull @NotLive @Unmodifiable protected Iterable<Credential> doResolve(
+            @Nullable final CriteriaSet criteria, @Nonnull final String storageContext,
+            @Nonnull final String storageKey) throws ResolverException {
+ 
+        final StorageRecord<String> privateKeyData;
+        final StorageRecord<String> certificateData;
+        final StorageRecord<String> crlData;
+
+        try {
+            privateKeyData = getStorageService().read(storageContext, storageKey + privateKeySuffix);
+            certificateData = getStorageService().read(storageContext, storageKey + certificateSuffix);
+            if (crlSuffix != null) {
+                crlData = getStorageService().read(storageContext, storageKey + crlSuffix);
+            } else {
+                crlData = null;
+            }
+            
+        } catch (final IOException e) {
+            throw new ResolverException("I/O exception retrieving credential data", e);
+        }
+        
+        if (privateKeyData == null && certificateData == null) {
+            log.debug("{}: No private key or certificate data resolved", getId());
+            return CollectionSupport.emptyList();
+        }
+        
+        // Populate a factory bean from the resolved material.
+        
+        final BasicX509CredentialFactoryBean factory = new BasicX509CredentialFactoryBean();
+        factory.setPrivateKeyPassword(privateKeyPassword);
+        if (privateKeyData != null) {
+            factory.setPrivateKeyResource(
+                    ResourceHelper.of(new ByteArrayResource(
+                            privateKeyData.getValue().getBytes(StandardCharsets.UTF_8))));
+        }
+        if (certificateData != null) {
+            factory.setCertificateResource(
+                    ResourceHelper.of(new ByteArrayResource(
+                            certificateData.getValue().getBytes(StandardCharsets.UTF_8))));
+        }
+        if (crlData != null) {
+            factory.setCRLs(
+                    CollectionSupport.singletonList(
+                            ResourceHelper.of(new ByteArrayResource(
+                                    crlData.getValue().getBytes(StandardCharsets.UTF_8)))));
+        }
+        
+        try {
+            final X509Credential credential = factory.getObject();
+            if (credential != null) {
+                log.debug("{}: Resolved X509Credential for caller", getId());
+                return CollectionSupport.singletonList(credential);
+            }
+            
+            log.warn("{}: Factory bean returned null", getId());
+            return CollectionSupport.emptyList();
+        } catch (final Exception e) {
+            throw new ResolverException("Exception during creation of X509Credential object", e);
+        }
+    }
+    
+}
\ No newline at end of file
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/storage/impl/FilesystemStorageService.java b/sp-server-impl/src/main/java/net/shibboleth/sp/storage/impl/FilesystemStorageService.java
index f5f7bea..598c15d 100644
--- a/sp-server-impl/src/main/java/net/shibboleth/sp/storage/impl/FilesystemStorageService.java
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/storage/impl/FilesystemStorageService.java
@@ -50,6 +50,11 @@ public class FilesystemStorageService extends AbstractStorageService {
     /** Base location of storage tree in filesystem. */
     @NonnullAfterInit private String storageBase;
     
+    /** Constructor. */
+    public FilesystemStorageService() {
+        readOnly = true;
+    }
+    
     /**
      * Sets whether the file system is clustered.
      * 
@@ -66,7 +71,7 @@ public class FilesystemStorageService extends AbstractStorageService {
     /**
      * Sets whether the service should operate read-only.
      * 
-     * <p>Defaults to false.</p>
+     * <p>Defaults to true.</p>
      * 
      * @param flag flag to set
      */
diff --git a/sp-server-impl/src/test/java/net/shibboleth/sp/storage/impl/FilesystemStorageServiceTest.java b/sp-server-impl/src/test/java/net/shibboleth/sp/storage/impl/FilesystemStorageServiceTest.java
index f0df889..7749029 100644
--- a/sp-server-impl/src/test/java/net/shibboleth/sp/storage/impl/FilesystemStorageServiceTest.java
+++ b/sp-server-impl/src/test/java/net/shibboleth/sp/storage/impl/FilesystemStorageServiceTest.java
@@ -226,7 +226,7 @@ public class FilesystemStorageServiceTest {
      * 
      * @throws ComponentInitializationException 
      */
-    @Nonnull private FilesystemStorageService getService(final boolean readOnly)
+    @Nonnull private StorageService getService(final boolean readOnly)
             throws ComponentInitializationException {
     
         final FilesystemStorageService ss = new FilesystemStorageService();

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


More information about the commits mailing list