/* * LDAPSecurityManager.java * * Created on January 29, 2006, 8:22 PM * * (C) R. Alexander Milowski alex@milowski.com */ package org.exist.security; import java.text.MessageFormat; import java.util.ArrayList; import java.util.HashMap; import java.util.Hashtable; import java.util.List; import java.util.Map; import javax.naming.Context; import javax.naming.NameNotFoundException; import javax.naming.NamingEnumeration; import javax.naming.NamingException; import javax.naming.directory.Attribute; import javax.naming.directory.Attributes; import javax.naming.directory.DirContext; import javax.naming.directory.InitialDirContext; import javax.naming.directory.SearchControls; import javax.naming.directory.SearchResult; import org.apache.log4j.Logger; import org.exist.security.xacml.ExistPDP; import org.exist.storage.BrokerPool; import org.exist.storage.DBBroker; /** * Note: A lot of this code is "borrowed" from Tomcat's JNDIRealm.java * @author R. Alexander Milowski */ public class LDAPSecurityManager implements SecurityManager { private final static Logger LOG = Logger.getLogger(SecurityManager.class); protected Map userByNameCache = new HashMap(); protected Map userByIdCache = new HashMap(); protected Map groupByNameCache = new HashMap(); protected Map groupByIdCache = new HashMap(); static String getProperty(String name,String defaultValue) { String value = System.getProperty(name); return value==null ? defaultValue : value; } protected String contextFactory = getProperty("security.ldap.contextFactory","com.sun.jndi.ldap.LdapCtxFactory"); protected String connectionURL = getProperty("security.ldap.connection.url",null); protected String userPasswordAttr = getProperty("security.ldap.attr.userPassword", "userPassword"); protected String userDigestPasswordAttr = getProperty("security.ldap.attr.userDigestPassword", "digestPassword"); protected String uidAttr = getProperty("security.ldap.attr.uid", "uid"); protected String uidNumberAttr = getProperty("security.ldap.attr.uidNumber", "uidNumber"); protected String gidNumberAttr = getProperty("security.ldap.attr.gidNumber", "gidNumber"); protected String groupNameAttr = getProperty("security.ldap.attr.groupName", "cn"); protected String groupMemberName = getProperty("security.ldap.attr.groupMemberName", "uniqueMember"); protected String groupClassName = getProperty("security.ldap.groupClass", "posixGroup"); protected String userClassName = getProperty("security.ldap.userClass", "posixAccount"); protected String userBase = getProperty("security.ldap.dn.user", null); protected String groupBase = getProperty("security.ldap.dn.group", null); protected DirContext context = null; /** * The message format used to form the distinguished name of a * user, with "{0}" marking the spot where the specified username * goes. */ protected String userByNamePattern = null; protected String userByIdPattern = null; protected MessageFormat userByNamePatternFormat = null; protected MessageFormat userByIdPatternFormat = null; protected String groupByIdPattern = null; protected String groupByNamePattern = null; protected MessageFormat groupByIdPatternFormat = null; protected MessageFormat groupByNamePatternFormat = null; protected ExistPDP pdp = null; /** Creates a new instance of LDAPSecurityManager */ public LDAPSecurityManager() { setUserByNamePattern(uidAttr+"={0},"+userBase); setUserByIdPattern(uidNumberAttr+"={0},"+userBase); setGroupByIdPattern(gidNumberAttr+"={0},"+groupBase); setGroupByNamePattern(groupNameAttr+"={0},"+groupBase); } /** * Set the message format pattern for selecting users in this Realm. * This may be one simple pattern, or multiple patterns to be tried, * separated by parentheses. (for example, either "cn={0}", or * "(cn={0})(cn={0},o=myorg)" Full LDAP search strings are also supported, * but only the "OR", "|" syntax, so "(|(cn={0})(cn={0},o=myorg))" is * also valid. Complex search strings with &, etc are NOT supported. * * @param pattern The new user pattern */ public void setUserByNamePattern(String pattern) { this.userByNamePattern = pattern; this.userByNamePatternFormat = new MessageFormat(userByNamePattern); } public void setUserByIdPattern(String pattern) { this.userByIdPattern = pattern; this.userByIdPatternFormat = new MessageFormat(userByIdPattern); } public void setGroupByIdPattern(String pattern) { this.groupByIdPattern = pattern; this.groupByIdPatternFormat = new MessageFormat(groupByIdPattern); } public void setGroupByNamePattern(String pattern) { this.groupByNamePattern = pattern; this.groupByNamePatternFormat = new MessageFormat(groupByNamePattern); } /** * Return a String representing the value of the specified attribute. * * @param attrId Attribute name * @param attrs Attributes containing the required value * * @exception NamingException if a directory server error occurs */ private String getAttributeValue(String attrId, Attributes attrs) throws NamingException { if (attrId == null || attrs == null) { return null; } Attribute attr = attrs.get(attrId); if (attr == null) { return (null); } Object value = attr.get(); if (value == null) { return (null); } String valueString = null; if (value instanceof byte[]) { valueString = new String((byte[]) value); } else { valueString = value.toString(); } return valueString; } protected Hashtable getDirectoryEnvironment() { if (connectionURL==null) { throw new IllegalStateException("The security.ldap.connection.url property is not set."); } if (userBase==null) { throw new IllegalStateException("The security.ldap.dn.user property is not set."); } if (groupBase==null) { throw new IllegalStateException("The security.ldap.dn.group property is not set."); } Hashtable env = new Hashtable(); LOG.info("security.ldap.contextFactory="+contextFactory); env.put(Context.INITIAL_CONTEXT_FACTORY, contextFactory); LOG.info("security.ldap.connection.url="+connectionURL); env.put(Context.PROVIDER_URL, connectionURL); return env; } // TODO: need an exception to throw public void attach(BrokerPool pool, DBBroker sysBroker) { try { context = new InitialDirContext(getDirectoryEnvironment()); Boolean enableXACML = (Boolean)sysBroker.getConfiguration().getProperty("xacml.enable"); if (enableXACML != null && enableXACML.booleanValue()) { pdp = new ExistPDP(pool); LOG.debug("XACML enabled"); } } catch (NamingException ex) { LOG.warn("Connecting to context failed for LDAP-based security: "+connectionURL,ex); } } protected User getUserByName(DirContext context, String username) throws NamingException { // Form the dn from the user pattern String dn = userByNamePatternFormat.format(new String[] { username }); LOG.info("Attempting to get user by: "+dn); return getUser(context,dn); } protected User getUserById(DirContext context, int uid) throws NamingException { LOG.info("Searching for "+uidNumberAttr+"="+uid+" in "+userBase); SearchControls constraints = new SearchControls(); constraints.setSearchScope(SearchControls.ONELEVEL_SCOPE); NamingEnumeration users = context.search(userBase,"("+uidNumberAttr+"="+uid+")",constraints); while (users.hasMore()) { SearchResult result = (SearchResult)users.next(); return newUserFromAttributes(context, result.getAttributes()); } return null; } protected Group getGroupById(DirContext context,int gid) throws NamingException { LOG.info("Searching for "+gidNumberAttr+"="+gid+" in "+groupBase); SearchControls constraints = new SearchControls(); constraints.setSearchScope(SearchControls.ONELEVEL_SCOPE); NamingEnumeration groups = context.search(groupBase,"("+gidNumberAttr+"="+gid+")",constraints); while (groups.hasMore()) { SearchResult result = (SearchResult)groups.next(); String cn = getAttributeValue(groupNameAttr, result.getAttributes()); LOG.info("Constructing group "+cn); return new Group(cn, gid); } return null; } protected Group getGroupByName(DirContext context,String name) throws NamingException { String g_dn = groupByNamePatternFormat.format(new String[] { name }); LOG.info("Attempting to get group by: "+g_dn); try { Attributes attrs = context.getAttributes(g_dn); String cn = getAttributeValue(groupNameAttr, attrs); int gid = Integer.parseInt(getAttributeValue(gidNumberAttr, attrs)); return new Group(cn, gid); } catch (NameNotFoundException e) { } return null; } protected User newUserFromAttributes(DirContext context,Attributes attrs) throws NamingException { String username = getAttributeValue(uidAttr,attrs); String password = getAttributeValue(userPasswordAttr, attrs); String digestPassword = getAttributeValue(userDigestPasswordAttr, attrs); String gid = getAttributeValue(gidNumberAttr, attrs); //String g_dn = groupByIdPatternFormat.format(new String[] { gid }); LOG.info("Searching for "+gidNumberAttr+"="+gid+" in "+groupBase); String mainGroup = null; SearchControls constraints = new SearchControls(); constraints.setSearchScope(SearchControls.ONELEVEL_SCOPE); NamingEnumeration groups = context.search(groupBase,"("+gidNumberAttr+"="+gid+")",constraints); while (mainGroup==null && groups.hasMore()) { SearchResult result = (SearchResult)groups.next(); mainGroup = getAttributeValue(groupNameAttr, result.getAttributes()); } if (mainGroup==null || mainGroup.length()==0) { throw new IllegalStateException("Main group "+gid+" for user "+username+" is not able to be found in LDAP for group property "+gidNumberAttr); } int uid = Integer.parseInt(getAttributeValue(uidNumberAttr, attrs)); LOG.info("Constructing user "+username+"/"+uid+" in group "+(mainGroup==null ? "<none>" : mainGroup)); User user = new User(username, null, mainGroup); user.setUID(uid); if (password!=null) { if (password.charAt(0)=='{') { int end = password.indexOf('}'); String type = password.substring(0,end+1); String value = password.substring(end+1); LOG.info(" digest: "+type+", "+value); if (!type.equals("{MD5}")) { throw new IllegalStateException("User "+username+" has a non-md5 digested password: "+type); } user.setEncodedPassword(value); } else { user.setPassword(password); } } if (digestPassword!=null) { user.setPasswordDigest(digestPassword); } LOG.info("Finding additional groups..."); String fullName = uidAttr+"="+username+","+userBase; groups = context.search(groupBase,"("+groupMemberName+"="+fullName+")",constraints); while (groups.hasMore()) { SearchResult result = (SearchResult)groups.next(); String name = getAttributeValue(groupNameAttr, result.getAttributes()); if (name==null || name.length()==0) { throw new IllegalStateException("Group associated with "+username+" does not have a valid name for attribute "+groupNameAttr); } if (!name.equals(mainGroup)) { LOG.info(" ...adding: "+name); user.addGroup(name); } } return user; } protected User getUser(DirContext context, String dn) throws NamingException { // Get required attributes from user entry Attributes attrs = null; try { attrs = context.getAttributes(dn); } catch (NameNotFoundException ex) { LOG.warn("Cannot find user "+dn,ex); return (null); } if (attrs == null) { return (null); } LOG.info("User "+dn+" found, attempting to find group and construct..."); return newUserFromAttributes(context,attrs); } public void addGroup(String name) { } public void deleteUser(String name) throws PermissionDeniedException { } public void deleteUser(User user) throws PermissionDeniedException { } public int getCollectionDefaultPerms() { return Permission.DEFAULT_PERM; } public Group getGroup(int gid) { Integer igid = new Integer(gid); Group group = (Group)groupByIdCache.get(igid); if (group==null) { try { group = getGroupById(context,gid); if (group!=null) { groupByIdCache.put(igid,group); } } catch (NamingException ex) { LOG.warn("Cannot get group by #"+gid+" due to exception.",ex); } } return group; } public Group getGroup(String name) { Group group = (Group)groupByIdCache.get(name); if (group==null) { try { group = getGroupByName(context,name); if (group!=null) { groupByNameCache.put(name,group); } } catch (NamingException ex) { LOG.warn("Cannot get group "+name+" due to exception.",ex); } } return group; } // This needs to be an enumeration public String[] getGroups() { try { SearchControls constraints = new SearchControls(); constraints.setSearchScope(SearchControls.ONELEVEL_SCOPE); NamingEnumeration groups = context.search(groupBase,"(objectClass="+groupClassName+")",constraints); List groupList = new ArrayList(); while (groups.hasMore()) { SearchResult result = (SearchResult)groups.next(); groupList.add(getAttributeValue(groupNameAttr, result.getAttributes())); } String [] retval = new String[groupList.size()]; System.arraycopy(groupList.toArray(), 0, retval,0, retval.length); return retval; } catch (NamingException ex) { LOG.warn("Cannot get a list of all groups due to exception.",ex); } return null; } public boolean isXACMLEnabled() { return pdp!=null; } public ExistPDP getPDP() { return pdp; } public int getResourceDefaultPerms() { return Permission.DEFAULT_PERM; } public User getUser(int uid) { Integer iuid = new Integer(uid); User user = (User)userByIdCache.get(iuid); if (user==null) { try { user = getUserById(context,uid); if (user!=null) { userByIdCache.put(iuid,user); } } catch (NamingException ex) { LOG.warn("Cannot get user by #"+uid+" due to exception.",ex); } } return user; } public User getUser(String name) { User user = (User)userByNameCache.get(name); if (user==null) { try { user = getUserByName(context,name); if (user!=null) { userByNameCache.put(name,user); } } catch (NamingException ex) { LOG.warn("Cannot get user "+name+" due to exception.",ex); } } return user; } // This needs to be an enumeration public User[] getUsers() { try { SearchControls constraints = new SearchControls(); constraints.setSearchScope(SearchControls.ONELEVEL_SCOPE); NamingEnumeration users = context.search(userBase,"(objectClass="+userClassName+")",constraints); List userList = new ArrayList(); while (users.hasMore()) { SearchResult result = (SearchResult)users.next(); userList.add(newUserFromAttributes(context,result.getAttributes())); } User [] retval = new User[userList.size()]; System.arraycopy(userList.toArray(), 0, retval,0, retval.length); return retval; } catch (NamingException ex) { LOG.warn("Cannot get the list of users due to exception.",ex); } return null; } // TODO: this shouldn't be in this interface public synchronized boolean hasAdminPrivileges(User user) { return user.hasDbaRole(); } // TODO: why is this here? public synchronized boolean hasUser(String name) { try { return getUserByName(context,name)!=null; } catch (NamingException ex) { LOG.warn("Cannot check for user "+name+" due to exception",ex); } return false; } // TODO: why is this here? public synchronized boolean hasGroup(String name) { try { return getGroupByName(context,name)!=null; } catch (NamingException ex) { LOG.warn("Cannot check for group "+name+" due to exception",ex); } return false; } // TODO: this should be addUser public void setUser(User user) { } }