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

Scott Cantor cantor.2 at osu.edu
Wed Oct 21 22:19:57 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=38b1037d8de4e49f0a80db12b4116d405c8cfbaa

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

commit 38b1037d8de4e49f0a80db12b4116d405c8cfbaa
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Wed Oct 21 18:19:51 2020 -0400

    IDP-1697 - StorageService-backed DataConnector
    
    https://issues.shibboleth.net/jira/browse/IDP-1697
    
    Spring parser, unit tests.
---
 .../storage/impl/StorageServiceDataConnector.java  |  16 +-
 .../impl/StorageServiceDataConnectorTest.java      |   8 +-
 .../impl/StorageServiceDataConnectorParser.java    | 256 +++++++++++++++++++++
 .../spring/dc/storage/impl/package-info.java       |  22 ++
 .../impl/AttributeResolverNamespaceHandler.java    |   3 +
 .../StorageServiceDataConnectorParserTest.java     | 225 ++++++++++++++++++
 .../resolver/spring/dc/storage/spring-beans.xml    |  31 +++
 .../storage-attribute-resolver-spring-context.xml  |  24 ++
 .../storage-attribute-resolver-v2-hybrid.xml       |  17 ++
 .../storage-attribute-resolver-v2-simple.xml       |  16 ++
 .../dc/storage/storage-attribute-resolver-v2.xml   |  22 ++
 .../schema/shibboleth-attribute-resolver.xsd       |  95 ++++++++
 12 files changed, 723 insertions(+), 12 deletions(-)

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
index d12d2509c..0853b16b4 100644
--- 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
@@ -62,7 +62,7 @@ public class StorageServiceDataConnector
     @NonnullAfterInit private StorageService storageService;
     
     /** ID of the attribute generated by this data connector if simple result mapping used. */
-    @NonnullAfterInit private String generatedAttribute;
+    @NonnullAfterInit private String generatedAttributeID;
     
     /** Whether no record is an error. */
     private boolean noResultAnError;
@@ -99,8 +99,8 @@ public class StorageServiceDataConnector
      * 
      * @return ID of the attribute generated by this connector
      */
