[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