Index: oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/LdapSettings.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/LdapSettings.java (revision ) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/LdapSettings.java (revision ) @@ -0,0 +1,191 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.apache.jackrabbit.oak.security.authentication.ldap; + +import java.util.HashMap; +import java.util.Map; + +public final class LdapSettings { + + //TODO support autocreate.user.membership + //TODO support autocreate.path + + public final static String KEY_HOST = "host"; + public final static String KEY_PORT = "port"; + public final static String KEY_SECURE = "secure"; + public final static String KEY_AUTHDN = "authDn"; + public final static String KEY_AUTHPW = "authPw"; + public final static String KEY_SEARCHTIMEOUT = "searchTimeout"; + public final static String KEY_USERROOT = "userRoot"; + public final static String KEY_USERFILTER = "userFilter"; + public final static String KEY_USERIDATTRIBUTE = "userIdAttribute"; + public final static String KEY_GROUPROOT = "groupRoot"; + public final static String KEY_GROUPFILTER = "groupFilter"; + public final static String KEY_GROUPMEMBERSHIPATTRIBUTE = "groupMembershipAttribute"; + public final static String KEY_GROUPNAMEATTRIBUTE = "groupNameAttribute"; + public final static String KEY_AUTOCREATEPATH = "autocreate.path"; + public final static String KEY_AUTOCREATEUSER = "autocreate.user."; + public final static String KEY_AUTOCREATEGROUP = "autocreate.group."; + + //Connection settings + private String host; + private int port = 389; + private boolean secure = false; + private String authDn = ""; + private String authPw = ""; + private int searchTimeout = 60000; + + //authentication settings + private String userRoot = ""; + private String userFilter = "(objectclass=person)"; + private String userIdAttribute = "uid"; + private String groupRoot = ""; + private String groupFilter = "(objectclass=groupOfUniqueNames)"; + private String groupMembershipAttribute = "uniquemember"; + private String groupNameAttribute = "cn"; + + //synchronization + private boolean splitPath = false; + private final Map userAttributes = new HashMap(); + private final Map groupAttributes = new HashMap(); + + public LdapSettings(Map options) { + if (options.containsKey(KEY_HOST)) { + this.host = (String) options.get(KEY_HOST); + } + if (options.containsKey(KEY_PORT)) { + String s = (String) options.get(KEY_PORT); + if (s != null && s.length() > 0) { + this.port = Integer.parseInt(s); + } + } + if (options.containsKey(KEY_SECURE)) { + String s = (String) options.get(KEY_SECURE); + if (s != null && s.length() > 0) { + this.secure = Boolean.parseBoolean(s); + } + } + if (options.containsKey(KEY_AUTHDN)) { + this.authDn = (String) options.get(KEY_AUTHDN); + } + if (options.containsKey(KEY_AUTHPW)) { + this.authPw = (String) options.get(KEY_AUTHPW); + } + if (options.containsKey(KEY_SEARCHTIMEOUT)) { + String s = (String) options.get(KEY_SEARCHTIMEOUT); + if (s != null && s.length() > 0) { + this.searchTimeout = Integer.parseInt(s); + } + } + if (options.containsKey(KEY_USERROOT)) { + this.userRoot = (String) options.get(KEY_USERROOT); + } + if (options.containsKey(KEY_USERFILTER)) { + this.userFilter = (String) options.get(KEY_USERFILTER); + } + if (options.containsKey(KEY_USERIDATTRIBUTE)) { + this.userIdAttribute = (String) options.get(KEY_USERIDATTRIBUTE); + } + if (options.containsKey(KEY_GROUPROOT)) { + this.groupRoot = (String) options.get(KEY_GROUPROOT); + } + if (options.containsKey(KEY_GROUPFILTER)) { + this.groupFilter = (String) options.get(KEY_GROUPFILTER); + } + if (options.containsKey(KEY_GROUPMEMBERSHIPATTRIBUTE)) { + this.groupMembershipAttribute = (String) options.get(KEY_GROUPMEMBERSHIPATTRIBUTE); + } + if (options.containsKey(KEY_GROUPNAMEATTRIBUTE)) { + this.groupNameAttribute = (String) options.get(KEY_GROUPNAMEATTRIBUTE); + } + if (options.containsKey(KEY_AUTOCREATEPATH)) { + this.splitPath = "splitdn".equals(options.get(KEY_AUTOCREATEPATH)); + } + for (String key : options.keySet()) { + if (key.startsWith(KEY_AUTOCREATEUSER)) { + this.userAttributes.put(key.substring(KEY_AUTOCREATEUSER.length()), (String) options.get(key)); + } + if (key.startsWith(KEY_AUTOCREATEGROUP)) { + this.groupAttributes.put(key.substring(KEY_AUTOCREATEGROUP.length()), (String) options.get(key)); + } + } + } + + public String getHost() { + return this.host; + } + + public int getPort() { + return this.port; + } + + public boolean isSecure() { + return this.secure; + } + + public String getAuthDn() { + return this.authDn; + } + + public String getAuthPw() { + return this.authPw; + } + + public int getSearchTimeout() { + return this.searchTimeout; + } + + public String getUserRoot() { + return this.userRoot; + } + + public String getUserFilter() { + return this.userFilter; + } + + public String getUserIdAttribute() { + return this.userIdAttribute; + } + + public String getGroupRoot() { + return this.groupRoot; + } + + public String getGroupFilter() { + return this.groupFilter; + } + + public String getGroupMembershipAttribute() { + return this.groupMembershipAttribute; + } + + public String getGroupNameAttribute() { + return this.groupNameAttribute; + } + + public boolean isSplitPath() { + return this.splitPath; + } + + public Map getUserAttributes() { + return this.userAttributes; + } + + public Map getGroupAttributes() { + return this.groupAttributes; + } +} Index: oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/ExternalUser.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/ExternalUser.java (revision 1421770) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/ExternalUser.java (revision ) @@ -33,7 +33,7 @@ String getPath(); - Set getGroups(); + Set getGroups(); Map getProperties(); } \ No newline at end of file Index: oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/LdapUser.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/LdapUser.java (revision ) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/LdapUser.java (revision ) @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.apache.jackrabbit.oak.security.authentication.ldap; + +import org.apache.jackrabbit.oak.security.principal.PrincipalImpl; +import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalUser; + +import java.security.Principal; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +public class LdapUser implements ExternalUser { + + private final String uid; + private final String pwd; + private final LdapSearch search; + + private String path; + private String dn; + private Principal principal; + private Set groups; + private Map properties = new HashMap(); + + public LdapUser(String uid, String pwd, LdapSearch search) { + this.uid = uid; + this.pwd = pwd; + this.search = search; + } + + @Override + public String getId() { + return this.uid; + } + + @Override + public String getPassword() { + return this.pwd; + } + + @Override + public Principal getPrincipal() { + if (this.principal == null) { + this.principal = new PrincipalImpl(this.uid); + } + return this.principal; + } + + @Override + public String getPath() { + //TODO also support splitdn mode + if (this.path == null) { + this.path = this.getDN(); + } + return this.path; + } + + @Override + public Set getGroups() { + if (this.groups == null) { + this.groups = this.search.findGroups(this); + } + return this.groups; + } + + @Override + public Map getProperties() { + return this.properties; + } + + public void setProperties(Map properties) { + this.properties = properties; + } + + public String getDN() { + return this.dn; + } + + public void setDN(String dn) { + this.dn = dn; + } +} Index: oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/JndiLdapSearch.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/JndiLdapSearch.java (revision ) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/JndiLdapSearch.java (revision ) @@ -0,0 +1,236 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.apache.jackrabbit.oak.security.authentication.ldap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +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.InitialDirContext; +import javax.naming.directory.SearchControls; +import javax.naming.directory.SearchResult; +import javax.security.auth.login.LoginException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class JndiLdapSearch implements LdapSearch { + + private static final Logger log = LoggerFactory.getLogger(JndiLdapSearch.class); + + private final LdapSettings settings; + private final Hashtable ldapEnvironment; + + public JndiLdapSearch(LdapSettings settings) { + this.settings = settings; + this.ldapEnvironment = createEnvironment(settings); + } + + private static Hashtable createEnvironment(LdapSettings settings) { + Hashtable env = new Hashtable(); + env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); + String url = "ldap://" + + settings.getHost() + + ":" + + settings.getPort(); + env.put(Context.PROVIDER_URL, url); + if (settings.isSecure()) { + env.put(Context.SECURITY_PROTOCOL, "ssl"); + } + String authDn = settings.getAuthDn(); + String authPw = settings.getAuthPw(); + if (authDn == null || authDn.length() == 0) { + env.put(Context.SECURITY_AUTHENTICATION, "none"); + } + else { + env.put(Context.SECURITY_AUTHENTICATION, "simple"); + env.put(Context.SECURITY_PRINCIPAL, authDn); + env.put(Context.SECURITY_CREDENTIALS,authPw); + } + return env; + } + + private Object parseAttributeValue(Attribute attribute) throws NamingException { + int size = attribute.size(); + if (size > 1) { + ArrayList values = new ArrayList(); + for (int k = 0; k < size; k++) { + values.add(String.valueOf(attribute.get(k))); + } + return values; + } + else { + return String.valueOf(attribute.get()); + } + } + + private void initProperties(LdapUser user, Attributes attributes) + throws NamingException { + NamingEnumeration namingEnumeration = attributes.getAll(); + Map properties = new HashMap(); + Map syncMap = user instanceof LdapGroup ? + this.settings.getGroupAttributes() : this.settings.getUserAttributes(); + while ( namingEnumeration.hasMore() ) { + Attribute attribute = namingEnumeration.next(); + String key = attribute.getID(); + if (syncMap.containsKey(key)) { + properties.put(syncMap.get(key), parseAttributeValue(attribute)); + } + } + user.setProperties(properties); + } + + private List search(String baseDN, String filter, int scope, String[] attributes) + throws NamingException { + + SearchControls constraints = new SearchControls(); + constraints.setSearchScope(SearchControls.SUBTREE_SCOPE); + constraints.setCountLimit(0); + constraints.setDerefLinkFlag(true); + constraints.setTimeLimit(settings.getSearchTimeout()); + List tmp = new ArrayList(); + InitialDirContext context = null; + try { + context = new InitialDirContext(this.ldapEnvironment); + NamingEnumeration namingEnumeration = context.search(baseDN, filter, attributes, constraints); + while (namingEnumeration.hasMore()) { + tmp.add(namingEnumeration.next()); + } + } catch (NamingException e) { + log.error("LDAP search failed", e); + } finally { + if (context != null) { + context.close(); + } + } + return tmp; + } + + private String compileSearchFilter(String baseFilter, String searchExpression) { + StringBuffer searchFilter = new StringBuffer("(&"); + + // Add search expression first, it's typically fairly specific + // so a server that evaluates clauses in order will perform well. + // See https://bugs.day.com/bugzilla/show_bug.cgi?id=36917 + if (!(searchExpression == null || "".equals(searchExpression))) { + if (!searchExpression.startsWith("(")) { + searchFilter.append("("); + } + searchFilter.append(searchExpression); + if (!searchExpression.endsWith(")")) { + searchFilter.append(")"); + } + } + + if (!(baseFilter == null || "".equals(baseFilter))) { + if (!baseFilter.startsWith("(")) { + searchFilter.append("("); + } + searchFilter.append(baseFilter); + if (!baseFilter.endsWith(")")) { + searchFilter.append(")"); + } + } + + searchFilter.append(")"); + return searchFilter.toString(); + } + + private List searchUser(String id) + throws NamingException { + Set attributeSet = new HashSet(this.settings.getUserAttributes().keySet()); + attributeSet.add(this.settings.getUserIdAttribute()); + String[] attributes = new String[attributeSet.size()]; + attributeSet.toArray(attributes); + return this.search(this.settings.getUserRoot(), + this.compileSearchFilter(this.settings.getUserFilter(), this.settings.getUserIdAttribute() + "=" + id), + SearchControls.SUBTREE_SCOPE, + attributes); + } + + private List searchGroups(String dn) + throws NamingException { + Set attributeSet = new HashSet(this.settings.getGroupAttributes().keySet()); + String[] attributes = new String[attributeSet.size()]; + attributeSet.toArray(attributes); + return search(this.settings.getGroupRoot(), + this.compileSearchFilter(this.settings.getGroupFilter(), this.settings.getGroupMembershipAttribute() + "=" + dn), + SearchControls.SUBTREE_SCOPE, + attributes); + } + + private boolean findUser(LdapUser user, String id) { + try { + List entries = this.searchUser(id); + if (entries.size() > 0) { + SearchResult entry = entries.get(0); + user.setDN(entry.getNameInNamespace()); + this.initProperties(user, entry.getAttributes()); + return true; + } else if (id.indexOf("\\") > -1) { + return this.findUser(user, id.substring(id.indexOf("\\") + 1)); + } + } catch (NamingException e) { + //TODO + } + return false; + } + + @Override + public boolean findUser(LdapUser user) { + return this.findUser(user, user.getId()); + } + + @Override + public Set findGroups(LdapUser user) { + final HashSet groups = new HashSet(); + List ldapEntries = null; + try { + ldapEntries = this.searchGroups(user.getDN()); + for (SearchResult entry : ldapEntries) { + LdapGroup group = new LdapGroup(entry.getNameInNamespace(), this); + groups.add(group); + this.initProperties(group, entry.getAttributes()); + } + } catch (NamingException e) { + //TODO + } + return groups; + } + + @Override + public void authenticate(LdapUser user) throws LoginException { + try { + Hashtable env = new Hashtable(this.ldapEnvironment); + env.put(Context.SECURITY_PRINCIPAL, user.getDN()); + env.put(Context.SECURITY_CREDENTIALS, user.getPassword()); + //TODO + env.put(Context.SECURITY_AUTHENTICATION, "simple"); + new InitialDirContext(env).close(); + } catch (NamingException e) { + throw new LoginException("Could not create initial LDAP context for user " + user.getDN() + ": " + e.getMessage()); + } + } +} Index: oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/LdapSearch.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/LdapSearch.java (revision ) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/LdapSearch.java (revision ) @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.apache.jackrabbit.oak.security.authentication.ldap; + +import javax.security.auth.login.LoginException; +import java.util.Set; + +public interface LdapSearch { + + boolean findUser(LdapUser user); + + Set findGroups(LdapUser user); + + void authenticate(LdapUser user) throws LoginException; +} Index: oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/LdapGroup.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/LdapGroup.java (revision ) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/LdapGroup.java (revision ) @@ -0,0 +1,27 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.apache.jackrabbit.oak.security.authentication.ldap; + +import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalGroup; + +public class LdapGroup extends LdapUser implements ExternalGroup { + + public LdapGroup(String dn, LdapSearch search) { + super(dn, null, search); + this.setDN(dn); + } +} Index: oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/LdapLoginModule.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/LdapLoginModule.java (revision ) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/LdapLoginModule.java (revision ) @@ -0,0 +1,94 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.apache.jackrabbit.oak.security.authentication.ldap; + +import javax.jcr.Credentials; +import javax.jcr.SimpleCredentials; +import javax.security.auth.Subject; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.login.LoginException; + +import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalLoginModule; +import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalUser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; + +public final class LdapLoginModule extends ExternalLoginModule { + + private static final Logger log = LoggerFactory.getLogger(ExternalLoginModule.class); + + private Credentials credentials; + private LdapUser ldapUser; + private boolean success; + + private LdapSearch search; + + @Override + public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) { + super.initialize(subject, callbackHandler, sharedState, options); + //TODO + //this.search = new DayClientLdapSearch(new LdapSettings(options)); + this.search = new JndiLdapSearch(new LdapSettings(options)); + } + + @Override + protected boolean loginSucceeded() { + return this.success; + } + + @Override + protected ExternalUser getExternalUser() { + if (this.ldapUser == null) { + Credentials creds = getCredentials(); + if (creds instanceof SimpleCredentials) { + String uid = ((SimpleCredentials) creds).getUserID(); + char[] pwd = ((SimpleCredentials) creds).getPassword(); + this.ldapUser = new LdapUser(uid, new String(pwd), this.search); + } + } + return this.ldapUser; + } + + @Override + public boolean login() throws LoginException { + getExternalUser(); + if (this.ldapUser != null && this.search.findUser(this.ldapUser)) { + this.search.authenticate(this.ldapUser); + this.success = true; + } + return this.success; + } + + @Override + protected Credentials getCredentials() { + if (this.credentials == null) { + this.credentials = super.getCredentials(); + } + return this.credentials; + } + + @Override + protected void clearState() { + super.clearState(); + this.success = false; + this.credentials = null; + this.ldapUser = null; + this.search = null; + } +}