|
@@ -0,0 +1,414 @@
|
|
|
+/* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
|
|
|
+ *
|
|
|
+ * Licensed 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.acegisecurity.userdetails.ldap;
|
|
|
+
|
|
|
+import org.acegisecurity.userdetails.UserDetails;
|
|
|
+import org.acegisecurity.userdetails.UsernameNotFoundException;
|
|
|
+import org.acegisecurity.userdetails.UserDetailsManager;
|
|
|
+import org.acegisecurity.ldap.LdapUtils;
|
|
|
+import org.acegisecurity.GrantedAuthority;
|
|
|
+import org.acegisecurity.GrantedAuthorityImpl;
|
|
|
+import org.acegisecurity.Authentication;
|
|
|
+import org.acegisecurity.context.SecurityContextHolder;
|
|
|
+import org.springframework.dao.DataAccessException;
|
|
|
+import org.springframework.util.Assert;
|
|
|
+import org.springframework.ldap.support.DistinguishedName;
|
|
|
+import org.springframework.ldap.support.DirContextAdapter;
|
|
|
+import org.springframework.ldap.LdapTemplate;
|
|
|
+import org.springframework.ldap.AttributesMapper;
|
|
|
+import org.springframework.ldap.ContextSource;
|
|
|
+import org.springframework.ldap.ContextExecutor;
|
|
|
+import org.springframework.ldap.SearchExecutor;
|
|
|
+import org.springframework.ldap.EntryNotFoundException;
|
|
|
+import org.apache.commons.logging.Log;
|
|
|
+import org.apache.commons.logging.LogFactory;
|
|
|
+
|
|
|
+import javax.naming.*;
|
|
|
+import javax.naming.ldap.LdapContext;
|
|
|
+import javax.naming.directory.*;
|
|
|
+import java.util.*;
|
|
|
+
|
|
|
+/**
|
|
|
+ * An Ldap implementation of UserDetailsManager.
|
|
|
+ * <p>
|
|
|
+ * It is designed around a standard setup where users and groups/roles are stored under separate contexts,
|
|
|
+ * defined by the "userDnBase" and "groupSearchBase" properties respectively.
|
|
|
+ * </p>
|
|
|
+ * <p>
|
|
|
+ * In this case, LDAP is being used purely to retrieve information and this class can be used in place of any other
|
|
|
+ * UserDetailsService for authentication. Authentication isn't performed directly against the directory, unlike with the
|
|
|
+ * LDAP authentication provider setup.
|
|
|
+ * </p>
|
|
|
+ *
|
|
|
+ *
|
|
|
+ * @author Luke Taylor
|
|
|
+ * @since 2.0
|
|
|
+ */
|
|
|
+public class LdapUserDetailsManager implements UserDetailsManager {
|
|
|
+ private final Log logger = LogFactory.getLog(LdapUserDetailsManager.class);
|
|
|
+
|
|
|
+ /** The DN under which users entries are stored */
|
|
|
+ private DistinguishedName userDnBase = new DistinguishedName("cn=users");
|
|
|
+ /** The DN under which groups are stored */
|
|
|
+ private DistinguishedName groupSearchBase = new DistinguishedName("cn=groups");
|
|
|
+
|
|
|
+ /** The attribute which contains the user login name, and which is used by default to build the DN for new users */
|
|
|
+ private String usernameAttributeName = "uid";
|
|
|
+ /** Password attribute name */
|
|
|
+ private String passwordAttributeName = "userPassword";
|
|
|
+
|
|
|
+ /** The attribute which corresponds to the role name of a group. */
|
|
|
+ private String groupRoleAttributeName ="cn";
|
|
|
+ /** The attribute which contains members of a group */
|
|
|
+ private String groupMemberAttributeName = "uniquemember";
|
|
|
+
|
|
|
+ private String rolePrefix = "ROLE_";
|
|
|
+
|
|
|
+ /** The pattern to be used for the user search. {0} is the user's DN */
|
|
|
+ private String groupSearchFilter = "(uniquemember={0})";
|
|
|
+ /**
|
|
|
+ * The strategy used to create a UserDetails object from the LDAP context, username and list of authorities.
|
|
|
+ * This should be set to match the required UserDetails implementation.
|
|
|
+ */
|
|
|
+ private UserDetailsContextMapper userDetailsMapper = new InetOrgPersonContextMapper();
|
|
|
+
|
|
|
+ private LdapTemplate template;
|
|
|
+
|
|
|
+ /** Default context mapper used to create a set of roles from a list of attributes */
|
|
|
+ private AttributesMapper roleMapper = new AttributesMapper() {
|
|
|
+
|
|
|
+ public Object mapFromAttributes(Attributes attributes) throws NamingException {
|
|
|
+ Attribute roleAttr = attributes.get(groupRoleAttributeName);
|
|
|
+
|
|
|
+ NamingEnumeration ne = roleAttr.getAll();
|
|
|
+ // assert ne.hasMore();
|
|
|
+ Object group = ne.next();
|
|
|
+ String role = group.toString();
|
|
|
+
|
|
|
+ return new GrantedAuthorityImpl(rolePrefix + role.toUpperCase());
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ private String[] attributesToRetrieve = null;
|
|
|
+
|
|
|
+ public LdapUserDetailsManager(ContextSource contextSource) {
|
|
|
+ template = new LdapTemplate(contextSource);
|
|
|
+ }
|
|
|
+
|
|
|
+ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException {
|
|
|
+ DistinguishedName dn = buildDn(username);
|
|
|
+ GrantedAuthority[] authorities = getUserAuthorities(dn, username);
|
|
|
+
|
|
|
+ logger.debug("Loading user '"+ username + "' with DN '" + dn + "'");
|
|
|
+
|
|
|
+ DirContextAdapter userCtx = loadUserAsContext(dn, username);
|
|
|
+
|
|
|
+ return userDetailsMapper.mapUserFromContext(userCtx, username, authorities);
|
|
|
+ }
|
|
|
+
|
|
|
+ private UserContext loadUserAsContext(final DistinguishedName dn, final String username) {
|
|
|
+ return (UserContext) template.executeReadOnly(new ContextExecutor() {
|
|
|
+ public Object executeWithContext(DirContext ctx) throws NamingException {
|
|
|
+ try {
|
|
|
+ Attributes attrs = ctx.getAttributes(dn, attributesToRetrieve);
|
|
|
+ return new UserContext(attrs, LdapUtils.getFullDn(dn, ctx));
|
|
|
+ } catch(NameNotFoundException notFound) {
|
|
|
+ throw new UsernameNotFoundException("User " + username + " not found", notFound);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Changes the password for the current user. The username is obtained from the security context.
|
|
|
+ * <p>
|
|
|
+ * If the old password is supplied, the update will be made by rebinding as the user, thus modifying the password
|
|
|
+ * using the user's permissions. If <code>oldPassword</code> is null, the update will be attempted using a
|
|
|
+ * standard read/write context supplied by the context source.
|
|
|
+ * </p>
|
|
|
+ *
|
|
|
+ * @param oldPassword the old password
|
|
|
+ * @param newPassword the new value of the password.
|
|
|
+ */
|
|
|
+ public void changePassword(final String oldPassword, final String newPassword) {
|
|
|
+ Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
|
|
+ Assert.notNull(authentication,
|
|
|
+ "No authentication object found in security context. Can't change current user's password!");
|
|
|
+
|
|
|
+ String username = authentication.getName();
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ logger.debug("Changing password for user '"+ username);
|
|
|
+
|
|
|
+ final DistinguishedName dn = buildDn(username);
|
|
|
+ final ModificationItem[] passwordChange = new ModificationItem[] {
|
|
|
+ new ModificationItem(DirContext.REPLACE_ATTRIBUTE, new BasicAttribute(passwordAttributeName, newPassword))
|
|
|
+ };
|
|
|
+
|
|
|
+ if(oldPassword == null) {
|
|
|
+ template.modifyAttributes(dn, passwordChange);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ template.executeReadWrite(new ContextExecutor() {
|
|
|
+
|
|
|
+ public Object executeWithContext(DirContext dirCtx) throws NamingException {
|
|
|
+ LdapContext ctx = (LdapContext) dirCtx;
|
|
|
+ ctx.removeFromEnvironment("com.sun.jndi.ldap.connect.pool");
|
|
|
+ ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, LdapUtils.getFullDn(dn, ctx).toUrl());
|
|
|
+ ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, oldPassword);
|
|
|
+ ctx.reconnect(null);
|
|
|
+
|
|
|
+ ctx.modifyAttributes(dn, passwordChange);
|
|
|
+
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ *
|
|
|
+ * @param dn the distinguished name of the entry - may be either relative to the base context
|
|
|
+ * or a complete DN including the name of the context (either is supported).
|
|
|
+ * @param username the user whose roles are required.
|
|
|
+ * @return the granted authorities returned by the group search
|
|
|
+ */
|
|
|
+ GrantedAuthority[] getUserAuthorities(final DistinguishedName dn, final String username) {
|
|
|
+ SearchExecutor se = new SearchExecutor() {
|
|
|
+ public NamingEnumeration executeSearch(DirContext ctx) throws NamingException {
|
|
|
+ DistinguishedName fullDn = LdapUtils.getFullDn(dn, ctx);
|
|
|
+ SearchControls ctrls = new SearchControls();
|
|
|
+ ctrls.setReturningAttributes(new String[] {groupRoleAttributeName});
|
|
|
+
|
|
|
+ return ctx.search(groupSearchBase, groupSearchFilter, new String[] {fullDn.toUrl(), username}, ctrls);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ LdapTemplate.AttributesMapperCallbackHandler roleCollector =
|
|
|
+ template.new AttributesMapperCallbackHandler(roleMapper);
|
|
|
+
|
|
|
+ template.search(se, roleCollector);
|
|
|
+ List authorities = roleCollector.getList();
|
|
|
+
|
|
|
+ return (GrantedAuthority[]) authorities.toArray(new GrantedAuthority[authorities.size()]);
|
|
|
+ }
|
|
|
+
|
|
|
+// protected String getRoleFilter(DistinguishedName dn, String username) {
|
|
|
+// return new EqualsFilter("uniquemember", dn.toString()).encode();
|
|
|
+// }
|
|
|
+
|
|
|
+ public void createUser(UserDetails user) {
|
|
|
+ DirContextAdapter ctx = new DirContextAdapter();
|
|
|
+ copyToContext(user, ctx);
|
|
|
+ DistinguishedName dn = buildDn(user.getUsername());
|
|
|
+ // Check for any existing authorities which might be set for this DN
|
|
|
+ GrantedAuthority[] authorities = getUserAuthorities(dn, user.getUsername());
|
|
|
+
|
|
|
+ if(authorities.length > 0) {
|
|
|
+ removeAuthorities(dn, authorities);
|
|
|
+ }
|
|
|
+
|
|
|
+ logger.debug("Creating new user '"+ user.getUsername() + "' with DN '" + dn + "'");
|
|
|
+
|
|
|
+ template.bind(dn, ctx, null);
|
|
|
+
|
|
|
+ addAuthorities(dn, user.getAuthorities());
|
|
|
+ }
|
|
|
+
|
|
|
+ public void updateUser(UserDetails user) {
|
|
|
+// Assert.notNull(attributesToRetrieve, "Configuration must specify a list of attributes in order to use update.");
|
|
|
+ DistinguishedName dn = buildDn(user.getUsername());
|
|
|
+
|
|
|
+ logger.debug("Updating user '"+ user.getUsername() + "' with DN '" + dn + "'");
|
|
|
+
|
|
|
+ GrantedAuthority[] authorities = getUserAuthorities(dn, user.getUsername());
|
|
|
+
|
|
|
+ UserContext ctx = loadUserAsContext(dn, user.getUsername());
|
|
|
+ ctx.setUpdateMode(true);
|
|
|
+ copyToContext(user, ctx);
|
|
|
+
|
|
|
+ // Remove the objectclass attribute from the list of mods (if present).
|
|
|
+ List mods = new LinkedList(Arrays.asList(ctx.getModificationItems()));
|
|
|
+
|
|
|
+ ListIterator modIt = mods.listIterator();
|
|
|
+ while(modIt.hasNext()) {
|
|
|
+ ModificationItem mod = (ModificationItem) modIt.next();
|
|
|
+ Attribute a = mod.getAttribute();
|
|
|
+ if("objectclass".equalsIgnoreCase(a.getID())) {
|
|
|
+ modIt.remove();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ template.modifyAttributes(dn, (ModificationItem[]) mods.toArray(new ModificationItem[mods.size()]));
|
|
|
+
|
|
|
+// template.rebind(dn, ctx, null);
|
|
|
+ // Remove the old authorities and replace them with the new one
|
|
|
+ removeAuthorities(dn, authorities);
|
|
|
+ addAuthorities(dn, user.getAuthorities());
|
|
|
+ }
|
|
|
+
|
|
|
+ public void deleteUser(String username) {
|
|
|
+ DistinguishedName dn = buildDn(username);
|
|
|
+ removeAuthorities(dn, getUserAuthorities(dn, username));
|
|
|
+ template.unbind(dn);
|
|
|
+ }
|
|
|
+
|
|
|
+ public boolean userExists(String username) {
|
|
|
+ DistinguishedName dn = buildDn(username);
|
|
|
+
|
|
|
+ try {
|
|
|
+ Object obj = template.lookup(dn);
|
|
|
+ if (obj instanceof Context) {
|
|
|
+ LdapUtils.closeContext((Context) obj);
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ } catch(EntryNotFoundException e) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Constructs a DN from a username.
|
|
|
+ * <p>
|
|
|
+ * The default implementation appends a name component to the <tt>userDnBase</tt> context using the
|
|
|
+ * <tt>usernameAttributeName</tt> property. So if the <tt>uid</tt> attribute is used to store the username, and the
|
|
|
+ * base DN is <tt>cn=users</tt> and we are creating a new user called "sam", then the DN will be
|
|
|
+ * <tt>uid=sam,cn=users</tt>.
|
|
|
+ *
|
|
|
+ * @param username the user name used for authentication.
|
|
|
+ * @return the corresponding DN, relative to the base context.
|
|
|
+ */
|
|
|
+ protected DistinguishedName buildDn(String username) {
|
|
|
+ DistinguishedName dn = new DistinguishedName(userDnBase);
|
|
|
+
|
|
|
+ dn.add(usernameAttributeName, username);
|
|
|
+
|
|
|
+ return dn;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Creates a DN from a group name.
|
|
|
+ *
|
|
|
+ * @param group the name of the group
|
|
|
+ * @return the DN of the corresponding group, including the groupSearchBase
|
|
|
+ */
|
|
|
+ protected DistinguishedName buildGroupDn(String group) {
|
|
|
+ DistinguishedName dn = new DistinguishedName(groupSearchBase);
|
|
|
+ dn.add(groupRoleAttributeName, group.toLowerCase());
|
|
|
+
|
|
|
+ return dn;
|
|
|
+ }
|
|
|
+
|
|
|
+ protected void copyToContext(UserDetails user, DirContextAdapter ctx) {
|
|
|
+ userDetailsMapper.mapUserToContext(user, ctx);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void addAuthorities(DistinguishedName userDn, GrantedAuthority[] authorities) {
|
|
|
+ modifyAuthorities(userDn, authorities, DirContext.ADD_ATTRIBUTE);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void removeAuthorities(DistinguishedName userDn, GrantedAuthority[] authorities) {
|
|
|
+ modifyAuthorities(userDn, authorities, DirContext.REMOVE_ATTRIBUTE);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void modifyAuthorities(final DistinguishedName userDn, final GrantedAuthority[] authorities, final int modType) {
|
|
|
+ template.executeReadWrite(new ContextExecutor() {
|
|
|
+ public Object executeWithContext(DirContext ctx) throws NamingException {
|
|
|
+ for(int i=0; i < authorities.length; i++) {
|
|
|
+ GrantedAuthority authority = authorities[i];
|
|
|
+ String group = convertAuthorityToGroup(authority);
|
|
|
+ DistinguishedName fullDn = LdapUtils.getFullDn(userDn, ctx);
|
|
|
+ ModificationItem addGroup = new ModificationItem(modType,
|
|
|
+ new BasicAttribute(groupMemberAttributeName, fullDn.toUrl()));
|
|
|
+
|
|
|
+ ctx.modifyAttributes(buildGroupDn(group), new ModificationItem[] {addGroup});
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ private String convertAuthorityToGroup(GrantedAuthority authority) {
|
|
|
+ String group = authority.getAuthority();
|
|
|
+
|
|
|
+ if(group.startsWith(rolePrefix)) {
|
|
|
+ group = group.substring(rolePrefix.length());
|
|
|
+ }
|
|
|
+
|
|
|
+ return group;
|
|
|
+ }
|
|
|
+
|
|
|
+ public void setUsernameAttributeName(String usernameAttributeName) {
|
|
|
+ this.usernameAttributeName = usernameAttributeName;
|
|
|
+ }
|
|
|
+
|
|
|
+ public void setPasswordAttributeName(String passwordAttributeName) {
|
|
|
+ this.passwordAttributeName = passwordAttributeName;
|
|
|
+ }
|
|
|
+
|
|
|
+ public void setGroupSearchBase(String groupSearchBase) {
|
|
|
+ this.groupSearchBase = new DistinguishedName(groupSearchBase);
|
|
|
+ }
|
|
|
+
|
|
|
+ public void setGroupRoleAttributeName(String groupRoleAttributeName) {
|
|
|
+ this.groupRoleAttributeName = groupRoleAttributeName;
|
|
|
+ }
|
|
|
+
|
|
|
+ public void setUserDnBase(String userDnBase) {
|
|
|
+ this.userDnBase = new DistinguishedName(userDnBase);
|
|
|
+ }
|
|
|
+
|
|
|
+ public void setAttributesToRetrieve(String[] attributesToRetrieve) {
|
|
|
+ Assert.notNull(attributesToRetrieve);
|
|
|
+ this.attributesToRetrieve = attributesToRetrieve;
|
|
|
+ }
|
|
|
+
|
|
|
+ public void setUserDetailsMapper(UserDetailsContextMapper userDetailsMapper) {
|
|
|
+ this.userDetailsMapper = userDetailsMapper;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Sets the name of the multi-valued attribute which holds the DNs of users who are members of a group.
|
|
|
+ * <p>
|
|
|
+ * Usually this will be <tt>uniquemember</tt> (the default value) or <tt>member</tt>.
|
|
|
+ * </p>
|
|
|
+ *
|
|
|
+ * @param groupMemberAttributeName the name of the attribute used to store group members.
|
|
|
+ */
|
|
|
+ public void setGroupMemberAttributeName(String groupMemberAttributeName) {
|
|
|
+ Assert.hasText(groupMemberAttributeName);
|
|
|
+ this.groupMemberAttributeName = groupMemberAttributeName;
|
|
|
+ this.groupSearchFilter = "(" + groupMemberAttributeName + "={0})";
|
|
|
+ }
|
|
|
+
|
|
|
+ public void setRoleMapper(AttributesMapper roleMapper) {
|
|
|
+ this.roleMapper = roleMapper;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * This class allows us to set the <tt>updateMode</tt> property of DirContextAdapter when updating existing users.
|
|
|
+ */
|
|
|
+ private static class UserContext extends DirContextAdapter {
|
|
|
+ public UserContext(Attributes pAttrs, Name dn) {
|
|
|
+ super(pAttrs, dn);
|
|
|
+ }
|
|
|
+
|
|
|
+ protected void setUpdateMode(boolean mode) {
|
|
|
+ super.setUpdateMode(mode);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|