[java-shib-profile] branch main updated: JSPROF-1 - Move RelyingParty "layer" into java-shib-profile

Scott Cantor cantor.2 at osu.edu
Wed Feb 15 15:56:46 UTC 2023


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

scantor pushed a commit to branch main
in repository java-shib-profile.

View the commit online:
http://git.shibboleth.net/view/?p=java-shib-profile.git;a=commit;h=58140ad4e37baa16e29d5a2c64a228673dc01632

The following commit(s) were added to refs/heads/main by this push:
     new 58140ad  JSPROF-1 - Move RelyingParty "layer" into java-shib-profile
58140ad is described below

commit 58140ad4e37baa16e29d5a2c64a228673dc01632
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Wed Feb 15 10:56:44 2023 -0500

    JSPROF-1 - Move RelyingParty "layer" into java-shib-profile
    
    https://shibboleth.atlassian.net/browse/JSPROF-1
    
    Migrate in predicate classes.
---
 shib-profile-api/pom.xml                           |  59 +++---
 .../config/logic/ResolveAttributesPredicate.java   |  51 ++++++
 .../profile/config/logic}/package-info.java        |   4 +-
 .../context/logic/AbstractAttributePredicate.java  | 139 ++++++++++++++
 .../logic/AbstractRelyingPartyPredicate.java       |  79 ++++++++
 .../context/logic/DateAttributePredicate.java      | 175 ++++++++++++++++++
 .../context/logic/DynamicAttributePredicate.java   | 199 +++++++++++++++++++++
 .../context/logic/LoopDetectionPredicate.java      | 136 ++++++++++++++
 .../context/logic/RegexAttributePredicate.java     | 127 +++++++++++++
 .../logic/RelyingPartyIdPredicate.java             |   2 +-
 .../profile/context/logic/ScriptedPredicate.java   | 132 ++++++++++++++
 .../context/logic/SimpleAttributePredicate.java    | 172 ++++++++++++++++++
 .../context/logic/SpringExpressionPredicate.java   |  54 ++++++
 .../context/logic/VerifiedProfilePredicate.java    |  26 ++-
 .../messaging/AbstractRelyingPartyPredicate.java   |  87 +++++++++
 .../context/logic/messaging}/package-info.java     |   4 +-
 .../profile/{ => context}/logic/package-info.java  |   4 +-
 .../context/logic/DateAttributePredicateTest.java  | 159 ++++++++++++++++
 .../logic/DynamicAttributePredicateTest.java       | 100 +++++++++++
 .../context/logic/LoopDetectionPredicateTest.java  |  82 +++++++++
 .../logic/RelyingPartyIdPredicateTest.java         |   2 +-
 .../context/logic/ScriptedPredicateTest.java       | 107 +++++++++++
 .../logic/SimpleAttributePredicateTest.java        | 117 ++++++++++++
 .../profile/logic/relyingPartyIdPredicates.xml     |  12 +-
 .../RelyingPartiesActivationConditionFactory.java  |   4 +-
 .../{ => context}/logic/impl/package-info.java     |   2 +-
 26 files changed, 1995 insertions(+), 40 deletions(-)

diff --git a/shib-profile-api/pom.xml b/shib-profile-api/pom.xml
index dc55e2d..385867b 100644
--- a/shib-profile-api/pom.xml
+++ b/shib-profile-api/pom.xml
@@ -22,6 +22,15 @@
 
     <dependencies>
         <!-- Compile Dependencies -->
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>shib-attribute-api</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>${opensaml.groupId}</groupId>
+            <artifactId>opensaml-core-api</artifactId>
+        </dependency>
         <dependency>
             <groupId>${opensaml.groupId}</groupId>
             <artifactId>opensaml-profile-api</artifactId>
@@ -39,6 +48,10 @@
             <artifactId>opensaml-xmlsec-api</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>${shib-shared.groupId}</groupId>
+            <artifactId>shib-security</artifactId>
+        </dependency>
         <dependency>
             <groupId>${shib-shared.groupId}</groupId>
             <artifactId>shib-service</artifactId>
@@ -48,6 +61,25 @@
             <artifactId>shib-spring</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>${spring.groupId}</groupId>
+            <artifactId>spring-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>${spring.groupId}</groupId>
+            <artifactId>spring-expression</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>io.dropwizard.metrics</groupId>
+            <artifactId>metrics-core</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+        </dependency>
+
         <!-- Provided Dependencies -->
         <dependency>
             <groupId>jakarta.servlet</groupId>
@@ -58,6 +90,12 @@
         <!-- Runtime Dependencies -->
 
         <!-- Test Dependencies -->
+        <dependency>
+            <groupId>${opensaml.groupId}</groupId>
+            <artifactId>opensaml-testing</artifactId>
+            <scope>test</scope>
+        </dependency>
+
         <dependency>
             <groupId>${spring.groupId}</groupId>
             <artifactId>spring-context</artifactId>
@@ -66,25 +104,4 @@
 
     </dependencies>
 
