[java-identity-provider] branch main updated: IDP-1697 - StorageService-backed DataConnector

Scott Cantor cantor.2 at osu.edu
Wed Oct 21 15:58:50 UTC 2020


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

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

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

The following commit(s) were added to refs/heads/main by this push:
       new  b0941d637 IDP-1697 - StorageService-backed DataConnector
b0941d637 is described below

commit b0941d637d479e7f4a99613031d5fe9fac55486d
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Wed Oct 21 11:58:47 2020 -0400

    IDP-1697 - StorageService-backed DataConnector
    
    https://issues.shibboleth.net/jira/browse/IDP-1697
    
    Implementation and unit tests done.
---
 idp-attribute-resolver-api/pom.xml                 |   4 +
 .../dc/storage/StorageMappingStrategy.java         |  33 +++
 .../resolver/dc/storage/StorageServiceSearch.java  |  46 ++++
 .../resolver/dc/storage/package-info.java          |  22 ++
 .../impl/ScriptedStorageMappingStrategy.java       | 229 +++++++++++++++++++
 .../storage/impl/SimpleStorageMappingStrategy.java |  66 ++++++
 .../storage/impl/StorageServiceDataConnector.java  | 169 ++++++++++++++
 .../dc/storage/impl/TemplatedSearchBuilder.java    | 244 +++++++++++++++++++++
 .../resolver/dc/storage/impl/package-info.java     |  22 ++
 .../impl/StorageServiceDataConnectorTest.java      | 203 +++++++++++++++++
 .../idp/attribute/resolver/impl/dc/storage/test.js |  18 ++
 .../attribute/resolver/impl/dc/storage/test.json   |  10 +
 12 files changed, 1066 insertions(+)

diff --git a/idp-attribute-resolver-api/pom.xml b/idp-attribute-resolver-api/pom.xml
index 572fcb064..31036924b 100644
--- a/idp-attribute-resolver-api/pom.xml
+++ b/idp-attribute-resolver-api/pom.xml
@@ -40,6 +40,10 @@
             <groupId>${opensaml.groupId}</groupId>
             <artifactId>opensaml-security-api</artifactId>
         </dependency>
+        <dependency>
+            <groupId>${opensaml.groupId}</groupId>
+            <artifactId>opensaml-storage-api</artifactId>
+        </dependency>
 
         <dependency>
             <groupId>net.shibboleth.utilities</groupId>
