[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