-    <profiles>
-        <!--
-            Pull in a Javascript engine for testing in Java
-            versions where the JDK doesn't provide one.
-        -->
-        <profile>
-            <id>get-nashorn</id>
-            <activation>
-                <jdk>[15,</jdk>
-            </activation>
-            <dependencies>
-                <dependency>
-                    <groupId>org.openjdk.nashorn</groupId>
-                    <artifactId>nashorn-core</artifactId>
-                    <version>${nashorn.jdk.version}</version>
-                    <scope>test</scope>
-                </dependency>
-            </dependencies>
-        </profile>
-    </profiles>
-
 </project>
diff --git a/shib-profile-api/src/main/java/net/shibboleth/profile/config/logic/ResolveAttributesPredicate.java b/shib-profile-api/src/main/java/net/shibboleth/profile/config/logic/ResolveAttributesPredicate.java
new file mode 100644
index 0000000..d88ab29
--- /dev/null
+++ b/shib-profile-api/src/main/java/net/shibboleth/profile/config/logic/ResolveAttributesPredicate.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.profile.config.logic;
+
+import javax.annotation.Nullable;
+
+import net.shibboleth.profile.config.AttributeResolvingProfileConfiguration;
+import net.shibboleth.profile.config.ProfileConfiguration;
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.profile.context.logic.AbstractRelyingPartyPredicate;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+/**
+ * Predicate to determine whether a profile request should resolve attributes.
+ * 
+ * @since 4.2.0
+ */
+public class ResolveAttributesPredicate extends AbstractRelyingPartyPredicate {
+
+    /** {@inheritDoc} */
+    public boolean test(@Nullable final ProfileRequestContext input) {
+        if (input != null) {
+            final RelyingPartyContext rpc = getRelyingPartyContext(input);
+            if (rpc != null) {
+                final ProfileConfiguration pc = rpc.getProfileConfig();
+                if (pc instanceof AttributeResolvingProfileConfiguration) {
+                    return ((AttributeResolvingProfileConfiguration) pc).isResolveAttributes(input);
+                }
+            }
+        }
+        
+        return false;
+    }
+
+}
\ No newline at end of file
diff --git a/shib-profile-impl/src/main/java/net/shibboleth/profile/logic/impl/package-info.java b/shib-profile-api/src/main/java/net/shibboleth/profile/config/logic/package-info.java
similarity index 89%
copy from shib-profile-impl/src/main/java/net/shibboleth/profile/logic/impl/package-info.java
copy to shib-profile-api/src/main/java/net/shibboleth/profile/config/logic/package-info.java
index 85190d0..865da32 100644
--- a/shib-profile-impl/src/main/java/net/shibboleth/profile/logic/impl/package-info.java
+++ b/shib-profile-api/src/main/java/net/shibboleth/profile/config/logic/package-info.java
@@ -16,6 +16,6 @@
  */
 
 /**
- * Implementation classes supporting profile predicates.
+ * Predicates based on profile configuration settings.
  */
-package net.shibboleth.profile.logic.impl;
\ No newline at end of file
+package net.shibboleth.profile.config.logic;
\ No newline at end of file
diff --git a/shib-profile-api/src/main/java/net/shibboleth/profile/context/logic/AbstractAttributePredicate.java b/shib-profile-api/src/main/java/net/shibboleth/profile/context/logic/AbstractAttributePredicate.java
new file mode 100644
index 0000000..8c78a9a
--- /dev/null
+++ b/shib-profile-api/src/main/java/net/shibboleth/profile/context/logic/AbstractAttributePredicate.java
@@ -0,0 +1,139 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.profile.context.logic;
+
+import java.util.Map;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.attribute.IdPAttribute;
+import net.shibboleth.idp.attribute.context.AttributeContext;
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.shared.annotation.constraint.NonnullElements;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Abstract base class for predicates operating on an {@link AttributeContext}.
+ */
+public abstract class AbstractAttributePredicate implements Predicate<ProfileRequestContext> {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(AbstractAttributePredicate.class);
+
+    /** Strategy function to lookup {@link AttributeContext}. */
+    @Nonnull private Function<ProfileRequestContext,AttributeContext> attributeContextLookupStrategy;
+
+    /** Whether to look at filtered or unfiltered attributes. */
+    private boolean useUnfilteredAttributes;
+
+    /** Constructor. */
+    public AbstractAttributePredicate() {
+        attributeContextLookupStrategy =
+                new ChildContextLookup<>(AttributeContext.class).compose(
+                        new ChildContextLookup<>(RelyingPartyContext.class));
+        useUnfilteredAttributes = true;
+    }
+    
+    /**
+     * Get the lookup strategy to use to locate the {@link AttributeContext}.
+     * 
+     * @return lookup strategy to use
+     */
+    @Nonnull public Function<ProfileRequestContext,AttributeContext> getAttributeContextLookupStrategy() {
+        return attributeContextLookupStrategy;
+    }
+
+    /**
+     * Set the lookup strategy to use to locate the {@link AttributeContext}.
+     * 
+     * @param strategy lookup function to use
+     */
+    public void setAttributeContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,AttributeContext> strategy) {
+
+        attributeContextLookupStrategy =
+                Constraint.isNotNull(strategy, "AttributeContext lookup strategy cannot be null");
+    }
+    
+    /**
+     * Get whether to source the input attributes from the unfiltered set.
+     * 
+     * @return whether to source the input attributes from the unfiltered set
+     */
+    public boolean isUseUnfilteredAttributes() {
+        return useUnfilteredAttributes;
+    }
+    
+    /**
+     * Set whether to source the input attributes from the unfiltered set.
+     * 
+     * <p>Defaults to true.</p>
+     * 
+     * @param flag flag to set
+     */
+    public void setUseUnfilteredAttributes(final boolean flag) {
+        useUnfilteredAttributes = flag;
+    }
+    
+    /** {@inheritDoc} */
+    public boolean test(@Nullable final ProfileRequestContext input) {
+        
+        final AttributeContext attributeCtx = attributeContextLookupStrategy.apply(input);
+        if (attributeCtx == null) {
+            log.warn("No AttributeContext located for evaluation");
+            return allowNullAttributeContext();
+        }
+        
+        final Map<String,IdPAttribute> attributes = useUnfilteredAttributes
+                ? attributeCtx.getUnfilteredIdPAttributes()
+                : attributeCtx.getIdPAttributes();
+
+        if (hasMatch(attributes)) {
+            log.debug("Context satisfied requirements");
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Get the result of the predicate in the case the attribute context is null.
+     * 
+     * @return null context result
+     */
+    protected boolean allowNullAttributeContext() {
+        return false;
+    }
+    
+    /**
+     * Abstract implementation of the condition to evaluate.
+     * 
+     * @param attributeMap  the attributes to evaluate
+     * 
+     * @return the condition result
+     */
+    protected abstract boolean hasMatch(@Nonnull @NonnullElements final Map<String,IdPAttribute> attributeMap);
+    
+}
\ No newline at end of file
diff --git a/shib-profile-api/src/main/java/net/shibboleth/profile/context/logic/AbstractRelyingPartyPredicate.java b/shib-profile-api/src/main/java/net/shibboleth/profile/context/logic/AbstractRelyingPartyPredicate.java
new file mode 100644
index 0000000..537216d
--- /dev/null
+++ b/shib-profile-api/src/main/java/net/shibboleth/profile/context/logic/AbstractRelyingPartyPredicate.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.profile.context.logic;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.shared.logic.Constraint;
+
+import java.util.function.Predicate;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.context.ProfileRequestContext;
+
+/**
+ * Base class for a predicate that evaluates a {@link ProfileRequestContext} and requires access to a
+ * {@link RelyingPartyContext}.
+ */
+public abstract class AbstractRelyingPartyPredicate implements Predicate<ProfileRequestContext> {
+
+    /** Strategy function to lookup RelyingPartyContext. */
+    @Nonnull private Function<ProfileRequestContext,RelyingPartyContext> relyingPartyContextLookupStrategy;
+
+    /** Constructor. */
+    public AbstractRelyingPartyPredicate() {
+        relyingPartyContextLookupStrategy = new ChildContextLookup<>(RelyingPartyContext.class);
+    }
+    
+    /**
+     * Get the lookup strategy to use to locate the {@link RelyingPartyContext}.
+     * 
+     * @return  lookup function to use
+     */
+    @Nonnull public Function<ProfileRequestContext,RelyingPartyContext> getRelyingPartyContextLookupStrategy() {
+        return relyingPartyContextLookupStrategy;
+    }
+
+    /**
+     * Set the lookup strategy to use to locate the {@link RelyingPartyContext}.
+     * 
+     * @param strategy lookup function to use
+     */
+    public void setRelyingPartyContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,RelyingPartyContext> strategy) {
+
+        relyingPartyContextLookupStrategy =
+                Constraint.isNotNull(strategy, "RelyingPartyContext lookup strategy cannot be null");
+    }
+
+    /**
+     * Returns the result of evaluating the injected lookup strategy to obtain the {@link RelyingPartyContext}.
+     * 
+     * @param input current profile request context
+     * 
+     * @return the {@link RelyingPartyContext} or null
+     */
+    @Nullable protected RelyingPartyContext getRelyingPartyContext(@Nullable final ProfileRequestContext input) {
+        return relyingPartyContextLookupStrategy.apply(input);
+    }
+    
+}
\ No newline at end of file
diff --git a/shib-profile-api/src/main/java/net/shibboleth/profile/context/logic/DateAttributePredicate.java b/shib-profile-api/src/main/java/net/shibboleth/profile/context/logic/DateAttributePredicate.java
new file mode 100644
index 0000000..315971d
--- /dev/null
+++ b/shib-profile-api/src/main/java/net/shibboleth/profile/context/logic/DateAttributePredicate.java
@@ -0,0 +1,175 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.profile.context.logic;
+
+import java.time.DateTimeException;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.format.DateTimeFormatter;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.attribute.DateTimeAttributeValue;
+import net.shibboleth.idp.attribute.IdPAttribute;
+import net.shibboleth.idp.attribute.IdPAttributeValue;
+import net.shibboleth.idp.attribute.StringAttributeValue;
+import net.shibboleth.shared.annotation.ParameterName;
+import net.shibboleth.shared.annotation.constraint.NonnullElements;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Provides a date/time matching predicate that compares a date-based attribute value against
+ * current system time with optional offset. By convention the predicate returns true if and only if
+ * the date represented by the attribute value is after the current system time; false otherwise.
+ * Thus the semantics are well-suited for cases such as evaluation of expiration dates.
+ *
+ * @author Marvin S. Addison
+ */
+public class DateAttributePredicate extends AbstractAttributePredicate {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(DateAttributePredicate.class);
+
+    /** Name of attribute to query for. */
+    @Nonnull @NotEmpty private final String attributeName;
+
+    /** Formatter used to parse string-based date attribute values. */
+    @Nullable private final DateTimeFormatter dateTimeFormatter;
+
+    /** Offset from system time used for date comparisons. */
+    @Nonnull private Duration systemTimeOffset;
+    
+    /** Result of predicate if attribute is missing or has no values. */
+    private boolean resultIfMissing;
+
+    /**
+     * Create a new instance that performs date comparisons against the given attribute.
+     *
+     * @param attribute Attribute name that provides candidate date values to test.
+     */
+    public DateAttributePredicate(@Nonnull @NotEmpty @ParameterName(name="attribute") final String attribute) {
+        attributeName = Constraint.isNotNull(attribute, "Attribute cannot be null");
+        dateTimeFormatter = null;
+        systemTimeOffset = Duration.ZERO;
+    }
+
+    /**
+     * Create a new instance that performs date comparisons against the given attribute
+     * using the given date parser.
+     *
+     * @param attribute Attribute name that provides candidate date values to test.
+     * @param formatter Date/time parser.
+     */
+    public DateAttributePredicate(@Nonnull @NotEmpty @ParameterName(name="attribute") final String attribute,
+            @Nonnull @ParameterName(name="formatter") final DateTimeFormatter formatter) {
+        
+        attributeName = Constraint.isNotNull(attribute, "Attribute cannot be null");
+        dateTimeFormatter = Constraint.isNotNull(formatter, "Formatter cannot be null");
+        systemTimeOffset = Duration.ZERO;
+    }
+
+    /**
+     * Create a new instance that performs date comparisons against the given attribute
+     * using the given date parser.
+     *
+     * @param attribute Attribute name that provides candidate date values to test.
+     * @param formatString date/time parsing string, currently based on {@link DateTimeFormatter}
+     */
+    public DateAttributePredicate(@Nonnull @NotEmpty @ParameterName(name="attribute") final String attribute,
+            @Nonnull @NotEmpty @ParameterName(name="formatString") final String formatString) {
+        attributeName = Constraint.isNotNull(attribute, "Attribute cannot be null");
+        dateTimeFormatter = DateTimeFormatter.ofPattern(
+                Constraint.isNotNull(formatString, "Format string cannot be null"));
+        systemTimeOffset = Duration.ZERO;
+    }
+
+    /**
+     * Set the system time offset, which affects the reference date for comparisons.
+     * 
+     * <p>By default all comparisons are against system time, i.e. zero offset.</p>
+     *
+     * @param offset System time offset. A negative value decreases the target date (sooner);
+     *                         a positive value increases the target date (later).
+     */
+    public void setOffset(@Nonnull final java.time.Duration offset) {
+        systemTimeOffset = Constraint.isNotNull(offset, "Offset cannot be null");
+    }
+    
+    /**
+     * Set the result to return if the attribute to check is missing or has no values.
+     * 
+     * @param flag  flag to set
+     */
+    public void setResultIfMissing(final boolean flag) {
+        resultIfMissing = flag;
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected boolean allowNullAttributeContext() {
+        return resultIfMissing;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean hasMatch(@Nonnull @NonnullElements final Map<String,IdPAttribute> attributeMap) {
+        
+        final IdPAttribute attribute = attributeMap.get(attributeName);
+        if (attribute == null) {
+            log.debug("Attribute {} not found in context, returning {}", attributeName, resultIfMissing);
+            return resultIfMissing;
+        } else if (attribute.getValues().isEmpty()) {
+            log.debug("Attribute {} has no values, returning {}", attributeName, resultIfMissing);
+            return resultIfMissing;
+        }
+        
+        final Instant now = Instant.now();
+        
+        String dateString;
+        for (final IdPAttributeValue value : attribute.getValues()) {
+            if (value instanceof DateTimeAttributeValue &&
+                    ((DateTimeAttributeValue) value).getValue().plus(systemTimeOffset).isAfter(now)) {
+                    return true;
+            } else if (value instanceof StringAttributeValue) {
+                if (dateTimeFormatter == null) {
+                    log.warn("No DateTimeFormatter configured, ignoring string value");
+                    continue;
+                }
+                dateString = ((StringAttributeValue) value).getValue();
+                try {
+                    assert dateTimeFormatter != null;
+                    if (Instant.from(dateTimeFormatter.parse(dateString)).plus(systemTimeOffset).isAfter(now)) {
+                        return true;
+                    }
+                } catch (final DateTimeException e) {
+                    log.warn("{} is not a valid date for the configured formatting string", dateString, e);
+                }
+            } else {
+                log.warn("Ignoring unsupported value type: {}", value.getClass().getName());
+            }
+        }
+        return false;
+    }
+    
+}
\ No newline at end of file
diff --git a/shib-profile-api/src/main/java/net/shibboleth/profile/context/logic/DynamicAttributePredicate.java b/shib-profile-api/src/main/java/net/shibboleth/profile/context/logic/DynamicAttributePredicate.java
new file mode 100644
index 0000000..384e113
--- /dev/null
+++ b/shib-profile-api/src/main/java/net/shibboleth/profile/context/logic/DynamicAttributePredicate.java
@@ -0,0 +1,199 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.profile.context.logic;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ListMultimap;
+
+import net.shibboleth.idp.attribute.IdPAttribute;
+import net.shibboleth.idp.attribute.IdPAttributeValue;
+import net.shibboleth.idp.attribute.StringAttributeValue;
+import net.shibboleth.idp.attribute.context.AttributeContext;
+import net.shibboleth.shared.annotation.constraint.NonnullElements;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+
+/**
+ * Predicate over an {@link AttributeContext} that derives the value(s) to match based
+ * on one or more supplied Functions instead of static values.
+ * 
+ * <p>Each function installed must return a value that matches a value of the attribute
+ * corresponding to the map key.</p>
+ * 
+ * <p>Functions may return a {@link String} or a {@link Collection} containing them.</p>
+ * 
+ * @since 3.4.0
+ */
+public class DynamicAttributePredicate extends AbstractAttributePredicate {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(DynamicAttributePredicate.class);
+
+    /** Map of attribute IDs to functions. */
+    @Nonnull @NonnullElements private ListMultimap<String,Function<ProfileRequestContext,Object>> attributeFunctionMap;
+    
+    /** Constructor. */
+    public DynamicAttributePredicate() {
+        attributeFunctionMap = ArrayListMultimap.create();
+    }
+    
+    /**
+     * Set the map of attribute/function pairs (as a map of function collections) to check for.
+     * 
+     * @param map   map of attribute/function pairs
+     */
+    public void setAttributeFunctionMap(
+            @Nonnull @NonnullElements final Map<String,Collection<Function<ProfileRequestContext,Object>>> map) {
+        Constraint.isNotNull(map, "Attribute/value map cannot be null");
+        
+        attributeFunctionMap.clear();
+        for (final Map.Entry<String,Collection<Function<ProfileRequestContext,Object>>> entry : map.entrySet()) {
+            final String attributeId = StringSupport.trimOrNull(entry.getKey());
+            attributeFunctionMap.putAll(attributeId, List.copyOf(entry.getValue()));
+        }
+    }
+
+    /** {@inheritDoc} */
+    public boolean test(@Nullable final ProfileRequestContext input) {
+        
+        final AttributeContext attributeCtx = getAttributeContextLookupStrategy().apply(input);
+        if (attributeCtx == null) {
+            log.warn("No AttributeContext located for evaluation");
+            return allowNullAttributeContext();
+        }
+        
+        final Map<String,IdPAttribute> attributes = isUseUnfilteredAttributes()
+                ? attributeCtx.getUnfilteredIdPAttributes()
+                : attributeCtx.getIdPAttributes();
+
+        if (hasMatch(input, attributes)) {
+            log.debug("Context satisfied requirements");
+            return true;
+        }
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean hasMatch(@Nonnull @NonnullElements final Map<String,IdPAttribute> attributeMap) {
+        log.error("Method should never be called");
+        return false;
+    }
+
+// Checkstyle: CyclomaticComplexity OFF
+    /**
+     * Implementation of the condition to evaluate.
+     * 
+     * @param profileRequestContext current profile request context
+     * @param attributeMap  the attributes to evaluate
+     * 
+     * @return the condition result
+     */
+    protected boolean hasMatch(@Nullable final ProfileRequestContext profileRequestContext,
+            @Nonnull @NonnullElements final Map<String,IdPAttribute> attributeMap) {
+        
+        for (final String id : attributeFunctionMap.keySet()) {
+            log.debug("Checking for attribute: {}", id);
+
+            final IdPAttribute attribute = attributeMap.get(id);
+            if (attribute == null) {
+                log.debug("Attribute {} not found in context", id);
+                return false;
+            }
+
+            boolean matched = false;
+
+            for (final Function<ProfileRequestContext,Object> fn : attributeFunctionMap.get(id)) {
+                
+                final Object candidate = fn.apply(profileRequestContext);
+                
+                if (candidate instanceof String) {
+                    matched = findMatch((String) candidate, attribute);
+                } else if (candidate instanceof Collection<?>) {
+                    for (final Object subcandidate : (Collection<?>) candidate) {
+                        if (subcandidate instanceof String) {
+                            if (findMatch((String) subcandidate, attribute)) {
+                                matched = true;
+                                break;
+                            }
+                        } else {
+                            log.error(
+                                  "Collection returned by function for attribute {} contained an unsupported type: {}",
+                                    id, subcandidate.getClass().getName());
+                        }
+                    }
+                } else {
+                    log.error("Function for attribute {} returned an unsupported type: {}", id,
+                            candidate.getClass().getName());
+                }
+                
+                if (matched) {
+                    break;
+                }
+            }
+
+            if (!matched) {
+                log.debug("Attribute {} values not matched", id);
+                return false;
+            }
+        }
+        
+        return true;
+    }
+// Checkstyle: CyclomaticComplexity ON
+    
+    /**
+     * Look for a matching value in an attribute.
+     * 
+     * @param toMatch   value to look for
+     * @param attribute attribute to check
+     * 
+     * @return true iff the value is one of the attribute's values
+     */
+    protected boolean findMatch(@Nonnull @NotEmpty final String toMatch, @Nonnull final IdPAttribute attribute) {
+        
+        if ("*".equals(toMatch)) {
+            log.debug("Wildcard (*) value rule for attribute {}", attribute.getId());
+            return true;
+        }
+        for (final IdPAttributeValue value : attribute.getValues()) {
+            if (value instanceof StringAttributeValue) {
+                if (toMatch.equals(((StringAttributeValue)value).getValue())) {
+                    log.debug("Found matching value ({}) in attribute {}", toMatch, attribute.getId());
+                    return true;
+                }
+            }
+        }
+        
+        return false;
+    }
+
+}
diff --git a/shib-profile-api/src/main/java/net/shibboleth/profile/context/logic/LoopDetectionPredicate.java b/shib-profile-api/src/main/java/net/shibboleth/profile/context/logic/LoopDetectionPredicate.java
new file mode 100644
index 0000000..f457338
--- /dev/null
+++ b/shib-profile-api/src/main/java/net/shibboleth/profile/context/logic/LoopDetectionPredicate.java
@@ -0,0 +1,136 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.profile.context.logic;
+
+import java.util.Map;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.core.metrics.MetricsSupport;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import com.codahale.metrics.Meter;
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.MetricRegistry.MetricSupplier;
+import com.codahale.metrics.SlidingTimeWindowMovingAverages;
+
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.shared.annotation.constraint.NonnullElements;
+import net.shibboleth.shared.annotation.constraint.Positive;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.FunctionSupport;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * A condition that relies on a {@link Meter} to detect looping SPs. 
+ *
+ * @since 4.1.0
+ */
+public class LoopDetectionPredicate extends AbstractRelyingPartyPredicate {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(LoopDetectionPredicate.class);
+
+    /** Count to trigger warning. */
+    private long threshold;
+    
+    /** Map of RP names to meter names. */
+    @Nonnull @NonnullElements private Map<String,String> relyingPartyMap;
+    
+    /** Lookup strategy to obtain subject name. */
+    @Nonnull private Function<ProfileRequestContext,String> usernameLookupStrategy;
+    
+    /** Constructor. */
+    public LoopDetectionPredicate() {
+        threshold = 20;
+        relyingPartyMap = CollectionSupport.emptyMap();
+        usernameLookupStrategy = FunctionSupport.constant(null);
+    }
+    
+    /**
+     * Set the warning threshold for the 1 minute moving average to exceed.
+     * 
+     * <p>Defaults to 20.</p>
+     * 
+     * @param value threshold to use
+     */
+    public void setThreshold(@Positive final long value) {
+        threshold = Constraint.isGreaterThan(0, value, "Threshold must be positive");
+    }
+    
+    /**
+     * Set the map of relying party names to meter names to track counts.
+     * 
+     * @param map map of RP/meter mappings
+     */
+    public void setRelyingPartyMap(@Nullable @NonnullElements final Map<String,String> map) {
+        if (map != null) {
+            relyingPartyMap = CollectionSupport.copyToMap(map);
+        } else {
+            relyingPartyMap = CollectionSupport.emptyMap();
+        }
+    }
+    
+    /**
+     * Set lookup strategy to obtain username.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setUsernameLookupStrategy(@Nonnull final Function<ProfileRequestContext,String> strategy) {
+        usernameLookupStrategy = Constraint.isNotNull(strategy, "Username lookup strategy cannot be null");
+    }
+    
+    /** {@inheritDoc} */
+    public boolean test(@Nullable final ProfileRequestContext input) {
+        
+        final String username = usernameLookupStrategy.apply(input);
+        final RelyingPartyContext rpCtx = getRelyingPartyContextLookupStrategy().apply(input);
+        
+        if (username != null && rpCtx != null && rpCtx.getRelyingPartyId() != null) {
+            String meterName = relyingPartyMap.get(rpCtx.getRelyingPartyId());
+            if (meterName != null) {
+                meterName = MetricRegistry.name("net.shibboleth.idp.loopDetection", meterName,
+                        username.replace(".",""));
+                final MetricRegistry registry = MetricsSupport.getMetricRegistry();
+                if (registry == null) {
+                    log.error("MetricRegistry was unavailable");
+                    return false;
+                }
+                final Meter meter = registry.meter(meterName,
+                        new MetricSupplier<Meter>() {
+                            public Meter newMetric() {
+                                return new Meter(new SlidingTimeWindowMovingAverages());
+                            }
+                        });
+                meter.mark();
+                final double rate = meter.getOneMinuteRate();
+                if (rate > threshold) {
+                    log.warn("Meter {} rate of {} exceeded threshold of {}", meterName, rate, threshold);
+                    return true;
+                }
+            }
+        }
+        
+        return false;
+    }
+
+}
\ No newline at end of file
diff --git a/shib-profile-api/src/main/java/net/shibboleth/profile/context/logic/RegexAttributePredicate.java b/shib-profile-api/src/main/java/net/shibboleth/profile/context/logic/RegexAttributePredicate.java
new file mode 100644
index 0000000..2256eb3
--- /dev/null
+++ b/shib-profile-api/src/main/java/net/shibboleth/profile/context/logic/RegexAttributePredicate.java
@@ -0,0 +1,127 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.profile.context.logic;
+
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.attribute.IdPAttribute;
+import net.shibboleth.idp.attribute.IdPAttributeValue;
+import net.shibboleth.idp.attribute.StringAttributeValue;
+import net.shibboleth.shared.annotation.constraint.NonnullElements;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+/**
+ * Predicate that evaluates an {@link net.shibboleth.idp.attribute.context.AttributeContext} and checks
+ * a specific attribute for value(s) that match a regular expression.
+ * 
+ * <p>This handles only simple string-valued data.</p>
+ */
+public class RegexAttributePredicate extends AbstractAttributePredicate {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(RegexAttributePredicate.class);
+
+    /** The attribute to evaluate. */
+    @Nullable @NotEmpty private String attributeId;
+
+    /** Regular expression. */
+    @Nullable private Pattern pattern;
+
+    /**
+     * Get the attribute ID to check.
+     * 
+     * @return the attribute ID to check
+     */
+    @Nullable @NotEmpty public String getAttributeId() {
+        return attributeId;
+    }
+
+    /**
+     * Set the attribute ID to check.
+     * 
+     * @param id the attribute ID to check
+     */
+    public void setAttributeId(@Nonnull @NotEmpty final String id) {
+        attributeId = Constraint.isNotNull(StringSupport.trimOrNull(id), "Attribute ID cannot be null or empty");
+    }
+
+    /**
+     * Get the pattern to match the attribute values against.
+     * 
+     * @return the pattern to match the attribute values against
+     */
+    @Nullable public Pattern getPattern() {
+        return pattern;
+    }
+
+    /**
+     * Set the pattern to match the attribute values against.
+     * 
+     * @param p the pattern to match the attribute values against
+     */
+    public void setPattern(@Nonnull final Pattern p) {
+        pattern = p;
+    }
+
+    /**
+     * Set the pattern to match the attribute values against.
+     * 
+     * @param s the pattern to match the attribute values against
+     */
+    public void setPattern(@Nonnull @NotEmpty final String s) {
+        pattern = Pattern.compile(s);
+    }
+
+    /** {@inheritDoc} */
+    @Override protected boolean hasMatch(@Nonnull @NonnullElements final Map<String, IdPAttribute> attributeMap) {
+
+        if (attributeId == null || pattern == null) {
+            log.warn("Attribute ID or regular expression were not set");
+            return false;
+        }
+        
+        final IdPAttribute attribute = attributeMap.get(attributeId);
+        if (attribute == null) {
+            log.debug("Attribute '{}' not found in context", attributeId);
+            return false;
+        }
+        
+        for (final IdPAttributeValue value : attribute.getValues()) {
+            if (value instanceof StringAttributeValue) {
+                assert pattern != null;
+                final Matcher m = pattern.matcher(((StringAttributeValue)value).getValue());
+                if (m.matches()) {
+                    log.debug("Found matching value '{}' in attribute '{}'", m.group(), attributeId);
+                    return true;
+                }
+            }
+        }
+        log.debug("Attribute '{}' values not matched", attributeId);
+        return false;
+    }
+
+}
diff --git a/shib-profile-api/src/main/java/net/shibboleth/profile/logic/RelyingPartyIdPredicate.java b/shib-profile-api/src/main/java/net/shibboleth/profile/context/logic/RelyingPartyIdPredicate.java
similarity index 98%
rename from shib-profile-api/src/main/java/net/shibboleth/profile/logic/RelyingPartyIdPredicate.java
rename to shib-profile-api/src/main/java/net/shibboleth/profile/context/logic/RelyingPartyIdPredicate.java
index 491f699..a14bbd3 100644
--- a/shib-profile-api/src/main/java/net/shibboleth/profile/logic/RelyingPartyIdPredicate.java
+++ b/shib-profile-api/src/main/java/net/shibboleth/profile/context/logic/RelyingPartyIdPredicate.java
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-package net.shibboleth.profile.logic;
+package net.shibboleth.profile.context.logic;
 
 import java.util.Collection;
 import java.util.function.Predicate;
diff --git a/shib-profile-api/src/main/java/net/shibboleth/profile/context/logic/ScriptedPredicate.java b/shib-profile-api/src/main/java/net/shibboleth/profile/context/logic/ScriptedPredicate.java
new file mode 100644
index 0000000..426c304
--- /dev/null
+++ b/shib-profile-api/src/main/java/net/shibboleth/profile/context/logic/ScriptedPredicate.java
@@ -0,0 +1,132 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.profile.context.logic;
+
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.script.ScriptContext;
+import javax.script.ScriptException;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.springframework.core.io.Resource;
+
+import net.shibboleth.shared.annotation.ParameterName;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.scripting.EvaluableScript;
+
+/**
+ * A {@link java.util.function.Predicate} which calls out to a supplied script.
+ */
+public class ScriptedPredicate
+        extends net.shibboleth.shared.logic.ScriptedPredicate<ProfileRequestContext> {
+    
+    /**
+     * Constructor.
+     * 
+     * @param theScript the script we will evaluate.
+     * @param extraInfo debugging information.
+     */
+    public ScriptedPredicate(@Nonnull @NotEmpty @ParameterName(name="theScript") final EvaluableScript theScript,
+            @Nullable @NotEmpty @ParameterName(name="extraInfo") final String extraInfo) {
+        super(theScript, extraInfo);
+        setInputType(ProfileRequestContext.class);
+    }
+
+    /**
+     * Constructor.
+     * 
+     * @param theScript the script we will evaluate.
+     */
+    public ScriptedPredicate(@Nonnull @NotEmpty @ParameterName(name="theScript") final EvaluableScript theScript) {
+        super(theScript);
+        setInputType(ProfileRequestContext.class);
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void prepareContext(@Nonnull final ScriptContext scriptContext, @Nullable final Object... input) {
+        super.prepareContext(scriptContext, input);
+        scriptContext.setAttribute("profileContext", input != null ? input[0] : null, ScriptContext.ENGINE_SCOPE);
+    }
+    
+    /**
+     * Factory to create {@link ScriptedPredicate} from a {@link Resource}.
+     * 
+     * @param resource the resource to look at
+     * @param engineName the language
+     * @return the predicate
+     * @throws ScriptException if the compile fails
+     * @throws IOException if the file doesn't exist.
+     */
+    public static ScriptedPredicate resourceScript(@Nonnull @NotEmpty final String engineName,
+            @Nonnull final Resource resource) throws ScriptException, IOException {
+        try (final InputStream is = resource.getInputStream()) {
+            final EvaluableScript script = new EvaluableScript();
+            script.setEngineName(engineName);
+            script.setScript(is);
+            script.initializeWithScriptException();
+            return new ScriptedPredicate(script, resource.getDescription());
+        }
+    }
+
+    /**
+     * Factory to create {@link ScriptedPredicate} from a {@link Resource}.
+     * 
+     * @param resource the resource to look at
+     * @return the predicate
+     * @throws ScriptException if the compile fails
+     * @throws IOException if the file doesn't exist.
+     */
+    public static ScriptedPredicate resourceScript(@Nonnull final Resource resource)
+            throws ScriptException, IOException {
+        return resourceScript(DEFAULT_ENGINE, resource);
+    }
+
+    /**
+     * Factory to create {@link ScriptedPredicate} from inline data.
+     * 
+     * @param scriptSource the script, as a string
+     * @param engineName the language
+     * @return the predicate
+     * @throws ScriptException if the compile fails
+     */
+    public static ScriptedPredicate inlineScript(@Nonnull @NotEmpty final String engineName,
+            @Nonnull @NotEmpty final String scriptSource) throws ScriptException {
+        final EvaluableScript script = new EvaluableScript();
+        script.setEngineName(engineName);
+        script.setScript(scriptSource);
+        script.initializeWithScriptException();
+        return new ScriptedPredicate(script, "Inline");
+    }
+
+    /**
+     * Factory to create {@link ScriptedPredicate} from inline data.
+     * 
+     * @param scriptSource the script, as a string
+     * @return the predicate
+     * @throws ScriptException if the compile fails
+     */
+    public static ScriptedPredicate inlineScript(@Nonnull @NotEmpty final String scriptSource) throws ScriptException {
+        return inlineScript(DEFAULT_ENGINE, scriptSource);
+    }
+
+}
\ No newline at end of file
diff --git a/shib-profile-api/src/main/java/net/shibboleth/profile/context/logic/SimpleAttributePredicate.java b/shib-profile-api/src/main/java/net/shibboleth/profile/context/logic/SimpleAttributePredicate.java
new file mode 100644
index 0000000..1c6b166
--- /dev/null
+++ b/shib-profile-api/src/main/java/net/shibboleth/profile/context/logic/SimpleAttributePredicate.java
@@ -0,0 +1,172 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.profile.context.logic;
+
+import java.util.Collection;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ListMultimap;
+
+import net.shibboleth.idp.attribute.IdPAttribute;
+import net.shibboleth.idp.attribute.IdPAttributeValue;
+import net.shibboleth.idp.attribute.ScopedStringAttributeValue;
+import net.shibboleth.idp.attribute.StringAttributeValue;
+import net.shibboleth.shared.annotation.constraint.NonnullElements;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+
+/**
+ * Predicate that evaluates an {@link net.shibboleth.idp.attribute.context.AttributeContext} and checks
+ * for particular attribute/value pairs.
+ * 
+ * <p>A map to a collection of strings is used to represent the attribute(s) and value(s) to evaluate.
+ * The values are evaluated as a disjunction (OR) and the attributes are evaluated as a conjunction (AND).</p>
+ * 
+ * <p>This handles only simple string-valued data, or if scope is supplied, requires scoped values.</p>
+ * 
+ * <p>For the special case of checking for an attribute's presence, regardless of values, the '*' value is
+ * supported. Note that this does NOT exclude pathological cases such as empty or null values. A more
+ * advanced predicate should be used to deal with such cases.</p>
+ */
+public class SimpleAttributePredicate extends AbstractAttributePredicate {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(SimpleAttributePredicate.class);
+
+    /** Map of attribute IDs to values. */
+    @Nonnull @NonnullElements private ListMultimap<String,String> attributeValueMap;
+    
+    /** Optional scope to check. */
+    @Nullable @NotEmpty private String scope;
+    
+    /** Constructor. */
+    public SimpleAttributePredicate() {
+        attributeValueMap = ArrayListMultimap.create();
+    }
+
+    /**
+     * Set the map of attribute/value pairs (as a map of string collections) to check for.
+     * 
+     * @param map   map of attribute/value pairs
+     */
+    public void setAttributeValueMap(@Nonnull @NonnullElements final Map<String,Collection<String>> map) {
+        Constraint.isNotNull(map, "Attribute/value map cannot be null");
+        
+        attributeValueMap.clear();
+        for (final Map.Entry<String,Collection<String>> entry : map.entrySet()) {
+            final String attributeId = StringSupport.trimOrNull(entry.getKey());
+            attributeValueMap.putAll(attributeId, StringSupport.normalizeStringCollection(entry.getValue()));
+        }
+    }
+    
+    /**
+     * Set a scope to check for.
+     * 
+     * <p>If set, values that "match" must be scoped with this value. A "*" will match any scope, but
+     * one must exist.</p>
+     * 
+     * @param s scope to check for
+     * 
+     * @since 4.2.0
+     */
+    public void setScope(@Nullable @NotEmpty final String s) {
+        scope = StringSupport.trimOrNull(s);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean allowNullAttributeContext() {
+        return attributeValueMap.isEmpty() && scope == null;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean hasMatch(final Map<String, IdPAttribute> attributeMap) {
+        for (final String id : attributeValueMap.keySet()) {
+            log.debug("Checking for attribute: {}", id);
+
+            final IdPAttribute attribute = attributeMap.get(id);
+            if (attribute == null) {
+                log.debug("Attribute {} not found in context", id);
+                return false;
+            }
+
+            boolean matched = false;
+
+            for (final String value : attributeValueMap.get(id)) {
+                if (findMatch(value, attribute)) {
+                    matched = true;
+                    break;
+                }
+            }
+
+            if (!matched) {
+                log.debug("Attribute {} values not matched", id);
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Look for a matching value in an attribute.
+     * 
+     * @param toMatch   value to look for
+     * @param attribute attribute to check
+     * 
+     * @return true iff the value is one of the attribute's values
+     */
+// Checkstyle: CyclomaticComplexity OFF
+    protected boolean findMatch(@Nonnull @NotEmpty final String toMatch, @Nonnull final IdPAttribute attribute) {
+        
+        if ("*".equals(toMatch) && scope == null) {
+            log.debug("Wildcard (*) value rule for attribute {}", attribute.getId());
+            return true;
+        }
+        
+        for (final IdPAttributeValue value : attribute.getValues()) {
+            if (scope != null && value instanceof ScopedStringAttributeValue) {
+                final String scopeCopy = scope;
+                if ("*".equals(toMatch) || toMatch.equals(((ScopedStringAttributeValue) value).getValue())) {
+                    if ("*".equals(scopeCopy) || scopeCopy.equals(((ScopedStringAttributeValue) value).getScope())) {
+                        log.debug("Found matching value ({}) and scope ({}) in attribute {}", toMatch, scopeCopy,
+                                attribute.getId());
+                        return true;
+                    }
+                }
+            } else if (scope == null && value instanceof StringAttributeValue) {
+                if (toMatch.equals(((StringAttributeValue) value).getValue())) {
+                    log.debug("Found matching value ({}) in attribute {}", toMatch, attribute.getId());
+                    return true;
+                }
+            }
+        }
+        
+        return false;
+    }
+// Checkstyle: CyclomaticComplexity ON
+
+}
\ No newline at end of file
diff --git a/shib-profile-api/src/main/java/net/shibboleth/profile/context/logic/SpringExpressionPredicate.java b/shib-profile-api/src/main/java/net/shibboleth/profile/context/logic/SpringExpressionPredicate.java
new file mode 100644
index 0000000..3305a0d
--- /dev/null
+++ b/shib-profile-api/src/main/java/net/shibboleth/profile/context/logic/SpringExpressionPredicate.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.profile.context.logic;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import net.shibboleth.shared.annotation.ParameterName;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.springframework.expression.EvaluationContext;
+
+/**
+ * Predicate whose condition is defined by an Spring EL expression.
+ * 
+ * @author Daniel Lutz
+ */
+public class SpringExpressionPredicate
+    extends net.shibboleth.shared.spring.expression.SpringExpressionPredicate<ProfileRequestContext> {
+
+    /**
+     * Constructor.
+     *
+     * @param expression the expression to evaluate
+     */
+    public SpringExpressionPredicate(@Nonnull @NotEmpty @ParameterName(name="expression") final String expression) {
+        super(expression);
+        setInputType(ProfileRequestContext.class);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void prepareContext(@Nonnull final EvaluationContext context, @Nullable final Object... input) {
+        super.prepareContext(context, input);
+        context.setVariable("profileContext", input != null ? input[0] : input);
+    }
+
+}
\ No newline at end of file
diff --git a/shib-profile-impl/src/main/java/net/shibboleth/profile/logic/impl/package-info.java b/shib-profile-api/src/main/java/net/shibboleth/profile/context/logic/VerifiedProfilePredicate.java
similarity index 55%
copy from shib-profile-impl/src/main/java/net/shibboleth/profile/logic/impl/package-info.java
copy to shib-profile-api/src/main/java/net/shibboleth/profile/context/logic/VerifiedProfilePredicate.java
index 85190d0..9121d5c 100644
--- a/shib-profile-impl/src/main/java/net/shibboleth/profile/logic/impl/package-info.java
+++ b/shib-profile-api/src/main/java/net/shibboleth/profile/context/logic/VerifiedProfilePredicate.java
@@ -15,7 +15,29 @@
  * limitations under the License.
  */
 
+package net.shibboleth.profile.context.logic;
+
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.profile.context.RelyingPartyContext;
+
 /**
- * Implementation classes supporting profile predicates.
+ * Predicate to determine whether a profile request is from a verified source.
  */
-package net.shibboleth.profile.logic.impl;
\ No newline at end of file
+public class VerifiedProfilePredicate extends AbstractRelyingPartyPredicate {
+
+    /** {@inheritDoc} */
+    public boolean test(@Nullable final ProfileRequestContext input) {
+        if (input != null) {
+            final RelyingPartyContext rpc = getRelyingPartyContext(input);
+            if (rpc != null) {
+                return rpc.isVerified();
+            }
+        }
+        
+        return true;
+    }
+
+}
\ No newline at end of file
diff --git a/shib-profile-api/src/main/java/net/shibboleth/profile/context/logic/messaging/AbstractRelyingPartyPredicate.java b/shib-profile-api/src/main/java/net/shibboleth/profile/context/logic/messaging/AbstractRelyingPartyPredicate.java
new file mode 100644
index 0000000..23892c8
--- /dev/null
+++ b/shib-profile-api/src/main/java/net/shibboleth/profile/context/logic/messaging/AbstractRelyingPartyPredicate.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.profile.context.logic.messaging;
+
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.InOutOperationContext;
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.messaging.context.navigate.RecursiveTypedParentContextLookup;
+
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * Abstract base class for a predicate that evaluates a {@link MessageContext} 
+ * and which requires a {@link RelyingPartyContext} obtained via a lookup function,
+ * by default a child of the {@link InOutOperationContext} 
+ * the parent of the specified {@link MessageContext}.
+ */
+public abstract class AbstractRelyingPartyPredicate implements Predicate<MessageContext> {
+
+    /**
+     * Strategy used to locate the {@link RelyingPartyContext} associated with a given {@link MessageContext}.
+     */
+    @Nonnull private Function<MessageContext,RelyingPartyContext> relyingPartyContextLookupStrategy;
+    
+    /** Constructor. */
+    public AbstractRelyingPartyPredicate() {
+        relyingPartyContextLookupStrategy =
+                new ChildContextLookup<>(RelyingPartyContext.class).compose(
+                        new RecursiveTypedParentContextLookup<>(InOutOperationContext.class));
+    }
+
+    /**
+     * Set the strategy used to locate the {@link RelyingPartyContext} associated with a given
+     * {@link MessageContext}.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setRelyingPartyContextLookupStrategy(
+            @Nonnull final Function<MessageContext,RelyingPartyContext> strategy) {
+        relyingPartyContextLookupStrategy =
+                Constraint.isNotNull(strategy, "RelyingPartyContext lookup strategy cannot be null");
+    }
+    
+    /**
+     * Get the strategy used to locate the {@link RelyingPartyContext} associated with a given
+     * {@link MessageContext}.
+     * 
+     * @return lookup strategy
+     */
+    @Nonnull public Function<MessageContext,RelyingPartyContext> getRelyingPartyContextLookupStrategy() {
+        return relyingPartyContextLookupStrategy;
+    }
+
+    /**
+     * Return the result of applying the lookup function to obtain the {@link RelyingPartyContext}.
+     * 
+     * @param input message context
+     * 
+     * @return the {@link RelyingPartyContext} or null
+     */
+    @Nullable protected RelyingPartyContext getRelyingPartyContext(@Nullable final MessageContext input) {
+        return relyingPartyContextLookupStrategy.apply(input);
+    }
+
+}
\ No newline at end of file
diff --git a/shib-profile-impl/src/main/java/net/shibboleth/profile/logic/impl/package-info.java b/shib-profile-api/src/main/java/net/shibboleth/profile/context/logic/messaging/package-info.java
similarity index 89%
copy from shib-profile-impl/src/main/java/net/shibboleth/profile/logic/impl/package-info.java
copy to shib-profile-api/src/main/java/net/shibboleth/profile/context/logic/messaging/package-info.java
index 85190d0..27ac960 100644
--- a/shib-profile-impl/src/main/java/net/shibboleth/profile/logic/impl/package-info.java
+++ b/shib-profile-api/src/main/java/net/shibboleth/profile/context/logic/messaging/package-info.java
@@ -16,6 +16,6 @@
  */
 
 /**
- * Implementation classes supporting profile predicates.
+ * Predicates that evaluate message contexts.
  */
-package net.shibboleth.profile.logic.impl;
\ No newline at end of file
+package net.shibboleth.profile.context.logic.messaging;
\ No newline at end of file
diff --git a/shib-profile-api/src/main/java/net/shibboleth/profile/logic/package-info.java b/shib-profile-api/src/main/java/net/shibboleth/profile/context/logic/package-info.java
similarity index 90%
rename from shib-profile-api/src/main/java/net/shibboleth/profile/logic/package-info.java
rename to shib-profile-api/src/main/java/net/shibboleth/profile/context/logic/package-info.java
index 79cd221..f67fb88 100644
--- a/shib-profile-api/src/main/java/net/shibboleth/profile/logic/package-info.java
+++ b/shib-profile-api/src/main/java/net/shibboleth/profile/context/logic/package-info.java
@@ -16,6 +16,6 @@
  */
 
 /**
- * Predicates that test against profile request state.
+ * Predicates that evaluate profile contexts.
  */
-package net.shibboleth.profile.logic;
\ No newline at end of file
+package net.shibboleth.profile.context.logic;
\ No newline at end of file
diff --git a/shib-profile-api/src/test/java/net/shibboleth/profile/context/logic/DateAttributePredicateTest.java b/shib-profile-api/src/test/java/net/shibboleth/profile/context/logic/DateAttributePredicateTest.java
new file mode 100644
index 0000000..1e73798
--- /dev/null
+++ b/shib-profile-api/src/test/java/net/shibboleth/profile/context/logic/DateAttributePredicateTest.java
@@ -0,0 +1,159 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.profile.context.logic;
+
+import net.shibboleth.idp.attribute.DateTimeAttributeValue;
+import net.shibboleth.idp.attribute.IdPAttribute;
+import net.shibboleth.idp.attribute.IdPAttributeValue;
+import net.shibboleth.idp.attribute.StringAttributeValue;
+import net.shibboleth.idp.attribute.context.AttributeContext;
+import net.shibboleth.profile.context.RelyingPartyContext;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import java.time.Instant;
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static org.testng.Assert.*;
+
+/**
+ * Unit test for {@link DateAttributePredicate}.
+ */
+ at SuppressWarnings("javadoc")
+public class DateAttributePredicateTest {
+    
+    private final java.time.format.DateTimeFormatter javaformatter =
+            java.time.format.DateTimeFormatter.ISO_ZONED_DATE_TIME;
+
+    @DataProvider(name = "test-data-java")
+    public Object[][] provideTestDataJava() {
+        return new Object[][] {
+                // Future date matches
+                new Object[] {
+                        new DateAttributePredicate("expirationDate", javaformatter),
+                        "expirationDate",
+                        javaDateStrings(java.time.Duration.ofDays(1)),
+                        true,
+                },
+                // Current date does not match
+                new Object[] {
+                        new DateAttributePredicate("expirationDate", javaformatter),
+                        "expirationDate",
+                        javaDateStrings(java.time.Duration.ZERO),
+                        false,
+                },
+                // Past date does not match
+                new Object[] {
+                        new DateAttributePredicate("expirationDate", javaformatter),
+                        "expirationDate",
+                        javaDateStrings(java.time.Duration.ofDays(-1)),
+                        false,
+                },
+                // Increase target date by 90 days
+                new Object[] {
+                        newJavaPredicate("expirationDate", java.time.Duration.ofDays(90)),
+                        "expirationDate",
+                        javaDateStrings(java.time.Duration.ofDays(91)),
+                        true,
+                },
+                // Decrease target date by 30 days
+                // e.g. expiration warning case
+                new Object[] {
+                        newJavaPredicate("expirationDate", java.time.Duration.ofDays(-30)),
+                        "expirationDate",
+                        javaDateStrings(java.time.Duration.ofDays(29)),
+                        false,
+                },
+        };
+    }
+
+
+    @Test(dataProvider = "test-data-java")
+    public void testJavaTime(
+            final DateAttributePredicate predicate,
+            final String attribute,
+            final String[] values,
+            final boolean expected) throws Exception {
+        assertEquals(predicate.test(createProfileRequestContext(attribute, values, null)), expected);
+    }
+
+    @Test
+    public void testDateTimeValues() {
+        final DateAttributePredicate predicate = new DateAttributePredicate("test");
+        
+        assertTrue(predicate.test(createProfileRequestContext("test", null,
+                new Instant[] {Instant.now().plus(java.time.Duration.ofMinutes(5))})));
+        
+        predicate.setOffset(java.time.Duration.ofMinutes(-10));
+        
+        assertFalse(predicate.test(createProfileRequestContext("test", null,
+                new Instant[] {Instant.now().plus(java.time.Duration.ofMinutes(5))})));
+    }
+    
+    
+    private ProfileRequestContext createProfileRequestContext(final String name, final String[] values, final Instant[] dtvalues) {
+        final ProfileRequestContext prc = new ProfileRequestContext();
+        final RelyingPartyContext rpc = new RelyingPartyContext();
+        final IdPAttribute attribute = new IdPAttribute(name);
+        final List<IdPAttributeValue> attributeValues = new ArrayList<>();
+        if (values != null) {
+            for (String value : values) {
+                attributeValues.add(new StringAttributeValue(value));
+            }
+        }
+        if (dtvalues != null) {
+            for (Instant value : dtvalues) {
+                attributeValues.add(new DateTimeAttributeValue(value));
+            }
+        }
+        attribute.setValues(attributeValues);
+        final AttributeContext ac = new AttributeContext();
+        ac.setIdPAttributes(Collections.singletonList(attribute));
+        ac.setUnfilteredIdPAttributes(Collections.singletonList(attribute));
+        rpc.addSubcontext(ac);
+        prc.addSubcontext(rpc);
+        return prc;
+    }
+
+    /**
+     * Produces an array of date strings that are offsets from current system time.
+     *
+     * @param offsets One or more durations that are added to the current system time.
+     *
+     * @return Array of date strings, one for each provided offset.
+     */
+    private String[] javaDateStrings(final java.time.Duration ... offsets) {
+        final String[] dates = new String[offsets.length];
+        for (int i = 0; i < offsets.length; i++) {
+            dates[i] = javaformatter.format(ZonedDateTime.now().plus(offsets[i]));
+        }
+        return dates;
+    }
+    
+    private DateAttributePredicate newJavaPredicate(final String attribute, final java.time.Duration offset) {
+        final DateAttributePredicate p = new DateAttributePredicate(attribute, javaformatter);
+        p.setOffset(offset);
+        return p;
+    }
+    
+}
diff --git a/shib-profile-api/src/test/java/net/shibboleth/profile/context/logic/DynamicAttributePredicateTest.java b/shib-profile-api/src/test/java/net/shibboleth/profile/context/logic/DynamicAttributePredicateTest.java
new file mode 100644
index 0000000..5aa8b54
--- /dev/null
+++ b/shib-profile-api/src/test/java/net/shibboleth/profile/context/logic/DynamicAttributePredicateTest.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.profile.context.logic;
+
+import net.shibboleth.idp.attribute.IdPAttribute;
+import net.shibboleth.idp.attribute.IdPAttributeValue;
+import net.shibboleth.idp.attribute.ScopedStringAttributeValue;
+import net.shibboleth.idp.attribute.StringAttributeValue;
+import net.shibboleth.idp.attribute.context.AttributeContext;
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.logic.FunctionSupport;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.testng.annotations.Test;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import static org.testng.Assert.*;
+
+/**
+ * Unit test for {@link DynamicAttributePredicate}.
+ */
+ at SuppressWarnings("javadoc")
+public class DynamicAttributePredicateTest {
+
+    @Test
+    public void testInvalid() {
+        final ProfileRequestContext prc = createProfileRequestContext("foo", List.of("bar", "baz"));
+        DynamicAttributePredicate predicate = new DynamicAttributePredicate();
+        
+        predicate.setAttributeFunctionMap(Map.of("foo", CollectionSupport.singleton(FunctionSupport.constant(List.of(10)))));
+        assertFalse(predicate.test(prc));
+
+        predicate.setAttributeFunctionMap(Map.of("foo", CollectionSupport.singleton(FunctionSupport.constant(10))));
+        assertFalse(predicate.test(prc));
+    }
+
+    @Test
+    public void testString() {
+        final ProfileRequestContext prc = createProfileRequestContext("foo", List.of("bar", "baz"));
+        DynamicAttributePredicate predicate = new DynamicAttributePredicate();
+        
+        predicate.setAttributeFunctionMap(Map.of("foo2", CollectionSupport.singleton(FunctionSupport.constant(List.of("bar")))));
+        assertFalse(predicate.test(prc));
+
+        predicate.setAttributeFunctionMap(Map.of("foo2", CollectionSupport.singleton(FunctionSupport.constant("bar"))));
+        assertFalse(predicate.test(prc));
+
+        predicate.setAttributeFunctionMap(Map.of("foo2", CollectionSupport.singleton(FunctionSupport.constant("*"))));
+        assertFalse(predicate.test(prc));
+
+        predicate.setAttributeFunctionMap(Map.of("foo", CollectionSupport.singleton(FunctionSupport.constant("*"))));
+        assertTrue(predicate.test(prc));
+
+        predicate.setAttributeFunctionMap(Map.of("foo", CollectionSupport.singleton(FunctionSupport.constant(List.of("bar", "baz")))));
+        assertTrue(predicate.test(prc));
+    }
+    
+    private ProfileRequestContext createProfileRequestContext(final String name, final Collection<String> values) {
+        final ProfileRequestContext prc = new ProfileRequestContext();
+        final RelyingPartyContext rpc = new RelyingPartyContext();
+        final IdPAttribute attribute = new IdPAttribute(name);
+        final List<IdPAttributeValue> attributeValues = new ArrayList<>();
+        for (final String value : values) {
+            final int i = value.indexOf('@');
+            if (i == -1) {
+                attributeValues.add(new StringAttributeValue(value));
+            } else {
+                attributeValues.add(new ScopedStringAttributeValue(value.substring(0,i), value.substring(i + 1)));
+            }
+        }
+        attribute.setValues(attributeValues);
+        final AttributeContext ac = new AttributeContext();
+        ac.setIdPAttributes(CollectionSupport.singletonList(attribute));
+        ac.setUnfilteredIdPAttributes(CollectionSupport.singletonList(attribute));
+        rpc.addSubcontext(ac);
+        prc.addSubcontext(rpc);
+        return prc;
+    }
+   
+}
\ No newline at end of file
diff --git a/shib-profile-api/src/test/java/net/shibboleth/profile/context/logic/LoopDetectionPredicateTest.java b/shib-profile-api/src/test/java/net/shibboleth/profile/context/logic/LoopDetectionPredicateTest.java
new file mode 100644
index 0000000..6d54ece
--- /dev/null
+++ b/shib-profile-api/src/test/java/net/shibboleth/profile/context/logic/LoopDetectionPredicateTest.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.profile.context.logic;
+
+import java.util.Map;
+
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.shared.logic.FunctionSupport;
+
+import org.opensaml.core.testing.OpenSAMLInitBaseTestCase;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+/** Unit test for {@link LoopDetectionPredicate}. */
+ at SuppressWarnings("javadoc")
+public class LoopDetectionPredicateTest extends OpenSAMLInitBaseTestCase {
+
+    private ProfileRequestContext prc;
+    private RelyingPartyContext rpCtx;
+    private LoopDetectionPredicate pred;
+    
+    @BeforeMethod
+    public void setUp() {
+        prc = new ProfileRequestContext();
+        rpCtx = prc.getOrCreateSubcontext(RelyingPartyContext.class);
+        pred = new LoopDetectionPredicate();
+        pred.setUsernameLookupStrategy(FunctionSupport.constant("jdoe.1"));
+    }
+    
+    @Test
+    public void testNoMap() {
+        Assert.assertFalse(pred.test(prc));
+        
+        rpCtx.setRelyingPartyId("foo");
+        Assert.assertFalse(pred.test(prc));
+    }
+
+    @Test
+    public void testNoMatch() {
+        pred.setRelyingPartyMap(Map.of("bar", "bar"));
+        
+        rpCtx.setRelyingPartyId("foo");
+        Assert.assertFalse(pred.test(prc));
+    }
+
+    @Test
+    public void testMatch() {
+        pred.setRelyingPartyMap(Map.of("foo", "foo"));
+        
+        rpCtx.setRelyingPartyId("foo");
+        Assert.assertFalse(pred.test(prc));
+    }
+
+    @Test
+    public void testExceed() throws InterruptedException {
+        pred.setRelyingPartyMap(Map.of("bar", "bar"));
+        
+        rpCtx.setRelyingPartyId("bar");
+        for (int i=0; i<20; ++i) {
+            Assert.assertFalse(pred.test(prc));
+        }
+        Assert.assertTrue(pred.test(prc));
+    }
+
+}
\ No newline at end of file
diff --git a/shib-profile-api/src/test/java/net/shibboleth/profile/logic/RelyingPartyIdPredicateTest.java b/shib-profile-api/src/test/java/net/shibboleth/profile/context/logic/RelyingPartyIdPredicateTest.java
similarity index 99%
rename from shib-profile-api/src/test/java/net/shibboleth/profile/logic/RelyingPartyIdPredicateTest.java
rename to shib-profile-api/src/test/java/net/shibboleth/profile/context/logic/RelyingPartyIdPredicateTest.java
index 607b0cf..2fd05d8 100644
--- a/shib-profile-api/src/test/java/net/shibboleth/profile/logic/RelyingPartyIdPredicateTest.java
+++ b/shib-profile-api/src/test/java/net/shibboleth/profile/context/logic/RelyingPartyIdPredicateTest.java
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-package net.shibboleth.profile.logic;
+package net.shibboleth.profile.context.logic;
 
 import net.shibboleth.profile.context.RelyingPartyContext;
 import net.shibboleth.shared.collection.CollectionSupport;
diff --git a/shib-profile-api/src/test/java/net/shibboleth/profile/context/logic/ScriptedPredicateTest.java b/shib-profile-api/src/test/java/net/shibboleth/profile/context/logic/ScriptedPredicateTest.java
new file mode 100644
index 0000000..1685116
--- /dev/null
+++ b/shib-profile-api/src/test/java/net/shibboleth/profile/context/logic/ScriptedPredicateTest.java
@@ -0,0 +1,107 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.profile.context.logic;
+
+import javax.script.ScriptException;
+
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.shared.spring.custom.SchemaTypeAwareXMLBeanDefinitionReader;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.springframework.context.support.GenericApplicationContext;
+import org.springframework.core.io.ClassPathResource;
+import org.testng.Assert;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+/**
+ * Tests for {@link ScriptedPredicate}.
+ */
+ at SuppressWarnings("javadoc")
+public class ScriptedPredicateTest {
+
+    private ProfileRequestContext withChild;
+
+    private ProfileRequestContext noChild;
+
+    @BeforeClass public void setup() {
+        withChild = new ProfileRequestContext();
+        withChild.getOrCreateSubcontext(RelyingPartyContext.class);
+        noChild = new ProfileRequestContext();
+    }
+
+    @Test public void simple() throws ScriptException {
+        ScriptedPredicate test = ScriptedPredicate.inlineScript("new java.lang.Boolean(true);");
+        Assert.assertTrue(test.test(withChild));
+
+        test = ScriptedPredicate.inlineScript("true");
+        Assert.assertTrue(test.test(withChild));
+
+        test = ScriptedPredicate.inlineScript("false");
+        Assert.assertFalse(test.test(withChild));
+
+        test = ScriptedPredicate.inlineScript("\"thirty\"");
+        Assert.assertFalse(test.test(withChild));
+    }
+    
+    @Test public void custom() throws ScriptException {
+        ScriptedPredicate test = ScriptedPredicate.inlineScript("custom;");
+        test.setCustomObject(Boolean.valueOf(true));
+        Assert.assertTrue(test.test(withChild));
+
+        test.setCustomObject(Boolean.valueOf(false));
+        Assert.assertFalse(test.test(withChild));
+
+    }
+    @Test public void inlineBean() throws ScriptException {
+
+        try (final GenericApplicationContext ctx = new GenericApplicationContext()) {
+            final SchemaTypeAwareXMLBeanDefinitionReader beanDefinitionReader =
+                    new SchemaTypeAwareXMLBeanDefinitionReader(ctx);
+
+            beanDefinitionReader.loadBeanDefinitions(new ClassPathResource(
+                    "/net/shibboleth/idp/profile/logic/inlineBean.xml"));
+
+            ctx.refresh();
+            final ScriptedPredicate rule = ctx.getBean(ScriptedPredicate.class);
+
+            Assert.assertTrue(rule.test(withChild));
+
+            Assert.assertFalse(rule.test(noChild));
+        }
+    }
+
+    @Test public void resourceBean() throws ScriptException {
+
+        try (final GenericApplicationContext ctx = new GenericApplicationContext()) {
+            final SchemaTypeAwareXMLBeanDefinitionReader beanDefinitionReader =
+                    new SchemaTypeAwareXMLBeanDefinitionReader(ctx);
+
+            beanDefinitionReader.loadBeanDefinitions(new ClassPathResource(
+                    "/net/shibboleth/idp/profile/logic/resourceBean.xml"));
+
+            ctx.refresh();
+            final ScriptedPredicate rule = ctx.getBean(ScriptedPredicate.class);
+
+            Assert.assertTrue(rule.test(withChild));
+
+            Assert.assertFalse(rule.test(noChild));
+        }
+    }
+
+}
diff --git a/shib-profile-api/src/test/java/net/shibboleth/profile/context/logic/SimpleAttributePredicateTest.java b/shib-profile-api/src/test/java/net/shibboleth/profile/context/logic/SimpleAttributePredicateTest.java
new file mode 100644
index 0000000..04e5038
--- /dev/null
+++ b/shib-profile-api/src/test/java/net/shibboleth/profile/context/logic/SimpleAttributePredicateTest.java
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.profile.context.logic;
+
+import net.shibboleth.idp.attribute.IdPAttribute;
+import net.shibboleth.idp.attribute.IdPAttributeValue;
+import net.shibboleth.idp.attribute.ScopedStringAttributeValue;
+import net.shibboleth.idp.attribute.StringAttributeValue;
+import net.shibboleth.idp.attribute.context.AttributeContext;
+import net.shibboleth.profile.context.RelyingPartyContext;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.testng.annotations.Test;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static org.testng.Assert.*;
+
+/**
+ * Unit test for {@link SimpleAttributePredicate}.
+ */
+ at SuppressWarnings("javadoc")
+public class SimpleAttributePredicateTest {
+
+    @Test
+    public void testString() {
+        final ProfileRequestContext prc = createProfileRequestContext("foo", List.of("bar", "baz"));
+        SimpleAttributePredicate predicate = new SimpleAttributePredicate();
+        
+        predicate.setAttributeValueMap(Map.of("foo2", List.of("bar")));
+        assertFalse(predicate.test(prc));
+        
+        predicate.setAttributeValueMap(Map.of("foo2", List.of("*")));
+        assertFalse(predicate.test(prc));
+
+        predicate.setAttributeValueMap(Map.of("foo", List.of("*")));
+        assertTrue(predicate.test(prc));
+
+        predicate.setAttributeValueMap(Map.of("foo", List.of("bar", "baz")));
+        assertTrue(predicate.test(prc));
+        
+        predicate.setScope("zorkmid");
+        assertFalse(predicate.test(prc));
+    }
+    
+    @Test
+    public void testScoped() {
+        final ProfileRequestContext prc = createProfileRequestContext("foo", List.of("bar at scope1", "baz at scope2"));
+        SimpleAttributePredicate predicate = new SimpleAttributePredicate();
+        
+        predicate.setAttributeValueMap(Map.of("foo", List.of("bar")));
+        assertTrue(predicate.test(prc));
+        
+        predicate.setAttributeValueMap(Map.of("foo", List.of("*")));
+        assertTrue(predicate.test(prc));
+
+        predicate.setScope("zorkmid");
+        assertFalse(predicate.test(prc));
+        
+        predicate.setScope("*");
+        assertTrue(predicate.test(prc));
+
+        predicate.setAttributeValueMap(Map.of("foo", List.of("bar", "baz")));
+        assertTrue(predicate.test(prc));
+        
+        predicate.setScope("scope1");
+        assertTrue(predicate.test(prc));
+
+        predicate.setScope("scope2");
+        assertTrue(predicate.test(prc));
+
+        predicate.setAttributeValueMap(Map.of("foo", List.of("bar")));
+        assertFalse(predicate.test(prc));
+    }
+    
+    private ProfileRequestContext createProfileRequestContext(final String name, final Collection<String> values) {
+        final ProfileRequestContext prc = new ProfileRequestContext();
+        final RelyingPartyContext rpc = new RelyingPartyContext();
+        final IdPAttribute attribute = new IdPAttribute(name);
+        final List<IdPAttributeValue> attributeValues = new ArrayList<>();
+        for (final String value : values) {
+            final int i = value.indexOf('@');
+            if (i == -1) {
+                attributeValues.add(new StringAttributeValue(value));
+            } else {
+                attributeValues.add(new ScopedStringAttributeValue(value.substring(0,i), value.substring(i + 1)));
+            }
+        }
+        attribute.setValues(attributeValues);
+        final AttributeContext ac = new AttributeContext();
+        ac.setIdPAttributes(Collections.singletonList(attribute));
+        ac.setUnfilteredIdPAttributes(Collections.singletonList(attribute));
+        rpc.addSubcontext(ac);
+        prc.addSubcontext(rpc);
+        return prc;
+    }
+   
+}
\ No newline at end of file
diff --git a/shib-profile-api/src/test/resources/net/shibboleth/profile/logic/relyingPartyIdPredicates.xml b/shib-profile-api/src/test/resources/net/shibboleth/profile/logic/relyingPartyIdPredicates.xml
index 811b4cb..5a8ca9a 100644
--- a/shib-profile-api/src/test/resources/net/shibboleth/profile/logic/relyingPartyIdPredicates.xml
+++ b/shib-profile-api/src/test/resources/net/shibboleth/profile/logic/relyingPartyIdPredicates.xml
@@ -18,17 +18,17 @@
         </property>
     </bean>
 
-    <bean class="net.shibboleth.profile.logic.RelyingPartyIdPredicate" id="candidate" c:candidate="Single"/>
+    <bean class="net.shibboleth.profile.context.logic.RelyingPartyIdPredicate" id="candidate" c:candidate="Single"/>
 
-    <bean class="net.shibboleth.profile.logic.RelyingPartyIdPredicate" id="candidates" c:candidates="#{ {'Single', 'Double'} }"/>
+    <bean class="net.shibboleth.profile.context.logic.RelyingPartyIdPredicate" id="candidates" c:candidates="#{ {'Single', 'Double'} }"/>
 
-    <bean class="net.shibboleth.profile.logic.RelyingPartyIdPredicate" id="pred" c:pred="true"/>
+    <bean class="net.shibboleth.profile.context.logic.RelyingPartyIdPredicate" id="pred" c:pred="true"/>
 
-    <bean class="net.shibboleth.profile.logic.RelyingPartyIdPredicate" id="candidate_0" c:candidate="Single"/>
+    <bean class="net.shibboleth.profile.context.logic.RelyingPartyIdPredicate" id="candidate_0" c:candidate="Single"/>
 
-    <bean class="net.shibboleth.profile.logic.RelyingPartyIdPredicate" id="candidates_0" c:candidates="#{ {'Single', 'Double'} }"/>
+    <bean class="net.shibboleth.profile.context.logic.RelyingPartyIdPredicate" id="candidates_0" c:candidates="#{ {'Single', 'Double'} }"/>
 
-    <bean class="net.shibboleth.profile.logic.RelyingPartyIdPredicate" id="pred_0" c:pred="true"/>
+    <bean class="net.shibboleth.profile.context.logic.RelyingPartyIdPredicate" id="pred_0" c:pred="true"/>
     
 </beans>
     
\ No newline at end of file
diff --git a/shib-profile-impl/src/main/java/net/shibboleth/profile/logic/impl/RelyingPartiesActivationConditionFactory.java b/shib-profile-impl/src/main/java/net/shibboleth/profile/context/logic/impl/RelyingPartiesActivationConditionFactory.java
similarity index 95%
rename from shib-profile-impl/src/main/java/net/shibboleth/profile/logic/impl/RelyingPartiesActivationConditionFactory.java
rename to shib-profile-impl/src/main/java/net/shibboleth/profile/context/logic/impl/RelyingPartiesActivationConditionFactory.java
index e6ca28c..6393757 100644
--- a/shib-profile-impl/src/main/java/net/shibboleth/profile/logic/impl/RelyingPartiesActivationConditionFactory.java
+++ b/shib-profile-impl/src/main/java/net/shibboleth/profile/context/logic/impl/RelyingPartiesActivationConditionFactory.java
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-package net.shibboleth.profile.logic.impl;
+package net.shibboleth.profile.context.logic.impl;
 
 import java.util.Collection;
 import java.util.Map;
@@ -29,7 +29,7 @@ import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 
 import net.shibboleth.idp.attribute.transcoding.AttributeTranscoderRegistry;
-import net.shibboleth.profile.logic.RelyingPartyIdPredicate;
+import net.shibboleth.profile.context.logic.RelyingPartyIdPredicate;
 import net.shibboleth.shared.primitive.LoggerFactory;
 import net.shibboleth.shared.primitive.StringSupport;
 
diff --git a/shib-profile-impl/src/main/java/net/shibboleth/profile/logic/impl/package-info.java b/shib-profile-impl/src/main/java/net/shibboleth/profile/context/logic/impl/package-info.java
similarity index 94%
rename from shib-profile-impl/src/main/java/net/shibboleth/profile/logic/impl/package-info.java
rename to shib-profile-impl/src/main/java/net/shibboleth/profile/context/logic/impl/package-info.java
index 85190d0..30d803f 100644
--- a/shib-profile-impl/src/main/java/net/shibboleth/profile/logic/impl/package-info.java
+++ b/shib-profile-impl/src/main/java/net/shibboleth/profile/context/logic/impl/package-info.java
@@ -18,4 +18,4 @@
 /**
  * Implementation classes supporting profile predicates.
  */
-package net.shibboleth.profile.logic.impl;
\ No newline at end of file
+package net.shibboleth.profile.context.logic.impl;
\ No newline at end of file

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


More information about the commits mailing list