/*
* Copyright (c) 2013 EMC Corporation
* All Rights Reserved
*/
package com.emc.storageos.auth.ldap;
import com.emc.storageos.auth.AuthenticationManager.ValidationFailureReason;
import com.emc.storageos.auth.impl.LdapFailureHandler;
import com.emc.storageos.auth.StorageOSPersonAttributeDao;
import com.emc.storageos.auth.impl.LdapOrADServer;
import com.emc.storageos.auth.impl.LdapServerList;
import com.emc.storageos.db.client.DbClient;
import com.emc.storageos.db.client.constraint.AlternateIdConstraint;
import com.emc.storageos.db.client.constraint.URIQueryResultList;
import com.emc.storageos.db.client.model.AuthnProvider;
import com.emc.storageos.db.client.model.AuthnProvider.ProvidersType;
import com.emc.storageos.db.client.model.StorageOSUserDAO;
import com.emc.storageos.db.client.model.StringSet;
import com.emc.storageos.db.client.model.TenantOrg;
import com.emc.storageos.db.client.model.UserGroup;
import com.emc.storageos.db.exceptions.DatabaseException;
import com.emc.storageos.model.usergroup.UserAttributeParam;
import com.emc.storageos.security.authorization.BasePermissionsHelper;
import com.emc.storageos.security.authorization.BasePermissionsHelper.UserMapping;
import com.emc.storageos.security.authorization.BasePermissionsHelper.UserMappingAttribute;
import com.emc.storageos.security.exceptions.SecurityException;
import com.emc.storageos.svcs.errorhandling.resources.APIException;
import com.emc.storageos.svcs.errorhandling.resources.UnauthorizedException;
import com.google.common.collect.Lists;
import org.apache.commons.httpclient.Credentials;
import org.apache.commons.httpclient.UsernamePasswordCredentials;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.dao.support.DataAccessUtils;
import org.springframework.ldap.AuthenticationException;
import org.springframework.ldap.CommunicationException;
import org.springframework.ldap.SizeLimitExceededException;
import org.springframework.ldap.core.AttributesMapper;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.filter.*;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import javax.naming.InvalidNameException;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.SearchControls;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;
import java.net.URI;
import java.util.*;
import java.util.Map.Entry;
import java.util.regex.Pattern;
/**
*
* Attribute repository for LDAP users
*/
public class StorageOSLdapPersonAttributeDao implements StorageOSPersonAttributeDao {
private static final Logger _log = LoggerFactory.getLogger(StorageOSPersonAttributeDao.class);
public static final String AD_DISTINGUISHED_NAME = "distinguishedName";
public static final String LDAP_DISTINGUISHED_NAME = "entryDN";
public static final String COMMON_NAME = "cn";
public static final String OBJECT_SID = "objectSid";
public static final String TOKEN_GROUPS = "tokenGroups";
public static final String OBJECT_CLASS = "objectClass";
/**
* The LdapTemplate to use to execute queries on the DirContext
*/
private String _baseDN;
private SearchControls _searchControls = new SearchControls();
private GroupWhiteList _groupWhiteList = GroupWhiteList.SID;
private DbClient _dbClient;
private String _filter;
private Set<String> _groupObjectClasses = new HashSet<String>();
private Set<String> _groupMemberAttributes = new HashSet<String>();
// LDAP server can have a maximum number of query results it will return at once.
// On AD the default is 1000 although this is user configurable
private int _maxPageSize = 1000;
private ProvidersType _type;
private LdapFailureHandler _failureHandler = new LdapFailureHandler();
private LdapServerList _ldapServers;
public StorageOSLdapPersonAttributeDao() {
super();
}
/**
* @return The base distinguished name to use for queries.
*/
public String getBaseDN() {
return this._baseDN;
}
/**
* @param baseDN
* The base distinguished name to use for queries.
*/
public void setBaseDN(final String baseDN) {
if (baseDN == null) {
this._baseDN = "";
} else {
this._baseDN = baseDN;
}
}
public SearchControls getSearchControls() {
return this._searchControls;
}
public void setSearchControls(final SearchControls searchControls) {
Assert.notNull(searchControls, "searchControls can not be null");
this._searchControls = searchControls;
}
public GroupWhiteList getGroupWhiteList() {
return _groupWhiteList;
}
public void setGroupWhiteList(final GroupWhiteList groupWhiteList) {
this._groupWhiteList = groupWhiteList;
}
public void setDbClient(final DbClient dbClient) {
_dbClient = dbClient;
}
public void setFilter(String filter) {
_filter = filter;
}
public void setMaxPageSize(int maxPageSize) {
_maxPageSize = maxPageSize;
}
public void setProviderType(ProvidersType type) {
_type = type;
}
public Set<String> getGroupObjectClasses() {
return _groupObjectClasses;
}
public void setGroupObjectClasses(Set<String> groupObjectClasses) {
this._groupObjectClasses = groupObjectClasses;
}
public Set<String> getGroupMemberAttributes() {
return _groupMemberAttributes;
}
public void setGroupMemberAttributes(Set<String> groupMemberAttributes) {
this._groupMemberAttributes = groupMemberAttributes;
}
/**
* Convert the binary objectSID into readable string format
*
* The byte array structure is (according to MSDN):
* <ol>
* <li>First element - one byte - revision of the SID structure (currently must be set to 0x01; the field name is Revision)
* <li>Second element - one byte - is the number of sub authorities in that SID (this field name is SubAuthorityCount )
* <li>The third element - 6 bytes - is identifying the authority under which the SID was created (field name is IdentifierAuthority).
* <li>The fourth is a variable length array of unsigned 32 bits integers identifies a principal relative to the IdentifierAuthority -
* the length of the array is determine by SubAuthorityCount (this field name is SubAuthority). Notice that the each sub authority is an
* integer, It is important to remember that Microsoft Windows is built around little endian and this is why the code below reads each
* sub authority 4 bytes from the high to the low byte.
* </ol>
*
* @param sid
* @return
*/
private String getSidAsString(byte[] sid) {
// Add the 'S' prefix
StringBuilder strSID = new StringBuilder("S-");
// sid[0] : in the array is the version (must be 1 but might
// change in the future)
strSID.append(sid[0]).append('-');
// sid[2..7] : the Authority
StringBuilder sb = new StringBuilder();
for (int t = 2; t <= 7; t++) {
String hexString = Integer.toHexString(sid[t] & 0xFF);
sb.append(hexString);
}
strSID.append(Long.parseLong(sb.toString(), 16));
// sid[1] : the sub authorities count
int count = sid[1];
// sid[8..end] : the sub authorities (these are Integers - notice
// the endian)
for (int i = 0; i < count; i++) {
int currSubAuthOffset = i * 4;
sb.setLength(0);
sb.append(String.format("%02X%02X%02X%02X",
(sid[11 + currSubAuthOffset] & 0xFF),
(sid[10 + currSubAuthOffset] & 0xFF),
(sid[9 + currSubAuthOffset] & 0xFF),
(sid[8 + currSubAuthOffset] & 0xFF)));
strSID.append('-').append(Long.parseLong(sb.toString(), 16));
}
// That's it - we have the SID
return strSID.toString();
}
/**
* Generate LDAP Group query filter
*
* @param dataAttribute
* @param queryValues
* @return
*/
private String getGroupSidQueryFilter(
String dataAttribute, List<String> queryValues) {
AndFilter queryBuilder = new AndFilter();
final Filter objClassfilter = new EqualsFilter("objectClass", "group");
queryBuilder.and(objClassfilter);
OrFilter sidQueryBuilder = new OrFilter();
for (final String queryValue : queryValues) {
final Filter filter;
if (!queryValue.contains("*")) {
filter = new EqualsFilter(dataAttribute, queryValue);
} else {
filter = new LikeFilter(dataAttribute, queryValue);
}
sidQueryBuilder.or(filter);
}
queryBuilder.and(sidQueryBuilder);
return queryBuilder.encode();
}
/*
* @see com.emc.storageos.auth.StorageOSPersonAttributeDao#isGroupValid(java.lang.String)
*/
@SuppressWarnings("unchecked")
@Override
public boolean isGroupValid(final String groupId,
ValidationFailureReason[] failureReason) {
String group = groupId.substring(0, groupId.lastIndexOf("@"));
String domain = groupId.substring(groupId.lastIndexOf("@") + 1);
// only one result is needed
final long countLimit = 1L;
// Check if the group is on the whitelist
if (isGroupOnWhiteList(group)) {
// If the group is on the whitelist search AD/LDAP to see if it is valid
final AndFilter queryBuilder = new AndFilter();
final Filter groupObjClassfilter = createGroupObjectClassFilter();
if (groupObjClassfilter == null) {
_log.error(
"Group {} could not be searched in LDAP due to missing or empty LDAP Group properties of Authentication Provider. "
+ "In order to work on groups in LDAP, kindly add the LDAP Group properties in Authentication Provider configuration.",
groupId);
failureReason[0] = ValidationFailureReason.LDAP_CANNOT_SEARCH_GROUP_IN_LDAP_MODE;
return false;
}
final Filter groupAttributeFilter = new EqualsFilter(_groupWhiteList.getType(), group);
queryBuilder.and(groupObjClassfilter);
queryBuilder.and(groupAttributeFilter);
String[] returnAttributes = { _groupWhiteList.getType(), COMMON_NAME, getDistinguishedNameAttribute() };
List<List<GroupAttribute>> queryGroupResults = null;
try {
queryGroupResults = searchAuthProvider(queryBuilder, returnAttributes, countLimit,
new GroupsMapper(_groupWhiteList.getType(), getDistinguishedNameAttribute()), failureReason);
} catch (SizeLimitExceededException e) {
_log.error(
"Multiple entries for group {} are found in AD/LDAP. Please use other group attributes such as objectSid or objectGUID to uniquely identify the group.",
groupId);
failureReason[0] = ValidationFailureReason.USER_OR_GROUP_NOT_FOUND_FOR_TENANT;
return false;
}
if (!CollectionUtils.isEmpty(queryGroupResults)) {
// validate if group DN match input group domain,
// this could be different in AD forest scenario.
GroupAttribute groupAttribute = queryGroupResults.get(0).get(0);
if (groupAttribute == null) {
_log.error("Group {} matching with domain {} not present in AD/LDAP/UserGroup", groupId, domain);
return false;
}
if (!groupAttribute.domainMatch(domain)) {
_log.error("Group {} has dn as {}, which doesn't match its domain",
groupId, groupAttribute.getGroupDistinguishedName());
return false;
}
_log.debug("Group {} is valid", groupId);
return true;
} else {
if (null != queryGroupResults) {
// null means Exception has been thrown and error logged already, empty means no group found in LDAP/AD
_log.error("Group {} matching with domain {} is not present in AD/LDAP/UserGroup", groupId, domain);
failureReason[0] = ValidationFailureReason.USER_OR_GROUP_NOT_FOUND_FOR_TENANT;
}
return false;
}
}
_log.error("Group {} is not on the whitelist", groupId);
failureReason[0] = ValidationFailureReason.USER_OR_GROUP_NOT_FOUND_FOR_TENANT;
return false;
}
/*
* @see com.emc.storageos.auth.StorageOSPersonAttributeDao#validateUser(java.lang.String, java.lang.String)
*/
@Override
public void validateUser(final String userId, final String tenantId, final String altTenantId) {
UsernamePasswordCredentials creds = new UsernamePasswordCredentials(userId, "");
StorageOSUserDAO user = getStorageOSUser(creds);
// the user must not be null and it must have tenant id
boolean belongsToTenant = user.getTenantId().equals(tenantId);
boolean belongsToAltTenant =
(altTenantId != null) && user.getTenantId().equals(altTenantId);
if (!(belongsToTenant || belongsToAltTenant)) {
throw APIException.badRequests.principalSearchFailed(userId);
}
}
/*
* @see com.emc.storageos.auth.StorageOSPersonAttributeDao#getPerson(java.lang.String)
*/
@Override
public StorageOSUserDAO getStorageOSUser(final Credentials credentials,
ValidationFailureReason[] failureReason) {
final String username = ((UsernamePasswordCredentials) credentials).getUserName();
UserAndTenants userAndTenants = getStorageOSUserAndTenants(username,
failureReason);
if (null != userAndTenants) {
StorageOSUserDAO user = userAndTenants._user;
Map<URI, UserMapping> tenants = userAndTenants._tenants;
if (null == tenants || tenants.isEmpty()) {
_log.error("User {} did not match any tenant", username);
} else if (tenants.keySet().size() > 1) {
_log.error("User {} mapped to tenants {}", username, tenants.keySet().toArray());
} else {
user.setTenantId(tenants.keySet().iterator().next().toString());
}
return user;
}
return null;
}
/*
* another implementation of getStorageOSUser which throws Exception with error message instead of using failure reason.
*/
@Override
public StorageOSUserDAO getStorageOSUser(final Credentials credentials) {
final String username = ((UsernamePasswordCredentials) credentials).getUserName();
ValidationFailureReason[] failureReason = new ValidationFailureReason[1];
UserAndTenants userAndTenants = getStorageOSUserAndTenants(username, failureReason);
if (userAndTenants == null) {
switch (failureReason[0]) {
case LDAP_CONNECTION_FAILED:
throw SecurityException.fatals
.communicationToLDAPResourceFailed();
case LDAP_MANAGER_AUTH_FAILED:
throw SecurityException.fatals.ldapManagerAuthenticationFailed();
case USER_OR_GROUP_NOT_FOUND_FOR_TENANT:
default:
throw APIException.badRequests.principalSearchFailed(username);
}
}
StorageOSUserDAO user = userAndTenants._user;
Map<URI, UserMapping> tenants = userAndTenants._tenants;
if (null == tenants || tenants.isEmpty()) {
_log.error("User {} did not match any tenant", username);
throw APIException.forbidden.userDoesNotMapToAnyTenancy(user.getUserName());
}
if (tenants.keySet().size() > 1) {
_log.error("User {} mapped to tenants {}", username, tenants.keySet().toArray());
throw APIException.forbidden.userBelongsToMultiTenancy(user.getUserName(), tenantName(tenants.keySet()));
}
user.setTenantId(tenants.keySet().iterator().next().toString());
return user;
}
private List<String> tenantName(Set<URI> uris) {
List<String> tenantNames = new ArrayList<>();
for (URI tId : uris) {
TenantOrg t = _dbClient.queryObject(TenantOrg.class, tId);
tenantNames.add(t.getLabel());
}
return tenantNames;
}
@Override
public Map<URI, UserMapping> getUserTenants(String username) {
ValidationFailureReason[] failureReason = new ValidationFailureReason[1];
UserAndTenants userAndTenants = getStorageOSUserAndTenants(username,
failureReason);
if (null != userAndTenants) {
return userAndTenants._tenants;
}
return null;
}
@Override
public Map<URI, UserMapping> peekUserTenants(String username, URI tenantURI, List<UserMapping> userMapping) {
ValidationFailureReason[] failureReason = new ValidationFailureReason[1];
UserAndTenants userAndTenants = getStorageOSUserAndTenants(username,
failureReason, tenantURI, userMapping);
if (null != userAndTenants) {
return userAndTenants._tenants;
}
return null;
}
/**
* @see com.emc.storageos.auth.StorageOSPersonAttributeDao#setFailureHandler(LdapFailureHandler)
* @param failureHandler
*/
@Override
public void setFailureHandler(LdapFailureHandler failureHandler) {
_failureHandler = failureHandler;
}
/**
* Search for the user in LDAP and create a StorageOSUserDAO and also
* Map the user to tenant(s)
*
* @param username name of the user
* @return an object containing the StorageOSUserDao and a list of tenants
*/
private UserAndTenants getStorageOSUserAndTenants(String username,
ValidationFailureReason[] failureReason) {
return getStorageOSUserAndTenants(username, failureReason, null, null);
}
private UserAndTenants getStorageOSUserAndTenants(String username,
ValidationFailureReason[] failureReason,
URI tenantURI, List<UserMapping> usermapping) {
BasePermissionsHelper permissionsHelper = new BasePermissionsHelper(_dbClient, false);
final String[] userDomain = username.split("@");
if (userDomain.length < 2) {
_log.error("Illegal username {} missing domain", username);
failureReason[0] = ValidationFailureReason.USER_OR_GROUP_NOT_FOUND_FOR_TENANT;
return null;
}
final String domain = userDomain[1];
final String ldapQuery = LdapFilterUtil.getPersonFilterWithValues(_filter, username);
if (ldapQuery == null) {
_log.error("Null query filter from string {} for username", _filter, username);
failureReason[0] = ValidationFailureReason.USER_OR_GROUP_NOT_FOUND_FOR_TENANT;
return null;
}
StringSet authnProviderDomains = getAuthnProviderDomains(domain);
List<String> attrs = new ArrayList<String>();
Map<URI, List<UserMapping>> tenantToMappingMap = permissionsHelper.getAllUserMappingsForDomain(authnProviderDomains);
if (_searchControls.getReturningAttributes() != null) {
Collections.addAll(attrs, _searchControls.getReturningAttributes());
}
if (tenantURI != null) {
tenantToMappingMap.put(tenantURI, usermapping);
}
printTenantToMappingMap(tenantToMappingMap);
// Add attributes that need to be released for tenant mapping
for (List<UserMapping> mappings : tenantToMappingMap.values()) {
if (mappings == null) {
continue;
}
for (UserMapping mapping : mappings) {
if (mapping.getAttributes() != null && !mapping.getAttributes().isEmpty()) {
for (UserMappingAttribute mappingAttribute : mapping.getAttributes()) {
attrs.add(mappingAttribute.getKey());
}
}
}
}
// Now get the returning attributes from the userGroup table.
getReturningAttributesFromUserGroups(permissionsHelper, domain, attrs);
// Create search controls with the additional attributes to return
SearchControls dnSearchControls = new SearchControls(
_searchControls.getSearchScope(), _searchControls.getCountLimit(),
_searchControls.getTimeLimit(), attrs.toArray(new String[attrs.size()]), _searchControls.getReturningObjFlag(),
_searchControls.getDerefLinkFlag());
Map<String, List<String>> userMappingAttributes = new HashMap<String, List<String>>();
StorageOSUserMapper userMapper = new StorageOSUserMapper(username, getDistinguishedNameAttribute(), userMappingAttributes);
// Execute the query
@SuppressWarnings("unchecked")
final List<StorageOSUserDAO> storageOSUsers = safeLdapSearch(_baseDN, ldapQuery,
dnSearchControls, userMapper, failureReason);
if (null == storageOSUsers) {
_log.error("Query for user {} failed", username);
return null;
}
StorageOSUserDAO storageOSUser = null;
try {
storageOSUser = DataAccessUtils.requiredUniqueResult(storageOSUsers);
if (null == storageOSUser) {
_log.error("Query for user {} yielded no results", username);
failureReason[0] = ValidationFailureReason.USER_OR_GROUP_NOT_FOUND_FOR_TENANT;
return null;
}
} catch (IncorrectResultSizeDataAccessException ex) {
_log.error("Query for user {} yielded incorrect number of results.", username, ex);
failureReason[0] = ValidationFailureReason.USER_OR_GROUP_NOT_FOUND_FOR_TENANT;
return null;
}
// If the type is AD then fetch the users tokenGroups
if (_type == AuthnProvider.ProvidersType.ad) {
List<String> groups = queryTokenGroups(ldapQuery, storageOSUser);
StringBuilder groupsString = new StringBuilder("[ ");
for (String group : groups) {
groupsString.append(group + " ");
storageOSUser.addGroup(group);
}
groupsString.append("]");
_log.debug("User {} adding groups {}", username, groupsString);
} else {
if (!updateGroupsAndRootGroupsInLDAPByMemberAttribute(storageOSUser, failureReason)) {
// null means Exception has been thrown and error logged already, empty means no group found in LDAP/AD
_log.info("User {} is not in any AD/LDAP groups.", storageOSUser.getDistinguishedName());
}
}
// Add the user's group based on the attributes.
addUserGroupsToUserGroupList(permissionsHelper, domain, storageOSUser);
return new UserAndTenants(storageOSUser, mapUserToTenant(authnProviderDomains, storageOSUser, userMappingAttributes, tenantToMappingMap,
failureReason));
}
/**
* Match the user to one and only one tenant if found user there attributes/groups
*
* @param domains
* @param storageOSUser
* @param attributeKeyValuesMap
* @param tenantToMappingMap
*/
private Map<URI, UserMapping> mapUserToTenant(StringSet domains, StorageOSUserDAO storageOSUser,
Map<String, List<String>> attributeKeyValuesMap, Map<URI,
List<UserMapping>> tenantToMappingMap, ValidationFailureReason[] failureReason) {
Map<URI, UserMapping> tenants = new HashMap<URI, UserMapping>();
if (CollectionUtils.isEmpty(domains)) {
return tenants;
}
List<UserMappingAttribute> userMappingAttributes = new ArrayList<UserMappingAttribute>();
for (Entry<String, List<String>> attributeKeyValues : attributeKeyValuesMap.entrySet()) {
UserMappingAttribute userMappingAttribute = new UserMappingAttribute();
userMappingAttribute.setKey(attributeKeyValues.getKey());
userMappingAttribute.setValues(attributeKeyValues.getValue());
userMappingAttributes.add(userMappingAttribute);
}
List<String> userMappingGroups = new ArrayList<String>();
if (null != storageOSUser.getGroups()) {
for (String group : storageOSUser.getGroups()) {
userMappingGroups.add((group.split("@")[0]).toUpperCase());
_log.debug("Adding user's group {} to usermapping group ", (group.split("@")[0]).toUpperCase());
}
}
for (Entry<URI, List<UserMapping>> tenantToMappingMapEntry : tenantToMappingMap.entrySet()) {
if (tenantToMappingMapEntry == null || tenantToMappingMapEntry.getValue() == null) {
continue;
}
for (String domain : domains) {
for (UserMapping userMapping : tenantToMappingMapEntry.getValue()) {
if (userMapping.isMatch(domain, userMappingAttributes, userMappingGroups)) {
tenants.put(tenantToMappingMapEntry.getKey(), userMapping);
}
}
}
}
// if no tenant was found then set it to the root tenant
// unless the root tenant is restricted by a mapping
if (tenants.isEmpty()) {
BasePermissionsHelper permissionsHelper = new BasePermissionsHelper(_dbClient, false);
TenantOrg rootTenant = permissionsHelper.getRootTenant();
// check if UserMappingMap parameter contains provider tenant or not.
// if yes, means Provider Tenant's user-mapping under modification.
if (tenantToMappingMap.containsKey(rootTenant.getId())) {
List<UserMapping> rootUserMapping = tenantToMappingMap.get(rootTenant.getId());
// check if the change is to remove all user-mapping from provider tenant.
// if yes, set user map to provider tenant.
if (CollectionUtils.isEmpty(rootUserMapping)) {
_log.debug("User {} did not match a tenant. Assigning to root tenant since root does not have any attribute mappings",
storageOSUser.getUserName());
tenants.put(rootTenant.getId(), null);
}
// provider tenant is not in UserMapping parameter, means no change to its user-mapping in this request,
// need to check if its original user-mapping is empty or not.
} else if (rootTenant.getUserMappings() == null || rootTenant.getUserMappings().isEmpty()) {
_log.debug("User {} did not match a tenant. Assigning to root tenant since root does not have any attribute mappings",
storageOSUser.getUserName());
tenants.put(rootTenant.getId(), null);
}
}
return tenants;
}
/**
* Do the Ldap search and catch any connection errors.
* Return null for caught exceptions, empty list for empty search result.
*
* @param base the search base
* @param ldapQuery the search query
* @param searchControls the LDAP search controls
* @param mapper the AttributeMapper
* @return List of objects
*/
private List safeLdapSearch(final String base, final String ldapQuery,
final SearchControls searchControls, final AttributesMapper mapper) {
ValidationFailureReason[] failureReason = new ValidationFailureReason[1];
return safeLdapSearch(base, ldapQuery, searchControls, mapper, failureReason);
}
/**
* Do the Ldap search and catch any connection errors.
* Return null for caught exceptions, empty list for empty search result.
*
* @param base the search base
* @param ldapQuery the search query
* @param searchControls the LDAP search controls
* @param mapper the AttributeMapper
* @param failureReason an output parameter which described the reason for the failure
* @return List of objects
*/
@SuppressWarnings("rawtypes")
private List safeLdapSearch(final String base, final String ldapQuery,
final SearchControls searchControls, final AttributesMapper mapper,
ValidationFailureReason[] failureReason) {
try {
_log.debug("Ldap query to get user's attributes is {}", ldapQuery);
return doLdapSearch(base, ldapQuery, searchControls, mapper);
} catch (AuthenticationException e) {
_log.error("Caught authentication exception connecting to ldap server", e);
failureReason[0] = ValidationFailureReason.LDAP_MANAGER_AUTH_FAILED;
return null;
}
}
private List doLdapSearch(String base, String ldapQuery, SearchControls searchControls, AttributesMapper mapper) {
List<LdapOrADServer> connectedServers = _ldapServers.getConnectedServers();
for (LdapOrADServer server : connectedServers) {
try {
return doLdapSearchOnSingleServer(base, ldapQuery, searchControls, mapper, server);
} catch (CommunicationException e) {
_failureHandler.handle(_ldapServers, server);
_log.info("Failed to connect to all AD/Ldap servers.", e);
}
}
// Going here means attempts on all servers failed
throw UnauthorizedException.unauthorized.ldapCommunicationException();
}
private List doLdapSearchOnSingleServer(String base, String ldapQuery, SearchControls searchControls, AttributesMapper mapper, LdapOrADServer server) {
LdapTemplate ldapTemplate = buildLdapTeamplate(server);
return ldapTemplate.search(base, ldapQuery, searchControls, mapper);
}
private LdapTemplate buildLdapTeamplate(LdapOrADServer server) {
LdapTemplate ldapTemplate = new LdapTemplate(server.getContextSource());
ldapTemplate.setIgnorePartialResultException(true); // To avoid the exceptions due to referrals returned
return ldapTemplate;
}
/**
* Get the user's token groups from AD. The groups returned will be on the
* whitelist that is configured for this authN provider. Also the group name
* in the list will be the configured user attribute.
*
* @param ldapQuery Configured LDAP query to search for the user
* @param storageOSUser The storageOSUser to get tokenGroups for
* @return the names of the white listed groups for the user.
*/
public List<String> queryTokenGroups(final String ldapQuery, final StorageOSUserDAO storageOSUser) {
List<String> groups = new ArrayList<String>();
SearchControls dnSearchControls;
String dn = storageOSUser.getDistinguishedName();
dnSearchControls = new SearchControls(
SearchControls.OBJECT_SCOPE,
1,
_searchControls.getTimeLimit(),
new String[] { TOKEN_GROUPS },
_searchControls.getReturningObjFlag(), _searchControls.getDerefLinkFlag());
@SuppressWarnings("unchecked")
List<List<String>> tokenGroupSids =
safeLdapSearch(dn, ldapQuery, dnSearchControls, new TokenGroupsMapper());
if (null == tokenGroupSids) {
_log.debug("No groups found for user: ", storageOSUser.getUserName());
return groups;
}
List<String> unFilteredGroups = resolveGroups(tokenGroupSids.get(0));
for (String groupName : unFilteredGroups) {
String groupNameWithoutDomain = groupName.substring(0, groupName.lastIndexOf("@"));
if (isGroupOnWhiteList(groupNameWithoutDomain)) {
groups.add(groupName);
}
}
return groups;
}
/**
* Given a list of group SIDs do an LDAP search to get the configured group attribute
*
* @param groupSids List of group object SIDs
* @return a list of groups resolved using the group sids
*/
public List<String> resolveGroups(final List<String> groupSids) {
List<String> resolvedGroups = new ArrayList<String>();
List<List<String>> partitionedGroupSids = Lists.partition(groupSids, _maxPageSize);
_log.debug("User is in {} number of token groups", groupSids.size());
if (partitionedGroupSids.size() > 1) {
_log.info("Partitioning group query into {} lists since max results is {}", partitionedGroupSids.size(), _maxPageSize);
}
for (List<String> groupSidPartition : partitionedGroupSids) {
SearchControls groupSearchControls = new SearchControls(
SearchControls.SUBTREE_SCOPE,
_maxPageSize, _searchControls.getTimeLimit(),
null, _searchControls.getReturningObjFlag(),
_searchControls.getDerefLinkFlag());
final String groupSidLdapQuery = getGroupSidQueryFilter(OBJECT_SID, groupSidPartition);
if (groupSidLdapQuery == null) {
_log.error("Group sid query filter was null when trying to resolve groups");
return resolvedGroups;
}
@SuppressWarnings("unchecked")
final List<List<GroupAttribute>> resolvedGroupAttributeList = safeLdapSearch(_baseDN, groupSidLdapQuery,
groupSearchControls, new GroupsMapper(_groupWhiteList.getType()));
if (null == resolvedGroupAttributeList) {
_log.error("Query to resolve groups returned no results");
return resolvedGroups;
}
for (List<GroupAttribute> resolvedGroupAttribute : resolvedGroupAttributeList) {
if (!resolvedGroupAttribute.isEmpty()) {
resolvedGroups.add(resolvedGroupAttribute.get(0).getGroupNameWithDomain());
}
}
}
return resolvedGroups;
}
/**
* Check if a group is on the whitelist for this authN provider
*
* @param groupId ID of the group to check
* @return true if the group is on the white list false otherwise
*/
private boolean isGroupOnWhiteList(String groupId) {
Pattern[] patterns = _groupWhiteList.getCompiledPatterns();
if (patterns != null && patterns.length > 0) {
for (Pattern pattern : patterns) {
if (pattern.matcher(groupId).matches()) {
return true;
}
}
} else {
return true;
}
return false;
}
/**
* Add the user group to the storageos user's
* group list if the storageos user's attributes and values matches
* with the user group configs.
*
* @param permissionsHelper to find and match the db objects.
* @param domain to find all the userMappings for the domain.
* @param storageOSUser to be updated with the list of user
* group to the storageos user's group list.
*/
private void addUserGroupsToUserGroupList(BasePermissionsHelper permissionsHelper, String domain, StorageOSUserDAO storageOSUser) {
if (StringUtils.isBlank(domain)) {
_log.error("Invalid domain {} to search user group", domain);
return;
}
List<UserGroup> userGroupList = permissionsHelper.getAllUserGroupForDomain(domain);
if (CollectionUtils.isEmpty(userGroupList)) {
_log.debug("Cannot find user mappings for the domain {}", domain);
return;
}
for (UserGroup userGroup : userGroupList) {
if (userGroup != null) {
if (permissionsHelper.matchUserAttributesToUserGroup(storageOSUser, userGroup)) {
_log.debug("Adding user group {} to the user", userGroup.getLabel());
storageOSUser.addGroup(userGroup.getLabel());
}
} else {
_log.info("Invalid user group returned while searching db with domain {}", domain);
}
}
}
/**
* Get the list of returning attributes AD or LDAP servers
* based on the configured user group.
*
* @param permissionsHelper to find and match the db objects.
* @param domain to find all the configured user group for the domain.
* @param attrs out param, to be updated with the list of attributes
* to be returned from the AD or LDAP servers.
*/
private void getReturningAttributesFromUserGroups(BasePermissionsHelper permissionsHelper, String domain, List<String> attrs) {
if (StringUtils.isBlank(domain)) {
_log.info("Invalid domain {} to search user group", domain);
return;
}
List<UserGroup> userGroupList = permissionsHelper.getAllUserGroupForDomain(domain);
if (CollectionUtils.isEmpty(userGroupList)) {
_log.debug("User group not found for the domain {}", domain);
return;
}
for (UserGroup userGroup : userGroupList) {
if (userGroup == null || CollectionUtils.isEmpty(userGroup.getAttributes())) {
continue;
}
for (String userAttributesString : userGroup.getAttributes()) {
if (StringUtils.isBlank(userAttributesString)) {
_log.info("Invalid user attributes param string {}", userAttributesString);
continue;
}
UserAttributeParam userAttributeParam = UserAttributeParam.fromString(userAttributesString);
if (userAttributeParam == null) {
_log.info("Conversion from user attributes param string {} to attributes param object failed.", userAttributesString);
continue;
}
_log.debug("Adding attribute {} to the returning attributes list", userAttributeParam.getKey());
attrs.add(userAttributeParam.getKey());
}
}
}
/**
* Checks whether searching groups in LDAP can be done or not. This is done based on
* the authentication provider's configuration. If the Authentication provider mode is
* LDAP, it should have LDAP Group properties like group object classes and group
* member attributes in order to search for the groups in LDAP.
*
* @return - return true if the authentication provider's configuration contains valid
* group object classes and group member attributes otherwise false.
*
*/
private boolean shouldSearchGroupInLDAP() {
boolean continueToGroupSearch = false;
if (!CollectionUtils.isEmpty(this._groupObjectClasses) &&
!CollectionUtils.isEmpty(this._groupMemberAttributes)) {
continueToGroupSearch = true;
}
return continueToGroupSearch;
}
/**
* Creates the objectClass filter for the AD or LDAP search. For AD, the only possible
* objectClass filter is "group" for LDAP, it can be any of "groupOfNames", "groupOfUniqueNames",
* "posixGroup", "organizationalRole". These are inputs from API payload. The default is "groupOfNames".
*
* @return - returns the objectClass filter based on the type of the authn provider.
* null for the ldap authn provider if authn provider configuration does not
* contain any ldap group search properties like object classes and group
* member attributes.
*
*/
private Filter createGroupObjectClassFilter() {
OrFilter groupObjectClassFilter = null;
if (_type == ProvidersType.ad) {
groupObjectClassFilter = new OrFilter();
final Filter localObjectClassFilter = new EqualsFilter("objectClass", "group");
groupObjectClassFilter.or(localObjectClassFilter);
} else {
if (shouldSearchGroupInLDAP()) {
groupObjectClassFilter = new OrFilter();
for (String objectClass : this._groupObjectClasses) {
final Filter localObjectClassFilter = new EqualsFilter("objectClass", objectClass);
groupObjectClassFilter.or(localObjectClassFilter);
}
}
}
return groupObjectClassFilter;
}
/**
* Returns the distinguished name's attribute type name based on the
* authentication provider type.
*
* @return - returns the DN attributeType name based on the authn
* provider type.
*
*/
private String getDistinguishedNameAttribute() {
String distinguishedNameAttr;
if (_type == ProvidersType.ad) {
distinguishedNameAttr = AD_DISTINGUISHED_NAME;
} else {
distinguishedNameAttr = LDAP_DISTINGUISHED_NAME;
}
return distinguishedNameAttr;
}
/**
* Finds the group based on one of (either groupOfNames, groupOfUniqueNames, posixGroup, organizationalRole)
* objectClass and one of these attribute (either member, uniqueMember, memberUid, roleOccupant) attributes
* from LDAP.
* This function finds multiple level user's group membership in the given search base.
*
* @param storageOSUser - A domain user whose group member is being found.
* @param failureReason - A string to be updated with failure reason if there is any failure.
*
* @return - true if the search is successful (or if the authentication provider is not in ldap mode).
* otherwise false.
*
*/
private boolean updateGroupsAndRootGroupsInLDAPByMemberAttribute(StorageOSUserDAO storageOSUser,
ValidationFailureReason[] failureReason) {
boolean foundUserGroups = false;
if (_type != ProvidersType.ldap) {
return foundUserGroups;
}
String memberEntryDN = storageOSUser.getDistinguishedName();
Set<String> allGroupsOfUser = new HashSet<String>();
findGroupsInLDAPByMemberAttribute(memberEntryDN, allGroupsOfUser, failureReason);
if (CollectionUtils.isEmpty(allGroupsOfUser)) {
return foundUserGroups;
}
for (String groupWithDomain : allGroupsOfUser) {
if (StringUtils.isNotBlank(groupWithDomain)) {
storageOSUser.addGroup(groupWithDomain);
foundUserGroups = true;
_log.debug("Group {} added to user {}", groupWithDomain, storageOSUser.getDistinguishedName());
}
}
return foundUserGroups;
}
/**
* Finds the group based on one of (ex., groupOfNames, groupOfUniqueNames, posixGroup, organizationalRole)
* objectClass and one of these attribute (ex., member, uniqueMember, memberUid, roleOccupant) attributes
* from LDAP.
*
* @param memberEntryDN - A value to be searched in the values of any of the given group's attribute.
* @param allGroupsOfUserWithDomain - An out param. Set all the groups with domain suffix to which the user is member off.
* @param failureReason - A string to be updated with failure reason if there is any failure.
*
*/
private void findGroupsInLDAPByMemberAttribute(String memberEntryDN, Set<String> allGroupsOfUserWithDomain,
ValidationFailureReason[] failureReason) {
if (_type != ProvidersType.ldap) {
_log.info("Non ldap authn provider.");
return;
}
if (StringUtils.isBlank(memberEntryDN)) {
_log.error("Invalid DN {} to search in ldap.", memberEntryDN);
}
final Filter groupObjectClassFilter = createGroupObjectClassFilter();
if (groupObjectClassFilter == null) {
// Empty LDAP group search properties. Just return true.
_log.info("Empty ldap group object classes or attributes.");
return;
}
final long countLimit = 0L;
final AndFilter queryBuilder = new AndFilter();
final OrFilter groupMemberAttributeFilter = new OrFilter();
for (String groupMemberAttribute : this._groupMemberAttributes) {
// Create or filter based on all the group member attributes given
// the user in either portal or API.
final Filter localGroupMemberAttributeFilter = new EqualsFilter(groupMemberAttribute, memberEntryDN);
groupMemberAttributeFilter.or(localGroupMemberAttributeFilter);
}
// Query filter is based on the given objectClasses, member attributes.
queryBuilder.and(groupObjectClassFilter);
queryBuilder.and(groupMemberAttributeFilter);
// Expecting the attributes from the search results that can be used to construct
// the GroupAttribute object.
Set<String> returnAttributesSet = new HashSet<String>(this._groupMemberAttributes);
returnAttributesSet.add(COMMON_NAME);
returnAttributesSet.add(OBJECT_CLASS);
returnAttributesSet.add(getDistinguishedNameAttribute());
String[] returnAttributes = returnAttributesSet.toArray(new String[returnAttributesSet.size()]);
List<List<GroupAttribute>> queryGroupResults = null;
try {
queryGroupResults = searchAuthProvider(queryBuilder, returnAttributes, countLimit,
new GroupsMapper(_groupWhiteList.getType(), getDistinguishedNameAttribute()), failureReason);
} catch (SizeLimitExceededException e) {
_log.error("Multiple entries for group are found in LDAP. Please use other group attributes such as cn or entryDN to uniquely identify the group.");
failureReason[0] = ValidationFailureReason.USER_OR_GROUP_NOT_FOUND_FOR_TENANT;
}
if (CollectionUtils.isEmpty(queryGroupResults)) {
_log.debug("{} is not a member of any group ", memberEntryDN);
return;
}
if (allGroupsOfUserWithDomain == null) {
allGroupsOfUserWithDomain = new HashSet<String>();
}
for (List<GroupAttribute> groupAttrResults : queryGroupResults) {
if (CollectionUtils.isEmpty(groupAttrResults)) {
continue;
}
for (GroupAttribute groupAttr : groupAttrResults) {
String groupDN = groupAttr.getGroupDistinguishedName();
allGroupsOfUserWithDomain.add(groupAttr.getGroupNameWithDomain());
_log.debug("Finding the higher level group of {}", groupDN);
findGroupsInLDAPByMemberAttribute(groupAttr.getGroupDistinguishedName(), allGroupsOfUserWithDomain, failureReason);
}
}
}
/**
* Searches the Authentication Provider (either LDAP or AD) based on the given
* search controls and required return attributes and return count limit.
*
* @param queryBuilder - A query to be used to search in authn provider.
* @param returnAttributes - An array of attributes to be returned from the search.
* @param countLimit - number of expected objects to be returns that matches the
* search query.
* @param mapper - An attribute mapper used to extract the required values from
* the search.
* @param failureReason - A string to be updated with failure reason if there is any failure.
*
* @return - Returns the list of GroupAttribute that matches the search criteria.
*
*/
@SuppressWarnings("unchecked")
private List<List<GroupAttribute>> searchAuthProvider(
Filter queryBuilder, String[] returnAttributes,
final long countLimit,
AttributesMapper mapper, ValidationFailureReason[] failureReason) throws SizeLimitExceededException {
SearchControls groupSearchControls = new SearchControls(
SearchControls.SUBTREE_SCOPE,
countLimit, _searchControls.getTimeLimit(),
returnAttributes, _searchControls.getReturningObjFlag(),
_searchControls.getDerefLinkFlag());
List<List<GroupAttribute>> queryGroupResults = null;
queryGroupResults = safeLdapSearch(_baseDN, queryBuilder.encode(), groupSearchControls, mapper, failureReason);
return queryGroupResults;
}
public void setLdapServers(LdapServerList ldapServers) {
_ldapServers = ldapServers;
}
/**
* Class to implement map tokenGroups attribute to a list of SID strings
*/
private class TokenGroupsMapper implements AttributesMapper {
@Override
public Object mapFromAttributes(Attributes attributes) throws NamingException {
List<String> tokenGroupSids = new ArrayList<String>();
Attribute tokenGroupsAttr = attributes.get(TOKEN_GROUPS);
if (null != tokenGroupsAttr) {
NamingEnumeration<?> tokenGroups = tokenGroupsAttr.getAll();
while (tokenGroups.hasMoreElements()) {
byte[] bytes = (byte[]) tokenGroups.nextElement();
if (null != bytes) {
tokenGroupSids.add(getSidAsString(bytes));
}
}
}
return tokenGroupSids;
}
}
/**
* Class to map a group Attributes to a string list of groups
*/
private class GroupsMapper implements AttributesMapper {
private final String _groupAttribute;
private final String _groupDNAttributeTypeName;
public GroupsMapper(String groupAttribute) {
this(groupAttribute, AD_DISTINGUISHED_NAME);
}
public GroupsMapper(String groupAttribute, String distinguishedNameId) {
super();
_groupAttribute = groupAttribute;
_groupDNAttributeTypeName = distinguishedNameId;
}
@Override
public Object mapFromAttributes(Attributes attributes) throws NamingException {
List<GroupAttribute> groups = new ArrayList<GroupAttribute>();
// get distinguishedName
Attribute dnAttr = attributes.get(_groupDNAttributeTypeName);
String dn = null;
if (null != dnAttr) {
dn = (String) dnAttr.get();
}
Attribute cnAttribute = attributes.get("cn");
String cn = null;
if (cnAttribute != null) {
cn = (String) cnAttribute.get();
}
Attribute groupAttr = attributes.get(_groupAttribute);
if (null != groupAttr) {
NamingEnumeration<?> groupAttrValues = groupAttr.getAll();
while (groupAttrValues.hasMoreElements()) {
Object group = groupAttrValues.nextElement();
if (null != group) {
String groupString;
if (group instanceof byte[]) {
groupString = getSidAsString((byte[]) group);
} else {
groupString = group.toString();
}
GroupAttribute groupObject = new GroupAttribute();
groupObject.setGroupAttributeValue(groupString);
groupObject.setGroupDistinguishedName(dn);
groupObject.setGroupCommonName(cn);
groups.add(groupObject);
}
}
}
return groups;
}
}
/**
* group attribute returns from ldap search, it contains
* 1. the attribute value specified by the search,
* 2. and distinguishName of the group object, which will be used for domain validation.
*/
private class GroupAttribute {
private String groupAttributeValue;
private String groupDistinguishedName;
private String groupCommonName;
private String groupDomain;
public void setGroupAttributeValue(String value) {
groupAttributeValue = value;
}
public void setGroupDistinguishedName(String dn) {
groupDistinguishedName = dn;
}
public String getGroupAttributeValue() {
return groupAttributeValue;
}
public String getGroupDistinguishedName() {
return groupDistinguishedName;
}
public String getGroupCommonName() {
return groupCommonName;
}
public void setGroupCommonName(String groupCommonName) {
this.groupCommonName = groupCommonName;
}
public String getGroupDomain() {
return groupDomain;
}
public void setGroupDomain(String groupDomain) {
this.groupDomain = groupDomain;
}
/**
* validate if the group object match the specified domain
*
* @param domain format as "vipr.com"
* @return
*/
public boolean domainMatch(String domain) {
if (domain == null || groupDistinguishedName == null) {
return false;
}
if (StringUtils.isBlank(this.groupDomain)) {
this.groupDomain = getDomainFromDN();
}
if (domain.equalsIgnoreCase(this.groupDomain)) {
return true;
}
return false;
}
/**
* construct domain from group's distinguish name,
*
* @return domain extract DC parts from dn, formatted as "vipr.com"
*/
public String getDomainFromDN() {
if (groupDistinguishedName == null) {
groupDistinguishedName = "";
}
String domainName = "";
try {
LdapName dn = new LdapName(groupDistinguishedName);
List<String> dcs = new LinkedList<String>();
for (Rdn rdn : dn.getRdns()) {
if (rdn.getType().equalsIgnoreCase("DC")) {
dcs.add(0, rdn.getValue().toString());
}
}
domainName = StringUtils.join(dcs, '.');
} catch (InvalidNameException e) {
_log.error("{} is not a standard dn", groupDistinguishedName);
}
return domainName;
}
/**
* construct group name with domain name
*/
public String getGroupNameWithDomain() {
if (StringUtils.isBlank(this.groupDomain)) {
this.groupDomain = getDomainFromDN();
}
return groupAttributeValue + "@" + this.groupDomain;
}
}
/**
* Private class to be a container for a StorageOSUserDAO
* And their tenants
*/
private class UserAndTenants {
public UserAndTenants(StorageOSUserDAO user,
Map<URI, UserMapping> tenants) {
_user = user;
_tenants = tenants;
}
private final StorageOSUserDAO _user;
private final Map<URI, UserMapping> _tenants;
}
private void printTenantToMappingMap(Map<URI, List<UserMapping>> maps) {
Iterator<URI> keys = maps.keySet().iterator();
_log.debug("user mapping: ");
while (keys.hasNext()) {
URI key = keys.next();
if (maps.get(key) != null) {
_log.debug(key + " = " + maps.get(key).toString());
}
}
}
/**
* Gets all the domains supported by the authn providers that supports
* the particular domain.
*
* @param domain to find the supported authn provider.
* @return returns all the supported domains of each authn provider
* supports the domain.
*/
private StringSet getAuthnProviderDomains(String domain) {
StringSet authnProviderDomains = new StringSet();
URIQueryResultList providers = new URIQueryResultList();
try {
_dbClient.queryByConstraint(AlternateIdConstraint.Factory
.getAuthnProviderDomainConstraint(domain.toLowerCase()), providers);
} catch (DatabaseException ex) {
_log.error(
"Could not query for authn providers to check for existing domain {}",
domain, ex.getStackTrace());
throw ex;
}
//Add all the domains of the AuthnProvider if it is not in disabled state.
//We expect only one authn provider here because, we cannot have multiple
//authn provider supporting same domain.
Iterator<URI> it = providers.iterator();
if (it.hasNext()) {
URI providerURI = it.next();
AuthnProvider provider = _dbClient.queryObject(AuthnProvider.class, providerURI);
if (provider != null && provider.getDisable() == false) {
authnProviderDomains.addAll(provider.getDomains());
}
}
return authnProviderDomains;
}
}