You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by pv...@apache.org on 2017/06/19 17:25:52 UTC

[1/2] nifi git commit: NIFI-4059: - Introducing the LdapUserGroupProvider. - Updating documentation accordingly. - Moving the IdentityMapping utilities so they were accessible.

Repository: nifi
Updated Branches:
  refs/heads/master ad6af1d94 -> 6bc6f955c


http://git-wip-us.apache.org/repos/asf/nifi/blob/6bc6f955/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/src/main/java/org/apache/nifi/ldap/tenants/TenantHolder.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/src/main/java/org/apache/nifi/ldap/tenants/TenantHolder.java b/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/src/main/java/org/apache/nifi/ldap/tenants/TenantHolder.java
new file mode 100644
index 0000000..2c0680e
--- /dev/null
+++ b/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/src/main/java/org/apache/nifi/ldap/tenants/TenantHolder.java
@@ -0,0 +1,165 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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 org.apache.nifi.ldap.tenants;
+
+
+import org.apache.nifi.authorization.Group;
+import org.apache.nifi.authorization.User;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A holder to provide atomic access to user group data structures.
+ */
+public class TenantHolder {
+
+    private final Set<User> allUsers;
+    private final Map<String,User> usersById;
+    private final Map<String,User> usersByIdentity;
+
+    private final Set<Group> allGroups;
+    private final Map<String,Group> groupsById;
+    private final Map<String, Set<Group>> groupsByUserIdentity;
+
+    /**
+     * Creates a new holder and populates all convenience data structures.
+     */
+    public TenantHolder(final Set<User> allUsers, final Set<Group> allGroups) {
+        // create a convenience map to retrieve a user by id
+        final Map<String, User> userByIdMap = Collections.unmodifiableMap(createUserByIdMap(allUsers));
+
+        // create a convenience map to retrieve a user by identity
+        final Map<String, User> userByIdentityMap = Collections.unmodifiableMap(createUserByIdentityMap(allUsers));
+
+        // create a convenience map to retrieve a group by id
+        final Map<String, Group> groupByIdMap = Collections.unmodifiableMap(createGroupByIdMap(allGroups));
+
+        // create a convenience map to retrieve the groups for a user identity
+        final Map<String, Set<Group>> groupsByUserIdentityMap = Collections.unmodifiableMap(createGroupsByUserIdentityMap(allGroups, allUsers));
+
+        // set all the holders
+        this.allUsers = allUsers;
+        this.allGroups = allGroups;
+        this.usersById = userByIdMap;
+        this.usersByIdentity = userByIdentityMap;
+        this.groupsById = groupByIdMap;
+        this.groupsByUserIdentity = groupsByUserIdentityMap;
+    }
+
+    /**
+     * Creates a Map from user identifier to User.
+     *
+     * @param users the set of all users
+     * @return the Map from user identifier to User
+     */
+    private Map<String,User> createUserByIdMap(final Set<User> users) {
+        Map<String,User> usersMap = new HashMap<>();
+        for (User user : users) {
+            usersMap.put(user.getIdentifier(), user);
+        }
+        return usersMap;
+    }
+
+    /**
+     * Creates a Map from user identity to User.
+     *
+     * @param users the set of all users
+     * @return the Map from user identity to User
+     */
+    private Map<String,User> createUserByIdentityMap(final Set<User> users) {
+        Map<String,User> usersMap = new HashMap<>();
+        for (User user : users) {
+            usersMap.put(user.getIdentity(), user);
+        }
+        return usersMap;
+    }
+
+    /**
+     * Creates a Map from group identifier to Group.
+     *
+     * @param groups the set of all groups
+     * @return the Map from group identifier to Group
+     */
+    private Map<String,Group> createGroupByIdMap(final Set<Group> groups) {
+        Map<String,Group> groupsMap = new HashMap<>();
+        for (Group group : groups) {
+            groupsMap.put(group.getIdentifier(), group);
+        }
+        return groupsMap;
+    }
+
+    /**
+     * Creates a Map from user identity to the set of Groups for that identity.
+     *
+     * @param groups all groups
+     * @param users all users
+     * @return a Map from User identity to the set of Groups for that identity
+     */
+    private Map<String, Set<Group>> createGroupsByUserIdentityMap(final Set<Group> groups, final Set<User> users) {
+        Map<String, Set<Group>> groupsByUserIdentity = new HashMap<>();
+
+        for (User user : users) {
+            Set<Group> userGroups = new HashSet<>();
+            for (Group group : groups) {
+                for (String groupUser : group.getUsers()) {
+                    if (groupUser.equals(user.getIdentifier())) {
+                        userGroups.add(group);
+                    }
+                }
+            }
+
+            groupsByUserIdentity.put(user.getIdentity(), userGroups);
+        }
+
+        return groupsByUserIdentity;
+    }
+
+    public Set<User> getAllUsers() {
+        return allUsers;
+    }
+
+    public Map<String, User> getUsersById() {
+        return usersById;
+    }
+
+    public Set<Group> getAllGroups() {
+        return allGroups;
+    }
+
+    public Map<String, Group> getGroupsById() {
+        return groupsById;
+    }
+
+    public User getUser(String identity) {
+        if (identity == null) {
+            throw new IllegalArgumentException("Identity cannot be null");
+        }
+        return usersByIdentity.get(identity);
+    }
+
+    public Set<Group> getGroups(String userIdentity) {
+        if (userIdentity == null) {
+            throw new IllegalArgumentException("User Identity cannot be null");
+        }
+        return groupsByUserIdentity.get(userIdentity);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6bc6f955/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/src/main/resources/META-INF/services/org.apache.nifi.authorization.UserGroupProvider
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/src/main/resources/META-INF/services/org.apache.nifi.authorization.UserGroupProvider b/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/src/main/resources/META-INF/services/org.apache.nifi.authorization.UserGroupProvider
new file mode 100755
index 0000000..1e80cc2
--- /dev/null
+++ b/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/src/main/resources/META-INF/services/org.apache.nifi.authorization.UserGroupProvider
@@ -0,0 +1,15 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF 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.
+org.apache.nifi.ldap.tenants.LdapUserGroupProvider

http://git-wip-us.apache.org/repos/asf/nifi/blob/6bc6f955/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/src/test/java/org/apache/nifi/ldap/tenants/LdapUserGroupProviderTest.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/src/test/java/org/apache/nifi/ldap/tenants/LdapUserGroupProviderTest.java b/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/src/test/java/org/apache/nifi/ldap/tenants/LdapUserGroupProviderTest.java
new file mode 100644
index 0000000..f2cc28e
--- /dev/null
+++ b/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/src/test/java/org/apache/nifi/ldap/tenants/LdapUserGroupProviderTest.java
@@ -0,0 +1,552 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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 org.apache.nifi.ldap.tenants;
+
+import org.apache.directory.server.annotations.CreateLdapServer;
+import org.apache.directory.server.annotations.CreateTransport;
+import org.apache.directory.server.core.annotations.ApplyLdifFiles;
+import org.apache.directory.server.core.annotations.CreateDS;
+import org.apache.directory.server.core.annotations.CreatePartition;
+import org.apache.directory.server.core.integ.AbstractLdapTestUnit;
+import org.apache.directory.server.core.integ.FrameworkRunner;
+import org.apache.nifi.attribute.expression.language.StandardPropertyValue;
+import org.apache.nifi.authorization.AuthorizerConfigurationContext;
+import org.apache.nifi.authorization.Group;
+import org.apache.nifi.authorization.UserAndGroups;
+import org.apache.nifi.authorization.UserGroupProviderInitializationContext;
+import org.apache.nifi.authorization.exception.AuthorizerCreationException;
+import org.apache.nifi.ldap.LdapAuthenticationStrategy;
+import org.apache.nifi.ldap.ReferralStrategy;
+import org.apache.nifi.util.NiFiProperties;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.util.Properties;
+import java.util.Set;
+
+import static org.apache.nifi.ldap.tenants.LdapUserGroupProvider.PROP_AUTHENTICATION_STRATEGY;
+import static org.apache.nifi.ldap.tenants.LdapUserGroupProvider.PROP_CONNECT_TIMEOUT;
+import static org.apache.nifi.ldap.tenants.LdapUserGroupProvider.PROP_GROUP_MEMBER_ATTRIBUTE;
+import static org.apache.nifi.ldap.tenants.LdapUserGroupProvider.PROP_GROUP_NAME_ATTRIBUTE;
+import static org.apache.nifi.ldap.tenants.LdapUserGroupProvider.PROP_GROUP_OBJECT_CLASS;
+import static org.apache.nifi.ldap.tenants.LdapUserGroupProvider.PROP_GROUP_SEARCH_BASE;
+import static org.apache.nifi.ldap.tenants.LdapUserGroupProvider.PROP_GROUP_SEARCH_FILTER;
+import static org.apache.nifi.ldap.tenants.LdapUserGroupProvider.PROP_GROUP_SEARCH_SCOPE;
+import static org.apache.nifi.ldap.tenants.LdapUserGroupProvider.PROP_MANAGER_DN;
+import static org.apache.nifi.ldap.tenants.LdapUserGroupProvider.PROP_MANAGER_PASSWORD;
+import static org.apache.nifi.ldap.tenants.LdapUserGroupProvider.PROP_PAGE_SIZE;
+import static org.apache.nifi.ldap.tenants.LdapUserGroupProvider.PROP_READ_TIMEOUT;
+import static org.apache.nifi.ldap.tenants.LdapUserGroupProvider.PROP_REFERRAL_STRATEGY;
+import static org.apache.nifi.ldap.tenants.LdapUserGroupProvider.PROP_SYNC_INTERVAL;
+import static org.apache.nifi.ldap.tenants.LdapUserGroupProvider.PROP_URL;
+import static org.apache.nifi.ldap.tenants.LdapUserGroupProvider.PROP_USER_GROUP_ATTRIBUTE;
+import static org.apache.nifi.ldap.tenants.LdapUserGroupProvider.PROP_USER_IDENTITY_ATTRIBUTE;
+import static org.apache.nifi.ldap.tenants.LdapUserGroupProvider.PROP_USER_OBJECT_CLASS;
+import static org.apache.nifi.ldap.tenants.LdapUserGroupProvider.PROP_USER_SEARCH_BASE;
+import static org.apache.nifi.ldap.tenants.LdapUserGroupProvider.PROP_USER_SEARCH_FILTER;
+import static org.apache.nifi.ldap.tenants.LdapUserGroupProvider.PROP_USER_SEARCH_SCOPE;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+@RunWith(FrameworkRunner.class)
+@CreateLdapServer(transports = {@CreateTransport(protocol = "LDAP")})
+@CreateDS(name = "nifi-example", partitions = {@CreatePartition(name = "example", suffix = "o=nifi")})
+@ApplyLdifFiles("nifi-example.ldif")
+public class LdapUserGroupProviderTest extends AbstractLdapTestUnit {
+
+    private static final String USER_SEARCH_BASE = "ou=users,o=nifi";
+    private static final String GROUP_SEARCH_BASE = "ou=groups,o=nifi";
+
+    private LdapUserGroupProvider ldapUserGroupProvider;
+
+    @Before
+    public void setup() {
+        final UserGroupProviderInitializationContext initializationContext = mock(UserGroupProviderInitializationContext.class);
+        when(initializationContext.getIdentifier()).thenReturn("identifier");
+
+        ldapUserGroupProvider = new LdapUserGroupProvider();
+        ldapUserGroupProvider.setNiFiProperties(getNiFiProperties(new Properties()));
+        ldapUserGroupProvider.initialize(initializationContext);
+    }
+
+    @Test(expected = AuthorizerCreationException.class)
+    public void testNoSearchBasesSpecified() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(null, null);
+        ldapUserGroupProvider.onConfigured(configurationContext);
+    }
+
+    @Test(expected = AuthorizerCreationException.class)
+    public void testUserSearchBaseSpecifiedButNoUserObjectClass() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, null);
+        when(configurationContext.getProperty(PROP_USER_OBJECT_CLASS)).thenReturn(new StandardPropertyValue(null, null));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+    }
+
+    @Test(expected = AuthorizerCreationException.class)
+    public void testUserSearchBaseSpecifiedButNoUserSearchScope() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, null);
+        when(configurationContext.getProperty(PROP_USER_SEARCH_SCOPE)).thenReturn(new StandardPropertyValue(null, null));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+    }
+
+    @Test(expected = AuthorizerCreationException.class)
+    public void testInvalidUserSearchScope() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, null);
+        when(configurationContext.getProperty(PROP_USER_SEARCH_SCOPE)).thenReturn(new StandardPropertyValue("not-valid", null));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+    }
+
+    @Test
+    public void testSearchUsersWithNoIdentityAttribute() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, null);
+        ldapUserGroupProvider.onConfigured(configurationContext);
+
+        assertEquals(8, ldapUserGroupProvider.getUsers().size());
+        assertNotNull(ldapUserGroupProvider.getUserByIdentity("cn=User 1,ou=users,o=nifi"));
+        assertTrue(ldapUserGroupProvider.getGroups().isEmpty());
+    }
+
+    @Test
+    public void testSearchUsersWithUidIdentityAttribute() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, null);
+        when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue("uid", null));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+
+        assertEquals(8, ldapUserGroupProvider.getUsers().size());
+        assertNotNull(ldapUserGroupProvider.getUserByIdentity("user1"));
+        assertTrue(ldapUserGroupProvider.getGroups().isEmpty());
+    }
+
+    @Test
+    public void testSearchUsersWithCnIdentityAttribute() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, null);
+        when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn", null));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+
+        assertEquals(8, ldapUserGroupProvider.getUsers().size());
+        assertNotNull(ldapUserGroupProvider.getUserByIdentity("User 1"));
+        assertTrue(ldapUserGroupProvider.getGroups().isEmpty());
+    }
+
+    @Test
+    public void testSearchUsersObjectSearchScope() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, null);
+        when(configurationContext.getProperty(PROP_USER_SEARCH_SCOPE)).thenReturn(new StandardPropertyValue(SearchScope.OBJECT.name(), null));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+
+        assertTrue(ldapUserGroupProvider.getUsers().isEmpty());
+        assertTrue(ldapUserGroupProvider.getGroups().isEmpty());
+    }
+
+    @Test
+    public void testSearchUsersSubtreeSearchScope() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration("o=nifi", null);
+        when(configurationContext.getProperty(PROP_USER_SEARCH_SCOPE)).thenReturn(new StandardPropertyValue(SearchScope.SUBTREE.name(), null));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+
+        assertEquals(8, ldapUserGroupProvider.getUsers().size());
+        assertTrue(ldapUserGroupProvider.getGroups().isEmpty());
+    }
+
+    @Test
+    public void testSearchUsersWithFilter() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, null);
+        when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue("uid", null));
+        when(configurationContext.getProperty(PROP_USER_SEARCH_FILTER)).thenReturn(new StandardPropertyValue("(uid=user1)", null));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+
+        assertEquals(1, ldapUserGroupProvider.getUsers().size());
+        assertNotNull(ldapUserGroupProvider.getUserByIdentity("user1"));
+        assertTrue(ldapUserGroupProvider.getGroups().isEmpty());
+    }
+
+    @Test
+    public void testSearchUsersWithPaging() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, null);
+        when(configurationContext.getProperty(PROP_PAGE_SIZE)).thenReturn(new StandardPropertyValue("1", null));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+
+        assertEquals(8, ldapUserGroupProvider.getUsers().size());
+        assertTrue(ldapUserGroupProvider.getGroups().isEmpty());
+    }
+
+    @Test
+    public void testSearchUsersWithGroupingNoGroupName() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, null);
+        when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue("uid", null));
+        when(configurationContext.getProperty(PROP_USER_GROUP_ATTRIBUTE)).thenReturn(new StandardPropertyValue("description", null)); // using description in lieu of memberof
+        ldapUserGroupProvider.onConfigured(configurationContext);
+
+        assertEquals(8, ldapUserGroupProvider.getUsers().size());
+        assertEquals(2, ldapUserGroupProvider.getGroups().size());
+
+        final UserAndGroups userAndGroups = ldapUserGroupProvider.getUserAndGroups("user4");
+        assertNotNull(userAndGroups.getUser());
+        assertEquals(1, userAndGroups.getGroups().size());
+        assertEquals("cn=team1,ou=groups,o=nifi", userAndGroups.getGroups().iterator().next().getName());
+    }
+
+    @Test
+    public void testSearchUsersWithGroupingAndGroupName() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, null);
+        when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue("uid", null));
+        when(configurationContext.getProperty(PROP_USER_GROUP_ATTRIBUTE)).thenReturn(new StandardPropertyValue("description", null)); // using description in lieu of memberof
+        when(configurationContext.getProperty(PROP_GROUP_NAME_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn", null));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+
+        assertEquals(8, ldapUserGroupProvider.getUsers().size());
+        assertEquals(2, ldapUserGroupProvider.getGroups().size());
+
+        final UserAndGroups userAndGroups = ldapUserGroupProvider.getUserAndGroups("user4");
+        assertNotNull(userAndGroups.getUser());
+        assertEquals(1, userAndGroups.getGroups().size());
+        assertEquals("team1", userAndGroups.getGroups().iterator().next().getName());
+    }
+
+    @Test(expected = AuthorizerCreationException.class)
+    public void testSearchGroupsWithoutMemberAttribute() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(null, GROUP_SEARCH_BASE);
+        ldapUserGroupProvider.onConfigured(configurationContext);
+    }
+
+    @Test(expected = AuthorizerCreationException.class)
+    public void testGroupSearchBaseSpecifiedButNoGroupObjectClass() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(null, GROUP_SEARCH_BASE);
+        when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("member", null));
+        when(configurationContext.getProperty(PROP_GROUP_OBJECT_CLASS)).thenReturn(new StandardPropertyValue(null, null));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+    }
+
+    @Test(expected = AuthorizerCreationException.class)
+    public void testUserSearchBaseSpecifiedButNoGroupSearchScope() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(null, GROUP_SEARCH_BASE);
+        when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("member", null));
+        when(configurationContext.getProperty(PROP_GROUP_SEARCH_SCOPE)).thenReturn(new StandardPropertyValue(null, null));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+    }
+
+    @Test(expected = AuthorizerCreationException.class)
+    public void testInvalidGroupSearchScope() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(null, GROUP_SEARCH_BASE);
+        when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("member", null));
+        when(configurationContext.getProperty(PROP_GROUP_SEARCH_SCOPE)).thenReturn(new StandardPropertyValue("not-valid", null));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+    }
+
+    @Test
+    public void testSearchGroupsWithNoNameAttribute() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(null, GROUP_SEARCH_BASE);
+        when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("member", null));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+
+        final Set<Group> groups = ldapUserGroupProvider.getGroups();
+        assertEquals(4, groups.size());
+        assertEquals(1, groups.stream().filter(group -> "cn=admins,ou=groups,o=nifi".equals(group.getName())).count());
+    }
+
+    @Test
+    public void testSearchGroupsWithPaging() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(null, GROUP_SEARCH_BASE);
+        when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("member", null));
+        when(configurationContext.getProperty(PROP_PAGE_SIZE)).thenReturn(new StandardPropertyValue("1", null));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+
+        assertEquals(4, ldapUserGroupProvider.getGroups().size());
+    }
+
+    @Test
+    public void testSearchGroupsObjectSearchScope() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(null, GROUP_SEARCH_BASE);
+        when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("member", null));
+        when(configurationContext.getProperty(PROP_GROUP_SEARCH_SCOPE)).thenReturn(new StandardPropertyValue(SearchScope.OBJECT.name(), null));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+
+        assertTrue(ldapUserGroupProvider.getUsers().isEmpty());
+        assertTrue(ldapUserGroupProvider.getGroups().isEmpty());
+    }
+
+    @Test
+    public void testSearchGroupsSubtreeSearchScope() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(null, "o=nifi");
+        when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("member", null));
+        when(configurationContext.getProperty(PROP_GROUP_SEARCH_SCOPE)).thenReturn(new StandardPropertyValue(SearchScope.SUBTREE.name(), null));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+
+        assertEquals(4, ldapUserGroupProvider.getGroups().size());
+    }
+
+    @Test
+    public void testSearchGroupsWithNameAttribute() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(null, GROUP_SEARCH_BASE);
+        when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("member", null));
+        when(configurationContext.getProperty(PROP_GROUP_NAME_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn", null));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+
+        final Set<Group> groups = ldapUserGroupProvider.getGroups();
+        assertEquals(4, groups.size());
+
+        final Group admins = groups.stream().filter(group -> "admins".equals(group.getName())).findFirst().orElse(null);
+        assertNotNull(admins);
+        assertFalse(admins.getUsers().isEmpty());
+        assertEquals(1, admins.getUsers().stream().map(
+                userIdentifier -> ldapUserGroupProvider.getUser(userIdentifier)).filter(
+                        user -> "cn=User 1,ou=users,o=nifi".equals(user.getIdentity())).count());
+    }
+
+    @Test
+    public void testSearchGroupsWithNoNameAndUserIdentityUidAttribute() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(null, GROUP_SEARCH_BASE);
+        when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("member", null));
+        when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue("uid", null));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+
+        final Set<Group> groups = ldapUserGroupProvider.getGroups();
+        assertEquals(4, groups.size());
+
+        final Group admins = groups.stream().filter(group -> "cn=admins,ou=groups,o=nifi".equals(group.getName())).findFirst().orElse(null);
+        assertNotNull(admins);
+        assertFalse(admins.getUsers().isEmpty());
+        assertEquals(1, admins.getUsers().stream().map(
+                userIdentifier -> ldapUserGroupProvider.getUser(userIdentifier)).filter(
+                user -> "user1".equals(user.getIdentity())).count());
+    }
+
+    @Test
+    public void testSearchGroupsWithNameAndUserIdentityCnAttribute() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(null, GROUP_SEARCH_BASE);
+        when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("member", null));
+        when(configurationContext.getProperty(PROP_GROUP_NAME_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn", null));
+        when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn", null));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+
+        final Set<Group> groups = ldapUserGroupProvider.getGroups();
+        assertEquals(4, groups.size());
+
+        final Group admins = groups.stream().filter(group -> "admins".equals(group.getName())).findFirst().orElse(null);
+        assertNotNull(admins);
+        assertFalse(admins.getUsers().isEmpty());
+        assertEquals(1, admins.getUsers().stream().map(
+                userIdentifier -> ldapUserGroupProvider.getUser(userIdentifier)).filter(
+                user -> "User 1".equals(user.getIdentity())).count());
+    }
+
+    @Test
+    public void testSearchGroupsWithFilter() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(null, GROUP_SEARCH_BASE);
+        when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("member", null));
+        when(configurationContext.getProperty(PROP_GROUP_SEARCH_FILTER)).thenReturn(new StandardPropertyValue("(cn=admins)", null));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+
+        final Set<Group> groups = ldapUserGroupProvider.getGroups();
+        assertEquals(1, groups.size());
+        assertEquals(1, groups.stream().filter(group -> "cn=admins,ou=groups,o=nifi".equals(group.getName())).count());
+    }
+
+    @Test
+    public void testSearchUsersAndGroupsNoMembership() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, GROUP_SEARCH_BASE);
+        ldapUserGroupProvider.onConfigured(configurationContext);
+
+        assertEquals(8, ldapUserGroupProvider.getUsers().size());
+
+        final Set<Group> groups = ldapUserGroupProvider.getGroups();
+        assertEquals(4, groups.size());
+        groups.forEach(group -> assertTrue(group.getUsers().isEmpty()));
+    }
+
+    @Test
+    public void testSearchUsersAndGroupsMembershipThroughUsers() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, GROUP_SEARCH_BASE);
+        when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue("uid", null));
+        when(configurationContext.getProperty(PROP_USER_GROUP_ATTRIBUTE)).thenReturn(new StandardPropertyValue("description", null)); // using description in lieu of memberof
+        when(configurationContext.getProperty(PROP_GROUP_NAME_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn", null));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+
+        assertEquals(8, ldapUserGroupProvider.getUsers().size());
+
+        final Set<Group> groups = ldapUserGroupProvider.getGroups();
+        assertEquals(4, groups.size());
+
+        final Group team1 = groups.stream().filter(group -> "team1".equals(group.getName())).findFirst().orElse(null);
+        assertNotNull(team1);
+        assertEquals(2, team1.getUsers().size());
+        assertEquals(2, team1.getUsers().stream().map(
+                userIdentifier -> ldapUserGroupProvider.getUser(userIdentifier)).filter(
+                user -> "user4".equals(user.getIdentity()) || "user5".equals(user.getIdentity())).count());
+
+        final Group team2 = groups.stream().filter(group -> "team2".equals(group.getName())).findFirst().orElse(null);
+        assertNotNull(team2);
+        assertEquals(2, team2.getUsers().size());
+        assertEquals(2, team2.getUsers().stream().map(
+                userIdentifier -> ldapUserGroupProvider.getUser(userIdentifier)).filter(
+                user -> "user6".equals(user.getIdentity()) || "user7".equals(user.getIdentity())).count());
+    }
+
+    @Test
+    public void testSearchUsersAndGroupsMembershipThroughGroups() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, GROUP_SEARCH_BASE);
+        when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue("uid", null));
+        when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("member", null));
+        when(configurationContext.getProperty(PROP_GROUP_NAME_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn", null));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+
+        assertEquals(8, ldapUserGroupProvider.getUsers().size());
+
+        final Set<Group> groups = ldapUserGroupProvider.getGroups();
+        assertEquals(4, groups.size());
+
+        final Group admins = groups.stream().filter(group -> "admins".equals(group.getName())).findFirst().orElse(null);
+        assertNotNull(admins);
+        assertEquals(2, admins.getUsers().size());
+        assertEquals(2, admins.getUsers().stream().map(
+                userIdentifier -> ldapUserGroupProvider.getUser(userIdentifier)).filter(
+                user -> "user1".equals(user.getIdentity()) || "user3".equals(user.getIdentity())).count());
+
+        final Group readOnly = groups.stream().filter(group -> "read-only".equals(group.getName())).findFirst().orElse(null);
+        assertNotNull(readOnly);
+        assertEquals(1, readOnly.getUsers().size());
+        assertEquals(1, readOnly.getUsers().stream().map(
+                userIdentifier -> ldapUserGroupProvider.getUser(userIdentifier)).filter(
+                user -> "user2".equals(user.getIdentity())).count());
+
+        final Group team1 = groups.stream().filter(group -> "team1".equals(group.getName())).findFirst().orElse(null);
+        assertNotNull(team1);
+        assertEquals(1, team1.getUsers().size());
+        assertEquals(1, team1.getUsers().stream().map(
+                userIdentifier -> ldapUserGroupProvider.getUser(userIdentifier)).filter(
+                user -> "user1".equals(user.getIdentity())).count());
+
+        final Group team2 = groups.stream().filter(group -> "team2".equals(group.getName())).findFirst().orElse(null);
+        assertNotNull(team2);
+        assertEquals(1, team2.getUsers().size());
+        assertEquals(1, team2.getUsers().stream().map(
+                userIdentifier -> ldapUserGroupProvider.getUser(userIdentifier)).filter(
+                user -> "user1".equals(user.getIdentity())).count());
+    }
+
+    @Test
+    public void testSearchUsersAndGroupsMembershipThroughUsersAndGroups() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, GROUP_SEARCH_BASE);
+        when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue("uid", null));
+        when(configurationContext.getProperty(PROP_USER_GROUP_ATTRIBUTE)).thenReturn(new StandardPropertyValue("description", null)); // using description in lieu of memberof
+        when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("member", null));
+        when(configurationContext.getProperty(PROP_GROUP_NAME_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn", null));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+
+        assertEquals(8, ldapUserGroupProvider.getUsers().size());
+
+        final Set<Group> groups = ldapUserGroupProvider.getGroups();
+        assertEquals(4, groups.size());
+
+        final Group admins = groups.stream().filter(group -> "admins".equals(group.getName())).findFirst().orElse(null);
+        assertNotNull(admins);
+        assertEquals(2, admins.getUsers().size());
+        assertEquals(2, admins.getUsers().stream().map(
+                userIdentifier -> ldapUserGroupProvider.getUser(userIdentifier)).filter(
+                user -> "user1".equals(user.getIdentity()) || "user3".equals(user.getIdentity())).count());
+
+        final Group readOnly = groups.stream().filter(group -> "read-only".equals(group.getName())).findFirst().orElse(null);
+        assertNotNull(readOnly);
+        assertEquals(1, readOnly.getUsers().size());
+        assertEquals(1, readOnly.getUsers().stream().map(
+                userIdentifier -> ldapUserGroupProvider.getUser(userIdentifier)).filter(
+                user -> "user2".equals(user.getIdentity())).count());
+
+        final Group team1 = groups.stream().filter(group -> "team1".equals(group.getName())).findFirst().orElse(null);
+        assertNotNull(team1);
+        assertEquals(3, team1.getUsers().size());
+        assertEquals(3, team1.getUsers().stream().map(
+                userIdentifier -> ldapUserGroupProvider.getUser(userIdentifier)).filter(
+                user -> "user1".equals(user.getIdentity()) || "user4".equals(user.getIdentity()) || "user5".equals(user.getIdentity())).count());
+
+        final Group team2 = groups.stream().filter(group -> "team2".equals(group.getName())).findFirst().orElse(null);
+        assertNotNull(team2);
+        assertEquals(3, team2.getUsers().size());
+        assertEquals(3, team2.getUsers().stream().map(
+                userIdentifier -> ldapUserGroupProvider.getUser(userIdentifier)).filter(
+                user -> "user1".equals(user.getIdentity()) || "user6".equals(user.getIdentity()) || "user7".equals(user.getIdentity())).count());
+    }
+
+    @Test
+    public void testUserIdentityMapping() throws Exception {
+        final Properties props = new Properties();
+        props.setProperty("nifi.security.identity.mapping.pattern.dn1", "^cn=(.*?),o=(.*?)$");
+        props.setProperty("nifi.security.identity.mapping.value.dn1", "$1");
+
+        final NiFiProperties properties = getNiFiProperties(props);
+        ldapUserGroupProvider.setNiFiProperties(properties);
+
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, null);
+        when(configurationContext.getProperty(PROP_USER_SEARCH_FILTER)).thenReturn(new StandardPropertyValue("(uid=user1)", null));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+
+        assertEquals(1, ldapUserGroupProvider.getUsers().size());
+        assertNotNull(ldapUserGroupProvider.getUserByIdentity("User 1,ou=users"));
+    }
+
+    private AuthorizerConfigurationContext getBaseConfiguration(final String userSearchBase, final String groupSearchBase) {
+        final AuthorizerConfigurationContext configurationContext = mock(AuthorizerConfigurationContext.class);
+        when(configurationContext.getProperty(PROP_URL)).thenReturn(new StandardPropertyValue("ldap://127.0.0.1:" + getLdapServer().getPort(), null));
+        when(configurationContext.getProperty(PROP_CONNECT_TIMEOUT)).thenReturn(new StandardPropertyValue("30 secs", null));
+        when(configurationContext.getProperty(PROP_READ_TIMEOUT)).thenReturn(new StandardPropertyValue("30 secs", null));
+        when(configurationContext.getProperty(PROP_REFERRAL_STRATEGY)).thenReturn(new StandardPropertyValue(ReferralStrategy.FOLLOW.name(), null));
+        when(configurationContext.getProperty(PROP_PAGE_SIZE)).thenReturn(new StandardPropertyValue(null, null));
+        when(configurationContext.getProperty(PROP_SYNC_INTERVAL)).thenReturn(new StandardPropertyValue("30 mins", null));
+
+        when(configurationContext.getProperty(PROP_AUTHENTICATION_STRATEGY)).thenReturn(new StandardPropertyValue(LdapAuthenticationStrategy.SIMPLE.name(), null));
+        when(configurationContext.getProperty(PROP_MANAGER_DN)).thenReturn(new StandardPropertyValue("uid=admin,ou=system", null));
+        when(configurationContext.getProperty(PROP_MANAGER_PASSWORD)).thenReturn(new StandardPropertyValue("secret", null));
+
+        when(configurationContext.getProperty(PROP_USER_SEARCH_BASE)).thenReturn(new StandardPropertyValue(userSearchBase, null));
+        when(configurationContext.getProperty(PROP_USER_OBJECT_CLASS)).thenReturn(new StandardPropertyValue("person", null));
+        when(configurationContext.getProperty(PROP_USER_SEARCH_SCOPE)).thenReturn(new StandardPropertyValue(SearchScope.ONE_LEVEL.name(), null));
+        when(configurationContext.getProperty(PROP_USER_SEARCH_FILTER)).thenReturn(new StandardPropertyValue(null, null));
+        when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue(null, null));
+        when(configurationContext.getProperty(PROP_USER_GROUP_ATTRIBUTE)).thenReturn(new StandardPropertyValue(null, null));
+
+        when(configurationContext.getProperty(PROP_GROUP_SEARCH_BASE)).thenReturn(new StandardPropertyValue(groupSearchBase, null));
+        when(configurationContext.getProperty(PROP_GROUP_OBJECT_CLASS)).thenReturn(new StandardPropertyValue("groupOfNames", null));
+        when(configurationContext.getProperty(PROP_GROUP_SEARCH_SCOPE)).thenReturn(new StandardPropertyValue(SearchScope.ONE_LEVEL.name(), null));
+        when(configurationContext.getProperty(PROP_GROUP_SEARCH_FILTER)).thenReturn(new StandardPropertyValue(null, null));
+        when(configurationContext.getProperty(PROP_GROUP_NAME_ATTRIBUTE)).thenReturn(new StandardPropertyValue(null, null));
+        when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue(null, null));
+
+        return configurationContext;
+    }
+
+    private NiFiProperties getNiFiProperties(final Properties properties) {
+        final NiFiProperties nifiProperties = Mockito.mock(NiFiProperties.class);
+        when(nifiProperties.getPropertyKeys()).thenReturn(properties.stringPropertyNames());
+
+        when(nifiProperties.getProperty(anyString())).then(new Answer<String>() {
+            @Override
+            public String answer(InvocationOnMock invocationOnMock) throws Throwable {
+                return properties.getProperty((String)invocationOnMock.getArguments()[0]);
+            }
+        });
+        return nifiProperties;
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi/blob/6bc6f955/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/src/test/resources/nifi-example.ldif
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/src/test/resources/nifi-example.ldif b/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/src/test/resources/nifi-example.ldif
new file mode 100644
index 0000000..2b5bdac
--- /dev/null
+++ b/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/src/test/resources/nifi-example.ldif
@@ -0,0 +1,136 @@
+## ---------------------------------------------------------------------------
+## Licensed to the Apache Software Foundation (ASF) under one or more
+## contributor license agreements.  See the NOTICE file distributed with
+## this work for additional information regarding copyright ownership.
+## The ASF 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.
+## ---------------------------------------------------------------------------
+
+version: 1
+
+dn: o=nifi
+objectclass: extensibleObject
+objectclass: top
+objectclass: domain
+dc: nifi
+o: nifi
+
+dn: ou=users,o=nifi
+objectClass: organizationalUnit
+objectClass: top
+ou: users
+
+dn: cn=User 1,ou=users,o=nifi
+objectClass: organizationalPerson
+objectClass: person
+objectClass: inetOrgPerson
+objectClass: top
+cn: User 1
+sn: User1
+uid: user1
+
+dn: cn=User 2,ou=users,o=nifi
+objectClass: organizationalPerson
+objectClass: person
+objectClass: inetOrgPerson
+objectClass: top
+cn: User 2
+sn: User2
+uid: user2
+
+dn: cn=User 3,ou=users,o=nifi
+objectClass: organizationalPerson
+objectClass: person
+objectClass: inetOrgPerson
+objectClass: top
+cn: User 3
+sn: User3
+uid: user3
+
+dn: cn=User 4,ou=users,o=nifi
+objectClass: organizationalPerson
+objectClass: person
+objectClass: inetOrgPerson
+objectClass: top
+cn: User 4
+sn: User4
+description: cn=team1,ou=groups,o=nifi
+uid: user4
+
+dn: cn=User 5,ou=users,o=nifi
+objectClass: organizationalPerson
+objectClass: person
+objectClass: inetOrgPerson
+objectClass: top
+cn: User 5
+sn: User5
+description: cn=team1,ou=groups,o=nifi
+uid: user5
+
+dn: cn=User 6,ou=users,o=nifi
+objectClass: organizationalPerson
+objectClass: person
+objectClass: inetOrgPerson
+objectClass: top
+cn: User 6
+sn: User6
+description: cn=team2,ou=groups,o=nifi
+uid: user6
+
+dn: cn=User 7,ou=users,o=nifi
+objectClass: organizationalPerson
+objectClass: person
+objectClass: inetOrgPerson
+objectClass: top
+cn: User 7
+sn: User7
+description: cn=team2,ou=groups,o=nifi
+uid: user7
+
+dn: cn=User 8,ou=users,o=nifi
+objectClass: organizationalPerson
+objectClass: person
+objectClass: inetOrgPerson
+objectClass: top
+cn: User 8
+sn: User8
+uid: user8
+
+dn: ou=groups,o=nifi
+objectClass: organizationalUnit
+objectClass: top
+ou: groups
+
+dn: cn=admins,ou=groups,o=nifi
+objectClass: groupOfNames
+objectClass: top
+cn: admins
+member: cn=User 1,ou=users,o=nifi
+member: cn=User 3,ou=users,o=nifi
+
+dn: cn=read-only,ou=groups,o=nifi
+objectClass: groupOfNames
+objectClass: top
+cn: read-only
+member: cn=User 2,ou=users,o=nifi
+
+dn: cn=team1,ou=groups,o=nifi
+objectClass: groupOfNames
+objectClass: top
+cn: team1
+member: cn=User 1,ou=users,o=nifi
+
+dn: cn=team2,ou=groups,o=nifi
+objectClass: groupOfNames
+objectClass: top
+cn: team2
+member: cn=User 1,ou=users,o=nifi


[2/2] nifi git commit: NIFI-4059: - Introducing the LdapUserGroupProvider. - Updating documentation accordingly. - Moving the IdentityMapping utilities so they were accessible.

Posted by pv...@apache.org.
NIFI-4059:
- Introducing the LdapUserGroupProvider.
- Updating documentation accordingly.
- Moving the IdentityMapping utilities so they were accessible.

Signed-off-by: Pierre Villard <pi...@gmail.com>

This closes #1923.


Project: http://git-wip-us.apache.org/repos/asf/nifi/repo
Commit: http://git-wip-us.apache.org/repos/asf/nifi/commit/6bc6f955
Tree: http://git-wip-us.apache.org/repos/asf/nifi/tree/6bc6f955
Diff: http://git-wip-us.apache.org/repos/asf/nifi/diff/6bc6f955

Branch: refs/heads/master
Commit: 6bc6f955c077bd4ce3bf06de15ac8693725ebc85
Parents: ad6af1d
Author: Matt Gilman <ma...@gmail.com>
Authored: Mon Jun 12 16:53:56 2017 -0400
Committer: Pierre Villard <pi...@gmail.com>
Committed: Mon Jun 19 19:25:33 2017 +0200

----------------------------------------------------------------------
 nifi-commons/nifi-security-utils/pom.xml        |   4 +
 .../authorization/util/IdentityMapping.java     |  48 ++
 .../authorization/util/IdentityMappingUtil.java | 145 ++++
 .../src/main/asciidoc/administration-guide.adoc |  98 +++
 .../nifi-framework/nifi-file-authorizer/pom.xml |   4 +
 .../authorization/util/IdentityMapping.java     |  48 --
 .../authorization/util/IdentityMappingUtil.java | 145 ----
 .../src/main/resources/conf/authorizers.xml     | 100 +++
 .../nifi-framework/nifi-site-to-site/pom.xml    |   4 +
 .../nifi-ldap-iaa-providers/pom.xml             |  15 +
 .../ldap/tenants/LdapUserGroupProvider.java     | 748 +++++++++++++++++++
 .../apache/nifi/ldap/tenants/SearchScope.java   |  28 +
 .../apache/nifi/ldap/tenants/TenantHolder.java  | 165 ++++
 ....apache.nifi.authorization.UserGroupProvider |  15 +
 .../ldap/tenants/LdapUserGroupProviderTest.java | 552 ++++++++++++++
 .../src/test/resources/nifi-example.ldif        | 136 ++++
 16 files changed, 2062 insertions(+), 193 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi/blob/6bc6f955/nifi-commons/nifi-security-utils/pom.xml
----------------------------------------------------------------------
diff --git a/nifi-commons/nifi-security-utils/pom.xml b/nifi-commons/nifi-security-utils/pom.xml
index b8f830a..5c9acc3 100644
--- a/nifi-commons/nifi-security-utils/pom.xml
+++ b/nifi-commons/nifi-security-utils/pom.xml
@@ -55,6 +55,10 @@
             <groupId>org.bouncycastle</groupId>
             <artifactId>bcpkix-jdk15on</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-properties</artifactId>
+        </dependency>
     </dependencies>
 </project>
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/6bc6f955/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/authorization/util/IdentityMapping.java
----------------------------------------------------------------------
diff --git a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/authorization/util/IdentityMapping.java b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/authorization/util/IdentityMapping.java
new file mode 100644
index 0000000..eb79c3a
--- /dev/null
+++ b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/authorization/util/IdentityMapping.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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 org.apache.nifi.authorization.util;
+
+import java.util.regex.Pattern;
+
+/**
+ * Holder to pass around the key, pattern, and replacement from an identity mapping in NiFiProperties.
+ */
+public class IdentityMapping {
+
+    private final String key;
+    private final Pattern pattern;
+    private final String replacementValue;
+
+    public IdentityMapping(String key, Pattern pattern, String replacementValue) {
+        this.key = key;
+        this.pattern = pattern;
+        this.replacementValue = replacementValue;
+    }
+
+    public String getKey() {
+        return key;
+    }
+
+    public Pattern getPattern() {
+        return pattern;
+    }
+
+    public String getReplacementValue() {
+        return replacementValue;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6bc6f955/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/authorization/util/IdentityMappingUtil.java
----------------------------------------------------------------------
diff --git a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/authorization/util/IdentityMappingUtil.java b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/authorization/util/IdentityMappingUtil.java
new file mode 100644
index 0000000..f7087ac
--- /dev/null
+++ b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/authorization/util/IdentityMappingUtil.java
@@ -0,0 +1,145 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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 org.apache.nifi.authorization.util;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.util.NiFiProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class IdentityMappingUtil {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(IdentityMappingUtil.class);
+    private static final Pattern backReferencePattern = Pattern.compile("\\$(\\d+)");
+
+    /**
+     * Builds the identity mappings from NiFiProperties.
+     *
+     * @param properties the NiFiProperties instance
+     * @return a list of identity mappings
+     */
+    public static List<IdentityMapping> getIdentityMappings(final NiFiProperties properties) {
+        final List<IdentityMapping> mappings = new ArrayList<>();
+
+        // go through each property
+        for (String propertyName : properties.getPropertyKeys()) {
+            if (StringUtils.startsWith(propertyName, NiFiProperties.SECURITY_IDENTITY_MAPPING_PATTERN_PREFIX)) {
+                final String key = StringUtils.substringAfter(propertyName, NiFiProperties.SECURITY_IDENTITY_MAPPING_PATTERN_PREFIX);
+                final String identityPattern = properties.getProperty(propertyName);
+
+                if (StringUtils.isBlank(identityPattern)) {
+                    LOGGER.warn("Identity Mapping property {} was found, but was empty", new Object[]{propertyName});
+                    continue;
+                }
+
+                final String identityValueProperty = NiFiProperties.SECURITY_IDENTITY_MAPPING_VALUE_PREFIX + key;
+                final String identityValue = properties.getProperty(identityValueProperty);
+
+                if (StringUtils.isBlank(identityValue)) {
+                    LOGGER.warn("Identity Mapping property {} was found, but corresponding value {} was not found",
+                            new Object[]{propertyName, identityValueProperty});
+                    continue;
+                }
+
+                final IdentityMapping identityMapping = new IdentityMapping(key, Pattern.compile(identityPattern), identityValue);
+                mappings.add(identityMapping);
+
+                LOGGER.debug("Found Identity Mapping with key = {}, pattern = {}, value = {}",
+                        new Object[] {key, identityPattern, identityValue});
+            }
+        }
+
+        // sort the list by the key so users can control the ordering in nifi.properties
+        Collections.sort(mappings, new Comparator<IdentityMapping>() {
+            @Override
+            public int compare(IdentityMapping m1, IdentityMapping m2) {
+                return m1.getKey().compareTo(m2.getKey());
+            }
+        });
+
+        return mappings;
+    }
+
+    /**
+     * Checks the given identity against each provided mapping and performs the mapping using the first one that matches.
+     * If none match then the identity is returned as is.
+     *
+     * @param identity the identity to map
+     * @param mappings the mappings
+     * @return the mapped identity, or the same identity if no mappings matched
+     */
+    public static String mapIdentity(final String identity, List<IdentityMapping> mappings) {
+        for (IdentityMapping mapping : mappings) {
+            Matcher m = mapping.getPattern().matcher(identity);
+            if (m.matches()) {
+                final String pattern = mapping.getPattern().pattern();
+                final String replacementValue = escapeLiteralBackReferences(mapping.getReplacementValue(), m.groupCount());
+                return identity.replaceAll(pattern, replacementValue);
+            }
+        }
+
+        return identity;
+    }
+
+    // If we find a back reference that is not valid, then we will treat it as a literal string. For example, if we have 3 capturing
+    // groups and the Replacement Value has the value is "I owe $8 to him", then we want to treat the $8 as a literal "$8", rather
+    // than attempting to use it as a back reference.
+    private static String escapeLiteralBackReferences(final String unescaped, final int numCapturingGroups) {
+        if (numCapturingGroups == 0) {
+            return unescaped;
+        }
+
+        String value = unescaped;
+        final Matcher backRefMatcher = backReferencePattern.matcher(value);
+        while (backRefMatcher.find()) {
+            final String backRefNum = backRefMatcher.group(1);
+            if (backRefNum.startsWith("0")) {
+                continue;
+            }
+            final int originalBackRefIndex = Integer.parseInt(backRefNum);
+            int backRefIndex = originalBackRefIndex;
+
+            // if we have a replacement value like $123, and we have less than 123 capturing groups, then
+            // we want to truncate the 3 and use capturing group 12; if we have less than 12 capturing groups,
+            // then we want to truncate the 2 and use capturing group 1; if we don't have a capturing group then
+            // we want to truncate the 1 and get 0.
+            while (backRefIndex > numCapturingGroups && backRefIndex >= 10) {
+                backRefIndex /= 10;
+            }
+
+            if (backRefIndex > numCapturingGroups) {
+                final StringBuilder sb = new StringBuilder(value.length() + 1);
+                final int groupStart = backRefMatcher.start(1);
+
+                sb.append(value.substring(0, groupStart - 1));
+                sb.append("\\");
+                sb.append(value.substring(groupStart - 1));
+                value = sb.toString();
+            }
+        }
+
+        return value;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6bc6f955/nifi-docs/src/main/asciidoc/administration-guide.adoc
----------------------------------------------------------------------
diff --git a/nifi-docs/src/main/asciidoc/administration-guide.adoc b/nifi-docs/src/main/asciidoc/administration-guide.adoc
index 37283bd..d3edbda 100644
--- a/nifi-docs/src/main/asciidoc/administration-guide.adoc
+++ b/nifi-docs/src/main/asciidoc/administration-guide.adoc
@@ -395,6 +395,39 @@ The default UserGroupProvider is the FileUserGroupProvider, however, you can dev
 * Legacy Authorized Users File - The full path to an existing authorized-users.xml that will be automatically be used to load the users and groups into the Users File.
 * Initial User Identity - The identity of a users and systems to seed the Users File. The name of each property must be unique, for example: "Initial User Identity A", "Initial User Identity B", "Initial User Identity C" or "Initial User Identity 1", "Initial User Identity 2", "Initial User Identity 3"
 
+Another option for the UserGroupProvider is the LdapUserGroupProvider. By default, this option is commented out but can be configured in lieu of the FileUserGroupProvider. This will sync users and groups from a directory server and will present them in NiFi UI in read only form. The LdapUserGroupProvider has the following properties:
+
+* Authentication Strategy - How the connection to the LDAP server is authenticated. Possible values are ANONYMOUS, SIMPLE, LDAPS, or START_TLS
+* Manager DN - The DN of the manager that is used to bind to the LDAP server to search for users.
+* Manager Password - The password of the manager that is used to bind to the LDAP server to search for users.
+* TLS - Keystore - Path to the Keystore that is used when connecting to LDAP using LDAPS or START_TLS.
+* TLS - Keystore Password - Password for the Keystore that is used when connecting to LDAP using LDAPS or START_TLS.
+* TLS - Keystore Type - Type of the Keystore that is used when connecting to LDAP using LDAPS or START_TLS (i.e. JKS or PKCS12).
+* TLS - Truststore - Path to the Truststore that is used when connecting to LDAP using LDAPS or START_TLS.
+* TLS - Truststore Password - Password for the Truststore that is used when connecting to LDAP using LDAPS or START_TLS.
+* TLS - Truststore Type - Type of the Truststore that is used when connecting to LDAP using LDAPS or START_TLS (i.e. JKS or PKCS12).
+* TLS - Client Auth - Client authentication policy when connecting to LDAP using LDAPS or START_TLS. Possible values are REQUIRED, WANT, NONE.
+* TLS - Protocol - Protocol to use when connecting to LDAP using LDAPS or START_TLS. (i.e. TLS, TLSv1.1, TLSv1.2, etc).
+* TLS - Shutdown Gracefully - Specifies whether the TLS should be shut down gracefully before the target context is closed. Defaults to false.
+* Referral Strategy - Strategy for handling referrals. Possible values are FOLLOW, IGNORE, THROW.
+* Connect Timeout - Duration of connect timeout. (i.e. 10 secs).
+* Read Timeout - Duration of read timeout. (i.e. 10 secs).
+* Url - Space-separated list of URLs of the LDAP servers (i.e. ldap://<hostname>:<port>).
+* Page Size - Sets the page size when retrieving users and groups. If not specified, no paging is performed.
+* Sync Interval - Duration of time between syncing users and groups. (i.e. 30 mins).
+* User Search Base - Base DN for searching for users (i.e. ou=users,o=nifi). Required to search users.
+* User Object Class - Object class for identifying users (i.e. person). Required if searching users.
+* User Search Scope - Search scope for searching users (ONE_LEVEL, OBJECT, or SUBTREE). Required if searching users.
+* User Search Filter - Filter for searching for users against the 'User Search Base' (i.e. (memberof=cn=team1,ou=groups,o=nifi) ). Optional.
+* User Identity Attribute - Attribute to use to extract user identity (i.e. cn). Optional. If not set, the entire DN is used.
+* User Group Name Attribute - Attribute to use to define group membership (i.e. memberof). Optional. If not set group membership will not be calculated through the users. Will rely on group membership being defined through 'Group Member Attribute' if set.
+* Group Search Base - Base DN for searching for groups (i.e. ou=groups,o=nifi). Required to search groups.
+* Group Object Class - Object class for identifying groups (i.e. groupOfNames). Required if searching groups.
+* Group Search Scope - Search scope for searching groups (ONE_LEVEL, OBJECT, or SUBTREE). Required if searching groups.
+* Group Search Filter - Filter for searching for groups against the 'Group Search Base'. Optional.
+* Group Name Attribute - Attribute to use to extract group name (i.e. cn). Optional. If not set, the entire DN is used.
+* Group Member Attribute - Attribute to use to define group membership (i.e. member). Optional. If not set group membership will not be calculated through the groups. Will rely on group member being defined through 'User Group Name Attribute' if set.
+
 The default AccessPolicyProvider is the FileAccessPolicyProvider, however, you can develop additional AccessPolicyProvider as extensions.  The FileAccessPolicyProvider has the following properties:
 
 * User Group Provider - The identifier for an User Group Provider defined above that will be used to access users and groups for use in the managed access policies.
@@ -488,6 +521,71 @@ After you have edited and saved the 'authorizers.xml' file, restart NiFi.  The 
 
 NOTE: For a brand new secure flow, providing the "Initial Admin Identity" gives that user access to get into the UI and to manage users, groups and policies.  But if that user wants to start modifying the flow, they need to grant themselves policies for the root process group. The system is unable to do this automatically because in a new flow the UUID of the root process group is not permanent until the flow.xml.gz is generated.  If the NiFi instance is an upgrade from an existing flow.xml.gz or a 1.x instance going from unsecure to secure, then the "Initial Admin Identity" user is automatically given the privileges to modify the flow.
 
+Here is an example loading users and groups from LDAP but still using file based access policies:
+
+----
+<authorizers>
+    <userGroupProvider>
+        <identifier>ldap-user-group-provider</identifier>
+        <class>org.apache.nifi.ldap.tenants.LdapUserGroupProvider</class>
+        <property name="Authentication Strategy">ANONYMOUS</property>
+
+        <property name="Manager DN"></property>
+        <property name="Manager Password"></property>
+
+        <property name="TLS - Keystore"></property>
+        <property name="TLS - Keystore Password"></property>
+        <property name="TLS - Keystore Type"></property>
+        <property name="TLS - Truststore"></property>
+        <property name="TLS - Truststore Password"></property>
+        <property name="TLS - Truststore Type"></property>
+        <property name="TLS - Client Auth"></property>
+        <property name="TLS - Protocol"></property>
+        <property name="TLS - Shutdown Gracefully"></property>
+
+        <property name="Referral Strategy">FOLLOW</property>
+        <property name="Connect Timeout">10 secs</property>
+        <property name="Read Timeout">10 secs</property>
+
+        <property name="Url">ldap://localhost:10389</property>
+        <property name="Page Size"></property>
+        <property name="Sync Interval">30 mins</property>
+
+        <property name="User Search Base">ou=users,o=nifi</property>
+        <property name="User Object Class">person</property>
+        <property name="User Search Scope">ONE_LEVEL</property>
+        <property name="User Search Filter"></property>
+        <property name="User Identity Attribute">cn</property>
+        <property name="User Group Name Attribute"></property>
+
+        <property name="Group Search Base">ou=groups,o=nifi</property>
+        <property name="Group Object Class">groupOfNames</property>
+        <property name="Group Search Scope">ONE_LEVEL</property>
+        <property name="Group Search Filter"></property>
+        <property name="Group Name Attribute">cn</property>
+        <property name="Group Member Attribute">member</property>
+    </userGroupProvider>
+    <accessPolicyProvider>
+        <identifier>file-access-policy-provider</identifier>
+        <class>org.apache.nifi.authorization.FileAccessPolicyProvider</class>
+        <property name="User Group Provider">ldap-user-group-provider</property>
+        <property name="Authorizations File">./conf/authorizations.xml</property>
+        <property name="Initial Admin Identity">John Smith</property>
+        <property name="Legacy Authorized Users File"></property>
+
+        <property name="Node Identity 1"></property>
+    </accessPolicyProvider>
+    <authorizer>
+        <identifier>managed-authorizer</identifier>
+        <class>org.apache.nifi.authorization.StandardManagedAuthorizer</class>
+        <property name="Access Policy Provider">file-access-policy-provider</property>
+    </authorizer>
+</authorizers>
+----
+
+The 'Initial Admin Identity' value would have loaded from the cn from John Smith's entry based on the 'User Identity Attribute' value.
+
+
 [[legacy-authorized-users]]
 Legacy Authorized Users (NiFi Instance Upgrade)
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

http://git-wip-us.apache.org/repos/asf/nifi/blob/6bc6f955/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-file-authorizer/pom.xml
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-file-authorizer/pom.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-file-authorizer/pom.xml
index bba0413..cbc3192 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-file-authorizer/pom.xml
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-file-authorizer/pom.xml
@@ -102,6 +102,10 @@
         </dependency>
         <dependency>
             <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-security-utils</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
             <artifactId>nifi-properties</artifactId>
         </dependency>
         <dependency>

http://git-wip-us.apache.org/repos/asf/nifi/blob/6bc6f955/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-authorization/src/main/java/org/apache/nifi/authorization/util/IdentityMapping.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-authorization/src/main/java/org/apache/nifi/authorization/util/IdentityMapping.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-authorization/src/main/java/org/apache/nifi/authorization/util/IdentityMapping.java
deleted file mode 100644
index eb79c3a..0000000
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-authorization/src/main/java/org/apache/nifi/authorization/util/IdentityMapping.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF 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 org.apache.nifi.authorization.util;
-
-import java.util.regex.Pattern;
-
-/**
- * Holder to pass around the key, pattern, and replacement from an identity mapping in NiFiProperties.
- */
-public class IdentityMapping {
-
-    private final String key;
-    private final Pattern pattern;
-    private final String replacementValue;
-
-    public IdentityMapping(String key, Pattern pattern, String replacementValue) {
-        this.key = key;
-        this.pattern = pattern;
-        this.replacementValue = replacementValue;
-    }
-
-    public String getKey() {
-        return key;
-    }
-
-    public Pattern getPattern() {
-        return pattern;
-    }
-
-    public String getReplacementValue() {
-        return replacementValue;
-    }
-
-}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6bc6f955/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-authorization/src/main/java/org/apache/nifi/authorization/util/IdentityMappingUtil.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-authorization/src/main/java/org/apache/nifi/authorization/util/IdentityMappingUtil.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-authorization/src/main/java/org/apache/nifi/authorization/util/IdentityMappingUtil.java
deleted file mode 100644
index f7087ac..0000000
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-authorization/src/main/java/org/apache/nifi/authorization/util/IdentityMappingUtil.java
+++ /dev/null
@@ -1,145 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF 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 org.apache.nifi.authorization.util;
-
-import org.apache.commons.lang3.StringUtils;
-import org.apache.nifi.util.NiFiProperties;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-public class IdentityMappingUtil {
-
-    private static final Logger LOGGER = LoggerFactory.getLogger(IdentityMappingUtil.class);
-    private static final Pattern backReferencePattern = Pattern.compile("\\$(\\d+)");
-
-    /**
-     * Builds the identity mappings from NiFiProperties.
-     *
-     * @param properties the NiFiProperties instance
-     * @return a list of identity mappings
-     */
-    public static List<IdentityMapping> getIdentityMappings(final NiFiProperties properties) {
-        final List<IdentityMapping> mappings = new ArrayList<>();
-
-        // go through each property
-        for (String propertyName : properties.getPropertyKeys()) {
-            if (StringUtils.startsWith(propertyName, NiFiProperties.SECURITY_IDENTITY_MAPPING_PATTERN_PREFIX)) {
-                final String key = StringUtils.substringAfter(propertyName, NiFiProperties.SECURITY_IDENTITY_MAPPING_PATTERN_PREFIX);
-                final String identityPattern = properties.getProperty(propertyName);
-
-                if (StringUtils.isBlank(identityPattern)) {
-                    LOGGER.warn("Identity Mapping property {} was found, but was empty", new Object[]{propertyName});
-                    continue;
-                }
-
-                final String identityValueProperty = NiFiProperties.SECURITY_IDENTITY_MAPPING_VALUE_PREFIX + key;
-                final String identityValue = properties.getProperty(identityValueProperty);
-
-                if (StringUtils.isBlank(identityValue)) {
-                    LOGGER.warn("Identity Mapping property {} was found, but corresponding value {} was not found",
-                            new Object[]{propertyName, identityValueProperty});
-                    continue;
-                }
-
-                final IdentityMapping identityMapping = new IdentityMapping(key, Pattern.compile(identityPattern), identityValue);
-                mappings.add(identityMapping);
-
-                LOGGER.debug("Found Identity Mapping with key = {}, pattern = {}, value = {}",
-                        new Object[] {key, identityPattern, identityValue});
-            }
-        }
-
-        // sort the list by the key so users can control the ordering in nifi.properties
-        Collections.sort(mappings, new Comparator<IdentityMapping>() {
-            @Override
-            public int compare(IdentityMapping m1, IdentityMapping m2) {
-                return m1.getKey().compareTo(m2.getKey());
-            }
-        });
-
-        return mappings;
-    }
-
-    /**
-     * Checks the given identity against each provided mapping and performs the mapping using the first one that matches.
-     * If none match then the identity is returned as is.
-     *
-     * @param identity the identity to map
-     * @param mappings the mappings
-     * @return the mapped identity, or the same identity if no mappings matched
-     */
-    public static String mapIdentity(final String identity, List<IdentityMapping> mappings) {
-        for (IdentityMapping mapping : mappings) {
-            Matcher m = mapping.getPattern().matcher(identity);
-            if (m.matches()) {
-                final String pattern = mapping.getPattern().pattern();
-                final String replacementValue = escapeLiteralBackReferences(mapping.getReplacementValue(), m.groupCount());
-                return identity.replaceAll(pattern, replacementValue);
-            }
-        }
-
-        return identity;
-    }
-
-    // If we find a back reference that is not valid, then we will treat it as a literal string. For example, if we have 3 capturing
-    // groups and the Replacement Value has the value is "I owe $8 to him", then we want to treat the $8 as a literal "$8", rather
-    // than attempting to use it as a back reference.
-    private static String escapeLiteralBackReferences(final String unescaped, final int numCapturingGroups) {
-        if (numCapturingGroups == 0) {
-            return unescaped;
-        }
-
-        String value = unescaped;
-        final Matcher backRefMatcher = backReferencePattern.matcher(value);
-        while (backRefMatcher.find()) {
-            final String backRefNum = backRefMatcher.group(1);
-            if (backRefNum.startsWith("0")) {
-                continue;
-            }
-            final int originalBackRefIndex = Integer.parseInt(backRefNum);
-            int backRefIndex = originalBackRefIndex;
-
-            // if we have a replacement value like $123, and we have less than 123 capturing groups, then
-            // we want to truncate the 3 and use capturing group 12; if we have less than 12 capturing groups,
-            // then we want to truncate the 2 and use capturing group 1; if we don't have a capturing group then
-            // we want to truncate the 1 and get 0.
-            while (backRefIndex > numCapturingGroups && backRefIndex >= 10) {
-                backRefIndex /= 10;
-            }
-
-            if (backRefIndex > numCapturingGroups) {
-                final StringBuilder sb = new StringBuilder(value.length() + 1);
-                final int groupStart = backRefMatcher.start(1);
-
-                sb.append(value.substring(0, groupStart - 1));
-                sb.append("\\");
-                sb.append(value.substring(groupStart - 1));
-                value = sb.toString();
-            }
-        }
-
-        return value;
-    }
-
-}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6bc6f955/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/authorizers.xml
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/authorizers.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/authorizers.xml
index 247c0e8..1cf29b1 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/authorizers.xml
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/authorizers.xml
@@ -53,6 +53,106 @@
     </userGroupProvider>
 
     <!--
+        The LdapUserGroupProvider will retrieve users and groups from an LDAP server. The users and groups
+        are not configurable.
+
+        'Authentication Strategy' - How the connection to the LDAP server is authenticated. Possible
+            values are ANONYMOUS, SIMPLE, LDAPS, or START_TLS.
+
+        'Manager DN' - The DN of the manager that is used to bind to the LDAP server to search for users.
+        'Manager Password' - The password of the manager that is used to bind to the LDAP server to
+            search for users.
+
+        'TLS - Keystore' - Path to the Keystore that is used when connecting to LDAP using LDAPS or START_TLS.
+        'TLS - Keystore Password' - Password for the Keystore that is used when connecting to LDAP
+            using LDAPS or START_TLS.
+        'TLS - Keystore Type' - Type of the Keystore that is used when connecting to LDAP using
+            LDAPS or START_TLS (i.e. JKS or PKCS12).
+        'TLS - Truststore' - Path to the Truststore that is used when connecting to LDAP using LDAPS or START_TLS.
+        'TLS - Truststore Password' - Password for the Truststore that is used when connecting to
+            LDAP using LDAPS or START_TLS.
+        'TLS - Truststore Type' - Type of the Truststore that is used when connecting to LDAP using
+            LDAPS or START_TLS (i.e. JKS or PKCS12).
+        'TLS - Client Auth' - Client authentication policy when connecting to LDAP using LDAPS or START_TLS.
+            Possible values are REQUIRED, WANT, NONE.
+        'TLS - Protocol' - Protocol to use when connecting to LDAP using LDAPS or START_TLS. (i.e. TLS,
+            TLSv1.1, TLSv1.2, etc).
+        'TLS - Shutdown Gracefully' - Specifies whether the TLS should be shut down gracefully
+            before the target context is closed. Defaults to false.
+
+        'Referral Strategy' - Strategy for handling referrals. Possible values are FOLLOW, IGNORE, THROW.
+        'Connect Timeout' - Duration of connect timeout. (i.e. 10 secs).
+        'Read Timeout' - Duration of read timeout. (i.e. 10 secs).
+
+        'Url' - Space-separated list of URLs of the LDAP servers (i.e. ldap://<hostname>:<port>).
+        'Page Size' - Sets the page size when retrieving users and groups. If not specified, no paging is performed.
+        'Sync Interval' - Duration of time between syncing users and groups. (i.e. 30 mins).
+
+        'User Search Base' - Base DN for searching for users (i.e. ou=users,o=nifi). Required to search users.
+        'User Object Class' - Object class for identifying users (i.e. person). Required if searching users.
+        'User Search Scope' - Search scope for searching users (ONE_LEVEL, OBJECT, or SUBTREE). Required if searching users.
+        'User Search Filter' - Filter for searching for users against the 'User Search Base' (i.e. (memberof=cn=team1,ou=groups,o=nifi) ). Optional.
+        'User Identity Attribute' - Attribute to use to extract user identity (i.e. cn). Optional. If not set, the entire DN is used.
+        'User Group Name Attribute' - Attribute to use to define group membership (i.e. memberof). Optional. If not set
+            group membership will not be calculated through the users. Will rely on group membership being defined
+            through 'Group Member Attribute' if set.
+
+        'Group Search Base' - Base DN for searching for groups (i.e. ou=groups,o=nifi). Required to search groups.
+        'Group Object Class' - Object class for identifying groups (i.e. groupOfNames). Required if searching groups.
+        'Group Search Scope' - Search scope for searching groups (ONE_LEVEL, OBJECT, or SUBTREE). Required if searching groups.
+        'Group Search Filter' - Filter for searching for groups against the 'Group Search Base'. Optional.
+        'Group Name Attribute' - Attribute to use to extract group name (i.e. cn). Optional. If not set, the entire DN is used.
+        'Group Member Attribute' - Attribute to use to define group membership (i.e. member). Optional. If not set
+            group membership will not be calculated through the groups. Will rely on group member being defined
+            through 'User Group Name Attribute' if set.
+
+        NOTE: Any identity mapping rules specified in nifi.properties will also be applied to the user identities.
+            Group names are not mapped.
+    -->
+    <!-- To enable the ldap-user-group-provider remove 2 lines. This is 1 of 2.
+    <userGroupProvider>
+        <identifier>ldap-user-group-provider</identifier>
+        <class>org.apache.nifi.ldap.tenants.LdapUserGroupProvider</class>
+        <property name="Authentication Strategy">START_TLS</property>
+
+        <property name="Manager DN"></property>
+        <property name="Manager Password"></property>
+
+        <property name="TLS - Keystore"></property>
+        <property name="TLS - Keystore Password"></property>
+        <property name="TLS - Keystore Type"></property>
+        <property name="TLS - Truststore"></property>
+        <property name="TLS - Truststore Password"></property>
+        <property name="TLS - Truststore Type"></property>
+        <property name="TLS - Client Auth"></property>
+        <property name="TLS - Protocol"></property>
+        <property name="TLS - Shutdown Gracefully"></property>
+
+        <property name="Referral Strategy">FOLLOW</property>
+        <property name="Connect Timeout">10 secs</property>
+        <property name="Read Timeout">10 secs</property>
+
+        <property name="Url"></property>
+        <property name="Page Size"></property>
+        <property name="Sync Interval">30 mins</property>
+
+        <property name="User Search Base"></property>
+        <property name="User Object Class">person</property>
+        <property name="User Search Scope">ONE_LEVEL</property>
+        <property name="User Search Filter"></property>
+        <property name="User Identity Attribute"></property>
+        <property name="User Group Name Attribute"></property>
+
+        <property name="Group Search Base"></property>
+        <property name="Group Object Class">group</property>
+        <property name="Group Search Scope">ONE_LEVEL</property>
+        <property name="Group Search Filter"></property>
+        <property name="Group Name Attribute"></property>
+        <property name="Group Member Attribute"></property>
+    </userGroupProvider>
+    To enable the ldap-user-group-provider remove 2 lines. This is 2 of 2. -->
+
+    <!--
         The FileAccessPolicyProvider will provide support for managing access policies which is backed by a file
         on the local file system.
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/6bc6f955/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-site-to-site/pom.xml
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-site-to-site/pom.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-site-to-site/pom.xml
index 9c27513..f6d36b2 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-site-to-site/pom.xml
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-site-to-site/pom.xml
@@ -28,6 +28,10 @@
         </dependency>
         <dependency>
             <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-security-utils</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
             <artifactId>nifi-administration</artifactId>
         </dependency>
         <dependency>

http://git-wip-us.apache.org/repos/asf/nifi/blob/6bc6f955/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/pom.xml
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/pom.xml b/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/pom.xml
index 643f3f5..e39a219 100644
--- a/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/pom.xml
+++ b/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/pom.xml
@@ -40,6 +40,10 @@
             <artifactId>nifi-security-utils</artifactId>
         </dependency>
         <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-properties</artifactId>
+        </dependency>
+        <dependency>
             <groupId>org.springframework.security</groupId>
             <artifactId>spring-security-ldap</artifactId>
         </dependency>
@@ -59,6 +63,17 @@
             <groupId>org.apache.commons</groupId>
             <artifactId>commons-lang3</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.apache.directory.server</groupId>
+            <artifactId>apacheds-all</artifactId>
+            <version>2.0.0-M20</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-expression-language</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
     <name>nifi-ldap-iaa-providers</name>
 </project>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi/blob/6bc6f955/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/src/main/java/org/apache/nifi/ldap/tenants/LdapUserGroupProvider.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/src/main/java/org/apache/nifi/ldap/tenants/LdapUserGroupProvider.java b/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/src/main/java/org/apache/nifi/ldap/tenants/LdapUserGroupProvider.java
new file mode 100644
index 0000000..d7d030c
--- /dev/null
+++ b/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/src/main/java/org/apache/nifi/ldap/tenants/LdapUserGroupProvider.java
@@ -0,0 +1,748 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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 org.apache.nifi.ldap.tenants;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.authentication.exception.ProviderDestructionException;
+import org.apache.nifi.authorization.AuthorizerConfigurationContext;
+import org.apache.nifi.authorization.Group;
+import org.apache.nifi.authorization.User;
+import org.apache.nifi.authorization.UserAndGroups;
+import org.apache.nifi.authorization.UserGroupProvider;
+import org.apache.nifi.authorization.UserGroupProviderInitializationContext;
+import org.apache.nifi.authorization.annotation.AuthorizerContext;
+import org.apache.nifi.authorization.exception.AuthorizationAccessException;
+import org.apache.nifi.authorization.exception.AuthorizerCreationException;
+import org.apache.nifi.authorization.util.IdentityMapping;
+import org.apache.nifi.authorization.util.IdentityMappingUtil;
+import org.apache.nifi.components.PropertyValue;
+import org.apache.nifi.util.NiFiProperties;
+import org.apache.nifi.ldap.LdapAuthenticationStrategy;
+import org.apache.nifi.ldap.LdapsSocketFactory;
+import org.apache.nifi.ldap.ReferralStrategy;
+import org.apache.nifi.security.util.SslContextFactory;
+import org.apache.nifi.security.util.SslContextFactory.ClientAuth;
+import org.apache.nifi.util.FormatUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.ldap.control.PagedResultsDirContextProcessor;
+import org.springframework.ldap.core.ContextSource;
+import org.springframework.ldap.core.DirContextAdapter;
+import org.springframework.ldap.core.DirContextOperations;
+import org.springframework.ldap.core.DirContextProcessor;
+import org.springframework.ldap.core.LdapTemplate;
+import org.springframework.ldap.core.LdapTemplate.NullDirContextProcessor;
+import org.springframework.ldap.core.support.AbstractContextMapper;
+import org.springframework.ldap.core.support.AbstractTlsDirContextAuthenticationStrategy;
+import org.springframework.ldap.core.support.DefaultTlsDirContextAuthenticationStrategy;
+import org.springframework.ldap.core.support.LdapContextSource;
+import org.springframework.ldap.core.support.SimpleDirContextAuthenticationStrategy;
+import org.springframework.ldap.core.support.SingleContextSource;
+import org.springframework.ldap.filter.AndFilter;
+import org.springframework.ldap.filter.EqualsFilter;
+import org.springframework.ldap.filter.HardcodedFilter;
+
+import javax.naming.Context;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.SearchControls;
+import javax.net.ssl.SSLContext;
+import java.io.IOException;
+import java.security.KeyManagementException;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.CertificateException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Abstract LDAP based implementation of a login identity provider.
+ */
+public class LdapUserGroupProvider implements UserGroupProvider {
+
+    private static final Logger logger = LoggerFactory.getLogger(LdapUserGroupProvider.class);
+
+    public static final String PROP_CONNECT_TIMEOUT = "Connect Timeout";
+    public static final String PROP_READ_TIMEOUT = "Read Timeout";
+    public static final String PROP_AUTHENTICATION_STRATEGY = "Authentication Strategy";
+    public static final String PROP_MANAGER_DN = "Manager DN";
+    public static final String PROP_MANAGER_PASSWORD = "Manager Password";
+    public static final String PROP_REFERRAL_STRATEGY = "Referral Strategy";
+    public static final String PROP_URL = "Url";
+    public static final String PROP_PAGE_SIZE = "Page Size";
+
+    public static final String PROP_USER_SEARCH_BASE = "User Search Base";
+    public static final String PROP_USER_OBJECT_CLASS = "User Object Class";
+    public static final String PROP_USER_SEARCH_SCOPE = "User Search Scope";
+    public static final String PROP_USER_SEARCH_FILTER = "User Search Filter";
+    public static final String PROP_USER_IDENTITY_ATTRIBUTE = "User Identity Attribute";
+    public static final String PROP_USER_GROUP_ATTRIBUTE = "User Group Name Attribute";
+
+    public static final String PROP_GROUP_SEARCH_BASE = "Group Search Base";
+    public static final String PROP_GROUP_OBJECT_CLASS = "Group Object Class";
+    public static final String PROP_GROUP_SEARCH_SCOPE = "Group Search Scope";
+    public static final String PROP_GROUP_SEARCH_FILTER = "Group Search Filter";
+    public static final String PROP_GROUP_NAME_ATTRIBUTE = "Group Name Attribute";
+    public static final String PROP_GROUP_MEMBER_ATTRIBUTE = "Group Member Attribute";
+
+    public static final String PROP_SYNC_INTERVAL = "Sync Interval";
+
+    private List<IdentityMapping> identityMappings;
+    private NiFiProperties properties;
+
+    private ScheduledExecutorService ldapSync;
+    private AtomicReference<TenantHolder> tenants = new AtomicReference<>(null);
+
+    private String userSearchBase;
+    private SearchScope userSearchScope;
+    private String userSearchFilter;
+    private String userIdentityAttribute;
+    private String userObjectClass;
+    private String userGroupNameAttribute;
+    private boolean useDnForUserIdentity;
+    private boolean performUserSearch;
+
+    private String groupSearchBase;
+    private SearchScope groupSearchScope;
+    private String groupSearchFilter;
+    private String groupMemberAttribute;
+    private String groupNameAttribute;
+    private String groupObjectClass;
+    private boolean useDnForGroupName;
+    private boolean performGroupSearch;
+
+    private Integer pageSize;
+
+    @Override
+    public void initialize(final UserGroupProviderInitializationContext initializationContext) throws AuthorizerCreationException {
+        ldapSync = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
+            final ThreadFactory factory = Executors.defaultThreadFactory();
+
+            @Override
+            public Thread newThread(Runnable r) {
+                final Thread thread = factory.newThread(r);
+                thread.setName(String.format("%s (%s) - background sync thread", getClass().getSimpleName(), initializationContext.getIdentifier()));
+                return thread;
+            }
+        });
+    }
+
+    @Override
+    public void onConfigured(final AuthorizerConfigurationContext configurationContext) throws AuthorizerCreationException {
+        final LdapContextSource context = new LdapContextSource();
+
+        final Map<String, Object> baseEnvironment = new HashMap<>();
+
+        // connect/read time out
+        setTimeout(configurationContext, baseEnvironment, PROP_CONNECT_TIMEOUT, "com.sun.jndi.ldap.connect.timeout");
+        setTimeout(configurationContext, baseEnvironment, PROP_READ_TIMEOUT, "com.sun.jndi.ldap.read.timeout");
+
+        // authentication strategy
+        final PropertyValue rawAuthenticationStrategy = configurationContext.getProperty(PROP_AUTHENTICATION_STRATEGY);
+        final LdapAuthenticationStrategy authenticationStrategy;
+        try {
+            authenticationStrategy = LdapAuthenticationStrategy.valueOf(rawAuthenticationStrategy.getValue());
+        } catch (final IllegalArgumentException iae) {
+            throw new AuthorizerCreationException(String.format("Unrecognized authentication strategy '%s'. Possible values are [%s]",
+                    rawAuthenticationStrategy.getValue(), StringUtils.join(LdapAuthenticationStrategy.values(), ", ")));
+        }
+
+        switch (authenticationStrategy) {
+            case ANONYMOUS:
+                context.setAnonymousReadOnly(true);
+                break;
+            default:
+                final String userDn = configurationContext.getProperty(PROP_MANAGER_DN).getValue();
+                final String password = configurationContext.getProperty(PROP_MANAGER_PASSWORD).getValue();
+
+                context.setUserDn(userDn);
+                context.setPassword(password);
+
+                switch (authenticationStrategy) {
+                    case SIMPLE:
+                        context.setAuthenticationStrategy(new SimpleDirContextAuthenticationStrategy());
+                        break;
+                    case LDAPS:
+                        context.setAuthenticationStrategy(new SimpleDirContextAuthenticationStrategy());
+
+                        // indicate a secure connection
+                        baseEnvironment.put(Context.SECURITY_PROTOCOL, "ssl");
+
+                        // get the configured ssl context
+                        final SSLContext ldapsSslContext = getConfiguredSslContext(configurationContext);
+                        if (ldapsSslContext != null) {
+                            // initialize the ldaps socket factory prior to use
+                            LdapsSocketFactory.initialize(ldapsSslContext.getSocketFactory());
+                            baseEnvironment.put("java.naming.ldap.factory.socket", LdapsSocketFactory.class.getName());
+                        }
+                        break;
+                    case START_TLS:
+                        final AbstractTlsDirContextAuthenticationStrategy tlsAuthenticationStrategy = new DefaultTlsDirContextAuthenticationStrategy();
+
+                        // shutdown gracefully
+                        final String rawShutdownGracefully = configurationContext.getProperty("TLS - Shutdown Gracefully").getValue();
+                        if (StringUtils.isNotBlank(rawShutdownGracefully)) {
+                            final boolean shutdownGracefully = Boolean.TRUE.toString().equalsIgnoreCase(rawShutdownGracefully);
+                            tlsAuthenticationStrategy.setShutdownTlsGracefully(shutdownGracefully);
+                        }
+
+                        // get the configured ssl context
+                        final SSLContext startTlsSslContext = getConfiguredSslContext(configurationContext);
+                        if (startTlsSslContext != null) {
+                            tlsAuthenticationStrategy.setSslSocketFactory(startTlsSslContext.getSocketFactory());
+                        }
+
+                        // set the authentication strategy
+                        context.setAuthenticationStrategy(tlsAuthenticationStrategy);
+                        break;
+                }
+                break;
+        }
+
+        // referrals
+        final String rawReferralStrategy = configurationContext.getProperty(PROP_REFERRAL_STRATEGY).getValue();
+
+        final ReferralStrategy referralStrategy;
+        try {
+            referralStrategy = ReferralStrategy.valueOf(rawReferralStrategy);
+        } catch (final IllegalArgumentException iae) {
+            throw new AuthorizerCreationException(String.format("Unrecognized referral strategy '%s'. Possible values are [%s]",
+                    rawReferralStrategy, StringUtils.join(ReferralStrategy.values(), ", ")));
+        }
+
+        // using the value as this needs to be the lowercase version while the value is configured with the enum constant
+        context.setReferral(referralStrategy.getValue());
+
+        // url
+        final String urls = configurationContext.getProperty(PROP_URL).getValue();
+
+        if (StringUtils.isBlank(urls)) {
+            throw new AuthorizerCreationException("LDAP identity provider 'Url' must be specified.");
+        }
+
+        // connection
+        context.setUrls(StringUtils.split(urls));
+
+        // raw user search base
+        final PropertyValue rawUserSearchBase = configurationContext.getProperty(PROP_USER_SEARCH_BASE);
+        final PropertyValue rawUserObjectClass = configurationContext.getProperty(PROP_USER_OBJECT_CLASS);
+        final PropertyValue rawUserSearchScope = configurationContext.getProperty(PROP_USER_SEARCH_SCOPE);
+
+        // if loading the users, ensure the object class set
+        if (rawUserSearchBase.isSet() && !rawUserObjectClass.isSet()) {
+            throw new AuthorizerCreationException("LDAP user group provider 'User Object Class' must be specified when 'User Search Base' is set.");
+        }
+
+        // if loading the users, ensure the search scope is set
+        if (rawUserSearchBase.isSet() && !rawUserSearchScope.isSet()) {
+            throw new AuthorizerCreationException("LDAP user group provider 'User Search Scope' must be specified when 'User Search Base' is set.");
+        }
+
+        // user search criteria
+        userSearchBase = rawUserSearchBase.getValue();
+        userObjectClass = rawUserObjectClass.getValue();
+        userSearchFilter = configurationContext.getProperty(PROP_USER_SEARCH_FILTER).getValue();
+        userIdentityAttribute = configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE).getValue();
+        userGroupNameAttribute = configurationContext.getProperty(PROP_USER_GROUP_ATTRIBUTE).getValue();
+
+        try {
+            userSearchScope = SearchScope.valueOf(rawUserSearchScope.getValue());
+        } catch (final IllegalArgumentException iae) {
+            throw new AuthorizerCreationException(String.format("Unrecognized user search scope '%s'. Possible values are [%s]",
+                    rawUserSearchScope.getValue(), StringUtils.join(SearchScope.values(), ", ")));
+        }
+
+        // determine user behavior
+        useDnForUserIdentity = StringUtils.isBlank(userIdentityAttribute);
+        performUserSearch = StringUtils.isNotBlank(userSearchBase);
+
+        // raw group search criteria
+        final PropertyValue rawGroupSearchBase = configurationContext.getProperty(PROP_GROUP_SEARCH_BASE);
+        final PropertyValue rawGroupObjectClass = configurationContext.getProperty(PROP_GROUP_OBJECT_CLASS);
+        final PropertyValue rawGroupSearchScope = configurationContext.getProperty(PROP_GROUP_SEARCH_SCOPE);
+
+        // if loading the groups, ensure the object class is set
+        if (rawGroupSearchBase.isSet() && !rawGroupObjectClass.isSet()) {
+            throw new AuthorizerCreationException("LDAP user group provider 'Group Object Class' must be specified when 'Group Search Base' is set.");
+        }
+
+        // if loading the groups, ensure the search scope is set
+        if (rawGroupSearchBase.isSet() && !rawGroupSearchScope.isSet()) {
+            throw new AuthorizerCreationException("LDAP user group provider 'Group Search Scope' must be specified when 'Group Search Base' is set.");
+        }
+
+        // group search criteria
+        groupSearchBase = rawGroupSearchBase.getValue();
+        groupObjectClass = rawGroupObjectClass.getValue();
+        groupSearchFilter = configurationContext.getProperty(PROP_GROUP_SEARCH_FILTER).getValue();
+        groupNameAttribute = configurationContext.getProperty(PROP_GROUP_NAME_ATTRIBUTE).getValue();
+        groupMemberAttribute = configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE).getValue();
+
+        try {
+            groupSearchScope = SearchScope.valueOf(rawGroupSearchScope.getValue());
+        } catch (final IllegalArgumentException iae) {
+            throw new AuthorizerCreationException(String.format("Unrecognized group search scope '%s'. Possible values are [%s]",
+                    rawGroupSearchScope.getValue(), StringUtils.join(SearchScope.values(), ", ")));
+        }
+
+        // determine group behavior
+        useDnForGroupName = StringUtils.isBlank(groupNameAttribute);
+        performGroupSearch = StringUtils.isNotBlank(groupSearchBase);
+
+        // ensure we are either searching users or groups (at least one must be specified)
+        if (!performUserSearch && !performGroupSearch) {
+            throw new AuthorizerCreationException("LDAP user group provider 'User Search Base' or 'Group Search Base' must be specified.");
+        }
+
+        // ensure group member attribute is set if searching groups but not users
+        if (performGroupSearch && !performUserSearch && StringUtils.isBlank(groupMemberAttribute)) {
+            throw new AuthorizerCreationException("'Group Member Attribute' is required when searching groups but not users.");
+        }
+
+        // get the page size if configured
+        final PropertyValue rawPageSize = configurationContext.getProperty(PROP_PAGE_SIZE);
+        if (rawPageSize.isSet() && StringUtils.isNotBlank(rawPageSize.getValue())) {
+            pageSize = rawPageSize.asInteger();
+        }
+
+        // extract the identity mappings from nifi.properties if any are provided
+        identityMappings = Collections.unmodifiableList(IdentityMappingUtil.getIdentityMappings(properties));
+
+        // set the base environment is necessary
+        if (!baseEnvironment.isEmpty()) {
+            context.setBaseEnvironmentProperties(baseEnvironment);
+        }
+
+        try {
+            // handling initializing beans
+            context.afterPropertiesSet();
+        } catch (final Exception e) {
+            throw new AuthorizerCreationException(e.getMessage(), e);
+        }
+
+        final PropertyValue rawSyncInterval = configurationContext.getProperty(PROP_SYNC_INTERVAL);
+        final long syncInterval;
+        if (rawSyncInterval.isSet()) {
+            try {
+                syncInterval = FormatUtils.getTimeDuration(rawSyncInterval.getValue(), TimeUnit.MILLISECONDS);
+            } catch (final IllegalArgumentException iae) {
+                throw new AuthorizerCreationException(String.format("The %s '%s' is not a valid time duration", PROP_SYNC_INTERVAL, rawSyncInterval.getValue()));
+            }
+        } else {
+            throw new AuthorizerCreationException("The 'Sync Interval' must be specified.");
+        }
+
+        try {
+            // perform the initial load, tenants must be loaded as the configured UserGroupProvider is supplied
+            // to the AccessPolicyProvider for granting initial permissions
+            load(context);
+
+            // ensure the tenants were successfully synced
+            if (tenants.get() == null) {
+                throw new AuthorizerCreationException("Unable to sync users and groups.");
+            }
+
+            // schedule the background thread to load the users/groups
+            ldapSync.scheduleWithFixedDelay(() -> load(context), syncInterval, syncInterval, TimeUnit.SECONDS);
+        } catch (final AuthorizationAccessException e) {
+            throw new AuthorizerCreationException(e);
+        }
+    }
+
+    @Override
+    public Set<User> getUsers() throws AuthorizationAccessException {
+        return tenants.get().getAllUsers();
+    }
+
+    @Override
+    public User getUser(String identifier) throws AuthorizationAccessException {
+        return tenants.get().getUsersById().get(identifier);
+    }
+
+    @Override
+    public User getUserByIdentity(String identity) throws AuthorizationAccessException {
+        return tenants.get().getUser(identity);
+    }
+
+    @Override
+    public Set<Group> getGroups() throws AuthorizationAccessException {
+        return tenants.get().getAllGroups();
+    }
+
+    @Override
+    public Group getGroup(String identifier) throws AuthorizationAccessException {
+        return tenants.get().getGroupsById().get(identifier);
+    }
+
+    @Override
+    public UserAndGroups getUserAndGroups(String identity) throws AuthorizationAccessException {
+        final TenantHolder holder = tenants.get();
+        return new UserAndGroups() {
+            @Override
+            public User getUser() {
+                return holder.getUser(identity);
+            }
+
+            @Override
+            public Set<Group> getGroups() {
+                return holder.getGroups(identity);
+            }
+        };
+    }
+
+    /**
+     * Reloads the tenants.
+     */
+    private void load(final ContextSource contextSource) {
+        // create the ldapTemplate based on the context source. use a single source context to use the same connection
+        // to support paging when configured
+        final SingleContextSource singleContextSource = new SingleContextSource(contextSource.getReadOnlyContext());
+        final LdapTemplate ldapTemplate = new LdapTemplate(singleContextSource);
+
+        try {
+            final List<User> userList = new ArrayList<>();
+            final List<Group> groupList = new ArrayList<>();
+
+            // group dn -> user identifiers lookup
+            final Map<String, Set<String>> groupDnToUserIdentifierMappings = new HashMap<>();
+
+            // user dn -> user lookup
+            final Map<String, User> userDnLookup = new HashMap<>();
+
+            if (performUserSearch) {
+                // search controls
+                final SearchControls userControls = new SearchControls();
+                userControls.setSearchScope(userSearchScope.ordinal());
+
+                // consider paging support for users
+                final DirContextProcessor userProcessor;
+                if (pageSize == null) {
+                    userProcessor = new NullDirContextProcessor();
+                } else {
+                    userProcessor = new PagedResultsDirContextProcessor(pageSize);
+                }
+
+                // looking for objects matching the user object class
+                final AndFilter userFilter = new AndFilter();
+                userFilter.and(new EqualsFilter("objectClass", userObjectClass));
+
+                // if a filter has been provided by the user, we add it to the filter
+                if (StringUtils.isNotBlank(userSearchFilter)) {
+                    userFilter.and(new HardcodedFilter(userSearchFilter));
+                }
+
+                do {
+                    userList.addAll(ldapTemplate.search(userSearchBase, userFilter.encode(), userControls, new AbstractContextMapper<User>() {
+                        @Override
+                        protected User doMapFromContext(DirContextOperations ctx) {
+                            final String dn = ctx.getDn().toString();
+
+                            // get the user identity
+                            final String identity = getUserIdentity(ctx);
+
+                            // build the user
+                            final User user = new User.Builder().identifierGenerateFromSeed(identity).identity(identity).build();
+                            userDnLookup.put(dn, user);
+
+                            if (StringUtils.isNotBlank(userGroupNameAttribute)) {
+                                final Attribute attributeGroups = ctx.getAttributes().get(userGroupNameAttribute);
+
+                                if (attributeGroups == null) {
+                                    logger.warn("User group name attribute [" + userGroupNameAttribute + "] does not exist. Ignoring group membership.");
+                                } else {
+                                    try {
+                                        final NamingEnumeration<String> groupDns = (NamingEnumeration<String>) attributeGroups.getAll();
+                                        while (groupDns.hasMoreElements()) {
+                                            // store the group dn -> user identifier mapping
+                                            groupDnToUserIdentifierMappings.computeIfAbsent(groupDns.next(), g -> new HashSet<>()).add(user.getIdentifier());
+                                        }
+                                    } catch (NamingException e) {
+                                        throw new AuthorizationAccessException("Error while retrieving user group name attribute [" + userIdentityAttribute + "].");
+                                    }
+                                }
+                            }
+
+                            return user;
+                        }
+                    }, userProcessor));
+                } while (hasMorePages(userProcessor));
+            }
+
+            if (performGroupSearch) {
+                final SearchControls groupControls = new SearchControls();
+                groupControls.setSearchScope(groupSearchScope.ordinal());
+
+                // consider paging support for groups
+                final DirContextProcessor groupProcessor;
+                if (pageSize == null) {
+                    groupProcessor = new NullDirContextProcessor();
+                } else {
+                    groupProcessor = new PagedResultsDirContextProcessor(pageSize);
+                }
+
+                // looking for objects matching the group object class
+                AndFilter groupFilter = new AndFilter();
+                groupFilter.and(new EqualsFilter("objectClass", groupObjectClass));
+
+                // if a filter has been provided by the user, we add it to the filter
+                if(StringUtils.isNotBlank(groupSearchFilter)) {
+                    groupFilter.and(new HardcodedFilter(groupSearchFilter));
+                }
+
+                do {
+                    groupList.addAll(ldapTemplate.search(groupSearchBase, groupFilter.encode(), groupControls, new AbstractContextMapper<Group>() {
+                        @Override
+                        protected Group doMapFromContext(DirContextOperations ctx) {
+                            final String dn = ctx.getDn().toString();
+
+                            // get the group identity
+                            final String name = getGroupName(ctx);
+
+                            if (!StringUtils.isBlank(groupMemberAttribute)) {
+                                Attribute attributeUsers = ctx.getAttributes().get(groupMemberAttribute);
+                                if (attributeUsers == null) {
+                                    logger.warn("Group member attribute [" + groupMemberAttribute + "] does not exist. Ignoring group membership.");
+                                } else {
+                                    try {
+                                        final NamingEnumeration<String> userDns = (NamingEnumeration<String>) attributeUsers.getAll();
+                                        while (userDns.hasMoreElements()) {
+                                            final String userDn = userDns.next();
+
+                                            if (performUserSearch) {
+                                                // find the user by dn add the identifier to this group
+                                                final User user = userDnLookup.get(userDn);
+
+                                                // ensure the user is known
+                                                if (user != null) {
+                                                    groupDnToUserIdentifierMappings.computeIfAbsent(dn, g -> new HashSet<>()).add(user.getIdentifier());
+                                                } else {
+                                                    logger.warn(String.format("%s contains member %s but that user was not found while searching users. Ignoring group membership.", name, userDn));
+                                                }
+                                            } else {
+                                                final String userIdentity;
+                                                if (useDnForUserIdentity) {
+                                                    // use the dn to avoid the unnecessary look up
+                                                    userIdentity = userDn;
+                                                } else {
+                                                    // lookup the user to extract the user identity
+                                                    userIdentity = getUserIdentity((DirContextAdapter) ldapTemplate.lookup(userDn));
+                                                }
+
+                                                // build the user
+                                                final User user = new User.Builder().identifierGenerateFromSeed(userIdentity).identity(userIdentity).build();
+
+                                                // add this user
+                                                userList.add(user);
+                                                groupDnToUserIdentifierMappings.computeIfAbsent(dn, g -> new HashSet<>()).add(user.getIdentifier());
+                                            }
+                                        }
+                                    } catch (NamingException e) {
+                                        throw new AuthorizationAccessException("Error while retrieving group name attribute [" + groupNameAttribute + "].");
+                                    }
+                                }
+                            }
+
+                            // build this group
+                            final Group.Builder groupBuilder = new Group.Builder().identifierGenerateFromSeed(name).name(name);
+
+                            // add all users that were associated with this group dn
+                            if (groupDnToUserIdentifierMappings.containsKey(dn)) {
+                                groupDnToUserIdentifierMappings.remove(dn).forEach(userIdentifier -> groupBuilder.addUser(userIdentifier));
+                            }
+
+                            return groupBuilder.build();
+                        }
+                    }, groupProcessor));
+                } while (hasMorePages(groupProcessor));
+
+                // any remaining groupDn's were referenced by a user but not found while searching groups
+                groupDnToUserIdentifierMappings.forEach((groupDn, userIdentifiers) -> {
+                    logger.warn(String.format("[%s] are members of %s but that group was not found while searching users. Ignoring group membership.",
+                            StringUtils.join(userIdentifiers, ", "), groupDn));
+                });
+            } else {
+                // groups are not being searched so lookup any groups identified while searching users
+                groupDnToUserIdentifierMappings.forEach((groupDn, userIdentifiers) -> {
+                    final String groupName;
+                    if (useDnForGroupName) {
+                        // use the dn to avoid the unnecessary look up
+                        groupName = groupDn;
+                    } else {
+                        groupName = getGroupName((DirContextAdapter) ldapTemplate.lookup(groupDn));
+                    }
+
+                    // define the group
+                    final Group.Builder groupBuilder = new Group.Builder().identifierGenerateFromSeed(groupName).name(groupName);
+
+                    // add each user
+                    userIdentifiers.forEach(userIdentifier -> groupBuilder.addUser(userIdentifier));
+
+                    // build the group
+                    groupList.add(groupBuilder.build());
+                });
+            }
+
+            // record the updated tenants
+            tenants.set(new TenantHolder(new HashSet<>(userList), new HashSet<>(groupList)));
+        } finally {
+            singleContextSource.destroy();
+        }
+    }
+
+    private boolean hasMorePages(final DirContextProcessor processor ) {
+        return processor instanceof PagedResultsDirContextProcessor && ((PagedResultsDirContextProcessor) processor).hasMore();
+    }
+
+    private String getUserIdentity(final DirContextOperations ctx) {
+        final String identity;
+
+        if (useDnForUserIdentity) {
+            identity = ctx.getDn().toString();
+        } else {
+            final Attribute attributeName = ctx.getAttributes().get(userIdentityAttribute);
+            if (attributeName == null) {
+                throw new AuthorizationAccessException("User identity attribute [" + userIdentityAttribute + "] does not exist.");
+            }
+
+            try {
+                identity = (String) attributeName.get();
+            } catch (NamingException e) {
+                throw new AuthorizationAccessException("Error while retrieving user name attribute [" + userIdentityAttribute + "].");
+            }
+        }
+
+        return IdentityMappingUtil.mapIdentity(identity, identityMappings);
+    }
+
+    private String getGroupName(final DirContextOperations ctx) {
+        final String name;
+
+        if (useDnForGroupName) {
+            name = ctx.getDn().toString();
+        } else {
+            final Attribute attributeName = ctx.getAttributes().get(groupNameAttribute);
+            if (attributeName == null) {
+                throw new AuthorizationAccessException("Group identity attribute [" + groupNameAttribute + "] does not exist.");
+            }
+
+            try {
+                name = (String) attributeName.get();
+            } catch (NamingException e) {
+                throw new AuthorizationAccessException("Error while retrieving group name attribute [" + groupNameAttribute + "].");
+            }
+        }
+
+        return name;
+    }
+
+    @AuthorizerContext
+    public void setNiFiProperties(NiFiProperties properties) {
+        this.properties = properties;
+    }
+
+    @Override
+    public final void preDestruction() throws ProviderDestructionException {
+        ldapSync.shutdown();
+        try {
+            if (!ldapSync.awaitTermination(10000, TimeUnit.MILLISECONDS)) {
+                logger.info("Failed to stop ldap sync thread in 10 sec. Terminating");
+                ldapSync.shutdownNow();
+            }
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+        }
+    }
+
+    private void setTimeout(final AuthorizerConfigurationContext configurationContext,
+                            final Map<String, Object> baseEnvironment,
+                            final String configurationProperty,
+                            final String environmentKey) {
+
+        final PropertyValue rawTimeout = configurationContext.getProperty(configurationProperty);
+        if (rawTimeout.isSet()) {
+            try {
+                final Long timeout = FormatUtils.getTimeDuration(rawTimeout.getValue(), TimeUnit.MILLISECONDS);
+                baseEnvironment.put(environmentKey, timeout.toString());
+            } catch (final IllegalArgumentException iae) {
+                throw new AuthorizerCreationException(String.format("The %s '%s' is not a valid time duration", configurationProperty, rawTimeout));
+            }
+        }
+    }
+
+    private SSLContext getConfiguredSslContext(final AuthorizerConfigurationContext configurationContext) {
+        final String rawKeystore = configurationContext.getProperty("TLS - Keystore").getValue();
+        final String rawKeystorePassword = configurationContext.getProperty("TLS - Keystore Password").getValue();
+        final String rawKeystoreType = configurationContext.getProperty("TLS - Keystore Type").getValue();
+        final String rawTruststore = configurationContext.getProperty("TLS - Truststore").getValue();
+        final String rawTruststorePassword = configurationContext.getProperty("TLS - Truststore Password").getValue();
+        final String rawTruststoreType = configurationContext.getProperty("TLS - Truststore Type").getValue();
+        final String rawClientAuth = configurationContext.getProperty("TLS - Client Auth").getValue();
+        final String rawProtocol = configurationContext.getProperty("TLS - Protocol").getValue();
+
+        // create the ssl context
+        final SSLContext sslContext;
+        try {
+            if (StringUtils.isBlank(rawKeystore) && StringUtils.isBlank(rawTruststore)) {
+                sslContext = null;
+            } else {
+                // ensure the protocol is specified
+                if (StringUtils.isBlank(rawProtocol)) {
+                    throw new AuthorizerCreationException("TLS - Protocol must be specified.");
+                }
+
+                if (StringUtils.isBlank(rawKeystore)) {
+                    sslContext = SslContextFactory.createTrustSslContext(rawTruststore, rawTruststorePassword.toCharArray(), rawTruststoreType, rawProtocol);
+                } else if (StringUtils.isBlank(rawTruststore)) {
+                    sslContext = SslContextFactory.createSslContext(rawKeystore, rawKeystorePassword.toCharArray(), rawKeystoreType, rawProtocol);
+                } else {
+                    // determine the client auth if specified
+                    final ClientAuth clientAuth;
+                    if (StringUtils.isBlank(rawClientAuth)) {
+                        clientAuth = ClientAuth.NONE;
+                    } else {
+                        try {
+                            clientAuth = ClientAuth.valueOf(rawClientAuth);
+                        } catch (final IllegalArgumentException iae) {
+                            throw new AuthorizerCreationException(String.format("Unrecognized client auth '%s'. Possible values are [%s]",
+                                    rawClientAuth, StringUtils.join(ClientAuth.values(), ", ")));
+                        }
+                    }
+
+                    sslContext = SslContextFactory.createSslContext(rawKeystore, rawKeystorePassword.toCharArray(), rawKeystoreType,
+                            rawTruststore, rawTruststorePassword.toCharArray(), rawTruststoreType, clientAuth, rawProtocol);
+                }
+            }
+        } catch (final KeyStoreException | NoSuchAlgorithmException | CertificateException | UnrecoverableKeyException | KeyManagementException | IOException e) {
+            throw new AuthorizerCreationException(e.getMessage(), e);
+        }
+
+        return sslContext;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/6bc6f955/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/src/main/java/org/apache/nifi/ldap/tenants/SearchScope.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/src/main/java/org/apache/nifi/ldap/tenants/SearchScope.java b/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/src/main/java/org/apache/nifi/ldap/tenants/SearchScope.java
new file mode 100644
index 0000000..66a3f08
--- /dev/null
+++ b/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/src/main/java/org/apache/nifi/ldap/tenants/SearchScope.java
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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 org.apache.nifi.ldap.tenants;
+
+/**
+ * Scope for searching a directory server.
+ */
+public enum SearchScope {
+
+    OBJECT,
+    ONE_LEVEL,
+    SUBTREE;
+
+}