blob: 23e350fc82ee9cf090a7ebba8de7e9e0d9f3b666 [file] [log] [blame]
/*
* 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.commons.lang3.StringUtils;
import org.apache.nifi.registry.properties.NiFiRegistryProperties;
import org.apache.nifi.registry.properties.util.IdentityMapping;
import org.apache.nifi.registry.properties.util.IdentityMappingUtil;
import org.apache.nifi.registry.security.authorization.AuthorizerConfigurationContext;
import org.apache.nifi.registry.security.authorization.Group;
import org.apache.nifi.registry.security.authorization.User;
import org.apache.nifi.registry.security.authorization.UserAndGroups;
import org.apache.nifi.registry.security.authorization.UserGroupProvider;
import org.apache.nifi.registry.security.authorization.UserGroupProviderInitializationContext;
import org.apache.nifi.registry.security.authorization.annotation.AuthorizerContext;
import org.apache.nifi.registry.security.authorization.exception.AuthorizationAccessException;
import org.apache.nifi.registry.security.exception.SecurityProviderCreationException;
import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException;
import org.apache.nifi.registry.security.ldap.LdapAuthenticationStrategy;
import org.apache.nifi.registry.security.ldap.LdapsSocketFactory;
import org.apache.nifi.registry.security.ldap.ReferralStrategy;
import org.apache.nifi.registry.security.util.SslContextFactory;
import org.apache.nifi.registry.security.util.SslContextFactory.ClientAuth;
import org.apache.nifi.registry.util.FormatUtils;
import org.apache.nifi.registry.util.PropertyValue;
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_GROUP_MEMBERSHIP_ENFORCE_CASE_SENSITIVITY = "Group Membership - Enforce Case Sensitivity";
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_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";
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_GROUP_MEMBER_REFERENCED_USER_ATTRIBUTE = "Group Member Attribute - Referenced User Attribute";
public static final String PROP_SYNC_INTERVAL = "Sync Interval";
private List<IdentityMapping> identityMappings;
private List<IdentityMapping> groupMappings;
private NiFiRegistryProperties 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 String userGroupReferencedGroupAttribute;
private boolean useDnForUserIdentity;
private boolean performUserSearch;
private String groupSearchBase;
private SearchScope groupSearchScope;
private String groupSearchFilter;
private String groupMemberAttribute;
private String groupMemberReferencedUserAttribute;
private String groupNameAttribute;
private String groupObjectClass;
private boolean useDnForGroupName;
private boolean performGroupSearch;
private Integer pageSize;
private boolean groupMembershipEnforceCaseSensitivity;
@Override
public void initialize(final UserGroupProviderInitializationContext initializationContext) throws SecurityProviderCreationException {
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 SecurityProviderCreationException {
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 SecurityProviderCreationException(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 SecurityProviderCreationException(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 SecurityProviderCreationException("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 SecurityProviderCreationException("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 SecurityProviderCreationException("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();
userGroupReferencedGroupAttribute = configurationContext.getProperty(PROP_USER_GROUP_REFERENCED_GROUP_ATTRIBUTE).getValue();
try {
userSearchScope = SearchScope.valueOf(rawUserSearchScope.getValue());
} catch (final IllegalArgumentException iae) {
throw new SecurityProviderCreationException(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 SecurityProviderCreationException("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 SecurityProviderCreationException("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();
groupMemberReferencedUserAttribute = configurationContext.getProperty(PROP_GROUP_MEMBER_REFERENCED_USER_ATTRIBUTE).getValue();
try {
groupSearchScope = SearchScope.valueOf(rawGroupSearchScope.getValue());
} catch (final IllegalArgumentException iae) {
throw new SecurityProviderCreationException(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 SecurityProviderCreationException("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 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())) {
pageSize = rawPageSize.asInteger();
}
// get whether group membership should be case sensitive
final String rawGroupMembershipEnforceCaseSensitivity = configurationContext.getProperty(PROP_GROUP_MEMBERSHIP_ENFORCE_CASE_SENSITIVITY).getValue();
groupMembershipEnforceCaseSensitivity = Boolean.parseBoolean(rawGroupMembershipEnforceCaseSensitivity);
// extract the identity mappings from nifi-registry.properties if any are provided
identityMappings = Collections.unmodifiableList(IdentityMappingUtil.getIdentityMappings(properties));
groupMappings = Collections.unmodifiableList(IdentityMappingUtil.getGroupMappings(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 SecurityProviderCreationException(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 SecurityProviderCreationException(String.format("The %s '%s' is not a valid time duration", PROP_SYNC_INTERVAL, rawSyncInterval.getValue()));
}
} else {
throw new SecurityProviderCreationException("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 SecurityProviderCreationException("Unable to sync users and groups.");
}
// schedule the background thread to load the users/groups
ldapSync.scheduleWithFixedDelay(() -> {
try {
load(context);
} catch (final Throwable t) {
logger.error("Failed to sync User/Groups from LDAP due to {}. Will try again in {} millis.", new Object[] {t.toString(), syncInterval});
if (logger.isDebugEnabled()) {
logger.error("", t);
}
}
}, syncInterval, syncInterval, TimeUnit.MILLISECONDS);
} catch (final AuthorizationAccessException e) {
throw new SecurityProviderCreationException(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>> groupToUserIdentifierMappings = new HashMap<>();
// user dn -> user lookup
final Map<String, User> userLookup = 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) {
// get the user identity
final String identity = getUserIdentity(ctx);
// build the user
final User user = new User.Builder().identifierGenerateFromSeed(identity).identity(identity).build();
// store the user for group member later
userLookup.put(getReferencedUserValue(ctx), user);
if (StringUtils.isNotBlank(userGroupNameAttribute)) {
final Attribute attributeGroups = ctx.getAttributes().get(userGroupNameAttribute);
if (attributeGroups == null) {
logger.debug("User group name attribute [{}] does not exist for {}. " +
"This may be due to misconfiguration or this user record may not have any group membership attributes defined. " +
"Ignoring group membership. ", userGroupNameAttribute, identity);
} else {
try {
final NamingEnumeration<String> groupValues = (NamingEnumeration<String>) attributeGroups.getAll();
while (groupValues.hasMoreElements()) {
final String groupValue = groupValues.next();
// if we are performing a group search, then we need to normalize the group value so that each
// user associating with it can be matched. if we are not performing a group search then these
// values will be used to actually build the group itself. case sensitivity is for group
// membership, not group identification.
final String groupValueNormalized;
if (performGroupSearch) {
groupValueNormalized = groupMembershipEnforceCaseSensitivity ? groupValue : groupValue.toLowerCase();
} else {
groupValueNormalized = groupValue;
}
// store the group -> user identifier mapping... if case sensitivity is disabled, the group reference value will
// be lowercased when adding to groupToUserIdentifierMappings
groupToUserIdentifierMappings.computeIfAbsent(groupValueNormalized, 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);
// 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.debug("Group member attribute [{}] does not exist for {}. " +
"This may be due to misconfiguration or this group record may not have any user attributes defined. " +
"Ignoring group membership.", groupMemberAttribute, name);
} else {
try {
final NamingEnumeration<String> userValues = (NamingEnumeration<String>) attributeUsers.getAll();
while (userValues.hasMoreElements()) {
final String userValue = userValues.next();
if (performUserSearch) {
// find the user by it's referenced attribute and add the identifier to this group.
// need to normalize here based on the desired case sensitivity. if case sensitivity
// is disabled, the user reference value will be lowercased when adding to userLookup
final String userValueNormalized = groupMembershipEnforceCaseSensitivity ? userValue : userValue.toLowerCase();
final User user = userLookup.get(userValueNormalized);
// ensure the user is known
if (user != null) {
groupToUserIdentifierMappings.computeIfAbsent(referencedGroupValue, g -> new HashSet<>()).add(user.getIdentifier());
} else {
logger.debug(String.format("%s contains member %s but that user was not found while searching users. " +
"This may be due to misconfiguration or because that user is not a NiFi Registry user as defined by the User Search Base and Filter. " +
"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.
// no need to normalize here since group membership is driven solely through this group (not through the userLookup
// populated above). we are either going to use this value directly as the user identity or we are going to query
// the directory server again which should handle the case sensitivity accordingly.
final String userDn = userValue;
final String userIdentity;
if (useDnForUserIdentity) {
// use the user value to avoid the unnecessary look up
userIdentity = IdentityMappingUtil.mapIdentity(userDn, identityMappings);
} 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);
groupToUserIdentifierMappings.computeIfAbsent(referencedGroupValue, 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 referenced group attribute
if (groupToUserIdentifierMappings.containsKey(referencedGroupValue)) {
groupToUserIdentifierMappings.remove(referencedGroupValue).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
groupToUserIdentifierMappings.forEach((referencedGroupValue, userIdentifiers) -> {
logger.debug(String.format("[%s] are members of %s but that group was not found while searching groups. " +
"This may be due to misconfiguration or because that group is not a NiFi Registry group as defined by the Group Search Base and Filter. " +
"Ignoring group membership.", 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
groupToUserIdentifierMappings.forEach((groupDn, userIdentifiers) -> {
final String groupName;
if (useDnForGroupName) {
// use the dn to avoid the unnecessary look up
groupName = IdentityMappingUtil.mapIdentity(groupDn, groupMappings);
} 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());
});
}
if (logger.isDebugEnabled()) {
logger.debug("-------------------------------------");
logger.debug("Loaded the following users from LDAP:");
userList.forEach((user) -> logger.debug(" - " + user));
logger.debug("--------------------------------------");
logger.debug("Loaded the following groups from LDAP:");
groupList.forEach((group) -> logger.debug(" - " + group));
logger.debug("--------------------------------------");
}
// 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 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 groupMembershipEnforceCaseSensitivity ? referencedUserValue : referencedUserValue.toLowerCase();
}
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 IdentityMappingUtil.mapIdentity(name, groupMappings);
}
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 groupMembershipEnforceCaseSensitivity ? referencedGroupValue : referencedGroupValue.toLowerCase();
}
@AuthorizerContext
public void setNiFiProperties(NiFiRegistryProperties properties) {
this.properties = properties;
}
@Override
public final void preDestruction() throws SecurityProviderDestructionException {
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 SecurityProviderCreationException(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 SecurityProviderCreationException("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 SecurityProviderCreationException(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 SecurityProviderCreationException(e.getMessage(), e);
}
return sslContext;
}
}