/*
* Copyright (C) 2013 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package net.sourceforge.guacamole.net.auth.ldap;
import com.novell.ldap.LDAPAttribute;
import com.novell.ldap.LDAPConnection;
import com.novell.ldap.LDAPEntry;
import com.novell.ldap.LDAPException;
import com.novell.ldap.LDAPSearchResults;
import java.io.UnsupportedEncodingException;
import java.util.Enumeration;
import java.util.Map;
import java.util.TreeMap;
import org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.net.auth.Credentials;
import net.sourceforge.guacamole.net.auth.ldap.properties.LDAPGuacamoleProperties;
import org.glyptodon.guacamole.GuacamoleServerException;
import org.glyptodon.guacamole.net.auth.simple.SimpleAuthenticationProvider;
import org.glyptodon.guacamole.properties.GuacamoleProperties;
import org.glyptodon.guacamole.protocol.GuacamoleConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Allows users to be authenticated against an LDAP server. Each user may have
* any number of authorized configurations. Authorized configurations may be
* shared.
*
* @author Michael Jumper
*/
public class LDAPAuthenticationProvider extends SimpleAuthenticationProvider {
/**
* Logger for this class.
*/
private Logger logger = LoggerFactory.getLogger(LDAPAuthenticationProvider.class);
// Courtesy of OWASP: https://www.owasp.org/index.php/Preventing_LDAP_Injection_in_Java
private static 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();
}
// Courtesy of OWASP: https://www.owasp.org/index.php/Preventing_LDAP_Injection_in_Java
private static String escapeDN(String name) {
StringBuilder sb = new StringBuilder();
if ((name.length() > 0) && ((name.charAt(0) == ' ') || (name.charAt(0) == '#'))) {
sb.append('\\'); // add the leading backslash if needed
}
for (int i = 0; i < name.length(); i++) {
char curChar = name.charAt(i);
switch (curChar) {
case '\\':
sb.append("\\\\");
break;
case ',':
sb.append("\\,");
break;
case '+':
sb.append("\\+");
break;
case '"':
sb.append("\\\"");
break;
case '<':
sb.append("\\<");
break;
case '>':
sb.append("\\>");
break;
case ';':
sb.append("\\;");
break;
default:
sb.append(curChar);
}
}
if ((name.length() > 1) && (name.charAt(name.length() - 1) == ' ')) {
sb.insert(sb.length() - 1, '\\'); // add the trailing backslash if needed
}
return sb.toString();
}
@Override
public Map<String, GuacamoleConfiguration> getAuthorizedConfigurations(Credentials credentials) throws GuacamoleException {
// Require username
if (credentials.getUsername() == null) {
logger.debug("Anonymous bind is not currently allowed by the LDAP authentication provider.");
return null;
}
// Require password, and do not allow anonymous binding
if (credentials.getPassword() == null
|| credentials.getPassword().length() == 0) {
logger.debug("Anonymous bind is not currently allowed by the LDAP authentication provider.");
return null;
}
// Connect to LDAP server
LDAPConnection ldapConnection;
try {
ldapConnection = new LDAPConnection();
ldapConnection.connect(
GuacamoleProperties.getRequiredProperty(LDAPGuacamoleProperties.LDAP_HOSTNAME),
GuacamoleProperties.getRequiredProperty(LDAPGuacamoleProperties.LDAP_PORT)
);
}
catch (LDAPException e) {
throw new GuacamoleServerException("Unable to connect to LDAP server.", e);
}
// Get username attribute
String username_attribute = GuacamoleProperties.getRequiredProperty(
LDAPGuacamoleProperties.LDAP_USERNAME_ATTRIBUTE
);
// Get user base DN
String user_base_dn = GuacamoleProperties.getRequiredProperty(
LDAPGuacamoleProperties.LDAP_USER_BASE_DN
);
// Construct user DN
String user_dn =
escapeDN(username_attribute) + "=" + escapeDN(credentials.getUsername())
+ "," + user_base_dn;
try {
// Bind as user
try {
ldapConnection.bind(
LDAPConnection.LDAP_V3,
user_dn,
credentials.getPassword().getBytes("UTF-8")
);
}
catch (UnsupportedEncodingException e) {
throw new GuacamoleException(e);
}
}
catch (LDAPException e) {
logger.debug("LDAP bind failed.", e);
return null;
}
// Get config base DN
String config_base_dn = GuacamoleProperties.getRequiredProperty(
LDAPGuacamoleProperties.LDAP_CONFIG_BASE_DN
);
// Pull all connections
try {
// Find all guac configs for this user
LDAPSearchResults results = ldapConnection.search(
config_base_dn,
LDAPConnection.SCOPE_SUB,
"(&(objectClass=guacConfigGroup)(member=" + escapeLDAPSearchFilter(user_dn) + "))",
null,
false
);
// Add all configs
Map<String, GuacamoleConfiguration> configs = new TreeMap<String, GuacamoleConfiguration>();
while (results.hasMore()) {
LDAPEntry entry = results.next();
// New empty configuration
GuacamoleConfiguration config = new GuacamoleConfiguration();
// Get CN
LDAPAttribute cn = entry.getAttribute("cn");
if (cn == null)
throw new GuacamoleException("guacConfigGroup without cn");
// Get protocol
LDAPAttribute protocol = entry.getAttribute("guacConfigProtocol");
if (protocol == null)
throw new GuacamoleException("guacConfigGroup without guacConfigProtocol");
// Set protocol
config.setProtocol(protocol.getStringValue());
// Get parameters, if any
LDAPAttribute parameterAttribute = entry.getAttribute("guacConfigParameter");
if (parameterAttribute != null) {
// For each parameter
Enumeration<String> parameters = parameterAttribute.getStringValues();
while (parameters.hasMoreElements()) {
String parameter = parameters.nextElement();
// Parse parameter
int equals = parameter.indexOf('=');
if (equals != -1) {
// Parse name
String name = parameter.substring(0, equals);
String value = parameter.substring(equals+1);
config.setParameter(name, value);
}
}
}
// Store config by CN
configs.put(cn.getStringValue(), config);
}
// Disconnect
ldapConnection.disconnect();
return configs;
}
catch (LDAPException e) {
throw new GuacamoleServerException("Error while querying for connections.", e);
}
}
}