/** * Licensed to Apereo under one or more contributor license agreements. See the NOTICE file * distributed with this work for additional information regarding copyright ownership. Apereo * licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use * this file except in compliance with the License. You may obtain a copy of the License at the * following location: * * <p>http://www.apache.org/licenses/LICENSE-2.0 * * <p>Unless required by applicable law or agreed to in writing, software distributed under the * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing permissions and * limitations under the License. */ package org.apereo.portal.groups.ldap; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.Hashtable; import java.util.Iterator; import java.util.List; import javax.naming.Context; import javax.naming.NamingEnumeration; import javax.naming.NamingException; import javax.naming.directory.Attribute; import javax.naming.directory.Attributes; import javax.naming.directory.BasicAttributes; import javax.naming.directory.DirContext; import javax.naming.directory.InitialDirContext; import javax.naming.directory.SearchControls; import javax.naming.directory.SearchResult; import javax.xml.parsers.ParserConfigurationException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apereo.portal.EntityIdentifier; import org.apereo.portal.ResourceMissingException; import org.apereo.portal.groups.EntityGroupImpl; import org.apereo.portal.groups.EntityImpl; import org.apereo.portal.groups.GroupsException; import org.apereo.portal.groups.IEntity; import org.apereo.portal.groups.IEntityGroup; import org.apereo.portal.groups.IEntityGroupStore; import org.apereo.portal.groups.IEntitySearcher; import org.apereo.portal.groups.IEntityStore; import org.apereo.portal.groups.IGroupMember; import org.apereo.portal.groups.ILockableEntityGroup; import org.apereo.portal.security.IPerson; import org.apereo.portal.spring.locator.EntityTypesLocator; import org.apereo.portal.utils.ResourceLoader; import org.apereo.portal.utils.SmartCache; import org.springframework.ldap.core.LdapEncoder; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.w3c.dom.Text; import org.xml.sax.SAXException; /** * LDAPGroupStore. * */ public class LDAPGroupStore implements IEntityGroupStore, IEntityStore, IEntitySearcher { private static final Log log = LogFactory.getLog(LDAPGroupStore.class); protected String url; protected String logonid; protected String logonpassword; protected String keyfield; protected String namefield; protected String usercontext = ""; protected HashMap groups; protected SmartCache contexts; protected SmartCache personkeys; protected static Class iperson = IPerson.class; protected static Class group = IEntityGroup.class; protected static short ELEMENT_NODE = Node.ELEMENT_NODE; public LDAPGroupStore() { Document config = null; try { config = ResourceLoader.getResourceAsDocument( this.getClass(), "/properties/groups/LDAPGroupStoreConfig.xml", true); } catch (IOException e) { throw new RuntimeException( "LDAPGroupStore: Unable to find configuration configuration document", e); } catch (ResourceMissingException e) { throw new RuntimeException( "LDAPGroupStore: Unable to find configuration configuration document", e); } catch (ParserConfigurationException e) { throw new RuntimeException( "LDAPGroupStore: Unable to parse configuration configuration document", e); } catch (SAXException e) { throw new RuntimeException( "LDAPGroupStore: Unable to parse configuration configuration document", e); } init(config); } public LDAPGroupStore(Document config) { init(config); } protected void init(Document config) { this.groups = new HashMap(); this.contexts = new SmartCache(120); config.normalize(); int refreshminutes = 120; Element root = config.getDocumentElement(); NodeList nl = root.getElementsByTagName("config"); if (nl.getLength() == 1) { Element conf = (Element) nl.item(0); Node cc = conf.getFirstChild(); //NodeList cl= conf.getF.getChildNodes(); //for(int i=0; i<cl.getLength(); i++){ while (cc != null) { if (cc.getNodeType() == ELEMENT_NODE) { Element c = (Element) cc; c.normalize(); Node t = c.getFirstChild(); if (t != null && t.getNodeType() == Node.TEXT_NODE) { String name = c.getNodeName(); String text = ((Text) t).getData(); //System.out.println(name+" = "+text); if (name.equals("url")) { url = text; } else if (name.equals("logonid")) { logonid = text; } else if (name.equals("logonpassword")) { logonpassword = text; } else if (name.equals("keyfield")) { keyfield = text; } else if (name.equals("namefield")) { namefield = text; } else if (name.equals("usercontext")) { usercontext = text; } else if (name.equals("refresh-minutes")) { try { refreshminutes = Integer.parseInt(text); } catch (Exception e) { } } } } cc = cc.getNextSibling(); } } else { throw new RuntimeException( "LDAPGroupStore: config file must contain one config element"); } this.personkeys = new SmartCache(refreshminutes * 60); NodeList gl = root.getChildNodes(); for (int j = 0; j < gl.getLength(); j++) { if (gl.item(j).getNodeType() == ELEMENT_NODE) { Element g = (Element) gl.item(j); if (g.getNodeName().equals("group")) { GroupShadow shadow = processXmlGroupRecursive(g); groups.put(shadow.key, shadow); } } } } protected String[] getPersonKeys(String groupKey) { String[] r = (String[]) personkeys.get(groupKey); if (r == null) { GroupShadow shadow = (GroupShadow) groups.get(groupKey); if (shadow.entities != null) { r = shadow.entities.getPersonKeys(); } else { r = new String[0]; } personkeys.put(groupKey, r); } return r; } protected GroupShadow processXmlGroupRecursive(Element groupElem) { GroupShadow shadow = new GroupShadow(); shadow.key = groupElem.getAttribute("key"); shadow.name = groupElem.getAttribute("name"); //System.out.println("Loading configuration for group "+shadow.name); ArrayList subgroups = new ArrayList(); NodeList nl = groupElem.getChildNodes(); for (int i = 0; i < nl.getLength(); i++) { if (nl.item(i).getNodeType() == ELEMENT_NODE) { Element e = (Element) nl.item(i); if (e.getNodeName().equals("group")) { GroupShadow sub = processXmlGroupRecursive(e); subgroups.add(sub); groups.put(sub.key, sub); } else if (e.getNodeName().equals("entity-set")) { shadow.entities = new EntitySet(e); } else if (e.getNodeName().equals("description")) { e.normalize(); Text t = (Text) e.getFirstChild(); if (t != null) { shadow.description = t.getData(); } } } } shadow.subgroups = (GroupShadow[]) subgroups.toArray(new GroupShadow[0]); return shadow; } protected class GroupShadow { protected String key; protected String name; protected String description; protected GroupShadow[] subgroups; protected EntitySet entities; } protected class EntitySet { public static final int FILTER = 1; public static final int UNION = 2; public static final int DIFFERENCE = 3; public static final int INTERSECTION = 4; public static final int SUBTRACT = 5; public static final int ATTRIBUTES = 6; protected int type; protected String filter; protected Attributes attributes; protected EntitySet[] subsets; protected EntitySet(Element entityset) { entityset.normalize(); Node n = entityset.getFirstChild(); while (n.getNodeType() != Node.ELEMENT_NODE) { n = n.getNextSibling(); } Element e = (Element) n; String type = e.getNodeName(); boolean collectSubsets = false; if (type.equals("filter")) { this.type = FILTER; filter = e.getAttribute("string"); } else if (type.equals("attributes")) { this.type = ATTRIBUTES; attributes = new BasicAttributes(); NodeList atts = e.getChildNodes(); for (int i = 0; i < atts.getLength(); i++) { if (atts.item(i).getNodeType() == ELEMENT_NODE) { Element a = (Element) atts.item(i); attributes.put(a.getAttribute("name"), a.getAttribute("value")); } } } else if (type.equals("union")) { this.type = UNION; collectSubsets = true; } else if (type.equals("intersection")) { this.type = INTERSECTION; collectSubsets = true; } else if (type.equals("difference")) { this.type = DIFFERENCE; collectSubsets = true; } else if (type.equals("subtract")) { this.type = SUBTRACT; collectSubsets = true; } if (collectSubsets) { ArrayList subs = new ArrayList(); NodeList nl = e.getChildNodes(); for (int i = 0; i < nl.getLength(); i++) { if (nl.item(i).getNodeType() == Node.ELEMENT_NODE) { EntitySet subset = new EntitySet((Element) nl.item(i)); subs.add(subset); } } subsets = (EntitySet[]) subs.toArray(new EntitySet[0]); } } protected String[] getPersonKeys() { ArrayList keys = new ArrayList(); //System.out.println("Loading keys!!"); String[] subkeys; switch (type) { case FILTER: //System.out.println("Performing ldap query!!"); DirContext context = getConnection(); NamingEnumeration userlist = null; SearchControls sc = new SearchControls(); sc.setSearchScope(SearchControls.SUBTREE_SCOPE); sc.setReturningAttributes(new String[] {keyfield}); try { userlist = context.search(usercontext, filter, sc); } catch (NamingException nex) { log.error("LDAPGroupStore: Unable to perform filter " + filter, nex); } processLdapResults(userlist, keys); break; case ATTRIBUTES: //System.out.println("Performing ldap attribute query!!"); DirContext context2 = getConnection(); NamingEnumeration userlist2 = null; try { userlist2 = context2.search(usercontext, attributes, new String[] {keyfield}); } catch (NamingException nex) { log.error("LDAPGroupStore: Unable to perform attribute search", nex); } processLdapResults(userlist2, keys); break; case UNION: for (int i = 0; i < subsets.length; i++) { subkeys = subsets[i].getPersonKeys(); for (int j = 0; j < subkeys.length; j++) { String key = subkeys[j]; if (!keys.contains(key)) { keys.add(key); } } } break; case INTERSECTION: if (subsets.length > 0) { // load initial keys from first entity set String[] interkeys = subsets[0].getPersonKeys(); // now set non-recurring keys to null for (int m = 1; m < subsets.length; m++) { subkeys = subsets[m].getPersonKeys(); for (int n = 0; n < interkeys.length; n++) { if (interkeys[n] != null) { boolean remove = true; for (int o = 0; o < subkeys.length; o++) { if (subkeys[o].equals(interkeys[n])) { // found a match, so far the intersection for this key is valid remove = false; break; } } if (remove) { interkeys[n] = null; } } } } for (int p = 0; p < interkeys.length; p++) { if (interkeys[p] != null) { keys.add(interkeys[p]); } } } break; case DIFFERENCE: if (subsets.length > 0) { ArrayList discardKeys = new ArrayList(); subkeys = subsets[0].getPersonKeys(); // load initial keys from first entity set for (int q = 0; q < subkeys.length; q++) { keys.add(subkeys[q]); } for (int r = 1; r < subsets.length; r++) { subkeys = subsets[r].getPersonKeys(); for (int s = 0; s < subkeys.length; s++) { String ky = subkeys[s]; if (keys.contains(ky)) { keys.remove(ky); discardKeys.add(ky); } else { if (!discardKeys.contains(ky)) { keys.add(ky); } } } } } break; case SUBTRACT: if (subsets.length > 0) { subkeys = subsets[0].getPersonKeys(); // load initial keys from first entity set for (int t = 0; t < subkeys.length; t++) { keys.add(subkeys[t]); } for (int u = 1; u < subsets.length; u++) { subkeys = subsets[u].getPersonKeys(); for (int v = 0; v < subkeys.length; v++) { String kyy = subkeys[v]; if (keys.contains(kyy)) { keys.remove(kyy); } } } } break; } return (String[]) keys.toArray(new String[0]); } } protected void processLdapResults(NamingEnumeration results, ArrayList keys) { //long time1 = System.currentTimeMillis(); //long casting=0; //long getting=0; //long setting=0; //long looping=0; //long loop1=System.currentTimeMillis(); try { while (results.hasMore()) { //long loop2 = System.currentTimeMillis(); //long cast1=System.currentTimeMillis(); //looping=looping+loop2-loop1; SearchResult result = (SearchResult) results.next(); //long cast2 = System.currentTimeMillis(); //long get1 = System.currentTimeMillis(); Attributes ldapattribs = result.getAttributes(); //long get2 = System.currentTimeMillis(); //long set1 = System.currentTimeMillis(); Attribute attrib = ldapattribs.get(keyfield); if (attrib != null) { keys.add(String.valueOf(attrib.get()).toLowerCase()); } //long set2 = System.currentTimeMillis(); //loop1=System.currentTimeMillis(); //casting=casting+cast2-cast1; //setting=setting+set2-set1; //getting=getting+get2-get1; } } catch (NamingException nex) { log.error("LDAPGroupStore: error processing results", nex); } finally { try { results.close(); } catch (Exception e) { } } //long time5 = System.currentTimeMillis(); //System.out.println("Result processing took "+(time5-time1)+": "+getting+" for getting, " // +setting+" for setting, "+casting+" for casting, "+looping+" for looping," // +(time5-loop1)+" for closing"); } protected DirContext getConnection() { //JNDI boilerplate to connect to an initial context DirContext context = (DirContext) contexts.get("context"); if (context == null) { Hashtable jndienv = new Hashtable(); jndienv.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); jndienv.put(Context.SECURITY_AUTHENTICATION, "simple"); if (url.startsWith("ldaps")) { // Handle SSL connections String newurl = url.substring(0, 4) + url.substring(5); jndienv.put(Context.SECURITY_PROTOCOL, "ssl"); jndienv.put(Context.PROVIDER_URL, newurl); } else { jndienv.put(Context.PROVIDER_URL, url); } if (logonid != null) jndienv.put(Context.SECURITY_PRINCIPAL, logonid); if (logonpassword != null) jndienv.put(Context.SECURITY_CREDENTIALS, logonpassword); try { context = new InitialDirContext(jndienv); } catch (NamingException nex) { log.error("LDAPGroupStore: unable to get context", nex); } contexts.put("context", context); } return context; } protected IEntityGroup makeGroup(GroupShadow shadow) throws GroupsException { IEntityGroup group = null; if (shadow != null) { group = new EntityGroupImpl(shadow.key, iperson); group.setDescription(shadow.description); group.setName(shadow.name); } return group; } protected GroupShadow getShadow(IEntityGroup group) { return (GroupShadow) groups.get(group.getLocalKey()); } public void delete(IEntityGroup group) throws GroupsException { throw new java.lang.UnsupportedOperationException( "LDAPGroupStore: Method delete() not supported."); } public IEntityGroup find(String key) throws GroupsException { return makeGroup((GroupShadow) this.groups.get(key)); } public Iterator findParentGroups(IGroupMember gm) throws GroupsException { ArrayList al = new ArrayList(); String key; GroupShadow[] shadows = getGroupShadows(); if (!gm.isGroup()) { key = gm.getKey(); for (int i = 0; i < shadows.length; i++) { String[] keys = getPersonKeys(shadows[i].key); for (int j = 0; j < keys.length; j++) { if (keys[j].equals(key)) { al.add(makeGroup(shadows[i])); break; } } } } if (gm.isGroup()) { key = ((IEntityGroup) gm).getLocalKey(); for (int i = 0; i < shadows.length; i++) { for (int j = 0; j < shadows[i].subgroups.length; j++) { if (shadows[i].subgroups[j].key.equals(key)) { al.add(makeGroup(shadows[i])); break; } } } } return al.iterator(); } public String[] findMemberGroupKeys(IEntityGroup group) throws GroupsException { List keys = new ArrayList(); for (Iterator itr = findMemberGroups(group); itr.hasNext(); ) { IEntityGroup eg = (IEntityGroup) itr.next(); keys.add(eg.getKey()); } return (String[]) keys.toArray(new String[keys.size()]); } public Iterator findMemberGroups(IEntityGroup group) throws GroupsException { ArrayList al = new ArrayList(); GroupShadow shadow = getShadow(group); for (int i = 0; i < shadow.subgroups.length; i++) { al.add(makeGroup(shadow.subgroups[i])); } return al.iterator(); } public IEntityGroup newInstance(Class entityType) throws GroupsException { throw new java.lang.UnsupportedOperationException( "LDAPGroupStore: Method newInstance() not supported"); } public void update(IEntityGroup group) throws GroupsException { throw new java.lang.UnsupportedOperationException( "LDAPGroupStore: Method update() not supported"); } public void updateMembers(IEntityGroup group) throws GroupsException { throw new java.lang.UnsupportedOperationException( "LDAPGroupStore: Method updateMembers() not supported"); } public ILockableEntityGroup findLockable(String key) throws GroupsException { throw new java.lang.UnsupportedOperationException( "LDAPGroupStore: Method findLockable() not supported"); } protected GroupShadow[] getGroupShadows() { return (GroupShadow[]) groups.values().toArray(new GroupShadow[0]); } public EntityIdentifier[] searchForGroups(String query, int method, Class leaftype) throws GroupsException { ArrayList ids = new ArrayList(); GroupShadow[] g = getGroupShadows(); int i; switch (method) { case IS: for (i = 0; i < g.length; i++) { if (g[i].name.equalsIgnoreCase(query)) { ids.add(new EntityIdentifier(g[i].key, group)); } } break; case STARTS_WITH: for (i = 0; i < g.length; i++) { if (g[i].name.toUpperCase().startsWith(query.toUpperCase())) { ids.add(new EntityIdentifier(g[i].key, group)); } } break; case ENDS_WITH: for (i = 0; i < g.length; i++) { if (g[i].name.toUpperCase().endsWith(query.toUpperCase())) { ids.add(new EntityIdentifier(g[i].key, group)); } } break; case CONTAINS: for (i = 0; i < g.length; i++) { if (g[i].name.toUpperCase().indexOf(query.toUpperCase()) > -1) { ids.add(new EntityIdentifier(g[i].key, group)); } } break; } return (EntityIdentifier[]) ids.toArray(new EntityIdentifier[0]); } public Iterator findEntitiesForGroup(IEntityGroup group) throws GroupsException { GroupShadow shadow = getShadow(group); ArrayList al = new ArrayList(); String[] keys = getPersonKeys(shadow.key); for (int i = 0; i < keys.length; i++) { al.add(new EntityImpl(keys[i], iperson)); } return al.iterator(); } public IEntity newInstance(String key, Class type) throws GroupsException { if (EntityTypesLocator.getEntityTypes().getEntityIDFromType(type) == null) { throw new GroupsException("Invalid group type: " + type); } return new EntityImpl(key, type); } public EntityIdentifier[] searchForEntities(String query, int method, Class type) throws GroupsException { if (type != group && type != iperson) return new EntityIdentifier[0]; // Guarantee that LDAP injection is prevented by replacing LDAP special characters // with escaped versions of the character query = LdapEncoder.filterEncode(query); ArrayList ids = new ArrayList(); switch (method) { case STARTS_WITH: query = query + "*"; break; case ENDS_WITH: query = "*" + query; break; case CONTAINS: query = "*" + query + "*"; break; } query = namefield + "=" + query; DirContext context = getConnection(); NamingEnumeration userlist = null; SearchControls sc = new SearchControls(); sc.setSearchScope(SearchControls.SUBTREE_SCOPE); sc.setReturningAttributes(new String[] {keyfield}); try { userlist = context.search(usercontext, query, sc); ArrayList keys = new ArrayList(); processLdapResults(userlist, keys); String[] k = (String[]) keys.toArray(new String[0]); for (int i = 0; i < k.length; i++) { ids.add(new EntityIdentifier(k[i], iperson)); } return (EntityIdentifier[]) ids.toArray(new EntityIdentifier[0]); } catch (NamingException nex) { throw new GroupsException("LDAPGroupStore: Unable to perform filter " + query, nex); } } /** * Answers if <code>group</code> contains <code>member</code>. * * @return boolean * @param group org.apereo.portal.groups.IEntityGroup * @param member org.apereo.portal.groups.IGroupMember */ public boolean contains(IEntityGroup group, IGroupMember member) throws GroupsException { boolean found = false; Iterator itr = (member.isGroup()) ? findMemberGroups(group) : findEntitiesForGroup(group); while (itr.hasNext() && !found) { found = member.equals(itr.next()); } return found; } }