Repository: nifi-registry Updated Branches: refs/heads/master 5042c8935 -> 4f00e36d4
NIFIREG-109 Expand LdapUserGroupProvider Config Expands on user-group configurability by allowing a referenced user or group attribute to be specified when determining group membership. Updates corresponding documentation. Adds test cases. This closes #82. Signed-off-by: Bryan Bende <[email protected]> Project: http://git-wip-us.apache.org/repos/asf/nifi-registry/repo Commit: http://git-wip-us.apache.org/repos/asf/nifi-registry/commit/4f00e36d Tree: http://git-wip-us.apache.org/repos/asf/nifi-registry/tree/4f00e36d Diff: http://git-wip-us.apache.org/repos/asf/nifi-registry/diff/4f00e36d Branch: refs/heads/master Commit: 4f00e36d41d7b235fc99e7597eb70107fda73bec Parents: 5042c89 Author: Kevin Doran <[email protected]> Authored: Tue Jan 9 17:08:59 2018 -0500 Committer: Bryan Bende <[email protected]> Committed: Thu Jan 11 16:35:24 2018 -0500 ---------------------------------------------------------------------- .../src/main/asciidoc/administration-guide.adoc | 18 +- nifi-registry-framework/pom.xml | 6 + .../ldap/tenants/LdapUserGroupProvider.java | 115 +++- .../ldap/tenants/LdapUserGroupProviderTest.java | 639 +++++++++++++++++++ .../src/test/resources/nifi-example.ldif | 166 +++++ .../src/main/resources/conf/authorizers.xml | 20 +- 6 files changed, 930 insertions(+), 34 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/4f00e36d/nifi-registry-docs/src/main/asciidoc/administration-guide.adoc ---------------------------------------------------------------------- diff --git a/nifi-registry-docs/src/main/asciidoc/administration-guide.adoc b/nifi-registry-docs/src/main/asciidoc/administration-guide.adoc index ca29e34..6bff730 100644 --- a/nifi-registry-docs/src/main/asciidoc/administration-guide.adoc +++ b/nifi-registry-docs/src/main/asciidoc/administration-guide.adoc @@ -261,14 +261,15 @@ This will sync users and groups from a directory server and will present them in * 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. +* 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. The value of this property is the name of the attribute in the user LDAP entry that associates them with a group. The value of that user attribute could be a dn or group name for instance. What value is expected is configured in the 'User Group Name Attribute - Referenced Group Attribute'. +* User Group Name Attribute - Referenced Group Attribute - If blank, the value of the attribute defined in 'User Group Name Attribute' is expected to be the full dn of the group. If not blank, this property will define the attribute of the group LDAP entry that the value of the attribute defined in 'User Group Name Attribute' is referencing (i.e. name). Use of this property requires that 'Group Search Base' is also configured. * 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 - 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. - +* 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 membership being defined through 'User Group Name Attribute' if set. The value of this property is the name of the attribute in the group LDAP entry that associates them with a user. The value of that group attribute could be a dn or memberUid for instance. What value is expected is configured in the 'Group Member Attribute - Referenced User Attribute'. (i.e. member: cn=User 1,ou=users,o=nifi vs. memberUid: user1) +* Group Member Attribute - Referenced User Attribute - If blank, the value of the attribute defined in 'Group Member Attribute' is expected to be the full dn of the user. If not blank, this property will define the attribute of the user LDAP entry that the value of the attribute defined in 'Group Member Attribute' is referencing (i.e. uid). Use of this property requires that 'User Search Base' is also configured. (i.e. member: cn=User 1,ou=users,o=nifi vs. memberUid: user1) Another option for the UserGroupProvider are composite implementations. This means that multiple sources/implementations can be configured and composed. For instance, an admin can configure users/groups to be loaded from a file and a directory server. There are two composite implementations, one that supports multiple UserGroupProviders and one that supports multiple UserGroupProviders and a single configurable UserGroupProvider. @@ -306,7 +307,7 @@ This initial admin user is granted access to the UI and given the ability to cre The value of this property could be a certificate DN , LDAP identity (DN or username), or a Kerberos principal. If you are the NiFi Registry administrator, add yourself as the âInitial Admin Identityâ. -Here is an example LDAP entry using the name John Smith: +Here is an example certificate DN entry using the name John Smith: ---- <authorizers> @@ -440,6 +441,7 @@ An Authorizer using an LdapUserGroupProvider would be configured as: <property name="User Search Filter"></property> <property name="User Identity Attribute">cn</property> <property name="User Group Name Attribute"></property> + <property name="User Group Name Attribute - Referenced Group Attribute"></property> <property name="Group Search Base">ou=groups,o=nifi</property> <property name="Group Object Class">groupOfNames</property> @@ -447,6 +449,7 @@ An Authorizer using an LdapUserGroupProvider would be configured as: <property name="Group Search Filter"></property> <property name="Group Name Attribute">cn</property> <property name="Group Member Attribute">member</property> + <property name="Group Member Attribute - Referenced User Attribute"></property> </userGroupProvider> <accessPolicyProvider> @@ -514,6 +517,7 @@ the member attribute of each group. The users from LDAP will be read only while <property name="User Search Filter"></property> <property name="User Identity Attribute">cn</property> <property name="User Group Name Attribute"></property> + <property name="User Group Name Attribute - Referenced Group Attribute"></property> <property name="Group Search Base">ou=groups,o=nifi</property> <property name="Group Object Class">groupOfNames</property> @@ -521,6 +525,7 @@ the member attribute of each group. The users from LDAP will be read only while <property name="Group Search Filter"></property> <property name="Group Name Attribute">cn</property> <property name="Group Member Attribute">member</property> + <property name="Group Member Attribute - Referenced User Attribute"></property> </userGroupProvider> <userGroupProvider> @@ -571,7 +576,7 @@ assume you have downloaded the binary for the nifi-toolkit. The `encrypt-config` command line tool can be used to encrypt NiFi Registry configuration by invoking the tool with the following command: ---- -./bin/encrypt-config nifi-registry [options] +./bin/encrypt-config --nifiRegistry [options] ---- * `-h`,`--help` Show usage information (this message) @@ -655,7 +660,6 @@ Sensitive configuration values are encrypted by the tool by default, however you To encrypt additional properties, specify them as comma-separated values in the `nifi.registry.sensitive.props.additional.keys` property. - If the 'nifi-registry.properties' file already has valid protected values and you wish to protect additional values using the same master key already present in your 'bootstrap.conf', then run the tool without specifying a new key: @@ -663,7 +667,7 @@ same master key already present in your 'bootstrap.conf', then run the tool with # bootstrap.conf already contains master key property # nifi-registy.properties has been updated for nifi.registry.sensitive.props.additional.keys=... -./bin/encrypt-config.sh nifi-registry -b bootstrap.conf -r nifi-registry.properties +./bin/encrypt-config.sh --nifiRegistry -b bootstrap.conf -r nifi-registry.properties ---- [sensistive_property_key_migration] http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/4f00e36d/nifi-registry-framework/pom.xml ---------------------------------------------------------------------- diff --git a/nifi-registry-framework/pom.xml b/nifi-registry-framework/pom.xml index e382745..423fbd5 100644 --- a/nifi-registry-framework/pom.xml +++ b/nifi-registry-framework/pom.xml @@ -308,5 +308,11 @@ <version>2.2.2</version> <scope>test</scope> </dependency> + <dependency> + <groupId>org.apache.directory.server</groupId> + <artifactId>apacheds-all</artifactId> + <version>2.0.0-M24</version> + <scope>test</scope> + </dependency> </dependencies> </project> http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/4f00e36d/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/tenants/LdapUserGroupProvider.java ---------------------------------------------------------------------- diff --git a/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/tenants/LdapUserGroupProvider.java b/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/tenants/LdapUserGroupProvider.java index 9c97a8e..984c890 100644 --- a/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/tenants/LdapUserGroupProvider.java +++ b/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/tenants/LdapUserGroupProvider.java @@ -103,6 +103,7 @@ public class LdapUserGroupProvider implements UserGroupProvider { 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_USER_GROUP_REFERENCED_GROUP_ATTRIBUTE = "User Group Name Attribute - Referenced Group Attribute"; public static final String PROP_GROUP_SEARCH_BASE = "Group Search Base"; public static final String PROP_GROUP_OBJECT_CLASS = "Group Object Class"; @@ -110,6 +111,8 @@ public class LdapUserGroupProvider implements UserGroupProvider { 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_GROUP_MEMBER_REFERENCED_USER_ATTRIBUTE = "Group Member Attribute - Referenced User Attribute"; + public static final String PROP_SYNC_INTERVAL = "Sync Interval"; @@ -125,6 +128,7 @@ public class LdapUserGroupProvider implements UserGroupProvider { private String userIdentityAttribute; private String userObjectClass; private String userGroupNameAttribute; + private String userGroupReferencedGroupAttribute; private boolean useDnForUserIdentity; private boolean performUserSearch; @@ -132,6 +136,7 @@ public class LdapUserGroupProvider implements UserGroupProvider { private SearchScope groupSearchScope; private String groupSearchFilter; private String groupMemberAttribute; + private String groupMemberReferencedUserAttribute; private String groupNameAttribute; private String groupObjectClass; private boolean useDnForGroupName; @@ -270,6 +275,7 @@ public class LdapUserGroupProvider implements UserGroupProvider { userSearchFilter = configurationContext.getProperty(PROP_USER_SEARCH_FILTER).getValue(); userIdentityAttribute = configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE).getValue(); userGroupNameAttribute = configurationContext.getProperty(PROP_USER_GROUP_ATTRIBUTE).getValue(); + userGroupReferencedGroupAttribute = configurationContext.getProperty(PROP_USER_GROUP_REFERENCED_GROUP_ATTRIBUTE).getValue(); try { userSearchScope = SearchScope.valueOf(rawUserSearchScope.getValue()); @@ -303,6 +309,7 @@ public class LdapUserGroupProvider implements UserGroupProvider { groupSearchFilter = configurationContext.getProperty(PROP_GROUP_SEARCH_FILTER).getValue(); groupNameAttribute = configurationContext.getProperty(PROP_GROUP_NAME_ATTRIBUTE).getValue(); groupMemberAttribute = configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE).getValue(); + groupMemberReferencedUserAttribute = configurationContext.getProperty(PROP_GROUP_MEMBER_REFERENCED_USER_ATTRIBUTE).getValue(); try { groupSearchScope = SearchScope.valueOf(rawGroupSearchScope.getValue()); @@ -325,6 +332,16 @@ public class LdapUserGroupProvider implements UserGroupProvider { throw new SecurityProviderCreationException("'Group Member Attribute' is required when searching groups but not users."); } + // ensure that performUserSearch is set when groupMemberReferencedUserAttribute is specified + if (StringUtils.isNotBlank(groupMemberReferencedUserAttribute) && !performUserSearch) { + throw new SecurityProviderCreationException("''User Search Base' must be set when specifying 'Group Member Attribute - Referenced User Attribute'."); + } + + // ensure that performGroupSearch is set when userGroupReferencedGroupAttribute is specified + if (StringUtils.isNotBlank(userGroupReferencedGroupAttribute) && !performGroupSearch) { + throw new SecurityProviderCreationException("'Group Search Base' must be set when specifying 'User Group Name Attribute - Referenced Group Attribute'."); + } + // get the page size if configured final PropertyValue rawPageSize = configurationContext.getProperty(PROP_PAGE_SIZE); if (rawPageSize.isSet() && StringUtils.isNotBlank(rawPageSize.getValue())) { @@ -430,10 +447,10 @@ public class LdapUserGroupProvider implements UserGroupProvider { final List<Group> groupList = new ArrayList<>(); // group dn -> user identifiers lookup - final Map<String, Set<String>> groupDnToUserIdentifierMappings = new HashMap<>(); + final Map<String, Set<String>> groupToUserIdentifierMappings = new HashMap<>(); // user dn -> user lookup - final Map<String, User> userDnLookup = new HashMap<>(); + final Map<String, User> userLookup = new HashMap<>(); if (performUserSearch) { // search controls @@ -461,14 +478,14 @@ public class LdapUserGroupProvider implements UserGroupProvider { 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); + + // store the user for group member later + userLookup.put(getReferencedUserValue(ctx), user); if (StringUtils.isNotBlank(userGroupNameAttribute)) { final Attribute attributeGroups = ctx.getAttributes().get(userGroupNameAttribute); @@ -477,10 +494,10 @@ public class LdapUserGroupProvider implements UserGroupProvider { 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()); + final NamingEnumeration<String> groupValues = (NamingEnumeration<String>) attributeGroups.getAll(); + while (groupValues.hasMoreElements()) { + // store the group -> user identifier mapping + groupToUserIdentifierMappings.computeIfAbsent(groupValues.next(), g -> new HashSet<>()).add(user.getIdentifier()); } } catch (NamingException e) { throw new AuthorizationAccessException("Error while retrieving user group name attribute [" + userIdentityAttribute + "]."); @@ -524,30 +541,36 @@ public class LdapUserGroupProvider implements UserGroupProvider { // get the group identity final String name = getGroupName(ctx); + // get the value of this group that may associate it to users + final String referencedGroupValue = getReferencedGroupValue(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(); + final NamingEnumeration<String> userValues = (NamingEnumeration<String>) attributeUsers.getAll(); + while (userValues.hasMoreElements()) { + final String userValue = userValues.next(); if (performUserSearch) { - // find the user by dn add the identifier to this group - final User user = userDnLookup.get(userDn); + // find the user by it's referenced attribute and add the identifier to this group + final User user = userLookup.get(userValue); // ensure the user is known if (user != null) { - groupDnToUserIdentifierMappings.computeIfAbsent(dn, g -> new HashSet<>()).add(user.getIdentifier()); + groupToUserIdentifierMappings.computeIfAbsent(referencedGroupValue, 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)); + logger.warn(String.format("%s contains member %s but that user was not found while searching users. Ignoring group membership.", name, userValue)); } } else { + // since performUserSearch is false, then the referenced group attribute must be blank... the user value must be the dn + final String userDn = userValue; + final String userIdentity; if (useDnForUserIdentity) { - // use the dn to avoid the unnecessary look up + // use the user value to avoid the unnecessary look up userIdentity = userDn; } else { // lookup the user to extract the user identity @@ -559,7 +582,7 @@ public class LdapUserGroupProvider implements UserGroupProvider { // add this user userList.add(user); - groupDnToUserIdentifierMappings.computeIfAbsent(dn, g -> new HashSet<>()).add(user.getIdentifier()); + groupToUserIdentifierMappings.computeIfAbsent(referencedGroupValue, g -> new HashSet<>()).add(user.getIdentifier()); } } } catch (NamingException e) { @@ -571,9 +594,9 @@ public class LdapUserGroupProvider implements UserGroupProvider { // 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)); + // add all users that were associated with this referenced group attribute + if (groupToUserIdentifierMappings.containsKey(referencedGroupValue)) { + groupToUserIdentifierMappings.remove(referencedGroupValue).forEach(userIdentifier -> groupBuilder.addUser(userIdentifier)); } return groupBuilder.build(); @@ -582,13 +605,15 @@ public class LdapUserGroupProvider implements UserGroupProvider { } while (hasMorePages(groupProcessor)); // any remaining groupDn's were referenced by a user but not found while searching groups - groupDnToUserIdentifierMappings.forEach((groupDn, userIdentifiers) -> { + groupToUserIdentifierMappings.forEach((referencedGroupValue, 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)); + StringUtils.join(userIdentifiers, ", "), referencedGroupValue)); }); } else { + // since performGroupSearch is false, then the referenced user attribute must be blank... the group value must be the dn + // groups are not being searched so lookup any groups identified while searching users - groupDnToUserIdentifierMappings.forEach((groupDn, userIdentifiers) -> { + groupToUserIdentifierMappings.forEach((groupDn, userIdentifiers) -> { final String groupName; if (useDnForGroupName) { // use the dn to avoid the unnecessary look up @@ -640,6 +665,27 @@ public class LdapUserGroupProvider implements UserGroupProvider { return IdentityMappingUtil.mapIdentity(identity, identityMappings); } + private String getReferencedUserValue(final DirContextOperations ctx) { + final String referencedUserValue; + + if (StringUtils.isBlank(groupMemberReferencedUserAttribute)) { + referencedUserValue = ctx.getDn().toString(); + } else { + final Attribute attributeName = ctx.getAttributes().get(groupMemberReferencedUserAttribute); + if (attributeName == null) { + throw new AuthorizationAccessException("Referenced user value attribute [" + groupMemberReferencedUserAttribute + "] does not exist."); + } + + try { + referencedUserValue = (String) attributeName.get(); + } catch (NamingException e) { + throw new AuthorizationAccessException("Error while retrieving reference user value attribute [" + groupMemberReferencedUserAttribute + "]."); + } + } + + return referencedUserValue; + } + private String getGroupName(final DirContextOperations ctx) { final String name; @@ -661,6 +707,27 @@ public class LdapUserGroupProvider implements UserGroupProvider { return name; } + private String getReferencedGroupValue(final DirContextOperations ctx) { + final String referencedGroupValue; + + if (StringUtils.isBlank(userGroupReferencedGroupAttribute)) { + referencedGroupValue = ctx.getDn().toString(); + } else { + final Attribute attributeName = ctx.getAttributes().get(userGroupReferencedGroupAttribute); + if (attributeName == null) { + throw new AuthorizationAccessException("Referenced group value attribute [" + userGroupReferencedGroupAttribute + "] does not exist."); + } + + try { + referencedGroupValue = (String) attributeName.get(); + } catch (NamingException e) { + throw new AuthorizationAccessException("Error while retrieving referenced group value attribute [" + userGroupReferencedGroupAttribute + "]."); + } + } + + return referencedGroupValue; + } + @AuthorizerContext public void setNiFiProperties(NiFiRegistryProperties properties) { this.properties = properties; http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/4f00e36d/nifi-registry-framework/src/test/java/org/apache/nifi/registry/security/ldap/tenants/LdapUserGroupProviderTest.java ---------------------------------------------------------------------- diff --git a/nifi-registry-framework/src/test/java/org/apache/nifi/registry/security/ldap/tenants/LdapUserGroupProviderTest.java b/nifi-registry-framework/src/test/java/org/apache/nifi/registry/security/ldap/tenants/LdapUserGroupProviderTest.java new file mode 100644 index 0000000..242cf28 --- /dev/null +++ b/nifi-registry-framework/src/test/java/org/apache/nifi/registry/security/ldap/tenants/LdapUserGroupProviderTest.java @@ -0,0 +1,639 @@ +/* + * 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.registry.security.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.registry.properties.NiFiRegistryProperties; +import org.apache.nifi.registry.security.authorization.AuthorizerConfigurationContext; +import org.apache.nifi.registry.security.authorization.Group; +import org.apache.nifi.registry.security.authorization.UserAndGroups; +import org.apache.nifi.registry.security.authorization.UserGroupProviderInitializationContext; +import org.apache.nifi.registry.security.exception.SecurityProviderCreationException; +import org.apache.nifi.registry.security.ldap.LdapAuthenticationStrategy; +import org.apache.nifi.registry.security.ldap.ReferralStrategy; +import org.apache.nifi.registry.util.StandardPropertyValue; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; + +import java.util.Properties; +import java.util.Set; + +import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_AUTHENTICATION_STRATEGY; +import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_CONNECT_TIMEOUT; +import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_GROUP_MEMBER_ATTRIBUTE; +import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_GROUP_MEMBER_REFERENCED_USER_ATTRIBUTE; +import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_GROUP_NAME_ATTRIBUTE; +import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_GROUP_OBJECT_CLASS; +import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_GROUP_SEARCH_BASE; +import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_GROUP_SEARCH_FILTER; +import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_GROUP_SEARCH_SCOPE; +import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_MANAGER_DN; +import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_MANAGER_PASSWORD; +import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_PAGE_SIZE; +import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_READ_TIMEOUT; +import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_REFERRAL_STRATEGY; +import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_SYNC_INTERVAL; +import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_URL; +import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_USER_GROUP_ATTRIBUTE; +import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_USER_GROUP_REFERENCED_GROUP_ATTRIBUTE; +import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_USER_IDENTITY_ATTRIBUTE; +import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_USER_OBJECT_CLASS; +import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_USER_SEARCH_BASE; +import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_USER_SEARCH_FILTER; +import static org.apache.nifi.registry.security.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 = SecurityProviderCreationException.class) + public void testNoSearchBasesSpecified() throws Exception { + final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(null, null); + ldapUserGroupProvider.onConfigured(configurationContext); + } + + @Test(expected = SecurityProviderCreationException.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)); + ldapUserGroupProvider.onConfigured(configurationContext); + } + + @Test(expected = SecurityProviderCreationException.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)); + ldapUserGroupProvider.onConfigured(configurationContext); + } + + @Test(expected = SecurityProviderCreationException.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")); + 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")); + 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")); + 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())); + 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())); + ldapUserGroupProvider.onConfigured(configurationContext); + + assertEquals(9, 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")); + when(configurationContext.getProperty(PROP_USER_SEARCH_FILTER)).thenReturn(new StandardPropertyValue("(uid=user1)")); + 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")); + 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")); + when(configurationContext.getProperty(PROP_USER_GROUP_ATTRIBUTE)).thenReturn(new StandardPropertyValue("description")); // 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")); + when(configurationContext.getProperty(PROP_USER_GROUP_ATTRIBUTE)).thenReturn(new StandardPropertyValue("description")); // using description in lieu of memberof + when(configurationContext.getProperty(PROP_GROUP_NAME_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn")); + 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 = SecurityProviderCreationException.class) + public void testSearchGroupsWithoutMemberAttribute() throws Exception { + final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(null, GROUP_SEARCH_BASE); + ldapUserGroupProvider.onConfigured(configurationContext); + } + + @Test(expected = SecurityProviderCreationException.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")); + when(configurationContext.getProperty(PROP_GROUP_OBJECT_CLASS)).thenReturn(new StandardPropertyValue(null)); + ldapUserGroupProvider.onConfigured(configurationContext); + } + + @Test(expected = SecurityProviderCreationException.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")); + when(configurationContext.getProperty(PROP_GROUP_SEARCH_SCOPE)).thenReturn(new StandardPropertyValue(null)); + ldapUserGroupProvider.onConfigured(configurationContext); + } + + @Test(expected = SecurityProviderCreationException.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")); + when(configurationContext.getProperty(PROP_GROUP_SEARCH_SCOPE)).thenReturn(new StandardPropertyValue("not-valid")); + 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")); + 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")); + when(configurationContext.getProperty(PROP_PAGE_SIZE)).thenReturn(new StandardPropertyValue("1")); + 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")); + when(configurationContext.getProperty(PROP_GROUP_SEARCH_SCOPE)).thenReturn(new StandardPropertyValue(SearchScope.OBJECT.name())); + 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")); + when(configurationContext.getProperty(PROP_GROUP_SEARCH_SCOPE)).thenReturn(new StandardPropertyValue(SearchScope.SUBTREE.name())); + 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")); + when(configurationContext.getProperty(PROP_GROUP_NAME_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn")); + 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")); + when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue("uid")); + 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")); + when(configurationContext.getProperty(PROP_GROUP_NAME_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn")); + when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn")); + 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")); + when(configurationContext.getProperty(PROP_GROUP_SEARCH_FILTER)).thenReturn(new StandardPropertyValue("(cn=admins)")); + 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")); + when(configurationContext.getProperty(PROP_USER_GROUP_ATTRIBUTE)).thenReturn(new StandardPropertyValue("description")); // using description in lieu of memberof + when(configurationContext.getProperty(PROP_GROUP_NAME_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn")); + 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")); + when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("member")); + when(configurationContext.getProperty(PROP_GROUP_NAME_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn")); + 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")); + when(configurationContext.getProperty(PROP_USER_GROUP_ATTRIBUTE)).thenReturn(new StandardPropertyValue("description")); // using description in lieu of memberof + when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("member")); + when(configurationContext.getProperty(PROP_GROUP_NAME_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn")); + 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.registry.security.identity.mapping.pattern.dn1", "^cn=(.*?),o=(.*?)$"); + props.setProperty("nifi.registry.security.identity.mapping.value.dn1", "$1"); + + final NiFiRegistryProperties 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)")); + ldapUserGroupProvider.onConfigured(configurationContext); + + assertEquals(1, ldapUserGroupProvider.getUsers().size()); + assertNotNull(ldapUserGroupProvider.getUserByIdentity("User 1,ou=users")); + } + + @Test(expected = SecurityProviderCreationException.class) + public void testReferencedGroupAttributeWithoutGroupSearchBase() throws Exception { + final AuthorizerConfigurationContext configurationContext = getBaseConfiguration("ou=users-2,o=nifi", null); + when(configurationContext.getProperty(PROP_USER_GROUP_REFERENCED_GROUP_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn")); + ldapUserGroupProvider.onConfigured(configurationContext); + } + + @Test + public void testReferencedGroupWithoutDefiningReferencedAttribute() throws Exception { + final AuthorizerConfigurationContext configurationContext = getBaseConfiguration("ou=users-2,o=nifi", "ou=groups-2,o=nifi"); + when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue("uid")); + when(configurationContext.getProperty(PROP_USER_OBJECT_CLASS)).thenReturn(new StandardPropertyValue("room")); // using room due to reqs of groupOfNames + when(configurationContext.getProperty(PROP_USER_GROUP_ATTRIBUTE)).thenReturn(new StandardPropertyValue("description")); // using description in lieu of member + when(configurationContext.getProperty(PROP_GROUP_NAME_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn")); + when(configurationContext.getProperty(PROP_GROUP_OBJECT_CLASS)).thenReturn(new StandardPropertyValue("room")); // using room due to reqs of groupOfNames + ldapUserGroupProvider.onConfigured(configurationContext); + + final Set<Group> groups = ldapUserGroupProvider.getGroups(); + assertEquals(1, groups.size()); + + final Group team3 = groups.stream().filter(group -> "team3".equals(group.getName())).findFirst().orElse(null); + assertNotNull(team3); + assertTrue(team3.getUsers().isEmpty()); + } + + @Test + public void testReferencedGroupUsingReferencedAttribute() throws Exception { + final AuthorizerConfigurationContext configurationContext = getBaseConfiguration("ou=users-2,o=nifi", "ou=groups-2,o=nifi"); + when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue("uid")); + when(configurationContext.getProperty(PROP_USER_GROUP_ATTRIBUTE)).thenReturn(new StandardPropertyValue("description")); // using description in lieu of member + when(configurationContext.getProperty(PROP_USER_GROUP_REFERENCED_GROUP_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn")); + when(configurationContext.getProperty(PROP_GROUP_NAME_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn")); + when(configurationContext.getProperty(PROP_GROUP_OBJECT_CLASS)).thenReturn(new StandardPropertyValue("room")); // using room because groupOfNames requires a member + ldapUserGroupProvider.onConfigured(configurationContext); + + final Set<Group> groups = ldapUserGroupProvider.getGroups(); + assertEquals(1, groups.size()); + + final Group team3 = groups.stream().filter(group -> "team3".equals(group.getName())).findFirst().orElse(null); + assertNotNull(team3); + assertEquals(1, team3.getUsers().size()); + assertEquals(1, team3.getUsers().stream().map( + userIdentifier -> ldapUserGroupProvider.getUser(userIdentifier)).filter( + user -> "user9".equals(user.getIdentity())).count()); + } + + @Test(expected = SecurityProviderCreationException.class) + public void testReferencedUserWithoutUserSearchBase() throws Exception { + final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(null, "ou=groups-2,o=nifi"); + when(configurationContext.getProperty(PROP_GROUP_MEMBER_REFERENCED_USER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("uid")); + ldapUserGroupProvider.onConfigured(configurationContext); + } + + @Test + public void testReferencedUserWithoutDefiningReferencedAttribute() throws Exception { + final AuthorizerConfigurationContext configurationContext = getBaseConfiguration("ou=users-2,o=nifi", "ou=groups-2,o=nifi"); + when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue("uid")); + when(configurationContext.getProperty(PROP_GROUP_OBJECT_CLASS)).thenReturn(new StandardPropertyValue("room")); // using room due to reqs of groupOfNames + when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("description")); // using description in lieu of member + when(configurationContext.getProperty(PROP_GROUP_NAME_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn")); + ldapUserGroupProvider.onConfigured(configurationContext); + + final Set<Group> groups = ldapUserGroupProvider.getGroups(); + assertEquals(1, groups.size()); + + final Group team3 = groups.stream().filter(group -> "team3".equals(group.getName())).findFirst().orElse(null); + assertNotNull(team3); + assertTrue(team3.getUsers().isEmpty()); + } + + @Test + public void testReferencedUserUsingReferencedAttribute() throws Exception { + final AuthorizerConfigurationContext configurationContext = getBaseConfiguration("ou=users-2,o=nifi", "ou=groups-2,o=nifi"); + when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue("sn")); + when(configurationContext.getProperty(PROP_GROUP_OBJECT_CLASS)).thenReturn(new StandardPropertyValue("room")); // using room due to reqs of groupOfNames + when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("description")); // using description in lieu of member + when(configurationContext.getProperty(PROP_GROUP_NAME_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn")); + when(configurationContext.getProperty(PROP_GROUP_MEMBER_REFERENCED_USER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("uid")); // does not need to be the same as user id attr + ldapUserGroupProvider.onConfigured(configurationContext); + + final Set<Group> groups = ldapUserGroupProvider.getGroups(); + assertEquals(1, groups.size()); + + final Group team3 = groups.stream().filter(group -> "team3".equals(group.getName())).findFirst().orElse(null); + assertNotNull(team3); + assertEquals(1, team3.getUsers().size()); + assertEquals(1, team3.getUsers().stream().map( + userIdentifier -> ldapUserGroupProvider.getUser(userIdentifier)).filter( + user -> "User9".equals(user.getIdentity())).count()); + } + + 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())); + when(configurationContext.getProperty(PROP_CONNECT_TIMEOUT)).thenReturn(new StandardPropertyValue("30 secs")); + when(configurationContext.getProperty(PROP_READ_TIMEOUT)).thenReturn(new StandardPropertyValue("30 secs")); + when(configurationContext.getProperty(PROP_REFERRAL_STRATEGY)).thenReturn(new StandardPropertyValue(ReferralStrategy.FOLLOW.name())); + when(configurationContext.getProperty(PROP_PAGE_SIZE)).thenReturn(new StandardPropertyValue(null)); + when(configurationContext.getProperty(PROP_SYNC_INTERVAL)).thenReturn(new StandardPropertyValue("30 mins")); + + when(configurationContext.getProperty(PROP_AUTHENTICATION_STRATEGY)).thenReturn(new StandardPropertyValue(LdapAuthenticationStrategy.SIMPLE.name())); + when(configurationContext.getProperty(PROP_MANAGER_DN)).thenReturn(new StandardPropertyValue("uid=admin,ou=system")); + when(configurationContext.getProperty(PROP_MANAGER_PASSWORD)).thenReturn(new StandardPropertyValue("secret")); + + when(configurationContext.getProperty(PROP_USER_SEARCH_BASE)).thenReturn(new StandardPropertyValue(userSearchBase)); + when(configurationContext.getProperty(PROP_USER_OBJECT_CLASS)).thenReturn(new StandardPropertyValue("person")); + when(configurationContext.getProperty(PROP_USER_SEARCH_SCOPE)).thenReturn(new StandardPropertyValue(SearchScope.ONE_LEVEL.name())); + when(configurationContext.getProperty(PROP_USER_SEARCH_FILTER)).thenReturn(new StandardPropertyValue(null)); + when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue(null)); + when(configurationContext.getProperty(PROP_USER_GROUP_ATTRIBUTE)).thenReturn(new StandardPropertyValue(null)); + when(configurationContext.getProperty(PROP_USER_GROUP_REFERENCED_GROUP_ATTRIBUTE)).thenReturn(new StandardPropertyValue(null)); + + when(configurationContext.getProperty(PROP_GROUP_SEARCH_BASE)).thenReturn(new StandardPropertyValue(groupSearchBase)); + when(configurationContext.getProperty(PROP_GROUP_OBJECT_CLASS)).thenReturn(new StandardPropertyValue("groupOfNames")); + when(configurationContext.getProperty(PROP_GROUP_SEARCH_SCOPE)).thenReturn(new StandardPropertyValue(SearchScope.ONE_LEVEL.name())); + when(configurationContext.getProperty(PROP_GROUP_SEARCH_FILTER)).thenReturn(new StandardPropertyValue(null)); + when(configurationContext.getProperty(PROP_GROUP_NAME_ATTRIBUTE)).thenReturn(new StandardPropertyValue(null)); + when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue(null)); + when(configurationContext.getProperty(PROP_GROUP_MEMBER_REFERENCED_USER_ATTRIBUTE)).thenReturn(new StandardPropertyValue(null)); + + return configurationContext; + } + + private NiFiRegistryProperties getNiFiProperties(final Properties properties) { + final NiFiRegistryProperties registryProperties = Mockito.mock(NiFiRegistryProperties.class); + when(registryProperties.getPropertyKeys()).thenReturn(properties.stringPropertyNames()); + when(registryProperties.getProperty(anyString())).then(invocationOnMock -> properties.getProperty((String) invocationOnMock.getArguments()[0])); + return registryProperties; + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/4f00e36d/nifi-registry-framework/src/test/resources/nifi-example.ldif ---------------------------------------------------------------------- diff --git a/nifi-registry-framework/src/test/resources/nifi-example.ldif b/nifi-registry-framework/src/test/resources/nifi-example.ldif new file mode 100644 index 0000000..c91feac --- /dev/null +++ b/nifi-registry-framework/src/test/resources/nifi-example.ldif @@ -0,0 +1,166 @@ +## --------------------------------------------------------------------------- +## 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: ou=users-2,o=nifi +objectClass: organizationalUnit +objectClass: top +ou: users-2 + +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 + +## since the embedded ldap does not support memberof, we are using description to simulate + +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: cn=User 9,ou=users-2,o=nifi +objectClass: organizationalPerson +objectClass: person +objectClass: inetOrgPerson +objectClass: top +cn: User 9 +sn: User9 +description: team3 +uid: user9 + +dn: ou=groups,o=nifi +objectClass: organizationalUnit +objectClass: top +ou: groups + +dn: ou=groups-2,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 + +## since the embedded ldap requires member to be fqdn, we are simulating using room and description + +dn: cn=team3,ou=groups-2,o=nifi +objectClass: room +objectClass: top +cn: team3 +description: user9 http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/4f00e36d/nifi-registry-resources/src/main/resources/conf/authorizers.xml ---------------------------------------------------------------------- diff --git a/nifi-registry-resources/src/main/resources/conf/authorizers.xml b/nifi-registry-resources/src/main/resources/conf/authorizers.xml index af0c531..772db61 100644 --- a/nifi-registry-resources/src/main/resources/conf/authorizers.xml +++ b/nifi-registry-resources/src/main/resources/conf/authorizers.xml @@ -90,7 +90,13 @@ '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. + through 'Group Member Attribute' if set. The value of this property is the name of the attribute in the user ldap entry that + associates them with a group. The value of that user attribute could be a dn or group name for instance. What value is expected + is configured in the 'User Group Name Attribute - Referenced Group Attribute'. + 'User Group Name Attribute - Referenced Group Attribute' - If blank, the value of the attribute defined in 'User Group Name Attribute' + is expected to be the full dn of the group. If not blank, this property will define the attribute of the group ldap entry that + the value of the attribute defined in 'User Group Name Attribute' is referencing (i.e. name). Use of this property requires that + 'Group Search Base' is also configured. '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. @@ -98,8 +104,14 @@ '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. + group membership will not be calculated through the groups. Will rely on group membership being defined + through 'User Group Name Attribute' if set. The value of this property is the name of the attribute in the group ldap entry that + associates them with a user. The value of that group attribute could be a dn or memberUid for instance. What value is expected + is configured in the 'Group Member Attribute - Referenced User Attribute'. (i.e. member: cn=User 1,ou=users,o=nifi-registry vs. memberUid: user1) + 'Group Member Attribute - Referenced User Attribute' - If blank, the value of the attribute defined in 'Group Member Attribute' + is expected to be the full dn of the user. If not blank, this property will define the attribute of the user ldap entry that + the value of the attribute defined in 'Group Member Attribute' is referencing (i.e. uid). Use of this property requires that + 'User Search Base' is also configured. (i.e. member: cn=User 1,ou=users,o=nifi-registry vs. memberUid: user1) NOTE: Any identity mapping rules specified in nifi-registry.properties will also be applied to the user identities. Group names are not mapped. @@ -137,6 +149,7 @@ <property name="User Search Filter"></property> <property name="User Identity Attribute"></property> <property name="User Group Name Attribute"></property> + <property name="User Group Name Attribute - Referenced Group Attribute"></property> <property name="Group Search Base"></property> <property name="Group Object Class">group</property> @@ -144,6 +157,7 @@ <property name="Group Search Filter"></property> <property name="Group Name Attribute"></property> <property name="Group Member Attribute"></property> + <property name="Group Member Attribute - Referenced User Attribute"></property> </userGroupProvider> To enable the ldap-user-group-provider remove 2 lines. This is 2 of 2. -->
