001package co.codewizards.cloudstore.rest.server.ldap;
002
003import static java.util.Objects.*;
004
005import java.util.ArrayList;
006import java.util.HashMap;
007import java.util.List;
008import java.util.Map;
009
010import javax.naming.AuthenticationException;
011import javax.naming.Context;
012import javax.naming.NamingEnumeration;
013import javax.naming.NamingException;
014import javax.naming.directory.DirContext;
015import javax.naming.directory.InitialDirContext;
016import javax.naming.directory.SearchControls;
017import javax.naming.directory.SearchResult;
018
019import co.codewizards.cloudstore.core.util.IOUtil;
020import co.codewizards.cloudstore.rest.server.auth.Auth;
021import co.codewizards.cloudstore.rest.server.auth.NotAuthorizedException;
022/**
023 * Authentication flow used by this client:
024 * At first DirContext is created, based on provided url, adminDn and adminPassword.
025 * Then on this DirContext instance search is executed for given LDAP query and queryDN.
026 *
027 * Search has SUBTREE_SCOPE.
028 * @see <a href="http://docs.oracle.com/javase/7/docs/api/javax/naming/directory/SearchControls.html#SUBTREE_SCOPE">SUBTREE_SCOPE</a>
029 *
030 * Query should contain template variable ${login}(one or more) which is replaced with userName from provided Auth object
031 *
032 * If search returns any results then credentials of DirContext are replaced with result's DN (as PRINCIPAL)
033 * and password from provided Auth (as CREDENTIALS), and lookup() is called.
034 *
035 * If lookup() doesn't throw AuthenticationException then authentication succeeded.
036 * @author Wojtek Wilk - wilk.wojtek at gmail.com
037 */
038public class QueryLdapClient implements LdapClient{
039
040        private static final String TEMPLATE_VARIABLE = "login";
041
042        private final String query;
043        private final String queryDn;
044        private final String url;
045        private final String adminDn;
046        private final char[] adminPassword;
047
048        public QueryLdapClient(String query, String queryDn, String url, String bindDn, char[] password) {
049                this.query = requireNonNull(query, "query");
050                this.queryDn = requireNonNull(queryDn, "queryDn");
051                this.url = requireNonNull(url, "url");
052                this.adminDn = requireNonNull(bindDn, "bindDn");
053                this.adminPassword = requireNonNull(password, "password");
054        }
055
056        @Override
057        public String authenticate(final Auth auth) {
058                try{
059                        final LdapConfig config = new LdapConfig(url, adminDn, adminPassword);
060                        final DirContext context = new InitialDirContext(config);
061
062                        List<String> usersDns = findAllUsersThatMatchQuery(context, auth);
063
064                        for(String userDn : usersDns)
065                                if(tryAuthenticate(context, userDn, auth.getPassword()))
066                                        return auth.getUserName();
067                }catch(NamingException e){
068                        throw new RuntimeException(e);
069                }
070                throw new NotAuthorizedException();
071        }
072
073        private List<String> findAllUsersThatMatchQuery(final DirContext context, final Auth auth) throws NamingException{
074                final NamingEnumeration<SearchResult> results = findUsersWithQuery(context, auth.getUserName());
075                List<String> usersDns = new ArrayList<>();
076                while(results.hasMore())
077                        usersDns.add(results.next().getNameInNamespace());
078                return usersDns;
079        }
080
081        private NamingEnumeration<SearchResult> findUsersWithQuery(DirContext context, String userName) throws NamingException{
082                final SearchControls searchControls = new SearchControls();
083        searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
084        final String replacedQuery = convertTemplate(query, userName);
085                return context.search(queryDn, replacedQuery, searchControls);
086        }
087
088        private boolean tryAuthenticate(final DirContext context, final String userName, final char[] password) throws NamingException{
089                try{
090                        context.addToEnvironment(Context.SECURITY_PRINCIPAL, userName);
091                        context.addToEnvironment(Context.SECURITY_CREDENTIALS, password);
092                        context.lookup(userName);
093                        return true;
094                } catch(AuthenticationException e){
095                        return false;
096                }
097        }
098
099        private String convertTemplate(final String template, final String username){
100                final Map<String, String> map = new HashMap<String, String>(1);
101                map.put(TEMPLATE_VARIABLE, username);
102                return IOUtil.replaceTemplateVariables(template, map);
103        }
104}