[java-plugin-shibd] branch main updated: JSHIBD-25 - Develop necessary CredentialResolvers for SP service

Codeberg noreply at shibboleth.net
Tue Aug 18 13:43:25 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/0459c413f65bc7526c25d53d0594e73b1501f866

The following commit(s) were added to refs/heads/main by this push:
     new 0459c41  JSHIBD-25 - Develop necessary CredentialResolvers for SP service
0459c41 is described below

commit 0459c413f65bc7526c25d53d0594e73b1501f866
Author: Scott Cantor <scott at restingparrotsoftware.com>
AuthorDate: Tue Aug 18 09:43:10 2026 -0400

    JSHIBD-25 - Develop necessary CredentialResolvers for SP service
    
    https://shibboleth.atlassian.net/browse/JSHIBD-25
    
    A static map-based resolver with Agent/Application/RelyingParty support.
---
 .../impl/StaticMapCredentialResolver.java          | 258 +++++++++++++++++++++
 .../sp/credential/impl/MockCredential.java         |  87 +++++++
 .../impl/StaticMapCredentialResolverTest.java      |  70 ++++++
 .../sp/credential/impl/staticmapresolver.xml       |  49 ++++
 4 files changed, 464 insertions(+)

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
new file mode 100644
index 0000000..1ecbfd4
--- /dev/null
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/credential/impl/StaticMapCredentialResolver.java
@@ -0,0 +1,258 @@
+/*
+ * 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.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Predicate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+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.slf4j.Logger;
+
+import net.shibboleth.shared.annotation.constraint.NotLive;
+import net.shibboleth.shared.annotation.constraint.Unmodifiable;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+import net.shibboleth.shared.resolver.ResolverException;
+import net.shibboleth.sp.AgentIDCriterion;
+import net.shibboleth.sp.ApplicationIDCriterion;
+import net.shibboleth.sp.credential.AbstractOrderedCredentialResolver;
+
+/**
+ * A static resolver that relies on maps to associate credentials with the core objects
+ * that constitute an SP deployment, primarily assuming limited, exception-driven rules
+ * to avoid scaling problems.
+ * 
+ * <p>The "exact" matching map is constructed as a nested three-level map, from
+ * Agent to Application to RelyingParty and finally to the list of credentials.
+ * Each layer uses a null key to signify "any" as a wildcard match.</p>
+ */
+public class StaticMapCredentialResolver extends AbstractOrderedCredentialResolver {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(StaticMapCredentialResolver.class); 
+
+    /** Maps between exact names of objects and credentials. */
+    @Nonnull private Map<String,Map<String,Map<String,List<Credential>>>> exactMatchCredentialMap;
+        
+    /** Maps between arbitrary conditions and credentials. */
+    @Nonnull private Map<Predicate<ProfileRequestContext>,List<Credential>> predicateCredentialMap;
+    
+    /** Constructor. */
+    public StaticMapCredentialResolver() {
+        exactMatchCredentialMap = CollectionSupport.emptyMap();
+        predicateCredentialMap = CollectionSupport.emptyMap();
+    }
+    
+    /**
+     * Installs mappings based on Agent and RelyingParty IDs.
+     * 
+     * <p>This method produces mappings that ignore the Application identifier, which is the
+     * recommended approach.</p>
+     * 
+     * <p>A null map key signifies a wildcard match (ignoring that component).</p>
+     * 
+     * @param map rules to add
+     */
+    public void setAgentRules(@Nonnull final Map<String,Map<String,List<Credential>>> map) {
+        checkSetterPreconditions();
+
+        exactMatchCredentialMap = new HashMap<>();
+        
+        map.forEach((agentId, rules) -> {
+            // Establish or access the nested Map for the given agent (which may be null).
+            if (!exactMatchCredentialMap.containsKey(agentId)) {
+                exactMatchCredentialMap.put(agentId, new HashMap<>());
+            }
+            final Map<String,Map<String,List<Credential>>> agentMap = exactMatchCredentialMap.get(agentId);
+            
+            // We are only concerned with the null key to this nested map, which signifies rules for any Application.
+            // If it exists, we supplement or overwrite those map keys, which are for relying parties. If not, we
+            // copy the new mappings into a new map registered against the null key.
+            
+            if (agentMap.containsKey(null)) {
+                agentMap.get(null).putAll(rules);
+            } else {
+                agentMap.put(null, new HashMap<>(rules));
+            }
+        });
+    }
+    
+    /**
+     * Installs mappings based on Agent, Application, and RelyingParty Ids.
+     * 
+     * <p>This method produces mappings that can include the Application identifier, which is the
+     * most complex approach.</p>
+     * 
+     * <p>Note that this method does not currently make a cooy of the map or its contents, so
+     * the input must not be subsequently modified.</p>
+     * 
+     * @param map rules to add
+     */
+    public void setAgentRulesEx(@Nonnull final Map<String,Map<String,Map<String,List<Credential>>>> map) {
+        checkSetterPreconditions();
+        
+        // Trying to copy this beyond the top level (because nulls are allowed) would be a mess.
+        // We don't expose this to callers, so using a reference works so long as this is used from
+        // Spring, as is expected to be the case.
+        exactMatchCredentialMap = map;
+    }
+    
+    /**
+     * Installs the map of conditions to apply to match to credentials.
+     * 
+     * <p>This is a generic approach that allows arbitrary rules to be implemented in scripts or in Java
+     * to determine whether particular credentials should be resolved for a request.</p>
+     * 
+     * @param map rules to add
+     */
+    public void setPredicateRules(@Nullable final Map<Predicate<ProfileRequestContext>,List<Credential>> map) {
+        checkSetterPreconditions();
+        
+        if (map != null) {
+            predicateCredentialMap = CollectionSupport.copyToMap(map);
+        } else {
+            predicateCredentialMap = CollectionSupport.emptyMap();
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nonnull @NotLive @Unmodifiable public Iterable<Credential> doResolve(@Nullable final CriteriaSet criteria)
+            throws ResolverException {
+
+        // Extract known criteria.
+        final AgentIDCriterion agentCriterion;
+        final ProfileRequestContextCriterion prcCriterion;
+        if (criteria != null) {
+            prcCriterion = criteria.get(ProfileRequestContextCriterion.class);
+            agentCriterion = criteria.get(AgentIDCriterion.class);
+        } else {
+            agentCriterion = null;
+            prcCriterion = null;
+        }
+        
+        final ArrayList<Credential> holder = new ArrayList<>();
+
+        if (agentCriterion != null) {
+            resolveForAgent(holder, agentCriterion.getId(), criteria);
+        }
+        resolveForAgent(holder, null, criteria);
+        
+        if (predicateCredentialMap.isEmpty()) {
+            return holder;
+        }
+        
+        log.debug("{}: Evaluating condition-based rules", getId());
+        if (prcCriterion == null) {
+            log.debug("{}: No ProfileRequestContextCriterion provided", getId());
+        }
+        
+        for (final Map.Entry<Predicate<ProfileRequestContext>,List<Credential>> entry
+                : predicateCredentialMap.entrySet()) {
+            
+            if (entry.getKey().test(prcCriterion != null ? prcCriterion.getProfileRequestContext() : null)) {
+                log.debug("{}: Condition applies, adding {} credentials", getId(), entry.getValue().size());
+                holder.addAll(entry.getValue());
+            }
+        }
+        
+        return holder;
+    }
+    
+    /**
+     * Resolves the nested mappings for the given Agent ID (or null) using the additional criteria.
+     * 
+     * @param accumulator list to populate
+     * @param agentID supplied Agent ID or null
+     * @param criteria remaining criteria
+     */
+    private void resolveForAgent(@Nonnull final List<Credential> accumulator, @Nullable final String agentID,
+            @Nullable final CriteriaSet criteria) {
+        
+        final Map<String,Map<String,List<Credential>>> appRules = exactMatchCredentialMap.get(agentID);
+        if (appRules == null) {
+            log.debug("{}: No rules installed for Agent ({})", getId(), agentID != null ? agentID : "any");
+            return;
+        }
+
+        log.debug("{}: Resolving credentials for Agent ({})", getId(), agentID != null ? agentID : "any");
+        
+        final ApplicationIDCriterion appCriterion =
+                criteria != null ? criteria.get(ApplicationIDCriterion.class) : null;
+        if (appCriterion != null) {
+            resolveForApplication(accumulator, appRules, appCriterion.getId(), criteria);
+        }
+        resolveForApplication(accumulator, appRules, null, criteria);
+    }
+    
+    /**
+     * Resolves the nested mappings for the given Application ID (or null) using the additional criteria.
+     * 
+     * @param accumulator list to populate
+     * @param applicationRules nested mappings
+     * @param applicationID supplied Application ID or null
+     * @param criteria remaining criteria
+     */
+    private void resolveForApplication(@Nonnull final List<Credential> accumulator,
+            @Nonnull final Map<String,Map<String,List<Credential>>> applicationRules,
+            @Nullable final String applicationID, @Nullable final CriteriaSet criteria) {
+        
+        final Map<String,List<Credential>> relyingPartyRules = applicationRules.get(applicationID);
+        if (relyingPartyRules == null) {
+            log.debug("{}: No rules installed for Application ({})", getId(),
+                    applicationID != null ? applicationID : "any");
+            return;
+        }
+
+        log.debug("{}: Resolving credentials for Application ({})", getId(),
+                applicationID != null ? applicationID : "any");
+        
+        final EntityIdCriterion rpCriterion = criteria != null ? criteria.get(EntityIdCriterion.class) : null;
+        if (rpCriterion != null) {
+            resolveForRelyingParty(accumulator, relyingPartyRules, rpCriterion.getEntityId());
+        }
+        resolveForRelyingParty(accumulator, relyingPartyRules, null);
+    }
+
+    /**
+     * Resolves the nested mappings for the given relying party ID (or null).
+     * 
+     * @param accumulator list to populate
+     * @param relyingPartyRules nested mappings
+     * @param relyingPartyID supplied relying party ID or null
+     */
+    private void resolveForRelyingParty(@Nonnull final List<Credential> accumulator,
+            @Nonnull final Map<String,List<Credential>> relyingPartyRules,
+            @Nullable final String relyingPartyID) {
+        
+        final List<Credential> creds = relyingPartyRules.get(relyingPartyID);
+        if (creds != null) {
+            accumulator.addAll(creds);
+        }
+        log.debug("{}: Resolved {} credential(s) for relying party ({})", getId(), creds != null ? creds.size() : 0,
+                relyingPartyID != null ? relyingPartyID : "any");
+    }
+    
+}
\ No newline at end of file
diff --git a/sp-server-impl/src/test/java/net/shibboleth/sp/credential/impl/MockCredential.java b/sp-server-impl/src/test/java/net/shibboleth/sp/credential/impl/MockCredential.java
new file mode 100644
index 0000000..5d952c8
--- /dev/null
+++ b/sp-server-impl/src/test/java/net/shibboleth/sp/credential/impl/MockCredential.java
@@ -0,0 +1,87 @@
+/*
+ * 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.security.PrivateKey;
+import java.security.PublicKey;
+import java.util.Collection;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.crypto.SecretKey;
+
+import org.opensaml.security.credential.Credential;
+import org.opensaml.security.credential.CredentialContextSet;
+import org.opensaml.security.credential.UsageType;
+
+import net.shibboleth.shared.collection.CollectionSupport;
+
+/**
+ * Testing class to supply a usable credential type.
+ */
+public class MockCredential implements Credential {
+
+    @Nullable private final String entityID; 
+    
+    /**
+     * Constructor.
+     *
+     * @param id entityID for testing
+     */
+    public MockCredential(@Nullable final String id) {
+        entityID = id;
+    }
+    
+    /** {@inheritDoc} */
+    @Nullable public CredentialContextSet getCredentialContextSet() {
+        return null;
+    }
+
+    /** {@inheritDoc} */
+    @Nonnull public Class<? extends Credential> getCredentialType() {
+        return this.getClass();
+    }
+
+    /** {@inheritDoc} */
+    @Nullable public String getEntityId() {
+        return entityID;
+    }
+
+    /** {@inheritDoc} */
+    @Nonnull public Collection<String> getKeyNames() {
+        return CollectionSupport.emptyList();
+    }
+
+    /** {@inheritDoc} */
+    @Nullable public PrivateKey getPrivateKey() {
+        return null;
+    }
+
+    /** {@inheritDoc} */
+    @Nullable public PublicKey getPublicKey() {
+        return null;
+    }
+
+    /** {@inheritDoc} */
+    @Nullable public SecretKey getSecretKey() {
+        return null;
+    }
+
+    /** {@inheritDoc} */
+    @Nonnull public UsageType getUsageType() {
+        return UsageType.UNSPECIFIED;
+    }
+
+}
diff --git a/sp-server-impl/src/test/java/net/shibboleth/sp/credential/impl/StaticMapCredentialResolverTest.java b/sp-server-impl/src/test/java/net/shibboleth/sp/credential/impl/StaticMapCredentialResolverTest.java
new file mode 100644
index 0000000..30b535b
--- /dev/null
+++ b/sp-server-impl/src/test/java/net/shibboleth/sp/credential/impl/StaticMapCredentialResolverTest.java
@@ -0,0 +1,70 @@
+/*
+ * 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.util.ArrayList;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import net.shibboleth.shared.resolver.CriteriaSet;
+import net.shibboleth.shared.resolver.ResolverException;
+
+import org.opensaml.security.credential.Credential;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.testng.AbstractTestNGSpringContextTests;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+/**
+ * Unit tests for {@link StaticMapCredentialResolver}.
+ */
+ at SuppressWarnings("javadoc")
+ at ContextConfiguration(
+        locations = {
+                "/net/shibboleth/sp/credential/impl/staticmapresolver.xml",
+                }
+        )
+ at Configuration
+public class StaticMapCredentialResolverTest extends AbstractTestNGSpringContextTests {
+
+    @Autowired
+    @Nonnull private StaticMapCredentialResolver resolver;
+    
+    @Test public void testPredicates_NoCriteria() throws ResolverException {
+        final List<Credential> creds = resolve(null);
+        Assert.assertEquals(creds.size(), 1);
+        Assert.assertEquals(creds.get(0).getEntityId(), "alwaysTrue");
+    }
+
+    /**
+     * Wraps a resolve call to capture the output into a list for ease of test validation.
+     * 
+     * @param criteria input to resolution
+     * 
+     * @return resolved list
+     * 
+     * @throws ResolverException
+     */
+    @Nonnull private List<Credential> resolve(@Nullable final CriteriaSet criteria) throws ResolverException {
+        final ArrayList<Credential> holder = new ArrayList<>();
+        resolver.resolve(criteria).forEach(holder::add);
+        return holder;
+    }
+
+}
\ No newline at end of file
diff --git a/sp-server-impl/src/test/resources/net/shibboleth/sp/credential/impl/staticmapresolver.xml b/sp-server-impl/src/test/resources/net/shibboleth/sp/credential/impl/staticmapresolver.xml
new file mode 100644
index 0000000..31ff725
--- /dev/null
+++ b/sp-server-impl/src/test/resources/net/shibboleth/sp/credential/impl/staticmapresolver.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:context="http://www.springframework.org/schema/context"
+       xmlns:util="http://www.springframework.org/schema/util"
+       xmlns:p="http://www.springframework.org/schema/p"
+       xmlns:c="http://www.springframework.org/schema/c"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+                           http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+                           
+       default-init-method="initialize"
+       default-destroy-method="destroy">
+
+    <context:annotation-config/>
+
+    <bean id="shibboleth.IdentifiableBeanPostProcessor"
+        class="net.shibboleth.shared.spring.config.IdentifiableBeanPostProcessor" />
+
+    <bean id="Mock" class="net.shibboleth.sp.credential.impl.MockCredential" abstract="true" />
+
+    <bean id="shibboleth.Conditions.FALSE"
+        class="net.shibboleth.shared.logic.PredicateSupport" factory-method="alwaysFalse" />
+    <bean id="shibboleth.Conditions.TRUE"
+        class="net.shibboleth.shared.logic.PredicateSupport" factory-method="alwaysTrue" />
+    
+    <bean id="shibboleth.Conditions.AND"
+        class="net.shibboleth.shared.logic.PredicateSupport" factory-method="and" abstract="true" />
+    <bean id="shibboleth.Conditions.NOT"
+        class="net.shibboleth.shared.logic.PredicateSupport" factory-method="not" abstract="true" />
+    <bean id="shibboleth.Conditions.OR"
+        class="net.shibboleth.shared.logic.PredicateSupport" factory-method="or" abstract="true" />
+    
+
+    <bean id="testResolver" class="net.shibboleth.sp.credential.impl.StaticMapCredentialResolver">
+    
+        <property name="predicateRules">
+            <map>
+                <entry key-ref="shibboleth.Conditions.TRUE">
+                    <bean parent="Mock" c:_0="alwaysTrue" />
+                </entry>
+                <entry key-ref="shibboleth.Conditions.FALSE">
+                    <bean parent="Mock" c:_0="alwaysFalse" />
+                </entry>
+            </map>
+        </property>
+    </bean>
+
+</beans>

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


More information about the commits mailing list