/** * Copyright (C) 2010 Orbeon, Inc. * * This program is free software; you can redistribute it and/or modify it under the terms of the * GNU Lesser General Public License as published by the Free Software Foundation; either version * 2.1 of the License, or (at your option) any later version. * * This program 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 Lesser General Public License for more details. * * The full text of the license is available at http://www.gnu.org/copyleft/lesser.html */ package org.orbeon.oxf.processor; import org.apache.commons.lang3.StringUtils; import org.apache.log4j.Logger; import org.orbeon.dom.*; import org.orbeon.oxf.common.OXFException; import org.orbeon.oxf.pipeline.api.PipelineContext; import org.orbeon.oxf.xml.SAXUtils; import org.orbeon.oxf.xml.XMLReceiver; import org.orbeon.oxf.util.LoggerFactory; import org.orbeon.oxf.xml.XPathUtils; import org.xml.sax.ContentHandler; import javax.naming.*; import javax.naming.directory.Attribute; import javax.naming.directory.*; import java.util.*; public class LDAPProcessor extends ProcessorImpl { static private Logger logger = LoggerFactory.createLogger(LDAPProcessor.class); public static final String INPUT_FILTER = "filter"; public static final String LDAP_CONFIG_NAMESPACE_URI = "http://orbeon.org/oxf/ldap/config"; public static final String LDAP_FILTER_NAMESPACE_URI = "http://orbeon.org/oxf/ldap/filter"; public static final String DEFAULT_HOST = "localhost"; public static final int DEFAULT_PORT = 389; public static final String LDAP_VERSION = "java.naming.ldap.version"; public static final String DEFAULT_LDAP_VERSION = "3"; //private static final String CTX_ENV = "java.naming.ldap.attributes.binary"; private static final String DEFAULT_CTX = "com.sun.jndi.ldap.LdapCtxFactory"; public static final String HOST_PROPERTY = "host"; public static final String PORT_PROPERTY = "port"; public static final String BIND_PROPERTY = "bind-dn"; public static final String PASSWORD_PROPERTY = "password"; public static final String PROTOCOL_PROPERTY = "protocol"; public static final String SCOPE_PROPERTY = "scope"; public LDAPProcessor() { addInputInfo(new ProcessorInputOutputInfo(INPUT_CONFIG, LDAP_CONFIG_NAMESPACE_URI)); addInputInfo(new ProcessorInputOutputInfo(INPUT_FILTER, LDAP_FILTER_NAMESPACE_URI)); addOutputInfo(new ProcessorInputOutputInfo(OUTPUT_DATA)); } public ProcessorOutput createOutput(String name) { final ProcessorOutput output = new ProcessorOutputImpl(LDAPProcessor.this, name) { public void readImpl(PipelineContext context, XMLReceiver xmlReceiver) { try { // Read configuration Config config = readCacheInputAsObject(context, getInputByName(INPUT_CONFIG), new CacheableInputReader<Config>() { public Config read(PipelineContext context, ProcessorInput input) { Config config = new Config(); Document doc = readInputAsOrbeonDom(context, input); // Try local configuration first String host = XPathUtils.selectStringValueNormalize(doc, "/config/host"); Integer port = XPathUtils.selectIntegerValue(doc, "/config/port"); String bindDN = XPathUtils.selectStringValueNormalize(doc, "/config/bind-dn"); String password = XPathUtils.selectStringValueNormalize(doc, "/config/password"); String protocol = XPathUtils.selectStringValueNormalize(doc, "/config/protocol"); String referral = XPathUtils.selectStringValueNormalize(doc, "/config/referral "); String scope = XPathUtils.selectStringValueNormalize(doc, "/config/scope"); //logger.info("Referral="+referral); // Override with properties if needed config.setHost(host != null ? host : getPropertySet().getString(HOST_PROPERTY)); config.setPort(port != null ? port.intValue() : getPropertySet().getInteger(PORT_PROPERTY).intValue()); config.setBindDN(bindDN != null ? bindDN : getPropertySet().getString(BIND_PROPERTY)); config.setPassword(password != null ? password : getPropertySet().getString(PASSWORD_PROPERTY)); config.setProtocol(protocol != null ? protocol : getPropertySet().getString(PROTOCOL_PROPERTY)); config.setScope(scope != null ? scope: getPropertySet().getString(SCOPE_PROPERTY)); // If not set use providers default. Valid values are follow, ignore, throw if (referral != null){ config.setReferral(referral); } // The password and bind DN are allowed to be blank if (password == null) config.setPassword(""); if (bindDN == null) config.setBindDN(""); config.setRootDN(XPathUtils.selectStringValueNormalize(doc, "/config/root-dn")); for (Iterator i = XPathUtils.selectNodeIterator(doc, "/config/attribute"); i.hasNext();) { Element e = (Element) i.next(); config.addAttribute(e.getTextTrim()); } return config; } }); Command command = readCacheInputAsObject(context, getInputByName(INPUT_FILTER), new CacheableInputReader<Command>() { public Command read(PipelineContext context, ProcessorInput input) { Command command; Document filterDoc = readInputAsOrbeonDom(context, input); String filterNodeName = filterDoc.getRootElement().getName(); if ("update".equals(filterNodeName)) { command = new Update(); command.setName(XPathUtils.selectStringValue(filterDoc, "/update/name")); parseAttributes(filterDoc, "/update/attribute", (Update) command); } else if ("add".equals(filterNodeName)) { command = new Add(); command.setName(XPathUtils.selectStringValue(filterDoc, "/add/name")); parseAttributes(filterDoc, "/add/attribute", (Add) command); } else if ("delete".equals(filterNodeName)) { command = new Delete(); command.setName(XPathUtils.selectStringValue(filterDoc, "/delete/name")); } else if ("filter".equals(filterNodeName)) { command = new Search(); command.setName(XPathUtils.selectStringValueNormalize(filterDoc, "/filter")); } else { throw new OXFException("Wrong command: use filter or update"); } return command; } }); if(logger.isDebugEnabled()) logger.debug("LDAP Command: "+command.toString()); DirContext ctx = connect(config); if (command instanceof Update) { update(ctx, (Update) command); outputSuccess(xmlReceiver, "update"); } else if (command instanceof Add) { add(ctx, (Add) command); outputSuccess(xmlReceiver, "add"); } else if (command instanceof Delete) { delete(ctx, (Delete) command); outputSuccess(xmlReceiver, "delete"); } else if (command instanceof Search) { // There was incorrect code here earlier testing on instanceof String[], which broke stuff. For // now assume all attrs are strings. final List attributesList = config.getAttributes(); final String[] attrs = new String[attributesList.size()]; attributesList.toArray(attrs); List results = search(ctx, config.getRootDN(), config.getScope(), command.getName(), attrs); serialize(results, config, xmlReceiver); } disconnect(ctx); } catch (Exception e) { throw new OXFException(e); } } }; addOutput(name, output); return output; } private void parseAttributes(Node filterDoc, String attributeXPath, CommandWithAttributes command) { for (Iterator i = XPathUtils.selectNodeIterator(filterDoc, attributeXPath); i.hasNext();) { Node curAttr = (Node) i.next(); String name = XPathUtils.selectStringValue(curAttr, "name"); List values = new ArrayList(); for (Iterator j = XPathUtils.selectNodeIterator(curAttr, "value"); j.hasNext();) { String value = ((Node) j.next()).getText(); values.add(value); } command.addAttribute(name, values); } } private void update(DirContext ctx, Update update) { try { ctx.modifyAttributes(update.getName(), DirContext.REPLACE_ATTRIBUTE, update.getAttributes()); } catch (NamingException e) { throw new OXFException("LDAP Update Failed", e); } } private void add(DirContext ctx, Add add) { try { ctx.createSubcontext(add.getName(), add.getAttributes()); } catch (NamingException e) { throw new OXFException("LDAP Add Failed", e); } } private void delete(DirContext ctx, Delete delete) { try { ctx.destroySubcontext(delete.getName()); } catch (NamingException e) { throw new OXFException("LDAP Delete Failed", e); } } private List search(DirContext ctx, String rootDN, String scope, String filter, String[] attributes) { try { List listResults = new ArrayList(); SearchControls constraints = new SearchControls(); constraints.setSearchScope(convertSearchScope(scope)); constraints.setReturningAttributes(attributes); try { if (scope != null && scope.toUpperCase().equals("ALLLEVELS")) { String[] levels = rootDN.split(","); for (int i = 0; i < levels.length; i++) { String[] currentLevels = new String[levels.length - i]; System.arraycopy(levels, i, currentLevels, 0, levels.length - i); String levelRootDN = StringUtils.join(currentLevels, ","); if (logger.isDebugEnabled()) logger.debug("LDAP Search on level " + levelRootDN); NamingEnumeration results = ctx.search(levelRootDN, filter, constraints); for (; results.hasMore(); ) { SearchResult result = (SearchResult) results.next(); listResults.add(result); } } } else { NamingEnumeration results = ctx.search(rootDN, filter, constraints); for (; results.hasMore(); ) { SearchResult result = (SearchResult) results.next(); listResults.add(result); } } } catch (NameNotFoundException e) { // for example in case of ALLLEVELS scope, if the LDAP database suffix has more than one component, the last iteration would result in NameNotFoundException } return listResults; } catch (NamingException e) { throw new OXFException("LDAP Search Failed", e); } } private void serialize(List results, Config config, ContentHandler ch) { try { ch.startDocument(); ch.startElement("", "results", "results", SAXUtils.EMPTY_ATTRIBUTES); for (Iterator i = results.iterator(); i.hasNext();) { SearchResult sr = (SearchResult) i.next(); ch.startElement("", "result", "result", SAXUtils.EMPTY_ATTRIBUTES); addElement(ch, "name", sr.getName()); try { addElement(ch, "fullname", sr.getNameInNamespace()); } catch (UnsupportedOperationException e) { // This seems to be the only way to know if sr contains a name! } Attributes attr = sr.getAttributes(); NamingEnumeration attrEn = attr.getAll(); while (attrEn.hasMoreElements()) { Attribute a = (Attribute) attrEn.next(); if (config.getAttributes().isEmpty() || config.getAttributes().contains(a.getID())) { ch.startElement("", "attribute", "attribute", SAXUtils.EMPTY_ATTRIBUTES); addElement(ch, "name", a.getID()); NamingEnumeration aEn = a.getAll(); while (aEn.hasMoreElements()) { Object o = aEn.next(); addElement(ch, "value", o.toString()); } ch.endElement("", "attribute", "attribute"); } } ch.endElement("", "result", "result"); } ch.endElement("", "results", "results"); ch.endDocument(); } catch (Exception e) { throw new OXFException(e); } } private void outputSuccess(ContentHandler ch, String operationName) { try { ch.startDocument(); addElement(ch, operationName, "success"); ch.endDocument(); }catch(Exception e) { throw new OXFException(e); } } private DirContext connect(Config config) { try { Properties env = new Properties(); env.put(Context.SECURITY_AUTHENTICATION, "simple"); env.put(Context.SECURITY_PRINCIPAL, config.getBindDN()); env.put(Context.SECURITY_CREDENTIALS, config.getPassword()); env.put(LDAP_VERSION, DEFAULT_LDAP_VERSION); env.put(Context.INITIAL_CONTEXT_FACTORY, DEFAULT_CTX); env.put(Context.PROVIDER_URL, "ldap://" + config.getHost() + ":" + config.getPort()); if (config.getReferral() != null){ env.put(Context.REFERRAL, config.getReferral()); } if (config.getProtocol() != null) env.put(Context.SECURITY_PROTOCOL, config.getProtocol()); env.put("com.sun.jndi.ldap.connect.pool", "true"); return new InitialDirContext(env); } catch (NamingException e) { throw new OXFException("LDAP connect Failed", e); } } private void disconnect(DirContext ctx) { try { if (ctx != null) ctx.close(); } catch (NamingException e) { throw new OXFException("LDAP disconnect Failed", e); } } private void addElement(ContentHandler contentHandler, String name, String value) throws Exception { if (value != null) { contentHandler.startElement("", name, name, SAXUtils.EMPTY_ATTRIBUTES); addString(contentHandler, value); contentHandler.endElement("", name, name); } } private void addString(ContentHandler contentHandler, String string) throws Exception { char[] charArray = string.toCharArray(); contentHandler.characters(charArray, 0, charArray.length); } private int convertSearchScope(String scope) { if (scope != null && scope.toUpperCase().equals("SUBTREE")) { return SearchControls.SUBTREE_SCOPE; } else if (scope != null && scope.toUpperCase().equals("OBJECT")) { return SearchControls.OBJECT_SCOPE; } else if (scope != null && (scope.toUpperCase().equals("ALLLEVELS") || scope.toUpperCase().equals("ONELEVEL"))) { return SearchControls.ONELEVEL_SCOPE; } else { return SearchControls.SUBTREE_SCOPE; } } private static class Config { private String host = DEFAULT_HOST; private int port = DEFAULT_PORT; private String bindDN; private String password; private String rootDN; private String protocol; private String referral; private String scope; private List attributes = new ArrayList(); public String getBindDN() { return bindDN; } public void setBindDN(String bindDN) { this.bindDN = bindDN; } public String getHost() { return host; } public void setHost(String host) { this.host = host; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public int getPort() { return port; } public void setPort(int port) { this.port = port; } public String getRootDN() { return rootDN; } public void setRootDN(String rootDN) { this.rootDN = rootDN; } public String getProtocol() { return protocol; } public void setProtocol(String protocol) { this.protocol = protocol; } public String getReferral() { return referral; } public void setReferral(String referral) { this.referral = referral; } public String getScope() { return scope; } public void setScope(String scope) { this.scope = scope; } public List getAttributes() { return attributes; } public void addAttribute(String attr) { this.attributes.add(attr); } public String toString() { return "Host: "+host + " Port: " + port + " Bind DN: "+bindDN + " password: "+password+" root: "+rootDN; } } private abstract static class Command { protected String name; public String getName() { return name; } public void setName(String name) { this.name = name; } } private abstract static class CommandWithAttributes extends Command { protected Attributes attributes = new BasicAttributes(true); public Attributes getAttributes() { return attributes; } public void addAttribute(String name, List values) { BasicAttribute ba = new BasicAttribute(name); for (Iterator i = values.iterator(); i.hasNext();) ba.add((String) i.next()); attributes.put(ba); } } private static class Search extends Command { public String toString() { return "Search Command: filter = " + name; } } private static class Update extends CommandWithAttributes { public String toString() { return "Update Command: name = " + name + " attributes = " + attributes.toString(); } } private static class Add extends CommandWithAttributes { public String toString() { return "Add Command: name = " + name + " attributes = " + attributes.toString(); } } private static class Delete extends Command { public String toString() { return "Delete Command: name = " + name; } } }