/*
*
* * This file is part of the Hesperides distribution.
* * (https://github.com/voyages-sncf-technologies/hesperides)
* * Copyright (c) 2016 VSCT.
* *
* * Hesperides is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as
* * published by the Free Software Foundation, version 3.
* *
* * Hesperides is distributed in the hope that it will be useful, but
* * WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* * General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*
*/
package com.vsct.dt.hesperides.security;
import com.google.common.base.Optional;
import com.vsct.dt.hesperides.security.model.User;
import io.dropwizard.auth.AuthenticationException;
import io.dropwizard.auth.Authenticator;
import io.dropwizard.auth.basic.BasicCredentials;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import java.util.Hashtable;
/**
* Created by william_montaz on 12/11/2014.
*/
public final class LDAPAuthenticator implements Authenticator<BasicCredentials, User> {
private static final Logger LOGGER = LoggerFactory.getLogger(LDAPAuthenticator.class);
/**
* AD matching rule.
* @link https://msdn.microsoft.com/en-us/library/aa746475(v=vs.85).aspx
*/
private static final String LDAP_MATCHING_RULE_IN_CHAIN_OID = "1.2.840.113556.1.4.1941";
private final LdapConfiguration configuration;
public LDAPAuthenticator(LdapConfiguration configuration) {
this.configuration = configuration;
}
@Override
public Optional<User> authenticate(final BasicCredentials credentials) throws AuthenticationException {
String username = credentials.getUsername();
String password = credentials.getPassword();
try {
try (AutoclosableDirContext context = buildContext(username, password)) {
//Get the user DN
SearchResult userSearched = searchUser(context, username);
//Check if user is in the prod group
final boolean prodUser = checkIfUserBelongsToGroup(context, userSearched.getNameInNamespace(), configuration.getProdGroupName());
final boolean techUser = checkIfUserBelongsToGroup(context, userSearched.getNameInNamespace(), configuration.getTechGroupName());
return Optional.of(new User(username, prodUser, techUser));
}
} catch (NamingException e) {
LOGGER.debug("{} failed to authenticate {}", username);
}
return Optional.absent();
}
private boolean checkIfUserBelongsToGroup(final AutoclosableDirContext context, final String userDN, final String groupName) throws
NamingException,
AuthenticationException {
String groupSearch = String.format("(CN=%s)", groupName);
SearchControls searchControls = new SearchControls();
searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
NamingEnumeration<SearchResult> groupResults = context.search(configuration.getRoleSearchBase(), groupSearch, searchControls);
SearchResult groupSearchResult;
if (groupResults.hasMoreElements()) {
groupSearchResult = groupResults.nextElement();
if (groupResults.hasMoreElements()) {
LOGGER.error("Expected to find only one group for " + configuration.getProdGroupName() + " but found more results");
return false;
}
} else {
LOGGER.error("Unable to find group {}", configuration.getProdGroupName());
return false;
}
//Search recursively to see if user is member of this group
//We search memberOf the prod group using user DN as base DN
//We should have one result if the user belongs to the group -> the user itself
String groupDN = groupSearchResult.getNameInNamespace();
searchControls.setSearchScope(SearchControls.OBJECT_SCOPE);
String memberOfSearch = String.format("(memberOf:%s:=%s)", LDAP_MATCHING_RULE_IN_CHAIN_OID, groupDN);
NamingEnumeration<SearchResult> memberOfSearchResults = context.search(userDN, memberOfSearch, searchControls);
if (memberOfSearchResults.hasMore()){
return true;
} else {
return false;
}
}
private SearchResult searchUser(final AutoclosableDirContext context, final String username) throws NamingException, AuthenticationException {
String searchfilter = String.format("(%s=%s)", configuration.getUserNameAttribute(), username);
SearchControls searchControls = new SearchControls();
searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
NamingEnumeration<SearchResult> results = context.search(configuration.getUserSearchBase(), searchfilter, searchControls);
SearchResult searchResult;
if (results.hasMoreElements()) {
searchResult = results.nextElement();
if (results.hasMoreElements()){
throw new AuthenticationException("Expected to find only one user for "+username+" but found more results");
}
} else {
throw new AuthenticationException("Unable to authenticate user "+username);
}
return searchResult;
}
private AutoclosableDirContext buildContext(final String username, final String password) throws NamingException {
Hashtable<String, String> env = contextConfiguration();
env.put(Context.SECURITY_PRINCIPAL, configuration.getAdDomain() + "\\" + username);
env.put(Context.SECURITY_CREDENTIALS, password);
return new AutoclosableDirContext(env);
}
private Hashtable<String, String> contextConfiguration() {
Hashtable<String, String> env = new Hashtable<>();
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, configuration.getUri());
env.put("com.sun.jndi.ldap.connect.timeout", String.valueOf(configuration.getConnectTimeout().toMilliseconds()));
env.put("com.sun.jndi.ldap.read.timeout", String.valueOf(configuration.getReadTimeout().toMilliseconds()));
env.put("com.sun.jndi.ldap.connect.pool", "true");
return env;
}
}