[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