package com.augur.httpd.authorities; import com.augur.httpd.Authority; import com.augur.httpd.HttpRequest; import com.augur.httpd.Session; import java.util.Hashtable; import javax.naming.Context; import javax.naming.NamingEnumeration; import javax.naming.NamingException; import javax.naming.directory.DirContext; import javax.naming.directory.InitialDirContext; import javax.naming.directory.SearchControls; import javax.naming.directory.SearchResult; import org.json.JSONException; import org.json.JSONObject; import org.json.RPCRequest; /** * An LDAP implementation of a TriLevelAuthority for the JAWS web server. * Its configuration requires a JSONObject containing the following: *
   {
    "ldap.url": "ldap://localhost:10389",
    "ldap.filter.user": "(uid={})",
    "ldap.filter.admin":"(&(cn=Admins)(member={}))",
    "ldap.filter.tech":"(&(cn=Techs)(member={}))",
    "ldap.filter.guest":"(&(cn=Staff)(member={}))",
    "ldap.context.users": "",
    "ldap.context.groups": "",
    "ldap.authentication": "none",
    "ldap.principal": "",
    "ldap.credentials": ""
  }
 * 
* * Copyright 2016 Augur Systems, Inc. All rights reserved. */ public class TriLevelLDAP extends TriLevel implements Authority { private static final boolean DEBUG = !true; private static final String INITIAL_CONTEXT_FACTORY = "com.sun.jndi.ldap.LdapCtxFactory"; private final Hashtable LDAP_ENV = new Hashtable<>(); // can't use Map private final SearchControls SEARCH_CONTROLS = new SearchControls(); // NOTE: Not a thread-safe class private String CONTEXT_USERS, CONTEXT_GROUPS; private String USER_FILTER, GRP_FILTER_ADMIN, GRP_FILTER_TECH, GRP_FILTER_GUEST; private String PROVIDER_URL; @Override public void init(JSONObject config) throws Exception { super.init(config); // The LDAP server's address this.PROVIDER_URL = config.optString("ldap.url", "ldap://localhost:389"); // Filter to find user's record; note that {} is replaced by the user-supplied ID this.USER_FILTER = config.optString("ldap.filter.user","(uid={})"); // Optional path to start the user search somewhere more specific than the root this.CONTEXT_USERS = config.optString("ldap.context.users",""); // Optional path to start the group search somewhere more specific than the root this.CONTEXT_GROUPS = config.optString("ldap.context.groups",""); // Filters for our three application roles... this.GRP_FILTER_ADMIN = config.optString("ldap.filter.admin","(&(cn=Admins)(member={}))"); this.GRP_FILTER_TECH = config.optString("ldap.filter.tech","(&(cn=Techs)(member={}))"); this.GRP_FILTER_GUEST = config.optString("ldap.filter.guest","(&(cn=Guests)(member={}))"); // Specify we want to use the LDAP protocol for this directory LDAP_ENV.put(Context.INITIAL_CONTEXT_FACTORY, INITIAL_CONTEXT_FACTORY); // URL to the LDAP server LDAP_ENV.put(Context.PROVIDER_URL, PROVIDER_URL); // "simple" if a read-only search account is required; "none" if anonymous LDAP_ENV.put(Context.SECURITY_AUTHENTICATION, config.optString("ldap.authentication","simple")); // The read-only account's DN, if not anonymous. e.g. "uid=admin,ou=system" LDAP_ENV.put(Context.SECURITY_PRINCIPAL, config.optString("ldap.principal","cn=read-only,dc=augur,dc=com")); // The read-only account's password, if not anonymous LDAP_ENV.put(Context.SECURITY_CREDENTIALS, config.optString("ldap.credentials","")); // Search filters will access their context's entire subtree... SEARCH_CONTROLS.setSearchScope(SearchControls.SUBTREE_SCOPE); } /** * Called by the user's login screen. * * @param httpRequest Contains the user-supplied ID and password * @return a JSONObject containing the user's authenticated role * @throws JSONException for any errors that should be displayed to the user. */ @Override public JSONObject login(HttpRequest httpRequest) throws Exception { System.out.println("[LDAP: login attempt from "+httpRequest.getRemoteAddress()+"]"); LEVEL level = LEVEL.NONE; String pd = httpRequest.getPostData(); if (pd == null) throw new JSONException("Bad login request: null data"); RPCRequest rpcRequest = new RPCRequest(pd); JSONObject params = rpcRequest.getRpcParams(); String dnFilter = USER_FILTER.replace("{}", params.optString("username","")); String dn = null; try { dn = findDN(dnFilter); } catch (NamingException ne) { throw new Exception("Problem communicating with the LDAP server to validate your login.", ne); } if (dn != null) { try { // throws NamingException if user id/pw fail... DirContext ctx = authenticate(dn, params.optString("password","")); // Not sure if it's safer to search groups under user's login, or the real-only/anonymous user. // try { ctx.close(); } catch (NamingException ne) { } // Close user's context // ctx = new InitialDirContext(LDAP_ENV); // Reopen with basic search access try { if (isGroupMember(ctx,GRP_FILTER_ADMIN.replace("{}", dn))) { level = LEVEL.ADMIN; } else if (isGroupMember(ctx,GRP_FILTER_TECH.replace("{}", dn))) { level = LEVEL.TECH; } else if (isGroupMember(ctx,GRP_FILTER_GUEST.replace("{}", dn))) { level = LEVEL.GUEST; } System.out.println("[LDAP: authenticated user \""+dn+"\" for role: "+level+"]"); } finally { try { ctx.close(); } catch (NamingException ne) { } } } // catch to log, but allow to return LEVEL.NONE, indicating a bad id/pw... catch (javax.naming.AuthenticationException ae) { System.out.println(ae.getMessage()); } catch (NamingException ne) { ne.printStackTrace(); } } else { System.out.println("[LDAP: user's DN not found with filter \""+dnFilter+"\"]"); } // Return the user's authorized level (a.k.a. "role") for our app... Session httpSession = httpRequest.getSession(); httpSession.put("auth-level", level); try { JSONObject response = new JSONObject(); response.put("level", level.ordinal()); response.put("name", level.name()); return response; } catch (JSONException jsone) // shouldn't happen { System.out.println("Unexpected error creating successful login response: "+jsone); return null; // no exception means success } } /** * Finds the unique distinguished name (DN) with the given filter. * * @param filter The String LDAP search filter * @return The String distinguished name (DN); null if not found. * @throws NamingException if more than one result is found */ public String findDN(String filter) throws NamingException { DirContext ctx = new InitialDirContext(LDAP_ENV); // throws NamingException try { if (DEBUG) { System.out.println("Searching with filter="+filter+", under context base: \""+CONTEXT_USERS+"\""); } NamingEnumeration results = ctx.search(CONTEXT_USERS, filter, SEARCH_CONTROLS); // throws NamingException if (results.hasMore()) { SearchResult searchResult = results.nextElement(); if (results.hasMore()) { throw new NamingException("Unexpectedly found multiple records for that user."); } else { return searchResult.getNameInNamespace(); } } else { return null; } } finally { try { ctx.close(); } catch (NamingException ne) { } } } private DirContext authenticate(String dn, String pw) throws NamingException { if (DEBUG) { System.out.println("Authenticating with dn=\""+dn+"\", pw=\""+pw+"\""); } Hashtable env = new Hashtable<>(LDAP_ENV); if (dn!=null && pw!=null) { env.put(Context.SECURITY_AUTHENTICATION,"simple"); env.put(Context.SECURITY_PRINCIPAL, dn); env.put(Context.SECURITY_CREDENTIALS, pw); } return new InitialDirContext(env); // throws NamingException if user id/pw fail } private boolean isGroupMember(DirContext ctx, String filter) throws NamingException { if (DEBUG) { System.out.println("Searching with filter "+filter+", under context base: \""+CONTEXT_GROUPS+"\""); } NamingEnumeration results = ctx.search(CONTEXT_GROUPS, filter, SEARCH_CONTROLS); // throws NamingException if (DEBUG) { System.out.println(" results.hasMore()? "+results.hasMore()); } return results.hasMore(); } }