/**
* 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;
}
}
}