diff --git a/idp-attribute-resolver-api/src/main/java/net/shibboleth/idp/attribute/resolver/dc/storage/StorageMappingStrategy.java b/idp-attribute-resolver-api/src/main/java/net/shibboleth/idp/attribute/resolver/dc/storage/StorageMappingStrategy.java
new file mode 100644
index 000000000..0a1b97cb4
--- /dev/null
+++ b/idp-attribute-resolver-api/src/main/java/net/shibboleth/idp/attribute/resolver/dc/storage/StorageMappingStrategy.java
@@ -0,0 +1,33 @@
+/*
+ * 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.dc.storage;
+
+import org.opensaml.storage.StorageRecord;
+
+import net.shibboleth.idp.attribute.IdPAttribute;
+import net.shibboleth.idp.attribute.resolver.dc.MappingStrategy;
+
+/**
+ * Strategy for mapping from a {@link StorageRecord} to a collection of {@link
+ * IdPAttribute} objects.
+ * 
+ * @since 4.1.0
+ */
+public interface StorageMappingStrategy extends MappingStrategy<StorageRecord<?>> {
+    
+}
\ No newline at end of file
diff --git a/idp-attribute-resolver-api/src/main/java/net/shibboleth/idp/attribute/resolver/dc/storage/StorageServiceSearch.java b/idp-attribute-resolver-api/src/main/java/net/shibboleth/idp/attribute/resolver/dc/storage/StorageServiceSearch.java
new file mode 100644
index 000000000..6f863e8d3
--- /dev/null
+++ b/idp-attribute-resolver-api/src/main/java/net/shibboleth/idp/attribute/resolver/dc/storage/StorageServiceSearch.java
@@ -0,0 +1,46 @@
+/*
+ * 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.dc.storage;
+
+import java.io.IOException;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.storage.StorageRecord;
+import org.opensaml.storage.StorageService;
+
+import net.shibboleth.idp.attribute.resolver.dc.ExecutableSearch;
+
+/**
+ * A search that can be executed against a {@link StorageService} to fetch a result.
+ * 
+ * @since 4.1.0
+ */
+public interface StorageServiceSearch extends ExecutableSearch {
+
+    /**
+     * Executes the search and returns the result.
+     * 
+     * @param storageService storage service to search
+     * 
+     * @return the result of the executed search
+     * 
+     * @throws IOException thrown if there is a problem executing the search
+     */
+    @Nonnull StorageRecord<?> execute(@Nonnull StorageService storageService) throws IOException;
+}
\ No newline at end of file
diff --git a/idp-attribute-resolver-api/src/main/java/net/shibboleth/idp/attribute/resolver/dc/storage/package-info.java b/idp-attribute-resolver-api/src/main/java/net/shibboleth/idp/attribute/resolver/dc/storage/package-info.java
new file mode 100644
index 000000000..d36e26cb3
--- /dev/null
+++ b/idp-attribute-resolver-api/src/main/java/net/shibboleth/idp/attribute/resolver/dc/storage/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+
+/**
+ * StorageService-backed data connector APIs.
+ */
+
+package net.shibboleth.idp.attribute.resolver.dc.storage;
diff --git a/idp-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/storage/impl/ScriptedStorageMappingStrategy.java b/idp-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/storage/impl/ScriptedStorageMappingStrategy.java
new file mode 100644
index 000000000..c55e0964b
--- /dev/null
+++ b/idp-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/storage/impl/ScriptedStorageMappingStrategy.java
@@ -0,0 +1,229 @@
+/*
+ * 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.dc.storage.impl;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.script.ScriptContext;
+import javax.script.ScriptException;
+
+import org.opensaml.storage.StorageRecord;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.shibboleth.idp.attribute.IdPAttribute;
+import net.shibboleth.idp.attribute.IdPAttributeValue;
+import net.shibboleth.idp.attribute.resolver.ResolutionException;
+import net.shibboleth.idp.attribute.resolver.dc.storage.StorageMappingStrategy;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.resource.Resource;
+import net.shibboleth.utilities.java.support.scripting.AbstractScriptEvaluator;
+import net.shibboleth.utilities.java.support.scripting.EvaluableScript;
+
+/**
+ * {@link StorageMappingStrategy} that relies on a script to map the record to the
+ * attribute set.
+ * 
+ * <p>Well-suited to JSON output formats that can be parsed by the scripting engine.</p>
+ * 
+ * @since 4.1.0
+ */
+public final class ScriptedStorageMappingStrategy extends AbstractScriptEvaluator
+        implements StorageMappingStrategy {
+    
+    /** The id of the object where the results go. */
+    @Nonnull public static final String RESULTS_STRING = "connectorResults";
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(ScriptedStorageMappingStrategy.class);
+    
+    /**
+     * Constructor.
+     *
+     * @param theScript the script to run
+     */
+    private ScriptedStorageMappingStrategy(@Nonnull final EvaluableScript theScript) {
+        super(theScript);
+    }
+    
+    /** {@inheritDoc} */
+    @Nonnull public Map<String,IdPAttribute> map(@Nonnull final StorageRecord<?> results)
+            throws ResolutionException {
+        log.debug("{} Handling StorageRecord", getLogPrefix());
+        
+        if (results == null) {
+            log.debug("{} StorageRecord was missing", getLogPrefix());
+            throw new ResolutionException(getLogPrefix() + " StorageRecord was missing");
+        }
+        
+        try {
+            return (Map<String,IdPAttribute>) evaluate(results);
+        } catch (final RuntimeException e) {
+            throw new ResolutionException(getLogPrefix() + " Script did not run successfully", e);
+        }
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void prepareContext(@Nonnull final ScriptContext scriptContext, @Nullable final Object... input) {
+        log.debug("{} Adding to-be-populated attribute set '{}' to script context", getLogPrefix(), RESULTS_STRING);
+        scriptContext.setAttribute(RESULTS_STRING, new HashSet<>(), ScriptContext.ENGINE_SCOPE);
+        
+        scriptContext.setAttribute("record", input[0], ScriptContext.ENGINE_SCOPE);
+        scriptContext.setAttribute("log", log, ScriptContext.ENGINE_SCOPE);
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    @Nullable protected Object finalizeContext(@Nonnull final ScriptContext scriptContext,
+            @Nullable final Object scriptResult) throws ScriptException {
+        
+        // The real result in our case is a variable in the context.
+        final Object res = scriptContext.getAttribute(RESULTS_STRING);
+
+        if (null == res) {
+            log.error("{} Could not locate output variable '{}' from script", getLogPrefix(), RESULTS_STRING);
+            throw new ScriptException("Could not locate output from script");
+        }
+        if (!(res instanceof Collection)) {
+            log.error("{} Output '{}' was of type '{}', expected '{}'", getLogPrefix(), res.getClass().getName(),
+                    Collection.class.getName());
+            throw new ScriptException("Output was of the wrong type");
+        }
+
+        final Collection<?> outputCollection = (Collection<?>) res;
+        final Map<String, IdPAttribute> outputMap = new HashMap<>(outputCollection.size());
+        for (final Object o : outputCollection) {
+            if (o instanceof IdPAttribute) {
+                final IdPAttribute attribute = (IdPAttribute) o;
+                if (null == attribute.getId()) {
+                    log.warn("{} Anonymous Attribute encountered, ignored", getLogPrefix());
+                } else {
+                    checkValues(attribute);
+                    outputMap.put(attribute.getId(), attribute);
+                }
+            } else {
+                log.warn("{} Output collection contained an object of type '{}', ignored", getLogPrefix(),
+                        o.getClass().getName());
+            }
+        }
+
+        return outputMap;
+    }
+
+    /**
+     * Ensure that all the values in the attribute are of the correct type.
+     * 
+     * @param attribute the attribute to look at
+     */
+    private void checkValues(final IdPAttribute attribute) {
+
+        if (null == attribute.getValues()) {
+            log.info("{} Attribute '{}' has no values provided.", getLogPrefix(), attribute.getId());
+            attribute.setValues(Collections.<IdPAttributeValue> emptyList());
+            return;
+        }
+        log.debug("{} Attribute '{}' has {} value(s).", getLogPrefix(), attribute.getId(),
+                attribute.getValues().size());
+        final List<IdPAttributeValue> inputValues = attribute.getValues();
+        final List<IdPAttributeValue> outputValues = new ArrayList<>(inputValues.size());
+
+        for (final Object o : inputValues) {
+            if (o instanceof IdPAttributeValue) {
+                outputValues.add((IdPAttributeValue) o);
+            } else {
+                log.error("{} Attribute '{} has attribute value of type {}.  This will be ignored", getLogPrefix(),
+                        attribute.getId(), o.getClass().getName());
+            }
+        }
+        attribute.setValues(outputValues);
+    }
+    
+    /**
+     * Factory to create {@link ScriptedStorageMappingStrategy} from a {@link Resource}.
+     * 
+     * @param engineName the language
+     * @param resource the resource to look at
+     * @return the function
+     * @throws ScriptException if the compile fails
+     * @throws IOException if the file doesn't exist.
+     */
+    @Nonnull static ScriptedStorageMappingStrategy 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 ScriptedStorageMappingStrategy(script);
+        }
+    }
+
+    /**
+     * Factory to create {@link ScriptedStorageMappingStrategy} from a {@link Resource}.
+     * 
+     * @param resource the resource to look at
+     * @return the function
+     * @throws ScriptException if the compile fails
+     * @throws IOException if the file doesn't exist.
+     */
+    @Nonnull static ScriptedStorageMappingStrategy resourceScript(@Nonnull final Resource resource)
+            throws ScriptException, IOException {
+        return resourceScript(DEFAULT_ENGINE, resource);
+    }
+
+    /**
+     * Factory to create {@link ScriptedStorageMappingStrategy} from inline data.
+     * 
+     * @param scriptSource the script, as a string
+     * @param engineName the language
+     * @return the function
+     * @throws ScriptException if the compile fails
+     */
+    @Nonnull static ScriptedStorageMappingStrategy 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 ScriptedStorageMappingStrategy(script);
+    }
+
+    /**
+     * Factory to create {@link ScriptedStorageMappingStrategy} from inline data.
+     * 
+     * @param scriptSource the script, as a string
+     * @return the function
+     * @throws ScriptException if the compile fails
+     */
+    @Nonnull static ScriptedStorageMappingStrategy inlineScript(@Nonnull @NotEmpty final String scriptSource)
+            throws ScriptException {
+        return inlineScript(DEFAULT_ENGINE, scriptSource);
+    }
+
+}
diff --git a/idp-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/storage/impl/SimpleStorageMappingStrategy.java b/idp-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/storage/impl/SimpleStorageMappingStrategy.java
new file mode 100644
index 000000000..0db5484b2
--- /dev/null
+++ b/idp-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/storage/impl/SimpleStorageMappingStrategy.java
@@ -0,0 +1,66 @@
+/*
+ * 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.dc.storage.impl;
+
+import java.util.Collections;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.storage.StorageRecord;
+
+import net.shibboleth.idp.attribute.IdPAttribute;
+import net.shibboleth.idp.attribute.StringAttributeValue;
+import net.shibboleth.idp.attribute.resolver.ResolutionException;
+import net.shibboleth.idp.attribute.resolver.dc.MappingStrategy;
+import net.shibboleth.idp.attribute.resolver.dc.storage.StorageMappingStrategy;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.primitive.StringSupport;
+
+/**
+ * {@link MappingStrategy} for pulling data out of {@link StorageRecord}.
+ * 
+ * @since 4.1.0
+ */
+public class SimpleStorageMappingStrategy implements StorageMappingStrategy {
+
+    /** ID of the attribute to create. */
+    @Nonnull @NotEmpty private final String attributeId;
+    
+    /**
+     * Constructor.
+     *
+     * @param id attribute ID to create
+     */
+    public SimpleStorageMappingStrategy(@Nonnull @NotEmpty final String id) {
+        attributeId = Constraint.isNotNull(StringSupport.trimOrNull(id), "Attribute ID cannot be null or empty");
+    }
+    
+    /** {@inheritDoc} */
+    @Nonnull @NonnullElements public Map<String,IdPAttribute> map(
+            @Nonnull final StorageRecord<?> results) throws ResolutionException {
+        
+        final IdPAttribute attribute = new IdPAttribute(attributeId);
+        attribute.setValues(Collections.singleton(StringAttributeValue.valueOf(results.getValue())));
+        
+        return Collections.singletonMap(attributeId, attribute);
+    }
+
+}
\ No newline at end of file
diff --git a/idp-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/storage/impl/StorageServiceDataConnector.java b/idp-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/storage/impl/StorageServiceDataConnector.java
new file mode 100644
index 000000000..d12d2509c
--- /dev/null
+++ b/idp-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/storage/impl/StorageServiceDataConnector.java
@@ -0,0 +1,169 @@
+/*
+ * 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.
+ */
+
+/*
+ * Derived from work (c) 2015 CSC, see included license.
+ */
+
+package net.shibboleth.idp.attribute.resolver.dc.storage.impl;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.storage.StorageRecord;
+import org.opensaml.storage.StorageService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import net.shibboleth.idp.attribute.IdPAttribute;
+import net.shibboleth.idp.attribute.resolver.NoResultAnErrorResolutionException;
+import net.shibboleth.idp.attribute.resolver.ResolutionException;
+import net.shibboleth.idp.attribute.resolver.dc.ValidationException;
+import net.shibboleth.idp.attribute.resolver.dc.Validator;
+import net.shibboleth.idp.attribute.resolver.dc.impl.AbstractSearchDataConnector;
+import net.shibboleth.idp.attribute.resolver.dc.storage.StorageMappingStrategy;
+import net.shibboleth.idp.attribute.resolver.dc.storage.StorageServiceSearch;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.primitive.StringSupport;
+
+/**
+ * This class implements a {@link net.shibboleth.idp.attribute.resolver.DataConnector}
+ * that obtains data from a {@link StorageService}.
+ * 
+ * @since 4.1.0
+ */
+public class StorageServiceDataConnector
+        extends AbstractSearchDataConnector<StorageServiceSearch,StorageMappingStrategy> {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(StorageServiceDataConnector.class);
+
+    /** The {@link StorageService} to use. */
+    @NonnullAfterInit private StorageService storageService;
+    
+    /** ID of the attribute generated by this data connector if simple result mapping used. */
+    @NonnullAfterInit private String generatedAttribute;
+    
+    /** Whether no record is an error. */
+    private boolean noResultAnError;
+    
+    /** Constructor. */
+    public StorageServiceDataConnector() {
+        setValidator(new Validator() {
+            public void validate() throws ValidationException {
+            }
+
+            public void setThrowValidateError(final boolean what) {
+            }
+
+            public boolean isThrowValidateError() {
+                return false;
+            }
+        });
+    }
+
+    /**
+     * Set the {@link StorageService} to use.
+     * 
+     * @param service storage service to use
+     */
+    public void setStorageService(@Nonnull final StorageService service) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+        storageService = Constraint.isNotNull(service, "StorageService cannot be null");
+    }
+    
+    /**
+     * Get the ID of the attribute generated by this connector if simple result mapping used.
+     * 
+     * @return ID of the attribute generated by this connector
+     */
+    @NonnullAfterInit public String getGeneratedAttributeId() {
+        return generatedAttribute;
+    }
+
+    /**
+     * Sets whether the lack of a returned record constitutes an error.
+     * 
+     * @param flag flag to set
+     */
+    public void setNoResultAnError(final boolean flag) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+        noResultAnError = flag;
+    }
+    
+    /**
+     * Set the ID of the attribute generated by this connector if simple result mapping used.
+     * 
+     * @param newAttributeId what to set.
+     */
+    public void setGeneratedAttributeId(@Nullable final String newAttributeId) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+        
+        generatedAttribute = StringSupport.trimOrNull(newAttributeId);
+    }
+    
+    /** {@inheritDoc} */
+    public void doInitialize() throws ComponentInitializationException {
+
+        if (storageService == null) {
+            throw new ComponentInitializationException(getLogPrefix() + " StorageService cannot be null");
+        }
+
+        if (getMappingStrategy() == null) {
+            if (generatedAttribute == null) {
+                throw new ComponentInitializationException(
+                        getLogPrefix() + " No mapping strategy or generated attribute ID set");
+            }
+            setMappingStrategy(new SimpleStorageMappingStrategy(generatedAttribute));
+        }
+        
+        super.doInitialize();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nullable protected Map<String,IdPAttribute> retrieveAttributes(@Nonnull final StorageServiceSearch executable)
+            throws ResolutionException {
+
+        try {
+            
+            final StorageRecord<?> record = executable.execute(storageService);
+            if (record == null) {
+                if (noResultAnError) {
+                    throw new NoResultAnErrorResolutionException(getLogPrefix() + " No record returned");
+                }
+                return Collections.emptyMap();
+            }
+            
+            return getMappingStrategy().map(record);
+        } catch (final IOException e) {
+            throw new ResolutionException(getLogPrefix() + " StorageService read failed", e);
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/idp-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/storage/impl/TemplatedSearchBuilder.java b/idp-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/storage/impl/TemplatedSearchBuilder.java
new file mode 100644
index 000000000..13e072321
--- /dev/null
+++ b/idp-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/storage/impl/TemplatedSearchBuilder.java
@@ -0,0 +1,244 @@
+/*
+ * 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.dc.storage.impl;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import net.shibboleth.idp.attribute.IdPAttributeValue;
+import net.shibboleth.idp.attribute.resolver.ResolutionException;
+import net.shibboleth.idp.attribute.resolver.context.AttributeResolutionContext;
+import net.shibboleth.idp.attribute.resolver.dc.ExecutableSearchBuilder;
+import net.shibboleth.idp.attribute.resolver.dc.storage.StorageServiceSearch;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.collection.Pair;
+import net.shibboleth.utilities.java.support.component.AbstractInitializableComponent;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.primitive.StringSupport;
+import net.shibboleth.utilities.java.support.velocity.Template;
+
+import org.apache.velocity.VelocityContext;
+import org.apache.velocity.app.VelocityEngine;
+import org.apache.velocity.exception.VelocityException;
+import org.opensaml.storage.StorageRecord;
+import org.opensaml.storage.StorageService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * An {@link ExecutableSearchBuilder} that generates the {@link StorageService} context and key
+ * using Velocity templates.
+ * 
+ * @since 4.1.0
+ */
+public class TemplatedSearchBuilder extends AbstractInitializableComponent
+        implements ExecutableSearchBuilder<StorageServiceSearch> {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(TemplatedSearchBuilder.class);
+
+    /** Context template to be evaluated. */
+    @NonnullAfterInit private Template contextTemplate;
+
+    /** Key template to be evaluated. */
+    @NonnullAfterInit private Template keyTemplate;
+    
+    /** Text of context template to be evaluated. */
+    @NonnullAfterInit private String contextTemplateText;
+
+    /** Text of key template to be evaluated. */
+    @NonnullAfterInit private String keyTemplateText;
+
+    /** VelocityEngine. */
+    @NonnullAfterInit private VelocityEngine engine;
+    
+    /** A custom object to inject into the template. */
+    @Nullable private Object customObject;
+    
+    /**
+     * Get the context template to be evaluated.
+     * 
+     * @return template
+     */
+    @NonnullAfterInit public Template getContextTemplate() {
+        return contextTemplate;
+    }
+
+    /**
+     * Get the key template to be evaluated.
+     * 
+     * @return template
+     */
+    @NonnullAfterInit public Template getKeyTemplate() {
+        return keyTemplate;
+    }
+
+    /**
+     * Get the context template text to be evaluated.
+     * 
+     * @return template text
+     */
+    @NonnullAfterInit public String getContextTemplateText() {
+        return contextTemplateText;
+    }
+
+    /**
+     * Set the context template to be evaluated.
+     * 
+     * @param text template to be evaluated
+     */
+    public void setContextTemplateText(@Nullable final String text) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+        contextTemplateText = StringSupport.trimOrNull(text);
+    }
+
+    /**
+     * Get the key template text to be evaluated.
+     * 
+     * @return template text
+     */
+    @NonnullAfterInit public String getKeyTemplateText() {
+        return keyTemplateText;
+    }
+
+    /**
+     * Set the key template to be evaluated.
+     * 
+     * @param text template to be evaluated
+     */
+    public void setKeyTemplateText(@Nullable final String text) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+        keyTemplateText = StringSupport.trimOrNull(text);
+    }
+
+    /**
+     * Get the {@link VelocityEngine} to be used.
+     * 
+     * @return template engine
+     */
+    @NonnullAfterInit public VelocityEngine getVelocityEngine() {
+        return engine;
+    }
+
+    /**
+     * Set the {@link VelocityEngine} to be used.
+     * 
+     * @param velocityEngine engine to be used
+     */
+    public void setVelocityEngine(@Nonnull final VelocityEngine velocityEngine) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+        engine = Constraint.isNotNull(velocityEngine, "Velocity engine cannot be null");
+    }
+    
+    /**
+     * Set the custom (externally provided) object.
+     * 
+     * @param object the custom object
+     */
+    public void setCustomObject(@Nullable final Object object) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+        customObject = object;
+    }
+    
+    /** {@inheritDoc} */
+    @Override protected void doInitialize() throws ComponentInitializationException {
+
+        if (null == engine) {
+            throw new ComponentInitializationException("Velocity engine cannot be null");
+        }
+
+        if (null == contextTemplateText) {
+            throw new ComponentInitializationException("Context template text cannot be null");
+        } else if (null == keyTemplateText) {
+            throw new ComponentInitializationException("Key template text cannot be null");
+        }
+
+        contextTemplate = Template.fromTemplate(engine, contextTemplateText);
+        keyTemplate = Template.fromTemplate(engine, keyTemplateText);
+    }
+
+    /** {@inheritDoc} */
+    @Override public StorageServiceSearch build(@Nonnull final AttributeResolutionContext resolutionContext,
+            @Nonnull final Map<String, List<IdPAttributeValue>> dependencyAttributes) throws ResolutionException {
+        
+        final Pair<String,String> searchParams = getContextAndKey(resolutionContext, dependencyAttributes);
+        
+        return new StorageServiceSearch() {
+            
+            /** {@inheritDoc} */
+            @Nullable public String getResultCacheKey() {
+                if (searchParams.getFirst() != null && searchParams.getSecond() != null) {
+                    return searchParams.getFirst() + "!" + searchParams.getSecond();
+                }
+                return null;
+            }
+
+            /** {@inheritDoc} */
+            public StorageRecord<?> execute(@Nonnull final StorageService storageService) throws IOException {
+                return storageService.read(searchParams.getFirst(), searchParams.getSecond());
+            }
+        };
+    }
+    
+    @Nonnull private Pair<String,String> getContextAndKey(@Nonnull final AttributeResolutionContext resolutionContext,
+            @Nonnull final Map<String, List<IdPAttributeValue>> dependencyAttributes) throws ResolutionException {
+
+        final VelocityContext context = new VelocityContext();
+        log.trace("Creating search criteria using attribute resolution context {}", resolutionContext);
+        context.put("resolutionContext", resolutionContext);
+        context.put("custom", customObject);
+
+        // inject dependencies
+        if (dependencyAttributes != null && !dependencyAttributes.isEmpty()) {
+            for (final Map.Entry<String, List<IdPAttributeValue>> entry : dependencyAttributes.entrySet()) {
+                final List<Object> values = new ArrayList<>(entry.getValue().size());
+                for (final IdPAttributeValue value : entry.getValue()) {
+                    values.add(value.getNativeValue());
+                }
+                log.trace("Adding dependency {} to context with {} value(s)", entry.getKey(), values.size());
+                context.put(entry.getKey(), values);
+            }
+        }
+
+        try {
+            final String ctx = contextTemplate.merge(context);
+            final String key = keyTemplate.merge(context);
+            log.debug("Produced search context '{}', key '{}'", ctx, key);
+            return new Pair<>(ctx, key);
+        } catch (final VelocityException e) {
+            log.error("Error running template engine: {}", e.getMessage());
+            throw new ResolutionException("Error running template engine", e);
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/idp-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/storage/impl/package-info.java b/idp-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/storage/impl/package-info.java
new file mode 100644
index 000000000..52b89e3f8
--- /dev/null
+++ b/idp-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/storage/impl/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+
+/**
+ * Implementation of StorageService-backed data connector.
+ */
+
+package net.shibboleth.idp.attribute.resolver.dc.storage.impl;
\ No newline at end of file
diff --git a/idp-attribute-resolver-impl/src/test/java/net/shibboleth/idp/attribute/resolver/dc/storage/impl/StorageServiceDataConnectorTest.java b/idp-attribute-resolver-impl/src/test/java/net/shibboleth/idp/attribute/resolver/dc/storage/impl/StorageServiceDataConnectorTest.java
new file mode 100644
index 000000000..066fed7a6
--- /dev/null
+++ b/idp-attribute-resolver-impl/src/test/java/net/shibboleth/idp/attribute/resolver/dc/storage/impl/StorageServiceDataConnectorTest.java
@@ -0,0 +1,203 @@
+/*
+ * 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.dc.storage.impl;
+
+import static org.testng.Assert.*;
+
+import java.io.IOException;
+import java.time.Duration;
+import java.util.Map;
+
+import javax.script.ScriptException;
+
+import org.opensaml.storage.impl.MemoryStorageService;
+import org.springframework.core.io.ClassPathResource;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+import org.testng.reporters.Files;
+
+import net.shibboleth.ext.spring.resource.ResourceHelper;
+import net.shibboleth.idp.attribute.IdPAttribute;
+import net.shibboleth.idp.attribute.StringAttributeValue;
+import net.shibboleth.idp.attribute.resolver.NoResultAnErrorResolutionException;
+import net.shibboleth.idp.attribute.resolver.ResolutionException;
+import net.shibboleth.idp.attribute.resolver.context.AttributeResolutionContext;
+import net.shibboleth.idp.attribute.resolver.dc.impl.TestCache;
+import net.shibboleth.idp.saml.impl.testing.TestSources;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.velocity.VelocityEngine;
+
+/**
+ * Tests for {@link HTTPDataConnector}
+ */
+ at SuppressWarnings("javadoc")
+public class StorageServiceDataConnectorTest {
+
+    private static final String TEST_CONNECTOR_NAME = "StorageServiceConnector";
+    
+    private static final String SCRIPT_PATH = "/net/shibboleth/idp/attribute/resolver/impl/dc/storage/";
+    
+    private MemoryStorageService storage;
+    private StorageServiceDataConnector connector;
+
+    @BeforeMethod public void setUp() throws Exception {
+        
+        storage = new MemoryStorageService();
+        storage.setId("ss");
+        storage.setCleanupInterval(Duration.ZERO);
+        storage.initialize();
+        
+        connector = new StorageServiceDataConnector();
+        connector.setId(TEST_CONNECTOR_NAME);
+        connector.setStorageService(storage);
+    }
+    
+    @AfterMethod public void tearDown() {
+        connector.destroy();
+        storage.destroy();
+    }
+    
+    @Test public void testSimpleMissing() throws ComponentInitializationException, ResolutionException, ScriptException, IOException {
+        
+        final TemplatedSearchBuilder builder = new TemplatedSearchBuilder();
+        builder.setContextTemplateText("foo");
+        builder.setKeyTemplateText("bar");
+        builder.setVelocityEngine(VelocityEngine.newVelocityEngine());
+        builder.initialize();
+        
+        connector.setExecutableSearchBuilder(builder);
+        connector.setGeneratedAttributeId("foobar");
+        connector.initialize();
+        
+        final AttributeResolutionContext context =
+                TestSources.createResolutionContext(TestSources.PRINCIPAL_ID, TestSources.IDP_ENTITY_ID,
+                        TestSources.SP_ENTITY_ID);
+
+        final Map<String,IdPAttribute> attrs = connector.resolve(context);
+        
+        assertTrue(attrs.isEmpty());
+    }
+
+    @Test(expectedExceptions=NoResultAnErrorResolutionException.class)
+    public void testSimpleMissingError() throws ComponentInitializationException, ResolutionException, ScriptException, IOException {
+        
+        final TemplatedSearchBuilder builder = new TemplatedSearchBuilder();
+        builder.setContextTemplateText("foo");
+        builder.setKeyTemplateText("bar");
+        builder.setVelocityEngine(VelocityEngine.newVelocityEngine());
+        builder.initialize();
+        
+        connector.setExecutableSearchBuilder(builder);
+        connector.setGeneratedAttributeId("foobar");
+        connector.setNoResultAnError(true);
+        connector.initialize();
+        
+        final AttributeResolutionContext context =
+                TestSources.createResolutionContext(TestSources.PRINCIPAL_ID, TestSources.IDP_ENTITY_ID,
+                        TestSources.SP_ENTITY_ID);
+        connector.resolve(context);
+    }
+
+    @Test public void testSimple() throws ComponentInitializationException, ResolutionException, ScriptException, IOException {
+        
+        final TemplatedSearchBuilder builder = new TemplatedSearchBuilder();
+        builder.setContextTemplateText("foo");
+        builder.setKeyTemplateText("bar");
+        builder.setVelocityEngine(VelocityEngine.newVelocityEngine());
+        builder.initialize();
+        
+        connector.setExecutableSearchBuilder(builder);
+        connector.setGeneratedAttributeId("foobar");
+        connector.initialize();
+        
+        storage.create("foo", "bar", "test", null);
+
+        final AttributeResolutionContext context =
+                TestSources.createResolutionContext(TestSources.PRINCIPAL_ID, TestSources.IDP_ENTITY_ID,
+                        TestSources.SP_ENTITY_ID);
+        final Map<String,IdPAttribute> attrs = connector.resolve(context);
+
+        assertEquals(attrs.size(), 1);
+        
+        assertEquals(attrs.get("foobar").getValues().size(), 1);
+        assertEquals(((StringAttributeValue) attrs.get("foobar").getValues().get(0)).getValue(), "test");
+    }
+    
+    @Test public void resolveWithCache() throws ComponentInitializationException, ResolutionException, ScriptException, IOException {
+        
+        final TemplatedSearchBuilder builder = new TemplatedSearchBuilder();
+        builder.setContextTemplateText("foo");
+        builder.setKeyTemplateText("bar");
+        builder.setVelocityEngine(VelocityEngine.newVelocityEngine());
+        builder.initialize();
+        
+        connector.setExecutableSearchBuilder(builder);
+        connector.setGeneratedAttributeId("foobar");
+        
+        final TestCache cache = new TestCache();
+        connector.setResultsCache(cache);
+
+        connector.initialize();
+        
+        storage.create("foo", "bar", "test", null);
+
+        final AttributeResolutionContext context =
+                TestSources.createResolutionContext(TestSources.PRINCIPAL_ID, TestSources.IDP_ENTITY_ID,
+                        TestSources.SP_ENTITY_ID);
+        
+        assertTrue(cache.size() == 0);
+        final Map<String,IdPAttribute> optional = connector.resolve(context);
+        assertTrue(cache.size() == 1);
+        assertEquals(cache.iterator().next(), optional);
+    }
+
+    @Test public void testScripted() throws ComponentInitializationException, ResolutionException, ScriptException, IOException {
+        
+        final TemplatedSearchBuilder builder = new TemplatedSearchBuilder();
+        builder.setContextTemplateText("foo");
+        builder.setKeyTemplateText("bar");
+        builder.setVelocityEngine(VelocityEngine.newVelocityEngine());
+        builder.initialize();
+        
+        connector.setExecutableSearchBuilder(builder);
+        
+        final ScriptedStorageMappingStrategy mapper = ScriptedStorageMappingStrategy.resourceScript(
+                ResourceHelper.of(new ClassPathResource((SCRIPT_PATH) + "test.js")));
+        
+        connector.setMappingStrategy(mapper);
+        
+        connector.initialize();
+        
+        storage.create("foo", "bar", Files.streamToString(getClass().getResourceAsStream(SCRIPT_PATH + "test.json")), null);
+
+        final AttributeResolutionContext context =
+                TestSources.createResolutionContext(TestSources.PRINCIPAL_ID, TestSources.IDP_ENTITY_ID,
+                        TestSources.SP_ENTITY_ID);
+        final Map<String,IdPAttribute> attrs = connector.resolve(context);
+
+        assertEquals(attrs.size(), 2);
+        
+        assertEquals(attrs.get("foo").getValues().size(), 1);
+        assertEquals(((StringAttributeValue) attrs.get("foo").getValues().get(0)).getValue(), "foo1");
+        
+        assertEquals(attrs.get("bar").getValues().size(), 2);
+        assertEquals(((StringAttributeValue)attrs.get("bar").getValues().get(0)).getValue(), "bar1");
+        assertEquals(((StringAttributeValue)attrs.get("bar").getValues().get(1)).getValue(), "bar2");
+    }
+}
\ No newline at end of file
diff --git a/idp-attribute-resolver-impl/src/test/resources/net/shibboleth/idp/attribute/resolver/impl/dc/storage/test.js b/idp-attribute-resolver-impl/src/test/resources/net/shibboleth/idp/attribute/resolver/impl/dc/storage/test.js
new file mode 100644
index 000000000..d5bc45416
--- /dev/null
+++ b/idp-attribute-resolver-impl/src/test/resources/net/shibboleth/idp/attribute/resolver/impl/dc/storage/test.js
@@ -0,0 +1,18 @@
+var result = JSON.parse(record.getValue());
+
+var IdPAttribute = Java.type("net.shibboleth.idp.attribute.IdPAttribute");
+var StringValue = Java.type("net.shibboleth.idp.attribute.StringAttributeValue");
+var HashSet = Java.type("java.util.HashSet");
+
+for (var i=0; i<result.length; i++) {
+
+    var attr = new IdPAttribute(result[i].name);
+    var values = new HashSet();
+    
+    for (var j=0; j<result[i].values.length; j++) {
+        values.add(new StringValue(result[i].values[j]));
+    }
+    
+    attr.setValues(values);
+    connectorResults.add(attr);
+}
diff --git a/idp-attribute-resolver-impl/src/test/resources/net/shibboleth/idp/attribute/resolver/impl/dc/storage/test.json b/idp-attribute-resolver-impl/src/test/resources/net/shibboleth/idp/attribute/resolver/impl/dc/storage/test.json
new file mode 100644
index 000000000..c213568b0
--- /dev/null
+++ b/idp-attribute-resolver-impl/src/test/resources/net/shibboleth/idp/attribute/resolver/impl/dc/storage/test.json
@@ -0,0 +1,10 @@
+[
+    {
+    "name" : "foo",
+    "values" : [ "foo1" ]
+    },
+    {
+    "name" : "bar",
+    "values" : [ "bar1", "bar2" ]
+    }
+]

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


More information about the commits mailing list