package io.cattle.platform.iaas.api.auth.integration.ldap;
import static javax.naming.directory.SearchControls.*;
import io.cattle.platform.api.auth.Identity;
import io.cattle.platform.core.constants.IdentityConstants;
import io.cattle.platform.iaas.api.auth.AbstractTokenUtil;
import io.cattle.platform.iaas.api.auth.integration.interfaces.IdentityProvider;
import io.cattle.platform.iaas.api.auth.integration.ldap.ad.LdapServiceContextPoolFactory;
import io.cattle.platform.iaas.api.auth.integration.ldap.interfaces.LDAPConstants;
import io.cattle.platform.pool.PoolConfig;
import io.github.ibuildthecloud.gdapi.exception.ClientVisibleException;
import io.github.ibuildthecloud.gdapi.util.ResponseCodes;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import javax.annotation.PostConstruct;
import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attributes;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;
import javax.naming.ldap.LdapName;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.pool2.impl.AbandonedConfig;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public abstract class LDAPIdentityProvider implements IdentityProvider{
private static final Logger log = LoggerFactory.getLogger(LDAPIdentityProvider.class);
@Override
public List<Identity> searchIdentities(String name, boolean exactMatch) {
if (!isConfigured()){
notConfigured();
}
List<Identity> identities = new ArrayList<>();
for (String scope : scopes()) {
identities.addAll(searchIdentities(name, scope, exactMatch));
}
return identities;
}
@Override
public List<Identity> searchIdentities(String name, String scope, boolean exactMatch) {
if (!isConfigured()){
notConfigured();
}
name = escapeLDAPSearchFilter(name);
if (getConstantsConfig().getUserScope().equalsIgnoreCase(scope)) {
return searchUser(name, exactMatch);
} else if(getConstantsConfig().getGroupScope().equalsIgnoreCase(scope)) {
return searchGroup(name, exactMatch);
} else{
throw new ClientVisibleException(ResponseCodes.BAD_REQUEST, "invalidScope", "Identity type is not valid for Ldap", null);
}
}
@Override
public Identity getIdentity(String distinguishedName, String scope) {
if (!isConfigured()){
notConfigured();
}
if (!getConstantsConfig().scopes().contains(scope)) {
throw new ClientVisibleException(ResponseCodes.BAD_REQUEST, "invalidScope", "Identity type is not valid for Ldap", null);
}
try {
return getObject(distinguishedName, scope);
}
catch (ServiceContextCreationException e){
throw new ClientVisibleException(ResponseCodes.SERVICE_UNAVAILABLE, "LdapDown", "Could not create service context: " + e.getMessage(), null);
} catch (ServiceContextRetrievalException e) {
throw new ClientVisibleException(ResponseCodes.SERVICE_UNAVAILABLE, "LdapDown", "Could not retrieve service context: " + e.getMessage(), null);
}
}
@Override
public Identity transform(Identity identity) {
if (getConstantsConfig().scopes().contains(identity.getExternalIdType())) {
return getIdentity(identity.getExternalId(), identity.getExternalIdType());
} else {
throw new ClientVisibleException(ResponseCodes.BAD_REQUEST,
IdentityConstants.INVALID_TYPE, "Ldap does not provide: " + identity.getExternalIdType(), null);
}
}
@Override
public Identity untransform(Identity identity) {
if (!getConstantsConfig().scopes().contains(identity.getExternalIdType())){
throw new ClientVisibleException(ResponseCodes.BAD_REQUEST,
IdentityConstants.INVALID_TYPE, "Ldap does not provide: " + identity.getExternalIdType(), null);
}
return identity;
}
protected List<Identity> searchGroup(String name, boolean exactMatch) {
String query;
if (exactMatch) {
query = "(&(" + getConstantsConfig().getGroupSearchField() + '=' + name + ")(" + getConstantsConfig().objectClass() + '='
+ getConstantsConfig().getGroupObjectClass() + "))";
} else {
query = "(&(" + getConstantsConfig().getGroupSearchField() + "=*" + name + "*)(" + getConstantsConfig().objectClass() + '='
+ getConstantsConfig().getGroupObjectClass() + "))";
}
log.trace("LDAPIdentityProvider searchGroup query: " + query);
return resultsToIdentities(searchLdap(query, getConstantsConfig().getGroupScope()));
}
protected List<Identity> searchUser(String name, boolean exactMatch) {
String query;
if (exactMatch)
{
query = "(&(" + getConstantsConfig().getUserSearchField() + '=' + name + ")(" + getConstantsConfig().objectClass() + '='
+ getConstantsConfig().getUserObjectClass() + "))";
} else {
query = "(&(" + getConstantsConfig().getUserSearchField() + "=*" + name + "*)(" + getConstantsConfig().objectClass() + '='
+ getConstantsConfig().getUserObjectClass() + "))";
}
log.trace("LDAPIdentityProvider searchUser query: " + query);
return resultsToIdentities(searchLdap(query, getConstantsConfig().getUserScope()));
}
protected List<Identity> resultsToIdentities(NamingEnumeration<SearchResult> results) {
List<Identity> identities = new ArrayList<>();
try {
if (!results.hasMore()) {
return identities;
}
} catch (NamingException e) {
return identities;
}
try {
while (results.hasMore()){
SearchResult result = results.next();
log.trace("LDAPIdentityProvider SearchResult: " + result);
LdapName dn = new LdapName(result.getNameInNamespace());
Identity identityObj = attributesToIdentity(dn);
if (identityObj != null) {
identities.add(identityObj);
}
}
} catch (NamingException e) {
//Ldap Referrals are causing this.
getLogger().debug("While iterating results from an ldap search.", e);
return identities;
}
return identities;
}
protected Identity getObject(String distinguishedName, String scope) {
LdapContext context = null;
try {
LdapName object = new LdapName(distinguishedName);
context = getServiceContext();
Attributes search;
search = context.getAttributes(object);
if (!isType(search, scope) && !hasPermission(search)){
return null;
}
return attributesToIdentity(object);
}
catch (NamingException e) {
getLogger().error("Failed to get object: {} : {}", distinguishedName, e.getExplanation());
if(!LDAPUtils.isRecoverable(e)) {
invalidateServiceContext(context);
context = null;
}
return null;
}
finally {
if (context != null) {
returnServiceContext(context);
}
}
}
protected Identity attributesToIdentity(LdapName dn){
LdapContext context = getServiceContext();
try {
Attributes search = context.getAttributes(dn);
log.trace("Attributes for dn: " + dn + " to translate: " + search);
String externalIdType;
String accountName;
String externalId = dn.toString();
String login;
if (isType(search, getConstantsConfig().getUserObjectClass())){
externalIdType = getConstantsConfig().getUserScope();
if (search.get(getConstantsConfig().getUserNameField()) != null) {
accountName = (String) search.get(getConstantsConfig().getUserNameField()).get();
} else {
accountName = externalId;
}
login = (String) search.get(getConstantsConfig().getUserLoginField()).get();
} else if (isType(search, getConstantsConfig().getGroupObjectClass())) {
externalIdType = getConstantsConfig().getGroupScope();
if (search.get(getConstantsConfig().getGroupNameField()) != null) {
accountName = (String) search.get(getConstantsConfig().getGroupNameField()).get();
} else {
accountName = externalId;
}
if (search.get(getConstantsConfig().getUserLoginField()) != null) {
login = (String) search.get(getConstantsConfig().getUserLoginField()).get();
} else {
login = accountName;
}
} else {
return null;
}
return new Identity(externalIdType, externalId, accountName, null, null, login);
} catch (NamingException e) {
getLogger().error("Failed to get attributes: {} : {}", dn, e.getExplanation());
if(!LDAPUtils.isRecoverable(e)) {
invalidateServiceContext(context);
context = null;
}
return null;
} finally {
if (context != null) {
returnServiceContext(context);
}
}
}
protected boolean isType(Attributes search, String type) {
NamingEnumeration<?> objectClass;
try {
objectClass = search.get(getConstantsConfig().objectClass()).getAll();
while (objectClass.hasMoreElements()) {
Object object = objectClass.next();
if ((object.toString()).equalsIgnoreCase(type)){
return true;
}
}
return false;
} catch (NamingException e) {
getLogger().info("Failed to determine if object is type:" + type, e);
return false;
}
}
protected NamingEnumeration<SearchResult> searchLdap(String query, String scope) {
SearchControls controls = new SearchControls();
LdapContext context = null;
controls.setSearchScope(SUBTREE_SCOPE);
NamingEnumeration<SearchResult> results;
try {
context = getServiceContext();
if (getConstantsConfig().getGroupScope().equalsIgnoreCase(scope) && StringUtils.isNotBlank(getConstantsConfig().getGroupSearchDomain())) {
results = context.search(getConstantsConfig().getGroupSearchDomain(), query, controls);
} else {
results = context.search(getConstantsConfig().getDomain(), query, controls);
}
} catch (NamingException e) {
getLogger().error("When searching ldap from /v1/identity Failed to search: " + query + " scope:" + getConstantsConfig().getDomain(), e);
if(!LDAPUtils.isRecoverable(e)) {
invalidateServiceContext(context);
context = null;
}
throw new ClientVisibleException(ResponseCodes.INTERNAL_SERVER_ERROR, getConstantsConfig().getConfig(),
"Could not lookup Organizational Unit", null);
} finally {
if (context != null){
returnServiceContext(context);
}
}
return results;
}
protected boolean hasPermission(Attributes attributes){
int permission;
try {
if (!isType(attributes, getConstantsConfig().getUserObjectClass())){
return true;
}
if (StringUtils.isNotBlank(getConstantsConfig().getUserEnabledAttribute())) {
permission = Integer.parseInt(attributes.get(getConstantsConfig().getUserEnabledAttribute()).get()
.toString());
} else {
return true;
}
} catch (NamingException e) {
getLogger().error("Failed to get USER_ENABLED_ATTRIBUTE.", e);
return false;
}
permission = permission & getConstantsConfig().getUserDisabledBitMask();
return permission != getConstantsConfig().getUserDisabledBitMask();
}
protected LdapContext login(String username, String password) {
if (StringUtils.isEmpty(password)) {
throw new UserLoginFailureException("Failed to login ldap User : Invalid Credentials");
}
Hashtable<String, String> props = new Hashtable<>();
props.put(Context.SECURITY_AUTHENTICATION, "simple");
props.put(Context.SECURITY_PRINCIPAL, username);
props.put(Context.SECURITY_CREDENTIALS, password);
props.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
LdapContext userContext;
try {
String url = "ldap://" + getConstantsConfig().getServer() + ':' + getConstantsConfig().getPort() + '/';
props.put(Context.PROVIDER_URL, url);
if (getConstantsConfig().getTls()) {
props.put(Context.SECURITY_PROTOCOL, "ssl");
}
userContext = new InitialLdapContext(props, null);
return userContext;
} catch (NamingException e) {
throw new UserLoginFailureException("Failed to login ldap User: " + LDAPUtils.errorCodeToDescription(e), e, username);
}
}
protected String escapeLDAPSearchFilter(String filter) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < filter.length(); i++) {
char curChar = filter.charAt(i);
switch (curChar) {
case '\\':
sb.append("\\5c");
break;
case '*':
sb.append("\\2a");
break;
case '(':
sb.append("\\28");
break;
case ')':
sb.append("\\29");
break;
case '\u0000':
sb.append("\\00");
break;
default:
sb.append(curChar);
}
}
return sb.toString();
}
protected LdapContext getServiceContext() {
try {
return getContextPool().borrowObject();
} catch (ServiceContextCreationException e) {
throw e;
} catch (Exception e) {
getLogger().error("Failed to get service context for ldap.", e);
throw new ServiceContextRetrievalException("Unable to borrow a service context from context pool.", e);
}
}
protected void returnServiceContext(LdapContext context) {
try {
getContextPool().returnObject(context);
} catch (Exception e) {
getLogger().info("Failed to return service context for ldap.", e);
}
}
protected void invalidateServiceContext(LdapContext context) {
try {
getContextPool().invalidateObject(context);
} catch (Exception e) {
getLogger().info("Failed to invalidate service context for ldap.", e);
}
}
@PostConstruct
public void init() {
if (getContextPool() == null) {
GenericObjectPoolConfig config = new GenericObjectPoolConfig();
PoolConfig.setConfig(config, "ldap.context.pool", "ldap.context.pool.", "global.pool.");
config.setTestOnBorrow(true);
LdapServiceContextPoolFactory serviceContextPoolFactory = new LdapServiceContextPoolFactory(getConstantsConfig());
setContextPool(new GenericObjectPool<>(serviceContextPoolFactory, config));
AbandonedConfig abandonedConfig = new AbandonedConfig();
abandonedConfig.setUseUsageTracking(true);
abandonedConfig.setRemoveAbandonedOnMaintenance(true);
abandonedConfig.setRemoveAbandonedOnBorrow(true);
abandonedConfig.setRemoveAbandonedTimeout(60);
getContextPool().setAbandonedConfig(abandonedConfig);
}
}
public void reset() {
if (getContextPool() != null) {
getContextPool().close();
setContextPool(null);
}
init();
}
protected abstract void setContextPool(GenericObjectPool<LdapContext> ldapContextGenericObjectPool);
protected abstract AbstractTokenUtil getTokenUtils();
protected abstract GenericObjectPool<LdapContext> getContextPool();
protected abstract LDAPConstants getConstantsConfig();
protected abstract Logger getLogger();
protected void notConfigured() {
throw new ClientVisibleException(ResponseCodes.SERVICE_UNAVAILABLE,
"NotConfigured", "Ldap is not configured", null);
}
public List<Identity> getIdentities(List<Map<String, String>> identitiesGiven) {
if (identitiesGiven == null || identitiesGiven.isEmpty()){
return new ArrayList<>();
}
List<Identity> identities = new ArrayList<>();
for (Map<String, String> identity: identitiesGiven){
String externalId = identity.get(IdentityConstants.EXTERNAL_ID);
String externalIdType = identity.get(IdentityConstants.EXTERNAL_ID_TYPE);
Identity gotIdentity = getIdentity(externalId, externalIdType);
if (gotIdentity == null) {
throw new ClientVisibleException(ResponseCodes.BAD_REQUEST, "InvalidIdentity", "Invalid Identity", null);
}
identities.add(gotIdentity);
}
return identities;
}
}