[java-shib-attribute] 02/02: Add a Thymeleaf templated search executor (for LDAP)and wire up. Plus tests

Rod Widdowson rdw at steadingsoftware.com
Thu Jan 25 15:00:07 UTC 2024


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

rdw pushed a commit to branch dev/IDP-226
in repository java-shib-attribute.

View the commit online:
http://git.shibboleth.net/view/?p=java-shib-attribute.git;a=commit;h=115e1f3d75cd6b18d9772bdebf84900682aa3c68

commit 115e1f3d75cd6b18d9772bdebf84900682aa3c68
Author: Rod Widdowson <rdw at steadingsoftware.com>
AuthorDate: Thu Jan 25 14:59:34 2024 +0000

    Add a Thymeleaf templated search executor (for LDAP)and wire up. Plus tests
---
 shib-attribute-resolver-api/pom.xml                |   5 +
 .../ThymeleafExecutableSearchFilterBuilder.java    | 254 +++++++++++++++++++++
 .../resolver/dc/impl/ThymeLeafSandbox.java         |   6 -
 .../dc/ldap/impl/LDAPDataConnectorTest.java        |  68 +++++-
 .../dc/ldap/impl/LDAPDataConnectorParser.java      |  23 +-
 .../schema/shibboleth-attribute-resolver.xsd       |   5 +
 .../dc/ldap/impl/LDAPDataConnectorParserTest.java  |  23 ++
 .../ldap/resolver/ldap-attribute-resolver-tl.xml   |  95 ++++++++
 8 files changed, 464 insertions(+), 15 deletions(-)

diff --git a/shib-attribute-resolver-api/pom.xml b/shib-attribute-resolver-api/pom.xml
index c82f8dbf7..fd02ab9a2 100644
--- a/shib-attribute-resolver-api/pom.xml
+++ b/shib-attribute-resolver-api/pom.xml
@@ -50,6 +50,11 @@
             <artifactId>shib-service</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>org.thymeleaf</groupId>
+            <artifactId>thymeleaf</artifactId>
+        </dependency>
+
         <dependency>
             <groupId>${shib-shared.groupId}</groupId>
             <artifactId>shib-velocity</artifactId>
