[java-identity-provider] branch maint-4 updated: IDP-1993 - New AttributeDefinition for DateTime values

Scott Cantor cantor.2 at osu.edu
Mon Aug 15 17:28:56 UTC 2022


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

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

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

The following commit(s) were added to refs/heads/maint-4 by this push:
     new 192b98c22 IDP-1993 - New AttributeDefinition for DateTime values
192b98c22 is described below

commit 192b98c22f577f60ae0eb4c0efc73c413f3304d2
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Mon Aug 15 13:28:53 2022 -0400

    IDP-1993 - New AttributeDefinition for DateTime values
    
    https://shibboleth.atlassian.net/browse/IDP-1993
    
    Also various test cleanups.
---
 .../ad/impl/DateTimeAttributeDefinition.java       | 222 ++++++++++++++++
 .../ad/impl/DateTimeAttributeDefinitionTest.java   | 291 +++++++++++++++++++++
 ...SAML1NameIdentifierAttributeDefinitionTest.java |   2 +-
 .../impl/SAML2NameIDAttributeDefinitionTest.java   |   2 +-
 .../resolver/ad/impl/ScriptedAttributeTest.java    |   2 +-
 .../resolver/ad/impl/SimpleAttributeTest.java      |   2 +-
 .../resolver/ad/impl/TemplateAttributeTest.java    |   2 +-
 .../ad/impl/DateTimeAttributeDefinitionParser.java |  71 +++++
 .../impl/AttributeResolverNamespaceHandler.java    |   3 +
 .../ad/DateTimeAttributeDefinitionParserTest.java  |  61 +++++
 .../ad/ScopedAttributeDefinitionParserTest.java    |   3 +-
 .../resolver/spring/ad/resolver/datetime.xml       |   7 +
 .../resolver/spring/ad/resolver/datetimeCustom.xml |  10 +
 .../schema/shibboleth-attribute-resolver.xsd       |  34 +++
 14 files changed, 705 insertions(+), 7 deletions(-)

