[java-identity-provider] branch main updated: IDP-2357 - Predicate to check contents of authenticated Subject

Scott Cantor cantor.2 at osu.edu
Tue Feb 18 19:42:55 UTC 2025


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

scantor pushed a commit to branch main
in repository java-identity-provider.

View the commit online:
http://git.shibboleth.net/view/?p=java-identity-provider.git;a=commit;h=e779bb38fc6a553f9632d3481f1f9c43f32a9e8a

The following commit(s) were added to refs/heads/main by this push:
     new e779bb38f IDP-2357 - Predicate to check contents of authenticated Subject
e779bb38f is described below

commit e779bb38fc6a553f9632d3481f1f9c43f32a9e8a
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Tue Feb 18 14:42:52 2025 -0500

    IDP-2357 - Predicate to check contents of authenticated Subject
    
    https://shibboleth.atlassian.net/browse/IDP-2357
    
    Implemented a base class and two seemingly useful options.
---
 .../AbstractAuthenticationResultPredicate.java     | 178 +++++++++++++++++++++
 .../logic/AuthenticationResultPredicate.java       |  40 +++++
 .../context/logic/PrincipalInResultPredicate.java  |  78 +++++++++
 .../idp/authn/context/logic/package-info.java      |  21 +++
 .../logic/AuthenticationResultPredicateTest.java   |  91 +++++++++++
 .../logic/PrincipalInResultPredicateTest.java      | 105 ++++++++++++
 .../net/shibboleth/idp/conf/conditions.xml         |   7 +-
 7 files changed, 519 insertions(+), 1 deletion(-)