diff --git a/shib-attribute-resolver-api/src/main/java/net/shibboleth/idp/attribute/resolver/dc/ldap/ThymeleafExecutableSearchFilterBuilder.java b/shib-attribute-resolver-api/src/main/java/net/shibboleth/idp/attribute/resolver/dc/ldap/ThymeleafExecutableSearchFilterBuilder.java
new file mode 100644
index 000000000..7d8a154bb
--- /dev/null
+++ b/shib-attribute-resolver-api/src/main/java/net/shibboleth/idp/attribute/resolver/dc/ldap/ThymeleafExecutableSearchFilterBuilder.java
@@ -0,0 +1,254 @@
+/*
+ * Licensed 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.ldap;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.ldaptive.FilterTemplate;
+import org.slf4j.Logger;
+import org.thymeleaf.IEngineConfiguration;
+import org.thymeleaf.TemplateEngine;
+import org.thymeleaf.context.Context;
+import org.thymeleaf.context.ITemplateContext;
+import org.thymeleaf.dialect.AbstractProcessorDialect;
+import org.thymeleaf.exceptions.TemplateProcessingException;
+import org.thymeleaf.model.ITemplateEnd;
+import org.thymeleaf.model.ITemplateStart;
+import org.thymeleaf.processor.IProcessor;
+import org.thymeleaf.processor.templateboundaries.AbstractTemplateBoundariesProcessor;
+import org.thymeleaf.processor.templateboundaries.ITemplateBoundariesStructureHandler;
+import org.thymeleaf.standard.StandardDialect;
+import org.thymeleaf.standard.inline.AbstractStandardInliner;
+import org.thymeleaf.standard.processor.StandardInliningTextProcessor;
+import org.thymeleaf.templatemode.TemplateMode;
+import org.thymeleaf.templateresolver.StringTemplateResolver;
+
+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.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+import net.shibboleth.shared.velocity.ThymeleafTemplate;
+
+/**
+ * An {@link ExecutableSearchBuilder} that generates the search filter to
+ * be executed by evaluating a {@link Template} against the currently resolved attributes within a
+ * {@link AttributeResolutionContext}.
+ */
+public class ThymeleafExecutableSearchFilterBuilder extends AbstractExecutableSearchFilterBuilder {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(ThymeleafExecutableSearchFilterBuilder.class);
+
+    /** Template evaluated to generate a search filter. */
+    @NonnullAfterInit private ThymeleafTemplate template;
+
+    /** Template (as Text) to be evaluated. */
+    @NonnullAfterInit private String templateText;
+
+    /** ThymeLeaf Engine. */
+    @Nonnull static private TemplateEngine engine = createEngine();
+
+    /**
+     * Gets the template to be evaluated.
+     * 
+     * @return the template
+     */
+    @NonnullAfterInit public ThymeleafTemplate getTemplate() {
+        return template;
+    }
+
+    /**
+     * Gets the template text to be evaluated.
+     * 
+     * @return the template text
+     */
+    @NonnullAfterInit public String getTemplateText() {
+        return templateText;
+    }
+
+    /**
+     * Sets the template to be evaluated.
+     * 
+     * @param velocityTemplate template to be evaluated
+     */
+    public void setTemplateText(@Nullable final String velocityTemplate) {
+        checkSetterPreconditions();
+
+        templateText = StringSupport.trimOrNull(velocityTemplate);
+    }
+
+    /** Method to allow private additions to the velocity context.
+     * @param velocityContext where to add the information
+     * @param resolutionContext current resolution context
+     *
+    protected void addExtraVelocityContext(@Nonnull final IContext velocityContext,
+            @Nonnull final AttributeResolutionContext resolutionContext) {
+    }
+    
+    /** {@inheritDoc} */
+    @Override protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+
+        final String localTemplateText = templateText;
+        if (null == localTemplateText) {
+            throw new ComponentInitializationException(
+                    "TemplatedExecutableStatementBuilder: template text must be non null");
+        }
+
+        template = ThymeleafTemplate.fromTemplate(engine, localTemplateText);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nonnull public ExecutableSearchFilter build(@Nonnull final AttributeResolutionContext resolutionContext,
+            @Nonnull final Map<String, List<IdPAttributeValue>> dependencyAttributes) throws ResolutionException {
+
+        final Context context = new Context();
+        log.trace("Creating search filter using attribute resolution context {}", resolutionContext);
+        context.setVariable("resolutionContext", resolutionContext);
+
+        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.setVariable(entry.getKey(), values);
+            }
+        }
+
+        try {
+            final FilterTemplate searchFilter = new FilterTemplate(merge(context));
+            return super.build(searchFilter);
+        } catch (final Exception e) {
+            log.error("Error running template: {}", e.getMessage());
+            throw new ResolutionException("Error running template", e);
+        }
+    }
+
+    /**
+     * Invokes {@link Template#merge(org.apache.velocity.context.Context)} on the supplied context.
+     * 
+     * @param context to merge
+     * 
+     * @return result of the merge operation
+     */
+    @Nonnull protected String merge(@Nonnull final Context context) {
+        final String result = template.merge(context).trim();
+        log.debug("Template text {} yields {}", templateText, result);
+        assert result != null;
+        return result;
+    }
+   
+    /** Create the one-off template engine  `used for 
+     * @return
+     */
+    private static TemplateEngine createEngine() {
+        final TemplateEngine engine = new TemplateEngine();
+        engine.setAdditionalDialects(CollectionSupport.singleton(new LdaptiveDialect()));
+        engine.setTemplateResolver(new StringTemplateResolver());
+        return engine;
+    }
+
+    /* 
+     * Classes used to create the engine.  This is an API module so we keep the classes inline and private (need exbedded into shib-shared)
+     */
+    
+    public static final class LdaptiveTemplateBoundariesProcessor extends AbstractTemplateBoundariesProcessor {
+
+        public static final int PRECEDENCE = 11;
+
+        public LdaptiveTemplateBoundariesProcessor(final TemplateMode templateMode) {
+            super(templateMode, PRECEDENCE);
+        }
+
+
+        @Override
+        public void doProcessTemplateStart(
+                final ITemplateContext context,
+                final ITemplateStart templateStart, final ITemplateBoundariesStructureHandler structureHandler) {
+
+            switch (getTemplateMode()) {
+
+            case TEXT:
+                structureHandler.setInliner(new LdaptiveInliner(context.getConfiguration()));
+                break;
+
+                default:
+                    throw new TemplateProcessingException(
+                            "Unrecognized template mode: " + getTemplateMode() + ", cannot initialize inlining!");
+
+            }
+
+        }
+
+
+        @Override
+        public void doProcessTemplateEnd(
+                final ITemplateContext context,
+                final ITemplateEnd templateEnd, final ITemplateBoundariesStructureHandler structureHandler) {
+
+            // Empty - nothing to be done on template end
+
+        }
+    }
+    
+    private static class LdaptiveInliner extends AbstractStandardInliner {
+
+        protected LdaptiveInliner(IEngineConfiguration configuration) {
+            super(configuration, TemplateMode.TEXT);
+        }
+
+        @Override
+        protected String produceEscapedOutput(Object value) {
+            if (value instanceof String str){ 
+                return FilterTemplate.encodeValue(str);
+            } else if (value instanceof byte[] array) {
+                return FilterTemplate.encodeValue((byte[]) array);
+            }
+            return value.toString();
+        }
+        
+    }
+
+    private static class LdaptiveDialect extends AbstractProcessorDialect {
+
+        private static final String DIALECT_NAME = "SANDBOX Dialect";
+
+
+        public LdaptiveDialect() {
+            super(DIALECT_NAME, "sandbox", StandardDialect.PROCESSOR_PRECEDENCE);
+        }
+
+        public Set<IProcessor> getProcessors(final String dialectPrefix) {
+            return CollectionSupport.setOf(
+                    new StandardInliningTextProcessor(TemplateMode.TEXT),
+                    new LdaptiveTemplateBoundariesProcessor(TemplateMode.TEXT));
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/shib-attribute-resolver-impl/src/test/java/net/shibboleth/idp/attribute/resolver/dc/impl/ThymeLeafSandbox.java b/shib-attribute-resolver-impl/src/test/java/net/shibboleth/idp/attribute/resolver/dc/impl/ThymeLeafSandbox.java
index c62a8fda5..b494598b3 100644
--- a/shib-attribute-resolver-impl/src/test/java/net/shibboleth/idp/attribute/resolver/dc/impl/ThymeLeafSandbox.java
+++ b/shib-attribute-resolver-impl/src/test/java/net/shibboleth/idp/attribute/resolver/dc/impl/ThymeLeafSandbox.java
@@ -25,18 +25,12 @@ import org.thymeleaf.TemplateSpec;
 import org.thymeleaf.context.Context;
 import org.thymeleaf.context.ITemplateContext;
 import org.thymeleaf.dialect.AbstractProcessorDialect;
-import org.thymeleaf.engine.EngineEventUtils;
 import org.thymeleaf.exceptions.TemplateProcessingException;
-import org.thymeleaf.inline.IInliner;
-import org.thymeleaf.inline.NoOpInliner;
 import org.thymeleaf.model.ITemplateEnd;
 import org.thymeleaf.model.ITemplateStart;
-import org.thymeleaf.model.IText;
 import org.thymeleaf.processor.IProcessor;
 import org.thymeleaf.processor.templateboundaries.AbstractTemplateBoundariesProcessor;
 import org.thymeleaf.processor.templateboundaries.ITemplateBoundariesStructureHandler;
-import org.thymeleaf.processor.text.AbstractTextProcessor;
-import org.thymeleaf.processor.text.ITextStructureHandler;
 import org.thymeleaf.standard.StandardDialect;
 import org.thymeleaf.standard.inline.AbstractStandardInliner;
 import org.thymeleaf.standard.processor.StandardInliningTextProcessor;
diff --git a/shib-attribute-resolver-impl/src/test/java/net/shibboleth/idp/attribute/resolver/dc/ldap/impl/LDAPDataConnectorTest.java b/shib-attribute-resolver-impl/src/test/java/net/shibboleth/idp/attribute/resolver/dc/ldap/impl/LDAPDataConnectorTest.java
index 827a0ba50..ea7ae5c27 100644
--- a/shib-attribute-resolver-impl/src/test/java/net/shibboleth/idp/attribute/resolver/dc/ldap/impl/LDAPDataConnectorTest.java
+++ b/shib-attribute-resolver-impl/src/test/java/net/shibboleth/idp/attribute/resolver/dc/ldap/impl/LDAPDataConnectorTest.java
@@ -49,6 +49,7 @@ import net.shibboleth.idp.attribute.resolver.dc.ldap.ParameterizedExecutableSear
 import net.shibboleth.idp.attribute.resolver.dc.ldap.SearchResultMappingStrategy;
 import net.shibboleth.idp.attribute.resolver.dc.ldap.StringAttributeValueMappingStrategy;
 import net.shibboleth.idp.attribute.resolver.dc.ldap.TemplatedExecutableSearchFilterBuilder;
+import net.shibboleth.idp.attribute.resolver.dc.ldap.ThymeleafExecutableSearchFilterBuilder;
 import net.shibboleth.idp.attribute.resolver.testing.TestSources;
 import net.shibboleth.shared.collection.CollectionSupport;
 import net.shibboleth.shared.component.ComponentInitializationException;
@@ -268,6 +269,13 @@ public class LDAPDataConnectorTest {
         assertEquals(filter.getResultCacheKey(), "(cn=domain\\5Cuser\\2A)");
     }
 
+    @Test public void resolveTemplateThymeleaf() throws ComponentInitializationException, ResolutionException {
+        final ThymeleafExecutableSearchFilterBuilder builder = new ThymeleafExecutableSearchFilterBuilder();
+        builder.setTemplateText("(uid=[[${resolutionContext.principal}]])");
+        builder.initialize();
+        resolve(builder);
+    }
+
     @Test public void resolveTemplate() throws ComponentInitializationException, ResolutionException {
         final TemplatedExecutableSearchFilterBuilder builder = new TemplatedExecutableSearchFilterBuilder();
         builder.setTemplateText("(uid=${resolutionContext.principal})");
@@ -275,10 +283,11 @@ public class LDAPDataConnectorTest {
         builder.initialize();
         resolve(builder);
     }
+
     
     @Test(expectedExceptions={ResolutionException.class}) public void resolveTemplateExcept() throws ComponentInitializationException, ResolutionException {
         final TemplatedExecutableSearchFilterBuilder builder = new TemplatedExecutableSearchFilterBuilder();
-        builder.setTemplateText("(uid=${resolutionContext.AttributeRecipientID.toString().substring(99, 106)})");
+        builder.setTemplateText("(uid=[[${resolutionContext.AttributeRecipientID.toString().substring(99, 106)}]])");
         final Properties props = new Properties();
         // TODO, should expose a way to set the strict prop underneath
         props.setProperty(RuntimeConstants.RUNTIME_REFERENCES_STRICT, "true");
@@ -289,6 +298,29 @@ public class LDAPDataConnectorTest {
         resolve(builder);
     }
 
+    @Test(enabled=false, expectedExceptions={ResolutionException.class}) public void resolveTemplateExceptThymeleaf() throws ComponentInitializationException, ResolutionException {
+        final ThymeleafExecutableSearchFilterBuilder builder = new ThymeleafExecutableSearchFilterBuilder();
+        builder.setTemplateText("(uid=[[${resolutionContext.AttributeRecipientID.toString().substring(99, 106)}]])");
+        fail("What do the proerties do for Velocity in the above");
+        builder.initialize();
+        resolve(builder);
+    }
+
+    @Test public void resolveTemplateWithDependsThymeleaf() throws ComponentInitializationException, ResolutionException {
+        final ThymeleafExecutableSearchFilterBuilder builder = new ThymeleafExecutableSearchFilterBuilder();
+        builder.setTemplateText("(&(cn=[[${resolutionContext.principal}]])(eduPersonAffiliation=[[${affiliation[0]}]]))");
+        builder.initialize();
+        final AttributeResolutionContext context =
+                TestSources.createResolutionContext(TestSources.PRINCIPAL_ID, TestSources.IDP_ENTITY_ID,
+                        TestSources.SP_ENTITY_ID);
+        final Map<String, List<IdPAttributeValue>> dependsAttributes = new HashMap<>();
+        final List<IdPAttributeValue> attributeValues = new ArrayList<>();
+        attributeValues.add(new StringAttributeValue("student"));
+        dependsAttributes.put("affiliation", attributeValues);
+        final ExecutableSearchFilter filter = builder.build(context, dependsAttributes);
+        assertEquals(filter.getSearchFilter().format(), "(&(cn=PETER_THE_PRINCIPAL)(eduPersonAffiliation=student))");
+        assertEquals(filter.getResultCacheKey(), "(&(cn=PETER_THE_PRINCIPAL)(eduPersonAffiliation=student))");
+    }
 
     @Test public void resolveTemplateWithDepends() throws ComponentInitializationException, ResolutionException {
         final TemplatedExecutableSearchFilterBuilder builder = new TemplatedExecutableSearchFilterBuilder();
@@ -307,6 +339,28 @@ public class LDAPDataConnectorTest {
         assertEquals(filter.getResultCacheKey(), "(&(cn=PETER_THE_PRINCIPAL)(eduPersonAffiliation=student))");
     }
 
+    @Test public void resolveTemplateWithMultiValueDependsThymeleaf() throws ComponentInitializationException, ResolutionException {
+        final ThymeleafExecutableSearchFilterBuilder builder = new ThymeleafExecutableSearchFilterBuilder();
+        builder.setTemplateText(
+                "(&(cn=[[${resolutionContext.principal}]])(eduPersonEntitlement=[[${entitlement[0]}]])(eduPersonEntitlement=[[${entitlement[1]}]]))");
+        builder.initialize();
+        final AttributeResolutionContext context =
+                TestSources.createResolutionContext(TestSources.PRINCIPAL_ID, TestSources.IDP_ENTITY_ID,
+                        TestSources.SP_ENTITY_ID);
+        final Map<String, List<IdPAttributeValue>> dependsAttributes = new HashMap<>();
+        final List<IdPAttributeValue> attributeValues = new ArrayList<>();
+        attributeValues.add(new StringAttributeValue("entitlement1"));
+        attributeValues.add(new StringAttributeValue("entitlement*"));
+        dependsAttributes.put("entitlement", attributeValues);
+        final ExecutableSearchFilter filter = builder.build(context, dependsAttributes);
+        assertEquals(
+                filter.getSearchFilter().format(),
+                "(&(cn=PETER_THE_PRINCIPAL)(eduPersonEntitlement=entitlement1)(eduPersonEntitlement=entitlement\\2A))");
+        assertEquals(
+                filter.getResultCacheKey(),
+                "(&(cn=PETER_THE_PRINCIPAL)(eduPersonEntitlement=entitlement1)(eduPersonEntitlement=entitlement\\2A))");
+    }
+
     @Test public void resolveTemplateWithMultiValueDepends() throws ComponentInitializationException, ResolutionException {
         final TemplatedExecutableSearchFilterBuilder builder = new TemplatedExecutableSearchFilterBuilder();
         builder.setTemplateText(
@@ -329,7 +383,17 @@ public class LDAPDataConnectorTest {
                 filter.getResultCacheKey(),
                 "(&(cn=PETER_THE_PRINCIPAL)(eduPersonEntitlement=entitlement1)(eduPersonEntitlement=entitlement\\2A))");
     }
-
+    @Test public void escapeTemplateThymeleaf() throws ComponentInitializationException, ResolutionException {
+        final ThymeleafExecutableSearchFilterBuilder builder = new ThymeleafExecutableSearchFilterBuilder();
+        builder.setTemplateText("(cn=[[${resolutionContext.principal}]])");
+        builder.initialize();
+        final AttributeResolutionContext context =
+                TestSources.createResolutionContext("domain\\user*", TestSources.IDP_ENTITY_ID,
+                        TestSources.SP_ENTITY_ID);
+        final ExecutableSearchFilter filter = builder.build(context, CollectionSupport.emptyMap());
+        assertEquals(filter.getSearchFilter().format(), "(cn=domain\\5Cuser\\2A)");
+        assertEquals(filter.getResultCacheKey(), "(cn=domain\\5Cuser\\2A)");
+    }
     @Test public void escapeTemplate() throws ComponentInitializationException, ResolutionException {
         final TemplatedExecutableSearchFilterBuilder builder = new TemplatedExecutableSearchFilterBuilder();
         builder.setTemplateText("(cn=${resolutionContext.principal})");
diff --git a/shib-attribute-resolver-spring/src/main/java/net/shibboleth/idp/attribute/resolver/spring/dc/ldap/impl/LDAPDataConnectorParser.java b/shib-attribute-resolver-spring/src/main/java/net/shibboleth/idp/attribute/resolver/spring/dc/ldap/impl/LDAPDataConnectorParser.java
index b644371a1..67da92dee 100644
--- a/shib-attribute-resolver-spring/src/main/java/net/shibboleth/idp/attribute/resolver/spring/dc/ldap/impl/LDAPDataConnectorParser.java
+++ b/shib-attribute-resolver-spring/src/main/java/net/shibboleth/idp/attribute/resolver/spring/dc/ldap/impl/LDAPDataConnectorParser.java
@@ -58,6 +58,7 @@ import org.w3c.dom.Element;
 
 import net.shibboleth.idp.attribute.resolver.dc.ldap.StringAttributeValueMappingStrategy;
 import net.shibboleth.idp.attribute.resolver.dc.ldap.TemplatedExecutableSearchFilterBuilder;
+import net.shibboleth.idp.attribute.resolver.dc.ldap.ThymeleafExecutableSearchFilterBuilder;
 import net.shibboleth.idp.attribute.resolver.dc.ldap.impl.ConnectionFactoryValidator;
 import net.shibboleth.idp.attribute.resolver.dc.ldap.impl.LDAPDataConnector;
 import net.shibboleth.idp.attribute.resolver.spring.dc.AbstractDataConnectorParser;
@@ -451,15 +452,23 @@ public class LDAPDataConnectorParser extends AbstractDataConnectorParser {
          * @return the bean definition for the template search builder.
          */
         @Nonnull public BeanDefinition createTemplateBuilder() {
-            final BeanDefinitionBuilder templateBuilder = BeanDefinitionBuilder.genericBeanDefinition(
-                    TemplatedExecutableSearchFilterBuilder.class);
-            templateBuilder.setInitMethodName("initialize");
-
+            final String useThymeleafAttr =  StringSupport.trimOrNull(configElement.getAttribute("useThymeleaf"));
+            boolean useTymeleaf = useThymeleafAttr  == null ? false : Boolean.parseBoolean(useThymeleafAttr);
             String velocityEngineRef = StringSupport.trimOrNull(configElement.getAttribute("templateEngine"));
-            if (null == velocityEngineRef) {
-                velocityEngineRef = "shibboleth.VelocityEngine";
+            final BeanDefinitionBuilder templateBuilder;
+            if (useTymeleaf) {
+                templateBuilder = BeanDefinitionBuilder.genericBeanDefinition(ThymeleafExecutableSearchFilterBuilder.class);
+                if (velocityEngineRef != null) {
+                    LOG.error("templateEngine is not valif for Thymeleaf scripting");
+                }
+            } else {
+                templateBuilder = BeanDefinitionBuilder.genericBeanDefinition(TemplatedExecutableSearchFilterBuilder.class);
+                if (null == velocityEngineRef) {
+                    velocityEngineRef = "shibboleth.VelocityEngine";
+                }
+                templateBuilder.addPropertyReference("velocityEngine", velocityEngineRef);
             }
-            templateBuilder.addPropertyReference("velocityEngine", velocityEngineRef);
+            templateBuilder.setInitMethodName("initialize");
 
             templateBuilder.addPropertyValue("templateText", getFilterText());
 
diff --git a/shib-attribute-resolver-spring/src/main/resources/schema/shibboleth-attribute-resolver.xsd b/shib-attribute-resolver-spring/src/main/resources/schema/shibboleth-attribute-resolver.xsd
index cca79e74f..594a42ca0 100644
--- a/shib-attribute-resolver-spring/src/main/resources/schema/shibboleth-attribute-resolver.xsd
+++ b/shib-attribute-resolver-spring/src/main/resources/schema/shibboleth-attribute-resolver.xsd
@@ -917,6 +917,11 @@
                         </documentation>
                     </annotation>
                 </attribute>
+                <attribute name="useThymeleaf" type="string">
+                    <annotation>
+                        <documentation>Do we use Velocity or Thymeleaf</documentation>
+                    </annotation>
+                </attribute>
                 <attribute name="useStartTLS" type="resolver:string">
                     <annotation>
                         <documentation>
diff --git a/shib-attribute-resolver-spring/src/test/java/net/shibboleth/idp/attribute/resolver/spring/dc/ldap/impl/LDAPDataConnectorParserTest.java b/shib-attribute-resolver-spring/src/test/java/net/shibboleth/idp/attribute/resolver/spring/dc/ldap/impl/LDAPDataConnectorParserTest.java
index 37c39852a..a58fcdbf1 100644
--- a/shib-attribute-resolver-spring/src/test/java/net/shibboleth/idp/attribute/resolver/spring/dc/ldap/impl/LDAPDataConnectorParserTest.java
+++ b/shib-attribute-resolver-spring/src/test/java/net/shibboleth/idp/attribute/resolver/spring/dc/ldap/impl/LDAPDataConnectorParserTest.java
@@ -166,6 +166,29 @@ public class LDAPDataConnectorParserTest {
         assertEquals(attrs.get("employeeNumber").getValues().get(0).getNativeValue(), "C2J20hMNp7NlUwQ+");
         assertNotNull(attrs.get("entryDN"));
     }
+
+    @Test public void tlConfig() throws Exception {
+        final LDAPDataConnector dataConnector =
+                getLdapDataConnector(new String[] {"net/shibboleth/idp/attribute/resolver/spring/dc/ldap/resolver/ldap-attribute-resolver-tl.xml"});
+        assertNotNull(dataConnector);
+        assertFalse(dataConnector.isFailFastInitialize());
+        doTest(dataConnector);
+        final StringAttributeValueMappingStrategy mappingStrategy =
+                (StringAttributeValueMappingStrategy) dataConnector.getMappingStrategy();
+        assertEquals(mappingStrategy.getResultRenamingMap().size(), 1);
+        assertEquals(mappingStrategy.getResultRenamingMap().get("homephone"), "phonenumber");
+
+        dataConnector.initialize();
+        final AttributeResolutionContext context =
+                TestSources.createResolutionContext(TestSources.PRINCIPAL_ID, TestSources.IDP_ENTITY_ID,
+                        TestSources.SP_ENTITY_ID);
+        final Map<String, IdPAttribute> attrs = dataConnector.resolve(context);
+        assert attrs != null;
+        assertEquals(2, attrs.size());
+        assertEquals(attrs.get("employeeNumber").getValues().get(0).getNativeValue(), "C2J20hMNp7NlUwQ+");
+        assertNotNull(attrs.get("entryDN"));
+    }
+
     
     @Test public void v2NoSec() throws Exception {
         final LDAPDataConnector dataConnector =
diff --git a/shib-attribute-resolver-spring/src/test/resources/net/shibboleth/idp/attribute/resolver/spring/dc/ldap/resolver/ldap-attribute-resolver-tl.xml b/shib-attribute-resolver-spring/src/test/resources/net/shibboleth/idp/attribute/resolver/spring/dc/ldap/resolver/ldap-attribute-resolver-tl.xml
new file mode 100644
index 000000000..398378d1f
--- /dev/null
+++ b/shib-attribute-resolver-spring/src/test/resources/net/shibboleth/idp/attribute/resolver/spring/dc/ldap/resolver/ldap-attribute-resolver-tl.xml
@@ -0,0 +1,95 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<AttributeResolver xmlns="urn:mace:shibboleth:2.0:resolver" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:sec="urn:mace:shibboleth:2.0:security"
+        xsi:schemaLocation="urn:mace:shibboleth:2.0:resolver http://shibboleth.net/schema/idp/shibboleth-attribute-resolver.xsd
+                            urn:mace:shibboleth:2.0:security http://shibboleth.net/schema/idp/shibboleth-security.xsd">
+
+    <DataConnector id="myLDAP" xsi:type="LDAPDirectory"
+        ldapURL="ldap://localhost:10389" 
+        baseDN="ou=people,dc=shibboleth,dc=net" 
+        principal="cn=Directory Manager"
+        principalCredential="password"
+        failFastInitialize="false"
+        useStartTLS="true"
+        startTLSTimeout="PT2S"
+        useThymeleaf="true"
+        searchTimeLimit="PT7S"
+        noResultIsError="true"
+        multipleResultsIsError="true"
+        noRetryDelay="PT5M"
+        trustFile="net/shibboleth/idp/attribute/resolver/spring/dc/ldap/resolver/ldap.crt"
+        connectTimeout="PT2S"
+        responseTimeout="PT4S">
+        
+        <FailoverDataConnector ref="2123"/>
+        
+        <ResultCache expireAfterAccess="PT10S" maximumCachedElements="25"/>
+
+        <Column columnName="homephone" attributeID="phonenumber" />
+
+        <ConnectionPool
+            minPoolSize="5"
+            maxPoolSize="10"
+            blockWaitTime="PT5S"
+            expirationTime="PT10M"
+            validatePeriodically="true"
+            validateTimerPeriod="PT15M"
+            validateDN="dc=shibboleth,dc=net"
+            validateFilter="(ou=people)" />
+
+
+        <StartTLSAuthenticationCredential xsi:type="sec:X509ResourceBacked" id="IdPtoLDAPCredential">
+            <sec:PrivateKey>src/test/resources/net/shibboleth/idp/attribute/resolver/spring/dc/ldap/client.key</sec:PrivateKey>
+            <sec:Certificate>src/test/resources/net/shibboleth/idp/attribute/resolver/spring/dc/ldap/client.crt</sec:Certificate>
+        </StartTLSAuthenticationCredential>
+
+        <StartTLSAuthenticationCredential xsi:type="sec:X509ResourceBacked" id="IdPtoLDAPCredential">
+            <sec:PrivateKey>src/test/resources/net/shibboleth/idp/attribute/resolver/spring/dc/ldap/client.key</sec:PrivateKey>
+            <sec:Certificate>src/test/resources/net/shibboleth/idp/attribute/resolver/spring/dc/ldap/client.crt</sec:Certificate>
+        </StartTLSAuthenticationCredential>
+
+        <StartTLSTrustCredential xsi:type="sec:X509ResourceBacked" id="IdPtoLDAPCredential">
+            <sec:PrivateKey>src/test/resources/net/shibboleth/idp/attribute/resolver/spring/dc/ldap/client.key</sec:PrivateKey>
+            <sec:Certificate>src/test/resources/net/shibboleth/idp/attribute/resolver/spring/dc/ldap/client.crt</sec:Certificate>
+        </StartTLSTrustCredential>
+
+        <StartTLSTrustCredential xsi:type="sec:X509ResourceBacked" id="IdPtoLDAPCredential">
+            <sec:PrivateKey>src/test/resources/net/shibboleth/idp/attribute/resolver/spring/dc/ldap/client.key</sec:PrivateKey>
+            <sec:Certificate>src/test/resources/net/shibboleth/idp/attribute/resolver/spring/dc/ldap/client.crt</sec:Certificate>
+        </StartTLSTrustCredential>
+
+        <InputAttributeDefinition ref="321"/>
+
+        <ReturnAttributes>a b g t employeeNumber</ReturnAttributes>
+        
+        <ReturnAttributes>a b g t employeeNumber</ReturnAttributes>
+
+        <BinaryAttributes>g employeeNumber</BinaryAttributes>
+
+        <FilterTemplate>
+            <![CDATA[
+                (uid=[[${resolutionContext.principal}]])
+            ]]>
+        </FilterTemplate>
+        
+        <ResultCache maximumCachedElements="20"/>
+        
+        <FilterTemplate>
+            <![CDATA[
+                (uid=[[${resolutionContext.principal}]]
+                )
+            ]]>
+        </FilterTemplate>
+        
+        <ConnectionPool
+            minPoolSize="5"
+            maxPoolSize="10"
+            blockWaitTime="PT5S"
+            expirationTime="PT10M"
+            validatePeriodically="true"
+            validateTimerPeriod="PT15M"
+            validateDN="dc=shibboleth,dc=net"
+            validateFilter="(ou=people)"/>
+        
+    </DataConnector>
+    
+</AttributeResolver>

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


More information about the commits mailing list