-    @NonnullAfterInit public String getGeneratedAttributeId() {
-        return generatedAttribute;
+    @NonnullAfterInit public String getGeneratedAttributeID() {
+        return generatedAttributeID;
     }
 
     /**
@@ -118,13 +118,13 @@ public class StorageServiceDataConnector
     /**
      * Set the ID of the attribute generated by this connector if simple result mapping used.
      * 
-     * @param newAttributeId what to set.
+     * @param id what to set.
      */
-    public void setGeneratedAttributeId(@Nullable final String newAttributeId) {
+    public void setGeneratedAttributeID(@Nullable final String id) {
         ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
         ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
         
-        generatedAttribute = StringSupport.trimOrNull(newAttributeId);
+        generatedAttributeID = StringSupport.trimOrNull(id);
     }
     
     /** {@inheritDoc} */
@@ -135,11 +135,11 @@ public class StorageServiceDataConnector
         }
 
         if (getMappingStrategy() == null) {
-            if (generatedAttribute == null) {
+            if (generatedAttributeID == null) {
                 throw new ComponentInitializationException(
                         getLogPrefix() + " No mapping strategy or generated attribute ID set");
             }
-            setMappingStrategy(new SimpleStorageMappingStrategy(generatedAttribute));
+            setMappingStrategy(new SimpleStorageMappingStrategy(generatedAttributeID));
         }
         
         super.doInitialize();
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
index 066fed7a6..86f66efb8 100644
--- 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
@@ -82,7 +82,7 @@ public class StorageServiceDataConnectorTest {
         builder.initialize();
         
         connector.setExecutableSearchBuilder(builder);
-        connector.setGeneratedAttributeId("foobar");
+        connector.setGeneratedAttributeID("foobar");
         connector.initialize();
         
         final AttributeResolutionContext context =
@@ -104,7 +104,7 @@ public class StorageServiceDataConnectorTest {
         builder.initialize();
         
         connector.setExecutableSearchBuilder(builder);
-        connector.setGeneratedAttributeId("foobar");
+        connector.setGeneratedAttributeID("foobar");
         connector.setNoResultAnError(true);
         connector.initialize();
         
@@ -123,7 +123,7 @@ public class StorageServiceDataConnectorTest {
         builder.initialize();
         
         connector.setExecutableSearchBuilder(builder);
-        connector.setGeneratedAttributeId("foobar");
+        connector.setGeneratedAttributeID("foobar");
         connector.initialize();
         
         storage.create("foo", "bar", "test", null);
@@ -148,7 +148,7 @@ public class StorageServiceDataConnectorTest {
         builder.initialize();
         
         connector.setExecutableSearchBuilder(builder);
-        connector.setGeneratedAttributeId("foobar");
+        connector.setGeneratedAttributeID("foobar");
         
         final TestCache cache = new TestCache();
         connector.setResultsCache(cache);
diff --git a/idp-attribute-resolver-spring/src/main/java/net/shibboleth/idp/attribute/resolver/spring/dc/storage/impl/StorageServiceDataConnectorParser.java b/idp-attribute-resolver-spring/src/main/java/net/shibboleth/idp/attribute/resolver/spring/dc/storage/impl/StorageServiceDataConnectorParser.java
new file mode 100644
index 000000000..0910c776d
--- /dev/null
+++ b/idp-attribute-resolver-spring/src/main/java/net/shibboleth/idp/attribute/resolver/spring/dc/storage/impl/StorageServiceDataConnectorParser.java
@@ -0,0 +1,256 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.attribute.resolver.spring.dc.storage.impl;
+
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.xml.namespace.QName;
+
+import net.shibboleth.ext.spring.util.SpringSupport;
+import net.shibboleth.idp.attribute.resolver.dc.storage.impl.ScriptedStorageMappingStrategy;
+import net.shibboleth.idp.attribute.resolver.dc.storage.impl.StorageServiceDataConnector;
+import net.shibboleth.idp.attribute.resolver.dc.storage.impl.TemplatedSearchBuilder;
+import net.shibboleth.idp.attribute.resolver.spring.dc.AbstractDataConnectorParser;
+import net.shibboleth.idp.attribute.resolver.spring.dc.impl.CacheConfigParser;
+import net.shibboleth.idp.attribute.resolver.spring.impl.AttributeResolverNamespaceHandler;
+import net.shibboleth.idp.profile.spring.relyingparty.metadata.ScriptTypeBeanParser;
+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;
+import net.shibboleth.utilities.java.support.xml.AttributeSupport;
+import net.shibboleth.utilities.java.support.xml.ElementSupport;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.beans.factory.support.BeanDefinitionBuilder;
+import org.springframework.beans.factory.xml.ParserContext;
+import org.w3c.dom.Element;
+
+/** Bean definition Parser for a {@link StorageServiceDataConnector}. */
+public class StorageServiceDataConnectorParser extends AbstractDataConnectorParser {
+
+    /** Schema type name. */
+    @Nonnull public static final QName TYPE_NAME =
+            new QName(AttributeResolverNamespaceHandler.NAMESPACE, "StorageService");
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(StorageServiceDataConnectorParser.class);
+
+    /** {@inheritDoc} */
+    @Override protected Class<StorageServiceDataConnector> getNativeBeanClass() {
+        return StorageServiceDataConnector.class;
+    }
+    
+    /** {@inheritDoc} */
+    @Override protected void doV2Parse(@Nonnull final Element config, @Nonnull final ParserContext parserContext,
+            @Nonnull final BeanDefinitionBuilder builder) {
+
+        log.debug("{} Parsing custom configuration {}", getLogPrefix(), config);
+
+        final V2Parser v2Parser = new V2Parser(config, getLogPrefix());
+        
+        final String searchBuilderID = v2Parser.getBeanSearchBuilderID();
+        if (searchBuilderID != null) {
+            builder.addPropertyReference("executableSearchBuilder", searchBuilderID);
+        } else {
+            builder.addPropertyValue("executableSearchBuilder", v2Parser.createTemplateBuilder());
+        }
+
+        final String mappingStrategyID = v2Parser.getBeanMappingStrategyID();
+        if (mappingStrategyID != null) {
+            builder.addPropertyReference("mappingStrategy", mappingStrategyID);
+            if (config.hasAttributeNS(null, "generatedAttributeID")) {
+                log.warn("{} Ignoring generatedAttributeID in favor of explicit mapping strategy", getLogPrefix());
+            }
+        } else {
+            final BeanDefinition def = v2Parser.createMappingStrategy(config.getAttributeNS(null, "id"));
+            if (def != null) {
+                builder.addPropertyValue("mappingStrategy", def);
+                if (config.hasAttributeNS(null, "generatedAttributeID")) {
+                    log.warn("{} Ignoring generatedAttributeID in favor of <RecordMapping> element", getLogPrefix());
+                }
+            } else {
+                builder.addPropertyValue("generatedAttributeID", config.getAttributeNS(null, "generatedAttributeID"));
+            }
+        }
+        
+        final String resultCacheBeanID = CacheConfigParser.getBeanResultCacheID(config);
+        if (null != resultCacheBeanID) {
+           builder.addPropertyReference("resultsCache", resultCacheBeanID);
+        } else {
+            builder.addPropertyValue("resultsCache", v2Parser.createCache(parserContext));
+        }
+        
+        builder.addPropertyReference("storageService", config.getAttributeNS(null, "storageServiceRef"));
+
+        final String noResultIsError =
+                AttributeSupport.getAttributeValue(config, new QName("noResultIsError"));
+        if (noResultIsError != null) {
+            builder.addPropertyValue("noResultAnError", SpringSupport.getStringValueAsBoolean(noResultIsError));
+        }
+
+        builder.setInitMethodName("initialize");
+        builder.setDestroyMethodName("destroy");
+    }
+
+    /**
+     * Utility class for parsing v2 schema configuration.
+     * 
+     */
+    protected static class V2Parser {
+
+        /** Base XML element. */
+        @Nonnull private final Element configElement;
+
+        /** Class logger. */
+        @Nonnull private final Logger log = LoggerFactory.getLogger(V2Parser.class);
+
+        /** Parent parser's log prefix.*/
+        @Nonnull @NotEmpty private final String logPrefix;
+
+        /**
+         * Creates a new V2Parser with the supplied element.
+         * 
+         * @param config StorageService DataConnector element
+         * @param prefix the parent parser's log prefix.
+         */
+        public V2Parser(@Nonnull final Element config,  @Nonnull @NotEmpty final String prefix) {
+            Constraint.isNotNull(config, "StorageService DataConnector element cannot be null");
+            configElement = config;
+            logPrefix = prefix;
+        }
+
+        /**
+         * Get the bean ID of an externally defined search builder.
+         * 
+         * @return search builder bean ID
+         */
+        @Nullable public String getBeanSearchBuilderID() {
+            return AttributeSupport.getAttributeValue(configElement, null, "executableSearchBuilderRef");
+        }
+        
+        /**
+         * Create the definition of the search builder.
+         * 
+         * @return the bean definition for the search builder, or null
+         */
+        @Nullable public BeanDefinition createTemplateBuilder() {
+            
+            final List<Element> contextTemplates = ElementSupport.getChildElements(configElement, 
+                    new QName(AttributeResolverNamespaceHandler.NAMESPACE, "ContextTemplate"));
+            final List<Element> keyTemplates = ElementSupport.getChildElements(configElement, 
+                    new QName(AttributeResolverNamespaceHandler.NAMESPACE, "KeyTemplate"));
+            if (contextTemplates.size() == 0 || keyTemplates.size() == 0) {
+                return null;
+            }
+            
+            final BeanDefinitionBuilder templateBuilder =
+                    BeanDefinitionBuilder.genericBeanDefinition(TemplatedSearchBuilder.class);
+            templateBuilder.setInitMethodName("initialize");
+            templateBuilder.setDestroyMethodName("destroy");
+
+            String velocityEngineRef = StringSupport.trimOrNull(configElement.getAttributeNS(null, "templateEngine"));
+            if (null == velocityEngineRef) {
+                velocityEngineRef = "shibboleth.VelocityEngine";
+            }
+            templateBuilder.addPropertyReference("velocityEngine", velocityEngineRef);
+
+            if (contextTemplates.size() > 1) {
+                log.warn("{} A maximum of 1 <ContextTemplate> should be specified; the first one has been used",
+                        getLogPrefix());
+            } else if (keyTemplates.size() > 1) {
+                log.warn("{} A maximum of 1 <KeyTemplate> should be specified; the first one has been used",
+                        getLogPrefix());
+            }
+            
+            final Element contextTemplate = contextTemplates.get(0);
+            final Element keyTemplate = keyTemplates.get(0);
+            
+            if (configElement.hasAttributeNS(null, "customObjectRef")) {
+                templateBuilder.addPropertyReference("customObject",
+                        configElement.getAttributeNS(null, "customObjectRef"));
+            }
+            
+            templateBuilder.addPropertyValue("contextTemplateText", contextTemplate.getTextContent());
+            templateBuilder.addPropertyValue("keyTemplateText", keyTemplate.getTextContent());
+            
+            return templateBuilder.getBeanDefinition();
+        }
+        
+        /**
+         * Get the bean ID of an externally defined mapping strategy.
+         * 
+         * @return mapping strategy bean ID
+         */
+        @Nullable public String getBeanMappingStrategyID() {
+            return AttributeSupport.getAttributeValue(configElement, null, "mappingStrategyRef");
+        }
+
+        /**
+         * Create the scripted result mapping strategy.
+         * 
+         * @param id the ID of the 
+         * 
+         * @return mapping strategy
+         */
+        @Nullable public BeanDefinition createMappingStrategy(@Nullable final String id) {
+
+            final List<Element> mappings = ElementSupport.getChildElements(configElement, 
+                    new QName(AttributeResolverNamespaceHandler.NAMESPACE, "RecordMapping"));
+    
+            if (mappings.size() > 1) {
+                log.warn("{} A maximum of 1 <RecordMapping> should be specified; the first one has been used",
+                        getLogPrefix());
+            } else if (mappings.isEmpty()) {
+                // No element means to fall back to simple record mapping.
+                return null;
+            }
+            
+            final BeanDefinitionBuilder mapper =
+                    ScriptTypeBeanParser.parseScriptType(ScriptedStorageMappingStrategy.class, mappings.get(0));
+            if (id != null) {
+                mapper.addPropertyValue("logPrefix", id + ':');
+            }
+            
+            return mapper.getBeanDefinition();
+        }
+        
+        /**
+         * Create the results cache. See {@link CacheConfigParser}.
+         * 
+         * @param parserContext bean parser context
+         * 
+         * @return results cache
+         */
+        @Nullable public BeanDefinition createCache(@Nonnull final ParserContext parserContext) {
+            final CacheConfigParser parser = new CacheConfigParser(configElement);
+            return parser.createCache(parserContext);
+        }
+
+        /** The parent parser's log prefix.
+         * @return the log prefix.
+         */
+        @Nonnull @NotEmpty private String getLogPrefix() {
+            return logPrefix;
+        }
+    }
+    
+}
\ No newline at end of file
diff --git a/idp-attribute-resolver-spring/src/main/java/net/shibboleth/idp/attribute/resolver/spring/dc/storage/impl/package-info.java b/idp-attribute-resolver-spring/src/main/java/net/shibboleth/idp/attribute/resolver/spring/dc/storage/impl/package-info.java
new file mode 100644
index 000000000..505e066d7
--- /dev/null
+++ b/idp-attribute-resolver-spring/src/main/java/net/shibboleth/idp/attribute/resolver/spring/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.
+ */
+
+/**
+ * Parser for StorageService data connector.
+ */
+
+package net.shibboleth.idp.attribute.resolver.spring.dc.storage.impl;
\ No newline at end of file
diff --git a/idp-attribute-resolver-spring/src/main/java/net/shibboleth/idp/attribute/resolver/spring/impl/AttributeResolverNamespaceHandler.java b/idp-attribute-resolver-spring/src/main/java/net/shibboleth/idp/attribute/resolver/spring/impl/AttributeResolverNamespaceHandler.java
index 4991dbc1e..be674ee5c 100644
--- a/idp-attribute-resolver-spring/src/main/java/net/shibboleth/idp/attribute/resolver/spring/impl/AttributeResolverNamespaceHandler.java
+++ b/idp-attribute-resolver-spring/src/main/java/net/shibboleth/idp/attribute/resolver/spring/impl/AttributeResolverNamespaceHandler.java
@@ -46,6 +46,7 @@ import net.shibboleth.idp.attribute.resolver.spring.dc.impl.StoredIdDataConnecto
 import net.shibboleth.idp.attribute.resolver.spring.dc.impl.SubjectDataConnectorParser;
 import net.shibboleth.idp.attribute.resolver.spring.dc.ldap.impl.LDAPDataConnectorParser;
 import net.shibboleth.idp.attribute.resolver.spring.dc.rdbms.impl.RDBMSDataConnectorParser;
+import net.shibboleth.idp.attribute.resolver.spring.dc.storage.impl.StorageServiceDataConnectorParser;
 import net.shibboleth.idp.attribute.resolver.spring.enc.impl.SAML1Base64AttributeEncoderParser;
 import net.shibboleth.idp.attribute.resolver.spring.enc.impl.SAML1ScopedStringAttributeEncoderParser;
 import net.shibboleth.idp.attribute.resolver.spring.enc.impl.SAML1StringAttributeEncoderParser;
@@ -115,6 +116,8 @@ public class AttributeResolverNamespaceHandler extends BaseSpringNamespaceHandle
         registerBeanDefinitionParser(StaticDataConnectorParser.TYPE_NAME_RESOLVER, new StaticDataConnectorParser());
         registerBeanDefinitionParser(StoredIdDataConnectorParser.TYPE_NAME_RESOLVER, new StoredIdDataConnectorParser());
         registerBeanDefinitionParser(SubjectDataConnectorParser.TYPE_NAME_RESOLVER, new SubjectDataConnectorParser());
+        registerBeanDefinitionParser(StorageServiceDataConnectorParser.TYPE_NAME,
+                new StorageServiceDataConnectorParser());
 
 
         // Encoders
diff --git a/idp-attribute-resolver-spring/src/test/java/net/shibboleth/idp/attribute/resolver/spring/dc/storage/StorageServiceDataConnectorParserTest.java b/idp-attribute-resolver-spring/src/test/java/net/shibboleth/idp/attribute/resolver/spring/dc/storage/StorageServiceDataConnectorParserTest.java
new file mode 100644
index 000000000..ca59117ce
--- /dev/null
+++ b/idp-attribute-resolver-spring/src/test/java/net/shibboleth/idp/attribute/resolver/spring/dc/storage/StorageServiceDataConnectorParserTest.java
@@ -0,0 +1,225 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.attribute.resolver.spring.dc.storage;
+
+import static org.testng.Assert.*;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import org.opensaml.storage.StorageService;
+import org.springframework.context.support.GenericApplicationContext;
+import org.springframework.core.env.PropertySource;
+import org.springframework.core.io.ResourceLoader;
+import org.springframework.mock.env.MockPropertySource;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.Test;
+import org.testng.reporters.Files;
+
+import net.shibboleth.ext.spring.resource.PreferFileSystemResourceLoader;
+import net.shibboleth.ext.spring.util.ApplicationContextBuilder;
+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.context.AttributeResolutionContext;
+import net.shibboleth.idp.attribute.resolver.dc.storage.impl.StorageServiceDataConnector;
+import net.shibboleth.idp.saml.impl.testing.TestSources;
+
+/** Test for {@link StorageServiceDataConnectorParser}. */
+ at SuppressWarnings("javadoc")
+public class StorageServiceDataConnectorParserTest {
+
+    private static final String SCRIPT_PATH = "/net/shibboleth/idp/attribute/resolver/impl/dc/storage/";
+    
+    private GenericApplicationContext pendingTeardownContext = null;
+    
+    @AfterMethod public void tearDownTestContext() {
+        if (null == pendingTeardownContext ) {
+            return;
+        }
+        pendingTeardownContext.close();
+        pendingTeardownContext = null;
+    }
+    
+    private void setTestContext(final GenericApplicationContext context) {
+        tearDownTestContext();
+        pendingTeardownContext = context;
+    }
+    
+    @Test public void v2Simple() throws Exception {
+        
+        final StorageServiceDataConnector connector =
+                getDataConnector(null,
+                        "net/shibboleth/idp/attribute/resolver/spring/dc/storage/storage-attribute-resolver-v2-simple.xml");
+        assertNotNull(connector);
+
+        pendingTeardownContext.getBean(StorageService.class).create("foo", "bar", "testdata", 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("test").getValues().size(), 1);
+        assertEquals(((StringAttributeValue)attrs.get("test").getValues().get(0)).getValue(), "testdata");
+        
+        assertTrue(connector.getResultsCache().size() == 1);
+    }    
+    
+    @Test public void v2Config() throws Exception {
+        
+        final MockPropertySource propSource = singletonPropertySource("context", "foo");
+        propSource.setProperty("key", "bar");
+        propSource.setProperty("scriptPath", (SCRIPT_PATH) + "test.js");
+        
+        final StorageServiceDataConnector connector =
+                getDataConnector(propSource,
+                        "net/shibboleth/idp/attribute/resolver/spring/dc/storage/storage-attribute-resolver-v2.xml");
+        assertNotNull(connector);
+
+        pendingTeardownContext.getBean(StorageService.class).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");
+        
+        assertTrue(connector.getResultsCache().size() == 1);
+    }
+
+     @Test(expectedExceptions=NoResultAnErrorResolutionException.class) public void v2Missing() throws Exception {
+        
+        final MockPropertySource propSource = singletonPropertySource("context", "foo");
+        propSource.setProperty("key", "baz");
+        propSource.setProperty("scriptPath", (SCRIPT_PATH) + "test.js");
+        propSource.setProperty("missingerror", "true");
+        
+        final StorageServiceDataConnector connector =
+                getDataConnector(propSource,
+                        "net/shibboleth/idp/attribute/resolver/spring/dc/storage/storage-attribute-resolver-v2.xml");
+        assertNotNull(connector);
+        
+        final AttributeResolutionContext context =
+                TestSources.createResolutionContext(TestSources.PRINCIPAL_ID, TestSources.IDP_ENTITY_ID,
+                        TestSources.SP_ENTITY_ID);
+        
+        connector.resolve(context);
+    }
+    
+    @Test public void v2MissingOk() throws Exception {
+        
+        final MockPropertySource propSource = singletonPropertySource("context", "foo");
+        propSource.setProperty("key", "baz");
+        propSource.setProperty("scriptPath", (SCRIPT_PATH) + "test.js");
+        
+        final StorageServiceDataConnector connector =
+                getDataConnector(propSource,
+                        "net/shibboleth/idp/attribute/resolver/spring/dc/storage/storage-attribute-resolver-v2.xml");
+        assertNotNull(connector);
+        
+        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 public void hybridConfig() throws Exception {
+        final MockPropertySource propSource = singletonPropertySource("context", "foo");
+        propSource.setProperty("key", "bar");
+        propSource.setProperty("scriptPath", (SCRIPT_PATH) + "test.js");
+        
+        final StorageServiceDataConnector connector =
+                getDataConnector(propSource,
+                        "net/shibboleth/idp/attribute/resolver/spring/dc/storage/storage-attribute-resolver-v2-hybrid.xml",
+                        "net/shibboleth/idp/attribute/resolver/spring/dc/storage/storage-attribute-resolver-spring-context.xml");
+        assertNotNull(connector);
+
+        pendingTeardownContext.getBean(StorageService.class).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");
+        
+        assertTrue(connector.getResultsCache().size() == 1);
+    }
+    
+    private StorageServiceDataConnector getDataConnector(final PropertySource<?> propSource, final String... beanDefinitions)
+            throws IOException {
+
+        final ResourceLoader loader = new PreferFileSystemResourceLoader();
+        
+        final ApplicationContextBuilder builder = new ApplicationContextBuilder();
+        builder.setName("ApplicationContext: " + StorageServiceDataConnectorParserTest.class);
+
+        final Collection<String> defs = new ArrayList<>(Arrays.asList(beanDefinitions));
+        defs.add("net/shibboleth/idp/attribute/resolver/spring/dc/storage/spring-beans.xml");
+
+        builder.setServiceConfigurations(defs.stream().map(s -> loader.getResource(s)).collect(Collectors.toList()));
+
+        if (propSource != null) {
+            builder.setPropertySources(Collections.singletonList(propSource));
+        }
+        
+        final GenericApplicationContext context = builder.build();
+
+        setTestContext(context);
+                
+        return (StorageServiceDataConnector) context.getBean("myStorage");
+    }
+
+    private MockPropertySource singletonPropertySource(final String name, final String value) {
+        final MockPropertySource propSource = new MockPropertySource("localProperties");
+        propSource.setProperty(name, value);
+        return propSource;
+    }
+
+}
diff --git a/idp-attribute-resolver-spring/src/test/resources/net/shibboleth/idp/attribute/resolver/spring/dc/storage/spring-beans.xml b/idp-attribute-resolver-spring/src/test/resources/net/shibboleth/idp/attribute/resolver/spring/dc/storage/spring-beans.xml
new file mode 100644
index 000000000..6034ae178
--- /dev/null
+++ b/idp-attribute-resolver-spring/src/test/resources/net/shibboleth/idp/attribute/resolver/spring/dc/storage/spring-beans.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xmlns:p="http://www.springframework.org/schema/p"
+    xmlns:c="http://www.springframework.org/schema/c"
+    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd"
+    default-init-method="initialize"
+    default-destroy-method="destroy">
+
+    <bean id="CustomObject" class="java.lang.String" c:_0="foo" />
+
+    <bean id="shibboleth.VelocityEngine" class="net.shibboleth.ext.spring.velocity.VelocityEngineFactoryBean">
+        <property name="velocityProperties">
+            <props>
+                <prop key="resource.loader">classpath, string</prop>
+                <prop key="classpath.resource.loader.class">
+                    org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader
+                </prop>
+                <prop key="string.resource.loader.class">
+                    org.apache.velocity.runtime.resource.loader.StringResourceLoader
+                </prop>
+            </props>
+        </property>
+    </bean>
+    
+    <bean id="shibboleth.PropertySourcesPlaceholderConfigurer"
+        class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer"
+        p:placeholderPrefix="%{" p:placeholderSuffix="}" />
+        
+    <bean id="myStorageService" p:id="myStorageService" class="org.opensaml.storage.impl.MemoryStorageService" />
+
+</beans>
\ No newline at end of file
diff --git a/idp-attribute-resolver-spring/src/test/resources/net/shibboleth/idp/attribute/resolver/spring/dc/storage/storage-attribute-resolver-spring-context.xml b/idp-attribute-resolver-spring/src/test/resources/net/shibboleth/idp/attribute/resolver/spring/dc/storage/storage-attribute-resolver-spring-context.xml
new file mode 100644
index 000000000..c2022342a
--- /dev/null
+++ b/idp-attribute-resolver-spring/src/test/resources/net/shibboleth/idp/attribute/resolver/spring/dc/storage/storage-attribute-resolver-spring-context.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xmlns:c="http://www.springframework.org/schema/c"
+    xmlns:p="http://www.springframework.org/schema/p"
+    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd"
+    default-init-method="initialize"
+    default-destroy-method="destroy">
+    
+    <bean id="cacheBuilder" class="com.google.common.cache.CacheBuilder" factory-method="from">
+        <constructor-arg value="expireAfterAccess=10s,maximumSize=25" />
+    </bean>
+    
+    <bean id="cache" factory-bean="cacheBuilder" factory-method="build" />
+    
+    <bean id="template" class="net.shibboleth.idp.attribute.resolver.dc.storage.impl.TemplatedSearchBuilder"
+        p:velocityEngine-ref="shibboleth.VelocityEngine"
+        p:contextTemplateText="%{context}"
+        p:keyTemplateText="%{key}" />
+
+    <bean id="mapping" class="net.shibboleth.idp.attribute.resolver.dc.storage.impl.ScriptedStorageMappingStrategy"
+        factory-method="resourceScript"
+        c:_0="%{scriptPath}" />
+    
+</beans>
\ No newline at end of file
diff --git a/idp-attribute-resolver-spring/src/test/resources/net/shibboleth/idp/attribute/resolver/spring/dc/storage/storage-attribute-resolver-v2-hybrid.xml b/idp-attribute-resolver-spring/src/test/resources/net/shibboleth/idp/attribute/resolver/spring/dc/storage/storage-attribute-resolver-v2-hybrid.xml
new file mode 100644
index 000000000..b581d64c5
--- /dev/null
+++ b/idp-attribute-resolver-spring/src/test/resources/net/shibboleth/idp/attribute/resolver/spring/dc/storage/storage-attribute-resolver-v2-hybrid.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<AttributeResolver 
+            xmlns="urn:mace:shibboleth:2.0:resolver" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
+            xsi:schemaLocation="urn:mace:shibboleth:2.0:resolver http://shibboleth.net/schema/idp/shibboleth-attribute-resolver.xsd">
+
+    <DataConnector id="myStorage" xsi:type="StorageService"
+            storageServiceRef="myStorageService"
+            customObjectRef="CustomObject"
+            noResultIsError="%{missingerror:false}"
+            executableSearchBuilderRef="template"
+            mappingStrategyRef="mapping">
+
+        <ResultCacheBean>cache</ResultCacheBean>
+            
+    </DataConnector>
+    
+</AttributeResolver>
diff --git a/idp-attribute-resolver-spring/src/test/resources/net/shibboleth/idp/attribute/resolver/spring/dc/storage/storage-attribute-resolver-v2-simple.xml b/idp-attribute-resolver-spring/src/test/resources/net/shibboleth/idp/attribute/resolver/spring/dc/storage/storage-attribute-resolver-v2-simple.xml
new file mode 100644
index 000000000..20b5a604f
--- /dev/null
+++ b/idp-attribute-resolver-spring/src/test/resources/net/shibboleth/idp/attribute/resolver/spring/dc/storage/storage-attribute-resolver-v2-simple.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<AttributeResolver 
+            xmlns="urn:mace:shibboleth:2.0:resolver" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
+            xsi:schemaLocation="urn:mace:shibboleth:2.0:resolver http://shibboleth.net/schema/idp/shibboleth-attribute-resolver.xsd">
+
+    <DataConnector id="myStorage" xsi:type="StorageService"
+            storageServiceRef="myStorageService" generatedAttributeID="test">
+            
+        <ContextTemplate>foo</ContextTemplate>
+        <KeyTemplate>bar</KeyTemplate>
+        
+        <ResultCache expireAfterWrite="PT10S" />
+        
+    </DataConnector>
+    
+</AttributeResolver>
diff --git a/idp-attribute-resolver-spring/src/test/resources/net/shibboleth/idp/attribute/resolver/spring/dc/storage/storage-attribute-resolver-v2.xml b/idp-attribute-resolver-spring/src/test/resources/net/shibboleth/idp/attribute/resolver/spring/dc/storage/storage-attribute-resolver-v2.xml
new file mode 100644
index 000000000..4f66f564d
--- /dev/null
+++ b/idp-attribute-resolver-spring/src/test/resources/net/shibboleth/idp/attribute/resolver/spring/dc/storage/storage-attribute-resolver-v2.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<AttributeResolver 
+            xmlns="urn:mace:shibboleth:2.0:resolver" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
+            xsi:schemaLocation="urn:mace:shibboleth:2.0:resolver http://shibboleth.net/schema/idp/shibboleth-attribute-resolver.xsd">
+
+    <DataConnector id="myStorage" xsi:type="StorageService"
+            storageServiceRef="myStorageService"
+            customObjectRef="CustomObject"
+            noResultIsError="%{missingerror:false}">
+            
+        <ContextTemplate >%{context}</ContextTemplate>
+        <KeyTemplate>%{key}</KeyTemplate>
+        
+        <RecordMapping>
+            <ScriptFile>%{scriptPath}</ScriptFile>
+        </RecordMapping>
+        
+        <ResultCache expireAfterWrite="PT10S" />
+        
+    </DataConnector>
+    
+</AttributeResolver>
diff --git a/idp-schema/src/main/resources/schema/shibboleth-attribute-resolver.xsd b/idp-schema/src/main/resources/schema/shibboleth-attribute-resolver.xsd
index ae84ee70b..c6922b34b 100644
--- a/idp-schema/src/main/resources/schema/shibboleth-attribute-resolver.xsd
+++ b/idp-schema/src/main/resources/schema/shibboleth-attribute-resolver.xsd
@@ -1483,6 +1483,101 @@
         </complexContent>
     </complexType>
 
+    <complexType name="StorageService">
+        <annotation>
+            <documentation>
+                A data connector definition that queries for a record via the IdP's StorageService API.
+                Simple and scripted handling of records, typically in JSON, is the primary use case.
+            </documentation>
+        </annotation>
+        <complexContent>
+            <extension base="resolver:BaseDataConnectorType">
+                <choice maxOccurs="unbounded">
+                    <element ref="resolver:InputAttributeDefinition"/>
+                    <element ref="resolver:InputDataConnector"/>                                        
+                    <element ref="resolver:FailoverDataConnector"/>
+                    <element name="ContextTemplate" type="resolver:string">
+                        <annotation>
+                            <documentation>
+                                A template that will be used to create the context to query.
+                            </documentation>
+                        </annotation>
+                    </element>
+                    <element name="KeyTemplate" type="resolver:string">
+                        <annotation>
+                            <documentation>
+                                A template that will be used to create the key to query.
+                            </documentation>
+                        </annotation>
+                    </element>
+                    <element name="RecordMapping" type="resolver:ScriptType">
+                        <annotation>
+                            <documentation>
+                                Maps the record into attributes by means of scripting.
+                                Mutually exclusive with generatedAttributeID attribute.
+                            </documentation>
+                        </annotation>
+                    </element>
+                    <element name="ResultCache" type="resolver:CacheConfigType"/>
+                    <element name="ResultCacheBean" type="resolver:string"/>
+                </choice>
+                <attribute name="storageServiceRef" type="resolver:string" use="required">
+                    <annotation>
+                        <documentation>
+                            Reference to Spring bean of the StorageService to use.
+                        </documentation>
+                    </annotation>
+                </attribute>
+                <attribute name="customObjectRef" type="resolver:string">
+                    <annotation>
+                        <documentation>
+                        Injected object into Context/Key template building process.
+                        </documentation>
+                    </annotation>
+                </attribute>
+                <attribute name="generatedAttributeID" type="resolver:string">
+                    <annotation>
+                        <documentation>
+                            The name of the attribute produced by this data connector if simple
+                            record mapping is used. Mutually exclusive with ResponseMapping element.
+                        </documentation>
+                    </annotation>
+                </attribute>
+                <attribute name="noResultIsError" type="resolver:string">
+                    <annotation>
+                        <documentation>
+                            A boolean flag indicating whether a search returning no record should be considered an error. If
+                            an error is raised and a failover dependency is defined for this connector the failover will
+                            be invoked.
+                            Default value is false.
+                        </documentation>
+                    </annotation>
+                </attribute>
+                <attribute name="templateEngine" type="resolver:string">
+                    <annotation>
+                        <documentation>
+                            Name of the template engine defined within the application.
+                        </documentation>
+                    </annotation>
+                </attribute>
+                <attribute name="executableSearchBuilderRef" type="resolver:string">
+                    <annotation>
+                        <documentation>
+                            Reference to a Spring bean providing the ExecutableSearchBuilder implementation to use.
+                        </documentation>
+                    </annotation>
+                </attribute>
+                <attribute name="mappingStrategyRef" type="resolver:string">
+                    <annotation>
+                        <documentation>
+                            Reference to a Spring bean providing the MappingStrategy implementation to use.
+                        </documentation>
+                    </annotation>
+                </attribute>
+            </extension>
+        </complexContent>
+    </complexType>
+
     <complexType name="StoredId">
         <annotation>
             <documentation>

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


More information about the commits mailing list