diff --git a/idp-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/ad/impl/DateTimeAttributeDefinition.java b/idp-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/ad/impl/DateTimeAttributeDefinition.java
new file mode 100644
index 000000000..f78e90b7a
--- /dev/null
+++ b/idp-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/ad/impl/DateTimeAttributeDefinition.java
@@ -0,0 +1,222 @@
+/*
+ * 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.idp.attribute.resolver.ad.impl;
+
+import java.time.DateTimeException;
+import java.time.Instant;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Predicates;
+
+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.resolver.AbstractAttributeDefinition;
+import net.shibboleth.idp.attribute.resolver.AttributeDefinition;
+import net.shibboleth.idp.attribute.resolver.PluginDependencySupport;
+import net.shibboleth.idp.attribute.resolver.ResolutionException;
+import net.shibboleth.idp.attribute.resolver.context.AttributeResolutionContext;
+import net.shibboleth.idp.attribute.resolver.context.AttributeResolverWorkContext;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/**
+ * An {@link AttributeDefinition} that creates an attribute whose values are the values
+ * of all its dependencies, passed through or converted into a {@link DateTimeAttributeValue}.
+ * 
+ * <p>Values already of this type are simple passed through, while {@link StringAttributeValue} objects
+ * are converted. It is optional whether to omit incompatible values or raise an error.</p>
+ * 
+ * @since 4.3.0
+ */
+ at ThreadSafe
+public class DateTimeAttributeDefinition extends AbstractAttributeDefinition {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(DateTimeAttributeDefinition.class);
+
+    /** Formatter for string to date/time conversion. */
+    @Nullable private DateTimeFormatter formatter;
+    
+    /** Convert numeric strings into epoch as seconds. */
+    private boolean epochInSeconds;
+    
+    /** Do we ignore converstion failures? */
+    private boolean ignoreConversionErrors;
+
+    /** Constructor. */
+    public DateTimeAttributeDefinition() {
+        epochInSeconds = true;
+    }
+    
+    /**
+     * Set whether to convert numeric string data into an epoch using seconds instead of milliseconds.
+     * 
+     * <p>Defaults to true.</p>
+     * 
+     * @param flag
+     */
+    public void setEpochInSeconds(final boolean flag) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+        
+        epochInSeconds = flag;
+    }
+    
+    /**
+     * Get whether to convert numeric string data into an epoch using seconds instead of milliseconds.
+     * 
+     * @return true iff epoch conversion should be based on seconds
+     */
+    public boolean isEpochInSeconds() {
+        return epochInSeconds;
+    }
+    
+    /**
+     * Set whether to ignore conversion failures.
+     * 
+     * @param flag flag to set
+     */
+    public void setIgnoreConversionErrors(final boolean flag) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+        
+        ignoreConversionErrors = flag;
+    }
+
+   /** 
+     * Get whether to ignore conversion failures.
+     * 
+     * @return whether to ignore conversion failures
+     */
+    public boolean isIgnoreConversionErrors() {
+        return ignoreConversionErrors;
+    }
+    
+    /**
+     * Set a formatter to use to convert string data into an {@link Instant}.
+     * 
+     * @param f formatter
+     */
+    public void setDateTimeFormatter(@Nullable final DateTimeFormatter f) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+        formatter = f;
+    }
+    
+    /**
+     * Get the formatter to use to convert string data into an {@link Instant}.
+     * 
+     * @return formatter
+     */
+    @Nullable public DateTimeFormatter getDateTimeFormatter() {
+        return formatter;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+
+        if (getDataConnectorDependencies().isEmpty() && getAttributeDependencies().isEmpty()) {
+            throw new ComponentInitializationException(getLogPrefix() + " no dependencies were configured");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nonnull protected IdPAttribute doAttributeDefinitionResolve(
+            @Nonnull final AttributeResolutionContext resolutionContext,
+            @Nonnull final AttributeResolverWorkContext workContext) throws ResolutionException {
+        Constraint.isNotNull(workContext, "AttributeResolverWorkContext cannot be null");
+
+        final IdPAttribute result = new IdPAttribute(getId());
+        
+        final List<IdPAttributeValue> values = PluginDependencySupport.getMergedAttributeValues(workContext,
+                getAttributeDependencies(), 
+                getDataConnectorDependencies(), 
+                getId());
+
+        final List<IdPAttributeValue> converted = values.stream()
+                .map(v -> convert(v))
+                .filter(Predicates.notNull())
+                .collect(Collectors.toUnmodifiableList());
+        
+        if (!ignoreConversionErrors && converted.size() != values.size()) {
+            throw new ResolutionException("Unable to convert all inputs to date/time values.");
+        }
+
+        result.setValues(converted);
+        return result;
+    }
+    
+    /**
+     * Convert an input value into a {@link DateTimeAttributeValue} if possible.
+     * 
+     * @param input input value
+     * 
+     * @return converted value or null
+     */
+    @Nullable protected DateTimeAttributeValue convert(@Nonnull final IdPAttributeValue input) {
+        
+        if (input instanceof DateTimeAttributeValue) {
+            return (DateTimeAttributeValue) input;
+        } else if (!(input instanceof StringAttributeValue)) {
+            log.info("{} Ignoring unsupported IdPAttributeValue type: {}", getLogPrefix(), input.getClass().getName());
+            return null;
+        }
+
+        final String stringValue = ((StringAttributeValue) input).getValue();
+        
+        try {
+            final Long longValue = Long.valueOf(stringValue);
+            return new DateTimeAttributeValue(
+                    epochInSeconds ? Instant.ofEpochSecond(longValue) : Instant.ofEpochMilli(longValue));
+        } catch (final DateTimeException e) {
+            log.info("{} Epoch value was out of range", getLogPrefix(), e);
+            return null;
+        } catch (final NumberFormatException e) {
+            // Nothing to do, we'll just try and convert via formatter.
+        }
+        
+        if (formatter == null) {
+            log.info("{} No DateTimeFormatter installed, unable to convert string value", getLogPrefix());
+            return null;
+        }
+        
+        try {
+            return new DateTimeAttributeValue(formatter.parse(stringValue, Instant::from));
+        } catch (final DateTimeException e) {
+            log.info("{} Error converting input value '{}' into Instant", getLogPrefix(), stringValue, e);
+            return null;
+        }
+    }
+  
+}
\ No newline at end of file
diff --git a/idp-attribute-resolver-impl/src/test/java/net/shibboleth/idp/attribute/resolver/ad/impl/DateTimeAttributeDefinitionTest.java b/idp-attribute-resolver-impl/src/test/java/net/shibboleth/idp/attribute/resolver/ad/impl/DateTimeAttributeDefinitionTest.java
new file mode 100644
index 000000000..4da7e410d
--- /dev/null
+++ b/idp-attribute-resolver-impl/src/test/java/net/shibboleth/idp/attribute/resolver/ad/impl/DateTimeAttributeDefinitionTest.java
@@ -0,0 +1,291 @@
+/*
+ * 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.idp.attribute.resolver.ad.impl;
+
+import static org.testng.Assert.*;
+
+import java.time.Instant;
+import java.time.format.DateTimeFormatter;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+import org.testng.annotations.Test;
+
+import net.shibboleth.idp.attribute.DateTimeAttributeValue;
+import net.shibboleth.idp.attribute.EmptyAttributeValue;
+import net.shibboleth.idp.attribute.IdPAttribute;
+import net.shibboleth.idp.attribute.IdPAttributeValue;
+import net.shibboleth.idp.attribute.StringAttributeValue;
+import net.shibboleth.idp.attribute.resolver.AbstractAttributeDefinition;
+import net.shibboleth.idp.attribute.resolver.AttributeDefinition;
+import net.shibboleth.idp.attribute.resolver.ResolutionException;
+import net.shibboleth.idp.attribute.resolver.ResolverAttributeDefinitionDependency;
+import net.shibboleth.idp.attribute.resolver.context.AttributeResolutionContext;
+import net.shibboleth.idp.attribute.resolver.context.AttributeResolverWorkContext;
+import net.shibboleth.idp.attribute.resolver.impl.AttributeResolverImpl;
+import net.shibboleth.idp.attribute.resolver.impl.AttributeResolverImplTest;
+import net.shibboleth.idp.saml.impl.testing.TestSources;
+import net.shibboleth.utilities.java.support.collection.LazySet;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+
+/** Unit test for {@link DateTimeAttributeDefinition}. */
+public class DateTimeAttributeDefinitionTest {
+
+    /** The name. */
+    private static final String TEST_ATTRIBUTE_NAME = "datetime";
+    private static final String STRING_SECS = "1659979872";
+    private static final String STRING_MSECS = "1659979872969";
+    private static final String STRING_ISO = "2022-08-08T17:31:12.969Z";
+
+    /**
+     * Test resolution of an empty definition to nothing.
+     * 
+     * @throws ResolutionException if resolution failed.
+     * @throws ComponentInitializationException if initialization fails (which it shouldn't).
+     */
+    @Test public void empty() throws ResolutionException, ComponentInitializationException {
+        final DateTimeAttributeDefinition simple = new DateTimeAttributeDefinition();
+        simple.setId(TEST_ATTRIBUTE_NAME);
+        try {
+            simple.initialize();
+            fail("no dependencies");
+        } catch (final ComponentInitializationException e) {
+            //OK
+        }
+        simple.setDataConnectorDependencies(Collections.singleton(TestSources.makeDataConnectorDependency("foo", "bar")));
+        simple.initialize();
+
+        final AttributeResolutionContext context = new AttributeResolutionContext();
+        context.getSubcontext(AttributeResolverWorkContext.class, true);
+        final IdPAttribute result = simple.resolve(context);
+
+        assertTrue(result.getValues().isEmpty());
+    }
+
+    /**
+     * Test handling of errors.
+     * 
+     * @throws ResolutionException
+     * @throws ComponentInitializationException
+     */
+    @Test public void errors() throws ResolutionException, ComponentInitializationException {
+        errors(true);
+        errors(false);
+    }
+
+    private void errors(boolean ignore) throws ComponentInitializationException, ResolutionException {
+        final AbstractAttributeDefinition sa = new AbstractAttributeDefinition() {
+
+            protected IdPAttribute doAttributeDefinitionResolve(AttributeResolutionContext resolutionContext,
+                    AttributeResolverWorkContext workContext) throws ResolutionException {
+                final IdPAttribute result = new IdPAttribute(TEST_ATTRIBUTE_NAME+"in");
+                result.setValues(List.of(EmptyAttributeValue.NULL, EmptyAttributeValue.ZERO_LENGTH, new StringAttributeValue(STRING_SECS)));
+                return result;
+            }
+        };
+        sa.setId(TEST_ATTRIBUTE_NAME+"in");
+        sa.initialize();
+
+        final DateTimeAttributeDefinition datetime = new DateTimeAttributeDefinition();
+        datetime.setId(TEST_ATTRIBUTE_NAME);
+        datetime.setAttributeDependencies(Set.of(TestSources.makeAttributeDefinitionDependency(TEST_ATTRIBUTE_NAME+"in")));
+        datetime.setIgnoreConversionErrors(ignore);
+        datetime.initialize();
+
+        final AttributeResolverImpl resolver = AttributeResolverImplTest.newAttributeResolverImpl("foo", Set.of(datetime, sa), Collections.emptySet());
+        resolver.initialize();
+
+        final AttributeResolutionContext context = new AttributeResolutionContext();
+        context.getSubcontext(AttributeResolverWorkContext.class, true);
+        
+        try {
+            resolver.resolveAttributes(context);
+        } catch (final ResolutionException e) {
+            if (ignore) {
+                fail("Did not ignore errors");
+            }
+        }
+
+        final IdPAttribute result = context.getResolvedIdPAttributes().get(TEST_ATTRIBUTE_NAME);
+        if (ignore) {
+            final int vals = result.getValues().size();
+            assertEquals(vals, 1);
+        } else {
+            assertNull(result);
+        }
+    }
+
+    /**
+     * Test when dependent on another attribute.
+     * 
+     * @throws ComponentInitializationException if initialization fails (which it shouldn't).
+     */
+    @Test public void attribute() throws ComponentInitializationException {
+
+        final DateTimeAttributeDefinition datetime = new DateTimeAttributeDefinition();
+        datetime.setId(TEST_ATTRIBUTE_NAME);
+
+        // Set the dependency on the attribute def.
+        final Set<ResolverAttributeDefinitionDependency> dependencySet = new LazySet<>();
+        dependencySet.add(TestSources.makeAttributeDefinitionDependency(TestSources.DEPENDS_ON_ATTRIBUTE_NAME_ATTR));
+        datetime.setAttributeDependencies(dependencySet);
+        datetime.initialize();
+
+        final Instant now = Instant.now();
+        
+        final IdPAttribute attr = new IdPAttribute(TestSources.DEPENDS_ON_ATTRIBUTE_NAME_ATTR);
+        attr.setValues(List.of(new DateTimeAttributeValue(now), StringAttributeValue.valueOf(STRING_SECS)));
+        
+        // And resolve
+        final Set<AttributeDefinition> am = new LazySet<>();
+        am.add(datetime);
+        am.add(TestSources.populatedStaticAttribute(attr));
+
+        final AttributeResolverImpl resolver = AttributeResolverImplTest.newAttributeResolverImpl("foo", am, null);
+        resolver.initialize();
+
+        final AttributeResolutionContext context = new AttributeResolutionContext();
+        try {
+            resolver.resolveAttributes(context);
+        } catch (final ResolutionException e) {
+            fail("resolution failed", e);
+        }
+        final Collection<IdPAttributeValue> values = context.getResolvedIdPAttributes().get(TEST_ATTRIBUTE_NAME).getValues();
+
+        assertEquals(values.size(), 2);
+        assertTrue(values.contains(new DateTimeAttributeValue(now)));
+        assertTrue(values.contains(new DateTimeAttributeValue(Instant.ofEpochSecond(Long.valueOf(STRING_SECS)))));
+    }
+    
+    /**
+     * Test when using ms units.
+     * 
+     * @throws ComponentInitializationException if initialization fails (which it shouldn't)
+     */
+    @Test public void millisecs() throws ComponentInitializationException {
+
+        final DateTimeAttributeDefinition datetime = new DateTimeAttributeDefinition();
+        datetime.setId(TEST_ATTRIBUTE_NAME);
+        datetime.setEpochInSeconds(false);
+
+        // Set the dependency on the attribute def.
+        final Set<ResolverAttributeDefinitionDependency> dependencySet = new LazySet<>();
+        dependencySet.add(TestSources.makeAttributeDefinitionDependency(TestSources.DEPENDS_ON_ATTRIBUTE_NAME_ATTR));
+        datetime.setAttributeDependencies(dependencySet);
+        datetime.initialize();
+        
+        final IdPAttribute attr = new IdPAttribute(TestSources.DEPENDS_ON_ATTRIBUTE_NAME_ATTR);
+        attr.setValues(List.of(StringAttributeValue.valueOf(STRING_MSECS)));
+        
+        // And resolve
+        final Set<AttributeDefinition> am = new LazySet<>();
+        am.add(datetime);
+        am.add(TestSources.populatedStaticAttribute(attr));
+
+        final AttributeResolverImpl resolver = AttributeResolverImplTest.newAttributeResolverImpl("foo", am, null);
+        resolver.initialize();
+
+        final AttributeResolutionContext context = new AttributeResolutionContext();
+        try {
+            resolver.resolveAttributes(context);
+        } catch (final ResolutionException e) {
+            fail("resolution failed", e);
+        }
+        final Collection<IdPAttributeValue> values = context.getResolvedIdPAttributes().get(TEST_ATTRIBUTE_NAME).getValues();
+
+        assertEquals(values.size(), 1);
+        assertTrue(values.contains(new DateTimeAttributeValue(Instant.ofEpochMilli(Long.valueOf(STRING_MSECS)))));
+    }
+
+    /**
+     * Test when using formatter.
+     * 
+     * @throws ComponentInitializationException if initialization fails (which it shouldn't)
+     */
+    @Test public void formatter() throws ComponentInitializationException {
+
+        final DateTimeAttributeDefinition datetime = new DateTimeAttributeDefinition();
+        datetime.setId(TEST_ATTRIBUTE_NAME);
+        datetime.setDateTimeFormatter(DateTimeFormatter.ISO_INSTANT);
+
+        // Set the dependency on the attribute def.
+        final Set<ResolverAttributeDefinitionDependency> dependencySet = new LazySet<>();
+        dependencySet.add(TestSources.makeAttributeDefinitionDependency(TestSources.DEPENDS_ON_ATTRIBUTE_NAME_ATTR));
+        datetime.setAttributeDependencies(dependencySet);
+        datetime.initialize();
+        
+        final IdPAttribute attr = new IdPAttribute(TestSources.DEPENDS_ON_ATTRIBUTE_NAME_ATTR);
+        attr.setValues(List.of(StringAttributeValue.valueOf(STRING_ISO)));
+        
+        // And resolve
+        final Set<AttributeDefinition> am = new LazySet<>();
+        am.add(datetime);
+        am.add(TestSources.populatedStaticAttribute(attr));
+
+        final AttributeResolverImpl resolver = AttributeResolverImplTest.newAttributeResolverImpl("foo", am, null);
+        resolver.initialize();
+
+        final AttributeResolutionContext context = new AttributeResolutionContext();
+        try {
+            resolver.resolveAttributes(context);
+        } catch (final ResolutionException e) {
+            fail("resolution failed", e);
+        }
+        final Collection<IdPAttributeValue> values = context.getResolvedIdPAttributes().get(TEST_ATTRIBUTE_NAME).getValues();
+
+        assertEquals(values.size(), 1);
+        assertTrue(values.contains(new DateTimeAttributeValue(Instant.ofEpochMilli(Long.valueOf(STRING_MSECS)))));
+    }
+
+    /**
+     * Test when using bad formatter.
+     * 
+     * @throws ComponentInitializationException if initialization fails (which it shouldn't)
+     * @throws ResolutionException the expected outcome
+     */
+    @Test(expectedExceptions=ResolutionException.class)
+    public void formatterError() throws ComponentInitializationException, ResolutionException {
+
+        final DateTimeAttributeDefinition datetime = new DateTimeAttributeDefinition();
+        datetime.setId(TEST_ATTRIBUTE_NAME);
+        datetime.setDateTimeFormatter(DateTimeFormatter.BASIC_ISO_DATE);
+
+        // Set the dependency on the attribute def.
+        final Set<ResolverAttributeDefinitionDependency> dependencySet = new LazySet<>();
+        dependencySet.add(TestSources.makeAttributeDefinitionDependency(TestSources.DEPENDS_ON_ATTRIBUTE_NAME_ATTR));
+        datetime.setAttributeDependencies(dependencySet);
+        datetime.initialize();
+        
+        final IdPAttribute attr = new IdPAttribute(TestSources.DEPENDS_ON_ATTRIBUTE_NAME_ATTR);
+        attr.setValues(List.of(StringAttributeValue.valueOf(STRING_ISO)));
+        
+        // And resolve
+        final Set<AttributeDefinition> am = new LazySet<>();
+        am.add(datetime);
+        am.add(TestSources.populatedStaticAttribute(attr));
+
+        final AttributeResolverImpl resolver = AttributeResolverImplTest.newAttributeResolverImpl("foo", am, null);
+        resolver.initialize();
+
+        final AttributeResolutionContext context = new AttributeResolutionContext();
+        resolver.resolveAttributes(context);
+    }
+
+}
\ No newline at end of file
diff --git a/idp-attribute-resolver-impl/src/test/java/net/shibboleth/idp/attribute/resolver/ad/impl/SAML1NameIdentifierAttributeDefinitionTest.java b/idp-attribute-resolver-impl/src/test/java/net/shibboleth/idp/attribute/resolver/ad/impl/SAML1NameIdentifierAttributeDefinitionTest.java
index aeb67b11b..7fe393741 100644
--- a/idp-attribute-resolver-impl/src/test/java/net/shibboleth/idp/attribute/resolver/ad/impl/SAML1NameIdentifierAttributeDefinitionTest.java
+++ b/idp-attribute-resolver-impl/src/test/java/net/shibboleth/idp/attribute/resolver/ad/impl/SAML1NameIdentifierAttributeDefinitionTest.java
@@ -56,7 +56,7 @@ import net.shibboleth.utilities.java.support.component.ComponentInitializationEx
 /**
  * Test for {@link SAML1NameIdentifierAttributeDefinition}.
  */
- at SuppressWarnings("javadoc")
+ at SuppressWarnings({"javadoc", "removal"})
 public class SAML1NameIdentifierAttributeDefinitionTest extends OpenSAMLInitBaseTestCase {
 
     /** The name. */
diff --git a/idp-attribute-resolver-impl/src/test/java/net/shibboleth/idp/attribute/resolver/ad/impl/SAML2NameIDAttributeDefinitionTest.java b/idp-attribute-resolver-impl/src/test/java/net/shibboleth/idp/attribute/resolver/ad/impl/SAML2NameIDAttributeDefinitionTest.java
index a7a64a0a0..1b7d77af0 100644
--- a/idp-attribute-resolver-impl/src/test/java/net/shibboleth/idp/attribute/resolver/ad/impl/SAML2NameIDAttributeDefinitionTest.java
+++ b/idp-attribute-resolver-impl/src/test/java/net/shibboleth/idp/attribute/resolver/ad/impl/SAML2NameIDAttributeDefinitionTest.java
@@ -58,7 +58,7 @@ import net.shibboleth.utilities.java.support.component.ComponentInitializationEx
 /**
  * Test for {@link SAML2NameIDAttributeDefinition}.
  */
- at SuppressWarnings("javadoc")
+ at SuppressWarnings({"javadoc", "removal"})
 public class SAML2NameIDAttributeDefinitionTest extends OpenSAMLInitBaseTestCase {
 
     /** The name. */
diff --git a/idp-attribute-resolver-impl/src/test/java/net/shibboleth/idp/attribute/resolver/ad/impl/ScriptedAttributeTest.java b/idp-attribute-resolver-impl/src/test/java/net/shibboleth/idp/attribute/resolver/ad/impl/ScriptedAttributeTest.java
index 5f3700ae8..a44adb612 100644
--- a/idp-attribute-resolver-impl/src/test/java/net/shibboleth/idp/attribute/resolver/ad/impl/ScriptedAttributeTest.java
+++ b/idp-attribute-resolver-impl/src/test/java/net/shibboleth/idp/attribute/resolver/ad/impl/ScriptedAttributeTest.java
@@ -69,7 +69,7 @@ import net.shibboleth.utilities.java.support.collection.LazySet;
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
 import net.shibboleth.utilities.java.support.scripting.EvaluableScript;
 
-/** test for {@link net.shibboleth.idp.attribute.resolver.ad.impl.ScriptedIdPAttributeImpl}. */
+/** Unit test for {@link ScriptedIdPAttributeImpl}. */
 @SuppressWarnings("javadoc")
 public class ScriptedAttributeTest extends XMLObjectBaseTestCase {
 
diff --git a/idp-attribute-resolver-impl/src/test/java/net/shibboleth/idp/attribute/resolver/ad/impl/SimpleAttributeTest.java b/idp-attribute-resolver-impl/src/test/java/net/shibboleth/idp/attribute/resolver/ad/impl/SimpleAttributeTest.java
index d22094f36..797b645ee 100644
--- a/idp-attribute-resolver-impl/src/test/java/net/shibboleth/idp/attribute/resolver/ad/impl/SimpleAttributeTest.java
+++ b/idp-attribute-resolver-impl/src/test/java/net/shibboleth/idp/attribute/resolver/ad/impl/SimpleAttributeTest.java
@@ -49,7 +49,7 @@ import net.shibboleth.idp.saml.impl.testing.TestSources;
 import net.shibboleth.utilities.java.support.collection.LazySet;
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
 
-/** test for {@link net.shibboleth.idp.attribute.resolver.ad.impl.SimpleAttributeDefinition}. */
+/** Unit test for {@link SimpleAttributeDefinition}. */
 @SuppressWarnings("javadoc")
 public class SimpleAttributeTest {
 
diff --git a/idp-attribute-resolver-impl/src/test/java/net/shibboleth/idp/attribute/resolver/ad/impl/TemplateAttributeTest.java b/idp-attribute-resolver-impl/src/test/java/net/shibboleth/idp/attribute/resolver/ad/impl/TemplateAttributeTest.java
index 271eeffbe..e19259a1c 100644
--- a/idp-attribute-resolver-impl/src/test/java/net/shibboleth/idp/attribute/resolver/ad/impl/TemplateAttributeTest.java
+++ b/idp-attribute-resolver-impl/src/test/java/net/shibboleth/idp/attribute/resolver/ad/impl/TemplateAttributeTest.java
@@ -45,7 +45,7 @@ import net.shibboleth.idp.saml.impl.testing.TestSources;
 import net.shibboleth.utilities.java.support.collection.LazySet;
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
 
-/** test for {@link net.shibboleth.idp.attribute.resolver.ad.impl.TemplateAttributeDefinition}. */
+/** Unit test for {@link TemplateAttributeDefinition}. */
 @SuppressWarnings("javadoc")
 public class TemplateAttributeTest {
 
diff --git a/idp-attribute-resolver-spring/src/main/java/net/shibboleth/idp/attribute/resolver/spring/ad/impl/DateTimeAttributeDefinitionParser.java b/idp-attribute-resolver-spring/src/main/java/net/shibboleth/idp/attribute/resolver/spring/ad/impl/DateTimeAttributeDefinitionParser.java
new file mode 100644
index 000000000..70ac6b6ba
--- /dev/null
+++ b/idp-attribute-resolver-spring/src/main/java/net/shibboleth/idp/attribute/resolver/spring/ad/impl/DateTimeAttributeDefinitionParser.java
@@ -0,0 +1,71 @@
+/*
+ * 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.idp.attribute.resolver.spring.ad.impl;
+
+import java.time.format.DateTimeFormatter;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.xml.namespace.QName;
+
+import org.springframework.beans.factory.support.BeanDefinitionBuilder;
+import org.springframework.beans.factory.xml.ParserContext;
+import org.w3c.dom.Element;
+
+import net.shibboleth.ext.spring.util.SpringSupport;
+import net.shibboleth.idp.attribute.resolver.ad.impl.DateTimeAttributeDefinition;
+import net.shibboleth.idp.attribute.resolver.spring.ad.BaseAttributeDefinitionParser;
+import net.shibboleth.idp.attribute.resolver.spring.impl.AttributeResolverNamespaceHandler;
+
+/** Bean definition parser for a {@link DateTimeAttributeDefinition}. */
+public class DateTimeAttributeDefinitionParser extends BaseAttributeDefinitionParser {
+
+    /** Schema type name. */
+    @Nonnull public static final QName TYPE_NAME_RESOLVER =
+            new QName(AttributeResolverNamespaceHandler.NAMESPACE, "DateTime");
+
+    /** {@inheritDoc} */
+    @Override protected Class<DateTimeAttributeDefinition> getBeanClass(@Nullable final Element element) {
+        return DateTimeAttributeDefinition.class;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void doParse(@Nonnull final Element config, @Nonnull final ParserContext parserContext,
+            @Nonnull final BeanDefinitionBuilder builder) {
+        super.doParse(config, parserContext, builder);
+
+        if (config.hasAttributeNS(null, "ignoreConversionErrors")) {
+            builder.addPropertyValue("ignoreConversionErrors",
+                    SpringSupport.getStringValueAsBoolean(config.getAttributeNS(null, "ignoreConversionErrors")));
+        }
+        
+        if (config.hasAttributeNS(null, "epochInSeconds")) {
+            builder.addPropertyValue("epochInSeconds",
+                    SpringSupport.getStringValueAsBoolean(config.getAttributeNS(null, "epochInSeconds")));
+        }
+        
+        if (config.hasAttributeNS(null, "formattingString")) {
+            final BeanDefinitionBuilder formatterBuilder =
+                    BeanDefinitionBuilder.genericBeanDefinition(DateTimeFormatter.class);
+            formatterBuilder.setFactoryMethod("ofPattern");
+            formatterBuilder.addConstructorArgValue(config.getAttributeNS(null, "formattingString"));
+            builder.addPropertyValue("dateTimeFormatter", formatterBuilder.getBeanDefinition());
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/idp-attribute-resolver-spring/src/main/java/net/shibboleth/idp/attribute/resolver/spring/impl/AttributeResolverNamespaceHandler.java b/idp-attribute-resolver-spring/src/main/java/net/shibboleth/idp/attribute/resolver/spring/impl/AttributeResolverNamespaceHandler.java
index 27a59aec9..36306b289 100644
--- a/idp-attribute-resolver-spring/src/main/java/net/shibboleth/idp/attribute/resolver/spring/impl/AttributeResolverNamespaceHandler.java
+++ b/idp-attribute-resolver-spring/src/main/java/net/shibboleth/idp/attribute/resolver/spring/impl/AttributeResolverNamespaceHandler.java
@@ -23,6 +23,7 @@ import org.springframework.beans.factory.xml.BeanDefinitionParser;
 
 import net.shibboleth.ext.spring.util.BaseSpringNamespaceHandler;
 import net.shibboleth.idp.attribute.resolver.spring.ad.impl.ContextDerivedAttributeDefinitionParser;
+import net.shibboleth.idp.attribute.resolver.spring.ad.impl.DateTimeAttributeDefinitionParser;
 import net.shibboleth.idp.attribute.resolver.spring.ad.impl.PrescopedAttributeDefinitionParser;
 import net.shibboleth.idp.attribute.resolver.spring.ad.impl.PrincipalNameAttributeDefinitionParser;
 import net.shibboleth.idp.attribute.resolver.spring.ad.impl.RegexSplitAttributeDefinitionParser;
@@ -104,6 +105,8 @@ public class AttributeResolverNamespaceHandler extends BaseSpringNamespaceHandle
         registerBeanDefinitionParser(ValueMapParser.TYPE_NAME_RESOLVER, new ValueMapParser());
         registerBeanDefinitionParser(MappedAttributeDefinitionParser.TYPE_NAME_RESOLVER,
                 new MappedAttributeDefinitionParser());
+        registerBeanDefinitionParser(DateTimeAttributeDefinitionParser.TYPE_NAME_RESOLVER,
+                new DateTimeAttributeDefinitionParser());
         
         // Data Connectors
         registerBeanDefinitionParser(ComputedIdDataConnectorParser.TYPE_NAME_RESOLVER, 
diff --git a/idp-attribute-resolver-spring/src/test/java/net/shibboleth/idp/attribute/resolver/spring/ad/DateTimeAttributeDefinitionParserTest.java b/idp-attribute-resolver-spring/src/test/java/net/shibboleth/idp/attribute/resolver/spring/ad/DateTimeAttributeDefinitionParserTest.java
new file mode 100644
index 000000000..1763c5789
--- /dev/null
+++ b/idp-attribute-resolver-spring/src/test/java/net/shibboleth/idp/attribute/resolver/spring/ad/DateTimeAttributeDefinitionParserTest.java
@@ -0,0 +1,61 @@
+/*
+ * 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.idp.attribute.resolver.spring.ad;
+
+import static org.testng.Assert.*;
+
+import static org.testng.Assert.assertEquals;
+
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+
+import org.testng.annotations.Test;
+
+import net.shibboleth.idp.attribute.resolver.ad.impl.DateTimeAttributeDefinition;
+import net.shibboleth.idp.attribute.resolver.spring.testing.BaseAttributeDefinitionParserTest;
+
+/**
+ * Test for {@link DateTimeAttributeDefinitionParser}.
+ */
+ at SuppressWarnings("javadoc")
+public class DateTimeAttributeDefinitionParserTest extends BaseAttributeDefinitionParserTest {
+
+    @Test public void defaults() {
+        DateTimeAttributeDefinition attrDef = getAttributeDefn("resolver/datetime.xml", DateTimeAttributeDefinition.class);
+
+        assertEquals(attrDef.getId(), "datetime");
+        assertTrue(attrDef.isEpochInSeconds());
+        assertFalse(attrDef.isIgnoreConversionErrors());
+        assertNull(attrDef.getDateTimeFormatter());
+    }
+
+    @Test public void custom() {
+        DateTimeAttributeDefinition attrDef = getAttributeDefn("resolver/datetimeCustom.xml", DateTimeAttributeDefinition.class);
+
+        assertEquals(attrDef.getId(), "datetime");
+        assertFalse(attrDef.isEpochInSeconds());
+        assertTrue(attrDef.isIgnoreConversionErrors());
+        
+        final DateTimeFormatter formatter = attrDef.getDateTimeFormatter();
+        assertNotNull(formatter);
+        assertEquals(formatter.format(ZonedDateTime.ofInstant(Instant.ofEpochSecond(100), ZoneId.of("UTC"))), "1970");
+    }
+
+}
\ No newline at end of file
diff --git a/idp-attribute-resolver-spring/src/test/java/net/shibboleth/idp/attribute/resolver/spring/ad/ScopedAttributeDefinitionParserTest.java b/idp-attribute-resolver-spring/src/test/java/net/shibboleth/idp/attribute/resolver/spring/ad/ScopedAttributeDefinitionParserTest.java
index 88421c0b2..aa348fdf5 100644
--- a/idp-attribute-resolver-spring/src/test/java/net/shibboleth/idp/attribute/resolver/spring/ad/ScopedAttributeDefinitionParserTest.java
+++ b/idp-attribute-resolver-spring/src/test/java/net/shibboleth/idp/attribute/resolver/spring/ad/ScopedAttributeDefinitionParserTest.java
@@ -27,12 +27,11 @@ import static org.testng.Assert.assertEquals;
 import org.testng.annotations.Test;
 
 import net.shibboleth.idp.attribute.resolver.ad.impl.ScopedAttributeDefinition;
-import net.shibboleth.idp.attribute.resolver.spring.ad.impl.SAML1NameIdentifierAttributeDefinitionParser;
 import net.shibboleth.idp.attribute.resolver.spring.testing.BaseAttributeDefinitionParserTest;
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
 
 /**
- * Test for {@link SAML1NameIdentifierAttributeDefinitionParser}.
+ * Test for {@link ScopedAttributeDefinitionParser}.
  */
 @SuppressWarnings("javadoc")
 public class ScopedAttributeDefinitionParserTest extends BaseAttributeDefinitionParserTest {
diff --git a/idp-attribute-resolver-spring/src/test/resources/net/shibboleth/idp/attribute/resolver/spring/ad/resolver/datetime.xml b/idp-attribute-resolver-spring/src/test/resources/net/shibboleth/idp/attribute/resolver/spring/ad/resolver/datetime.xml
new file mode 100644
index 000000000..fd98e9487
--- /dev/null
+++ b/idp-attribute-resolver-spring/src/test/resources/net/shibboleth/idp/attribute/resolver/spring/ad/resolver/datetime.xml
@@ -0,0 +1,7 @@
+
+<AttributeDefinition xmlns="urn:mace:shibboleth:2.0:resolver"
+	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:type="DateTime" id="datetime"
+	xsi:schemaLocation="urn:mace:shibboleth:2.0:resolver http://shibboleth.net/schema/idp/shibboleth-attribute-resolver.xsd">
+	<InputAttributeDefinition ref="TheOrphan" />
+</AttributeDefinition>
diff --git a/idp-attribute-resolver-spring/src/test/resources/net/shibboleth/idp/attribute/resolver/spring/ad/resolver/datetimeCustom.xml b/idp-attribute-resolver-spring/src/test/resources/net/shibboleth/idp/attribute/resolver/spring/ad/resolver/datetimeCustom.xml
new file mode 100644
index 000000000..8448ea78d
--- /dev/null
+++ b/idp-attribute-resolver-spring/src/test/resources/net/shibboleth/idp/attribute/resolver/spring/ad/resolver/datetimeCustom.xml
@@ -0,0 +1,10 @@
+
+<AttributeDefinition xmlns="urn:mace:shibboleth:2.0:resolver"
+	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:type="DateTime" id="datetime"
+	xsi:schemaLocation="urn:mace:shibboleth:2.0:resolver http://shibboleth.net/schema/idp/shibboleth-attribute-resolver.xsd"
+    epochInSeconds="false"
+    ignoreConversionErrors="true"
+    formattingString="YYYY">
+	<InputAttributeDefinition ref="TheOrphan" />
+</AttributeDefinition>
diff --git a/idp-schema/src/main/resources/schema/shibboleth-attribute-resolver.xsd b/idp-schema/src/main/resources/schema/shibboleth-attribute-resolver.xsd
index 948e3bb3a..714085c97 100644
--- a/idp-schema/src/main/resources/schema/shibboleth-attribute-resolver.xsd
+++ b/idp-schema/src/main/resources/schema/shibboleth-attribute-resolver.xsd
@@ -648,6 +648,40 @@
             </extension>
         </complexContent>
     </complexType>
+
+    <complexType name="DateTime">
+        <annotation>
+            <documentation>An attribute definition producing date/time values.</documentation>
+        </annotation>
+        <complexContent>
+            <extension base="resolver:BaseAttributeDefinitionType">
+                <choice maxOccurs="unbounded" minOccurs="0">
+                    <element ref="resolver:InputAttributeDefinition"/>
+                    <element ref="resolver:InputDataConnector"/>
+                    <element name="DisplayName" type="resolver:LocalizedStringType"/>
+                    <element name="DisplayDescription" type="resolver:LocalizedStringType"/>
+                    <element ref="resolver:AttributeEncoder"/>
+                </choice>
+                <attribute name="ignoreConversionErrors" type="resolver:string">
+                    <annotation>
+                        <documentation>Are conversion errors ignored? (default: FALSE)</documentation>
+                    </annotation>
+                </attribute>
+                <attribute name="epochInSeconds" type="resolver:string">
+                    <annotation>
+                        <documentation>
+                            Use seconds as epoch unit instead of millseconds when converting numeric data (default: TRUE)
+                        </documentation>
+                    </annotation>
+                </attribute>
+                <attribute name="formattingString" type="resolver:string">
+                    <annotation>
+                        <documentation>A formatting string to use converting string data.</documentation>
+                    </annotation>
+                </attribute>
+            </extension>
+        </complexContent>
+    </complexType>
     
     <complexType name="Decrypted">
         <annotation>

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


More information about the commits mailing list