diff --git a/idp-authn-api/src/main/java/net/shibboleth/idp/authn/context/logic/AbstractAuthenticationResultPredicate.java b/idp-authn-api/src/main/java/net/shibboleth/idp/authn/context/logic/AbstractAuthenticationResultPredicate.java
new file mode 100644
index 000000000..3199a32a1
--- /dev/null
+++ b/idp-authn-api/src/main/java/net/shibboleth/idp/authn/context/logic/AbstractAuthenticationResultPredicate.java
@@ -0,0 +1,178 @@
+/*
+ * 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.idp.authn.context.logic;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+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 net.shibboleth.idp.authn.AuthenticationResult;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.authn.context.SubjectContext;
+import net.shibboleth.idp.authn.principal.AuthenticationResultPrincipal;
+import net.shibboleth.shared.collection.CollectionSupport;
+
+/**
+ * Base class for predicates that operate on {@link AuthenticationResult} instances.
+ * 
+ * <p>This layer handles accessing active results and filters by flow ID,
+ * subsequently invoking a checker method for each result. Both OR and AND semantics
+ * are supported across multiple results.</p>
+ * 
+ * <p>An empty result list evaluates to false regardless of those semantics.</p>
+ * 
+ * <p>If no flows are specified, then only the absence of any results will
+ * result in a true outcome.</p>
+ * 
+ * @since 5.2.0
+ */
+public abstract class AbstractAuthenticationResultPredicate implements Predicate<ProfileRequestContext> {
+    
+    /** Whether to pull results from {@link AuthenticationContext} instead of {@link SubjectContext}. */
+    private boolean includeIntermediateResults;
+    
+    /** Flow IDs to filter results against, ignoring the others. */
+    @Nonnull private Set<String> includedFlows;
+        
+    /** Constructor. */
+    public AbstractAuthenticationResultPredicate() {
+        includedFlows = CollectionSupport.emptySet();
+    }
+    
+    /**
+     * Gets whether pull results from {@link AuthenticationContext} instead of {@link SubjectContext}.
+     * 
+     * @return as above
+     */
+    public boolean isIncludeIntermediateResults() {
+        return includeIntermediateResults;
+    }
+    
+    /**
+     * Sets whether pull results from {@link AuthenticationContext} instead of {@link SubjectContext}.
+     * 
+     * <p>Defaults to false, meaning reliance only on {@link SubjectContext}.
+     * 
+     * @param flag flag to set
+     */
+    public void setIncludeIntermediateResults(final boolean flag) {
+        includeIntermediateResults = flag;
+    }
+    
+    /**
+     * Gets the specific flow IDs (with the "authn/" prefix added) to pull results from, ignoring others.
+     * 
+     * @return as above
+     */
+    @Nonnull public Set<String> getIncludedFlows() {
+        return includedFlows;
+    }
+    
+    /**
+     * Sets specific flow IDs (minus the "authn/" prefix) to pull results from, ignoring others.
+     * 
+     * @param flows flow IDs to include in evaluation of condition
+     */
+    public void setIncludedFlows(@Nullable final Collection<String> flows) {
+       if (flows != null) {
+           includedFlows = flows.stream()
+                   .map(s -> {
+                       return "authn/" + s;
+                       })
+                   .collect(CollectionSupport.nonnullCollector(Collectors.toUnmodifiableSet())).get();
+       } else {
+           includedFlows = CollectionSupport.emptySet();
+       }
+    }
+    
+    /** {@inheritDoc} */
+    public boolean test(@Nullable final ProfileRequestContext input) {
+        
+        if (input == null) {
+            return false;
+        }
+        
+        return getAuthenticationResults(input).stream().anyMatch(ar -> {
+            assert ar != null;
+            return doMatch(input, ar);
+        });
+    }
+    
+    /**
+     * Extract all of the relevant results from either the active {@link AuthenticationContext} or
+     * {@link SubjectContext} as directed, including any nested results such as found with the MFA flow.
+     * 
+     * @param profileRequestContext profile request context
+     * 
+     * @return filtered results
+     */
+    @Nonnull protected Collection<AuthenticationResult> getAuthenticationResults(
+            @Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        if (includedFlows.isEmpty()) {
+            return CollectionSupport.emptyList();
+        }
+        
+        final Collection<AuthenticationResult> filtered = new ArrayList<>();
+        
+        final Map<String,AuthenticationResult> results;
+        if (includeIntermediateResults) {
+            final AuthenticationContext authContext = profileRequestContext.getSubcontext(AuthenticationContext.class);
+            results = authContext != null ? authContext.getActiveResults() : CollectionSupport.emptyMap();
+        } else {
+            final SubjectContext subjectContext = profileRequestContext.getSubcontext(SubjectContext.class);
+            results = subjectContext != null ? subjectContext.getAuthenticationResults() : CollectionSupport.emptyMap();
+        }
+
+        results.values().forEach(result -> {
+            if (includedFlows.contains(result.getAuthenticationFlowId())) {
+                filtered.add(result);
+            }
+            
+            final Set<AuthenticationResultPrincipal> nested =
+                    result.getSubject().getPrincipals(AuthenticationResultPrincipal.class);
+            filtered.addAll(
+                    nested.stream().map(AuthenticationResultPrincipal::getAuthenticationResult).filter(ar -> {
+                            return includedFlows.contains(ar.getAuthenticationFlowId());
+                        }).collect(CollectionSupport.nonnullCollector(Collectors.toUnmodifiableList())).get()
+                    );
+        });
+        
+        return filtered;
+    }
+    
+    /**
+     * Evaluate an {@link AuthenticationResult} for a match.
+     * 
+     * <p>The default evaluation is simply that its existence is sufficient, but subclasses
+     * may override for more specific cases.</p>
+     * 
+     * @param profileRequestContext profile request context
+     * @param result result to evaluate
+     * 
+     * @return true iff the subclass deems this a matching result
+     */
+    protected abstract boolean doMatch(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationResult result);
+    
+}
\ No newline at end of file
diff --git a/idp-authn-api/src/main/java/net/shibboleth/idp/authn/context/logic/AuthenticationResultPredicate.java b/idp-authn-api/src/main/java/net/shibboleth/idp/authn/context/logic/AuthenticationResultPredicate.java
new file mode 100644
index 000000000..1047939d7
--- /dev/null
+++ b/idp-authn-api/src/main/java/net/shibboleth/idp/authn/context/logic/AuthenticationResultPredicate.java
@@ -0,0 +1,40 @@
+/*
+ * 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.idp.authn.context.logic;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.idp.authn.AuthenticationResult;
+
+/**
+ * Condition that checks for the existence of at least one active {@link AuthenticationResult} instances
+ * of a given flow.
+ * 
+ * @since 5.2.0
+ */
+public class AuthenticationResultPredicate extends AbstractAuthenticationResultPredicate {         
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doMatch(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationResult result) {
+        
+        // The base class filters the results to the flows we intend to check for.
+        return true;
+    }
+    
+}
\ No newline at end of file
diff --git a/idp-authn-api/src/main/java/net/shibboleth/idp/authn/context/logic/PrincipalInResultPredicate.java b/idp-authn-api/src/main/java/net/shibboleth/idp/authn/context/logic/PrincipalInResultPredicate.java
new file mode 100644
index 000000000..6461e5562
--- /dev/null
+++ b/idp-authn-api/src/main/java/net/shibboleth/idp/authn/context/logic/PrincipalInResultPredicate.java
@@ -0,0 +1,78 @@
+/*
+ * 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.idp.authn.context.logic;
+
+import java.security.Principal;
+import java.util.Collection;
+import java.util.Set;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.security.auth.Subject;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.idp.authn.AuthenticationResult;
+import net.shibboleth.shared.collection.CollectionSupport;
+
+/**
+ * Condition that checks for the presence of one or more {@link Principal} instances
+ * in a {@link Subject} in any of the {@link AuthenticationResult} instances evaluated
+ * by the parent class.
+ * 
+ * @since 5.2.0
+ */
+public class PrincipalInResultPredicate extends AbstractAuthenticationResultPredicate {
+
+    /** Principals to check for. */
+    @Nonnull private Set<Principal> principals;
+
+    /** Constructor. */
+    public PrincipalInResultPredicate() {
+        principals = CollectionSupport.emptySet();
+    }
+
+    /**
+     * Sets the principals to check for.
+     * 
+     * @param prins principals to check for
+     */
+    public void setPrincipals(@Nullable final Collection<Principal> prins) {
+        if (prins != null) {
+            principals = CollectionSupport.copyToSet(prins);
+        } else {
+            principals = CollectionSupport.emptySet();
+        }
+    }
+
+    /**
+     * Match returns true iff at least one of the candidates is found in this
+     * result's {@link Subject}.
+     * 
+     * @param profileRequestContext profile request context
+     * @param result result to evaluate
+     * 
+     * @return as above
+     */
+    protected boolean doMatch(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationResult result) {
+        
+        final Set<Principal> contents = result.getSubject().getPrincipals();
+        return principals.stream().anyMatch(p -> {
+            return contents.contains(p);
+        });
+    }
+    
+}
\ No newline at end of file
diff --git a/idp-authn-api/src/main/java/net/shibboleth/idp/authn/context/logic/package-info.java b/idp-authn-api/src/main/java/net/shibboleth/idp/authn/context/logic/package-info.java
new file mode 100644
index 000000000..84b365347
--- /dev/null
+++ b/idp-authn-api/src/main/java/net/shibboleth/idp/authn/context/logic/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+/**
+ * Predicates related to authentication context content.
+ */
+ at NonnullElements
+package net.shibboleth.idp.authn.context.logic;
+
+import net.shibboleth.shared.annotation.constraint.NonnullElements;
diff --git a/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/context/logic/AuthenticationResultPredicateTest.java b/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/context/logic/AuthenticationResultPredicateTest.java
new file mode 100644
index 000000000..4396be87c
--- /dev/null
+++ b/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/context/logic/AuthenticationResultPredicateTest.java
@@ -0,0 +1,91 @@
+/*
+ * 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.idp.authn.context.logic;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.security.auth.Subject;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.testng.Assert;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import net.shibboleth.idp.authn.AuthenticationResult;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.authn.context.SubjectContext;
+import net.shibboleth.idp.authn.principal.AuthenticationResultPrincipal;
+import net.shibboleth.idp.profile.testing.RequestContextBuilder;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.ComponentInitializationException;
+
+/**
+ * Unit test for {@link AuthenticationResultPredicate}.
+ */
+ at SuppressWarnings("javadoc")
+public class AuthenticationResultPredicateTest {
+
+    protected ProfileRequestContext prc;
+    protected Map<String,AuthenticationResult> results;
+    
+    @BeforeClass
+    public void initResults() {
+        results = new HashMap<>();
+        
+        results.put("authn/Foo", new AuthenticationResult("authn/Foo", new Subject()));
+        
+        final AuthenticationResult nested = new AuthenticationResult("authn/Nested", new Subject());
+        
+        final Subject subject = new Subject();
+        subject.getPrincipals().add(new AuthenticationResultPrincipal(nested));
+        results.put("authn/MFA", new AuthenticationResult("authn/MFA", subject));
+    }
+    
+    @BeforeMethod
+    public void setUp() throws ComponentInitializationException {
+        prc = new RequestContextBuilder().buildProfileRequestContext();
+    }
+    
+    @Test
+    public void testResultFinal() {
+        final AuthenticationResultPredicate condition = new AuthenticationResultPredicate();
+        condition.setIncludeIntermediateResults(false);
+        condition.setIncludedFlows(CollectionSupport.listOf("Nested", "Absent"));
+        
+        // Test without adding to tree, so no match.
+        Assert.assertFalse(condition.test(prc));
+        
+        // Add to tree and test again.
+        prc.ensureSubcontext(SubjectContext.class).getAuthenticationResults().putAll(results);
+        Assert.assertTrue(condition.test(prc));
+    }
+    
+    @Test
+    public void testResultIntermediate() {
+        final AuthenticationResultPredicate condition = new AuthenticationResultPredicate();
+        condition.setIncludeIntermediateResults(true);
+        condition.setIncludedFlows(CollectionSupport.listOf("Nested", "Absent"));
+        
+        // Test without adding to tree, so no match.
+        Assert.assertFalse(condition.test(prc));
+        
+        // Add to tree and test again.
+        prc.ensureSubcontext(AuthenticationContext.class).getActiveResults().putAll(results);
+        Assert.assertTrue(condition.test(prc));
+    }
+    
+}
\ No newline at end of file
diff --git a/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/context/logic/PrincipalInResultPredicateTest.java b/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/context/logic/PrincipalInResultPredicateTest.java
new file mode 100644
index 000000000..f8bec284c
--- /dev/null
+++ b/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/context/logic/PrincipalInResultPredicateTest.java
@@ -0,0 +1,105 @@
+/*
+ * 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.idp.authn.context.logic;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.security.auth.Subject;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.testng.Assert;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import net.shibboleth.idp.authn.AuthenticationResult;
+import net.shibboleth.idp.authn.context.SubjectContext;
+import net.shibboleth.idp.authn.principal.AuthenticationResultPrincipal;
+import net.shibboleth.idp.authn.testing.TestPrincipal;
+import net.shibboleth.idp.profile.testing.RequestContextBuilder;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.ComponentInitializationException;
+
+/**
+ * Unit test for {@link PrincipalInResultPredicate}.
+ */
+ at SuppressWarnings("javadoc")
+public class PrincipalInResultPredicateTest {
+
+    protected ProfileRequestContext prc;
+    protected Map<String,AuthenticationResult> results;
+    
+    @BeforeClass
+    public void initResults() {
+        results = new HashMap<>();
+        
+        // authn/Foo has Principals zorkmid
+        // authn/Nested has Principal frobnitz
+        // authn/MFA has a nested result for authn/Nested but no other Principals.
+        
+        Subject subject = new Subject();
+        subject.getPrincipals().add(new TestPrincipal("zorkmid"));
+        results.put("authn/Foo", new AuthenticationResult("authn/Foo", subject));
+        
+        subject = new Subject();
+        subject.getPrincipals().add(new TestPrincipal("frobnitz"));
+        final AuthenticationResult nested = new AuthenticationResult("authn/Nested", subject);
+        
+        subject = new Subject();
+        subject.getPrincipals().add(new AuthenticationResultPrincipal(nested));
+        results.put("authn/MFA", new AuthenticationResult("authn/MFA", subject));
+    }
+    
+    @BeforeMethod
+    public void setUp() throws ComponentInitializationException {
+        prc = new RequestContextBuilder().buildProfileRequestContext();
+    }
+    
+    @Test
+    public void testNoCandidates() {
+        final PrincipalInResultPredicate condition = new PrincipalInResultPredicate();
+        condition.setIncludeIntermediateResults(false);
+        condition.setIncludedFlows(CollectionSupport.listOf("Nested", "Absent"));
+        condition.setPrincipals(null);
+        
+        // Test without adding to tree, so no match.
+        Assert.assertFalse(condition.test(prc));
+    }
+    
+    @Test
+    public void testPrincipalNoMatch() {
+        final PrincipalInResultPredicate condition = new PrincipalInResultPredicate();
+        condition.setIncludeIntermediateResults(false);
+        condition.setIncludedFlows(CollectionSupport.listOf("Nested", "Absent"));
+        condition.setPrincipals(CollectionSupport.setOf(new TestPrincipal("zorkmid"), new TestPrincipal("foo")));
+        
+        prc.ensureSubcontext(SubjectContext.class).getAuthenticationResults().putAll(results);
+        Assert.assertFalse(condition.test(prc));
+    }
+
+    @Test
+    public void testPrincipalMatch() {
+        final PrincipalInResultPredicate condition = new PrincipalInResultPredicate();
+        condition.setIncludeIntermediateResults(false);
+        condition.setIncludedFlows(CollectionSupport.listOf("Nested", "Absent"));
+        condition.setPrincipals(CollectionSupport.setOf(new TestPrincipal("zorkmid"), new TestPrincipal("frobnitz")));
+        
+        // Add to tree and test again.
+        prc.ensureSubcontext(SubjectContext.class).getAuthenticationResults().putAll(results);
+        Assert.assertTrue(condition.test(prc));
+    }
+
+}
\ No newline at end of file
diff --git a/idp-conf-impl/src/main/resources/net/shibboleth/idp/conf/conditions.xml b/idp-conf-impl/src/main/resources/net/shibboleth/idp/conf/conditions.xml
index 7e7157eb4..77c4a2584 100644
--- a/idp-conf-impl/src/main/resources/net/shibboleth/idp/conf/conditions.xml
+++ b/idp-conf-impl/src/main/resources/net/shibboleth/idp/conf/conditions.xml
@@ -67,7 +67,12 @@
     <bean id="shibboleth.TagCandidate" abstract="true"
         class="org.opensaml.saml.common.profile.logic.EntityAttributesPredicate.Candidate" />
     
-
+    <!-- Active authentication result conditions. -->
+    <bean id="shibboleth.Conditions.AuthenticationResult"
+        class="net.shibboleth.idp.authn.context.logic.AuthenticationResultPredicate" abstract="true" />
+    <bean id="shibboleth.Conditions.PrincipalInnResult"
+        class="net.shibboleth.idp.authn.context.logic.PrincipalInResultPredicate" abstract="true" />
+    
     <!-- IdPAttribute conditions. -->
     <bean id="shibboleth.Conditions.SimpleAttribute"
         class="net.shibboleth.profile.context.logic.SimpleAttributePredicate" abstract="true" />

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


More information about the commits mailing list