From: Mathieu Baudier Date: Tue, 25 Nov 2014 13:37:07 +0000 (+0000) Subject: New project conventions X-Git-Tag: argeo-commons-2.1.30~513 X-Git-Url: http://git.argeo.org/?a=commitdiff_plain;h=2f136bfa1e3b45a7a622822517d428fde0de4fa6;p=lgpl%2Fargeo-commons.git New project conventions git-svn-id: https://svn.argeo.org/commons/trunk@7535 4cfe0d0a-d680-48aa-b62c-e0a02a3f76cc --- diff --git a/org.argeo.security.ldap/.classpath b/org.argeo.security.ldap/.classpath index 5641c7ca3..d2953a684 100644 --- a/org.argeo.security.ldap/.classpath +++ b/org.argeo.security.ldap/.classpath @@ -1,7 +1,9 @@ - - >> - - + + + + diff --git a/org.argeo.security.ldap/bnd.bnd b/org.argeo.security.ldap/bnd.bnd new file mode 100644 index 000000000..12f35fa05 --- /dev/null +++ b/org.argeo.security.ldap/bnd.bnd @@ -0,0 +1,4 @@ +Import-Package: org.springframework.core,\ +org.springframework.dao,\ +javax.jcr.nodetype,\ +* \ No newline at end of file diff --git a/org.argeo.security.ldap/build.properties b/org.argeo.security.ldap/build.properties index 5fc538bc8..30f715358 100644 --- a/org.argeo.security.ldap/build.properties +++ b/org.argeo.security.ldap/build.properties @@ -1,4 +1 @@ -source.. = src/main/java/ -output.. = target/classes/ -bin.includes = META-INF/,\ - . +source.. = src/ diff --git a/org.argeo.security.ldap/src/main/java/org/argeo/security/ldap/ArgeoLdapShaPasswordEncoder.java b/org.argeo.security.ldap/src/main/java/org/argeo/security/ldap/ArgeoLdapShaPasswordEncoder.java deleted file mode 100644 index ea22ef350..000000000 --- a/org.argeo.security.ldap/src/main/java/org/argeo/security/ldap/ArgeoLdapShaPasswordEncoder.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (C) 2007-2012 Argeo GmbH - * - * 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.argeo.security.ldap; - -import org.springframework.security.providers.ldap.authenticator.LdapShaPasswordEncoder; - -/** - * {@link LdapShaPasswordEncoder} allowing to configure the usage of salt (APache - * Directory Server 1.0 does not support bind with SSHA) - */ -public class ArgeoLdapShaPasswordEncoder extends LdapShaPasswordEncoder { - private Boolean useSalt = true; - - @Override - public String encodePassword(String rawPass, Object salt) { - return super.encodePassword(rawPass, useSalt ? salt : null); - } - - public void setUseSalt(Boolean useSalt) { - this.useSalt = useSalt; - } - -} diff --git a/org.argeo.security.ldap/src/main/java/org/argeo/security/ldap/ArgeoLdapUserDetailsManager.java b/org.argeo.security.ldap/src/main/java/org/argeo/security/ldap/ArgeoLdapUserDetailsManager.java deleted file mode 100644 index 0c7368f1f..000000000 --- a/org.argeo.security.ldap/src/main/java/org/argeo/security/ldap/ArgeoLdapUserDetailsManager.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright (C) 2007-2012 Argeo GmbH - * - * 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.argeo.security.ldap; - -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.Random; -import java.util.Set; -import java.util.TreeSet; - -import org.argeo.ArgeoException; -import org.argeo.security.UserAdminService; -import org.springframework.ldap.core.ContextSource; -import org.springframework.security.Authentication; -import org.springframework.security.GrantedAuthority; -import org.springframework.security.context.SecurityContextHolder; -import org.springframework.security.providers.encoding.PasswordEncoder; -import org.springframework.security.userdetails.UserDetails; -import org.springframework.security.userdetails.ldap.LdapUserDetailsManager; - -/** Extends {@link LdapUserDetailsManager} by adding password encoding support. */ -public class ArgeoLdapUserDetailsManager extends LdapUserDetailsManager - implements UserAdminService { - private String superUsername = "root"; - private ArgeoUserAdminDaoLdap userAdminDao; - private PasswordEncoder passwordEncoder; - private final Random random; - - public ArgeoLdapUserDetailsManager(ContextSource contextSource) { - super(contextSource); - this.random = createRandom(); - } - - private static Random createRandom() { - try { - return SecureRandom.getInstance("SHA1PRNG"); - } catch (NoSuchAlgorithmException e) { - return new Random(System.currentTimeMillis()); - } - } - - @Override - public void changePassword(String oldPassword, String newPassword) { - Authentication authentication = SecurityContextHolder.getContext() - .getAuthentication(); - if (authentication == null) - throw new ArgeoException( - "Cannot change password without authentication"); - String username = authentication.getName(); - UserDetails userDetails = loadUserByUsername(username); - String currentPassword = userDetails.getPassword(); - if (currentPassword == null) - throw new ArgeoException("Cannot access current password"); - if (!passwordEncoder - .isPasswordValid(currentPassword, oldPassword, null)) - throw new ArgeoException("Old password invalid"); - // Spring Security LDAP 2.0 is buggy when used with OpenLDAP and called - // with oldPassword argument - super.changePassword(null, encodePassword(newPassword)); - } - - public void newRole(String role) { - userAdminDao.createRole(role, superUsername); - } - - public void synchronize() { - for (String username : userAdminDao.listUsers()) - loadUserByUsername(username); - // TODO: find a way to remove from JCR - } - - public void deleteRole(String role) { - userAdminDao.deleteRole(role); - } - - public Set listUsers() { - return userAdminDao.listUsers(); - } - - public Set listUsersInRole(String role) { - Set lst = new TreeSet( - userAdminDao.listUsersInRole(role)); - Iterator it = lst.iterator(); - while (it.hasNext()) { - if (it.next().equals(superUsername)) { - it.remove(); - break; - } - } - return lst; - } - - public List listUserRoles(String username) { - UserDetails userDetails = loadUserByUsername(username); - List roles = new ArrayList(); - for (GrantedAuthority ga : userDetails.getAuthorities()) { - roles.add(ga.getAuthority()); - } - return Collections.unmodifiableList(roles); - } - - public Set listEditableRoles() { - return userAdminDao.listEditableRoles(); - } - - protected String encodePassword(String password) { - if (!password.startsWith("{")) { - byte[] salt = new byte[16]; - random.nextBytes(salt); - return passwordEncoder.encodePassword(password, salt); - } else { - return password; - } - } - - public void setPasswordEncoder(PasswordEncoder passwordEncoder) { - this.passwordEncoder = passwordEncoder; - } - - public void setSuperUsername(String superUsername) { - this.superUsername = superUsername; - } - - public void setUserAdminDao(ArgeoUserAdminDaoLdap userAdminDao) { - this.userAdminDao = userAdminDao; - } - -} diff --git a/org.argeo.security.ldap/src/main/java/org/argeo/security/ldap/ArgeoUserAdminDaoLdap.java b/org.argeo.security.ldap/src/main/java/org/argeo/security/ldap/ArgeoUserAdminDaoLdap.java deleted file mode 100644 index 37d2a06a0..000000000 --- a/org.argeo.security.ldap/src/main/java/org/argeo/security/ldap/ArgeoUserAdminDaoLdap.java +++ /dev/null @@ -1,195 +0,0 @@ -/* - * Copyright (C) 2007-2012 Argeo GmbH - * - * 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.argeo.security.ldap; - -import java.util.Collections; -import java.util.List; -import java.util.Set; -import java.util.TreeSet; - -import javax.naming.Name; -import javax.naming.NamingException; -import javax.naming.directory.DirContext; - -import org.springframework.ldap.core.ContextExecutor; -import org.springframework.ldap.core.ContextMapper; -import org.springframework.ldap.core.DirContextAdapter; -import org.springframework.ldap.core.DistinguishedName; -import org.springframework.ldap.core.LdapTemplate; -import org.springframework.ldap.core.support.BaseLdapPathContextSource; -import org.springframework.security.ldap.LdapUsernameToDnMapper; -import org.springframework.security.ldap.LdapUtils; - -/** - * Wraps low-level LDAP operation on user and roles, used by - * {@link ArgeoLdapUserDetailsManager} - */ -public class ArgeoUserAdminDaoLdap { - private String userBase; - private String usernameAttribute; - private String groupBase; - private String[] groupClasses; - - private String groupRoleAttribute; - private String groupMemberAttribute; - private String defaultRole; - private String rolePrefix; - - private final LdapTemplate ldapTemplate; - private LdapUsernameToDnMapper usernameMapper; - - /** - * Standard constructor, using the LDAP context source shared with Spring - * Security components. - */ - public ArgeoUserAdminDaoLdap(BaseLdapPathContextSource contextSource) { - this.ldapTemplate = new LdapTemplate(contextSource); - } - - @SuppressWarnings("unchecked") - public synchronized Set listUsers() { - List usernames = (List) ldapTemplate.listBindings( - new DistinguishedName(userBase), new ContextMapper() { - public Object mapFromContext(Object ctxArg) { - DirContextAdapter ctx = (DirContextAdapter) ctxArg; - return ctx.getStringAttribute(usernameAttribute); - } - }); - - return Collections - .unmodifiableSortedSet(new TreeSet(usernames)); - } - - @SuppressWarnings("unchecked") - public Set listEditableRoles() { - return Collections.unmodifiableSortedSet(new TreeSet( - ldapTemplate.listBindings(groupBase, new ContextMapper() { - public Object mapFromContext(Object ctxArg) { - String groupName = ((DirContextAdapter) ctxArg) - .getStringAttribute(groupRoleAttribute); - String roleName = convertGroupToRole(groupName); - return roleName; - } - }))); - } - - @SuppressWarnings("unchecked") - public Set listUsersInRole(String role) { - return (Set) ldapTemplate.lookup( - buildGroupDn(convertRoleToGroup(role)), new ContextMapper() { - public Object mapFromContext(Object ctxArg) { - DirContextAdapter ctx = (DirContextAdapter) ctxArg; - String[] userDns = ctx - .getStringAttributes(groupMemberAttribute); - TreeSet set = new TreeSet(); - for (String userDn : userDns) { - DistinguishedName dn = new DistinguishedName(userDn); - String username = dn.getValue(usernameAttribute); - set.add(username); - } - return Collections.unmodifiableSortedSet(set); - } - }); - } - - public void createRole(String role, final String superuserName) { - String group = convertRoleToGroup(role); - DistinguishedName superuserDn = (DistinguishedName) ldapTemplate - .executeReadWrite(new ContextExecutor() { - public Object executeWithContext(DirContext ctx) - throws NamingException { - return LdapUtils.getFullDn( - usernameMapper.buildDn(superuserName), ctx); - } - }); - - Name groupDn = buildGroupDn(group); - DirContextAdapter context = new DirContextAdapter(); - context.setAttributeValues("objectClass", groupClasses); - context.setAttributeValue("cn", group); - // Add superuser because cannot create empty group - context.setAttributeValue(groupMemberAttribute, superuserDn.toString()); - ldapTemplate.bind(groupDn, context, null); - } - - public void deleteRole(String role) { - String group = convertRoleToGroup(role); - Name dn = buildGroupDn(group); - ldapTemplate.unbind(dn); - } - - /** Maps a role (ROLE_XXX) to the related LDAP group (xxx) */ - protected String convertRoleToGroup(String role) { - String group = role; - if (group.startsWith(rolePrefix)) { - group = group.substring(rolePrefix.length()); - group = group.toLowerCase(); - } - return group; - } - - /** Maps anLDAP group (xxx) to the related role (ROLE_XXX) */ - protected String convertGroupToRole(String groupName) { - groupName = groupName.toUpperCase(); - - return rolePrefix + groupName; - } - - protected Name buildGroupDn(String name) { - return new DistinguishedName(groupRoleAttribute + "=" + name + "," - + groupBase); - } - - public void setUserBase(String userBase) { - this.userBase = userBase; - } - - public void setUsernameAttribute(String usernameAttribute) { - this.usernameAttribute = usernameAttribute; - } - - public void setGroupBase(String groupBase) { - this.groupBase = groupBase; - } - - public void setGroupRoleAttribute(String groupRoleAttributeName) { - this.groupRoleAttribute = groupRoleAttributeName; - } - - public void setGroupMemberAttribute(String groupMemberAttributeName) { - this.groupMemberAttribute = groupMemberAttributeName; - } - - public void setDefaultRole(String defaultRole) { - this.defaultRole = defaultRole; - } - - public void setRolePrefix(String rolePrefix) { - this.rolePrefix = rolePrefix; - } - - public void setUsernameMapper(LdapUsernameToDnMapper usernameMapper) { - this.usernameMapper = usernameMapper; - } - - public String getDefaultRole() { - return defaultRole; - } - - public void setGroupClasses(String[] groupClasses) { - this.groupClasses = groupClasses; - } -} diff --git a/org.argeo.security.ldap/src/main/java/org/argeo/security/ldap/jcr/JcrLdapSynchronizer.java b/org.argeo.security.ldap/src/main/java/org/argeo/security/ldap/jcr/JcrLdapSynchronizer.java deleted file mode 100644 index 3e9e2cbfa..000000000 --- a/org.argeo.security.ldap/src/main/java/org/argeo/security/ldap/jcr/JcrLdapSynchronizer.java +++ /dev/null @@ -1,619 +0,0 @@ -/* - * Copyright (C) 2007-2012 Argeo GmbH - * - * 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.argeo.security.ldap.jcr; - -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Random; -import java.util.SortedSet; -import java.util.UUID; - -import javax.jcr.Node; -import javax.jcr.NodeIterator; -import javax.jcr.Repository; -import javax.jcr.RepositoryException; -import javax.jcr.Session; -import javax.jcr.query.Query; -import javax.jcr.version.VersionManager; -import javax.naming.Name; -import javax.naming.directory.BasicAttribute; -import javax.naming.directory.DirContext; -import javax.naming.directory.ModificationItem; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.argeo.ArgeoException; -import org.argeo.jcr.ArgeoNames; -import org.argeo.jcr.ArgeoTypes; -import org.argeo.jcr.JcrUtils; -import org.argeo.security.SecurityUtils; -import org.argeo.security.jcr.JcrSecurityModel; -import org.argeo.security.jcr.JcrUserDetails; -import org.argeo.security.jcr.SimpleJcrSecurityModel; -import org.springframework.ldap.core.ContextMapper; -import org.springframework.ldap.core.DirContextAdapter; -import org.springframework.ldap.core.DirContextOperations; -import org.springframework.ldap.core.DistinguishedName; -import org.springframework.ldap.core.LdapTemplate; -import org.springframework.security.GrantedAuthority; -import org.springframework.security.ldap.LdapUsernameToDnMapper; -import org.springframework.security.providers.encoding.PasswordEncoder; -import org.springframework.security.userdetails.UserDetails; -import org.springframework.security.userdetails.ldap.UserDetailsContextMapper; - -/** Makes sure that LDAP and JCR are in line. */ -public class JcrLdapSynchronizer implements UserDetailsContextMapper, - ArgeoNames { - private final static Log log = LogFactory.getLog(JcrLdapSynchronizer.class); - - // LDAP - private LdapTemplate ldapTemplate; - /** - * LDAP template whose context source has an object factory set to null. see - * this - */ - // private LdapTemplate rawLdapTemplate; - - private String userBase; - private String usernameAttribute; - private String passwordAttribute; - private String[] userClasses; - // private String defaultUserRole ="ROLE_USER"; - - // private NamingListener ldapUserListener; - // private SearchControls subTreeSearchControls; - private LdapUsernameToDnMapper usernameMapper; - - private PasswordEncoder passwordEncoder; - private final Random random; - - // JCR - /** Admin session on the main workspace */ - private Session nodeSession; - private Repository repository; - - // private JcrProfileListener jcrProfileListener; - private JcrSecurityModel jcrSecurityModel = new SimpleJcrSecurityModel(); - - // Mapping - private Map propertyToAttributes = new HashMap(); - - public JcrLdapSynchronizer() { - random = createRandom(); - } - - public void init() { - try { - nodeSession = repository.login(); - - // TODO put this in a different thread, and poll the LDAP server - // until it is up - try { - synchronize(); - - // LDAP - // subTreeSearchControls = new SearchControls(); - // subTreeSearchControls - // .setSearchScope(SearchControls.SUBTREE_SCOPE); - // LDAP listener - // ldapUserListener = new LdapUserListener(); - // rawLdapTemplate.executeReadOnly(new ContextExecutor() { - // public Object executeWithContext(DirContext ctx) - // throws NamingException { - // EventDirContext ectx = (EventDirContext) ctx.lookup(""); - // ectx.addNamingListener(userBase, "(" - // + usernameAttribute + "=*)", - // subTreeSearchControls, ldapUserListener); - // return null; - // } - // }); - } catch (Exception e) { - log.error("Could not synchronize and listen to LDAP," - + " probably because the LDAP server is not available." - + " Restart the system as soon as possible.", e); - } - - // JCR - // String[] nodeTypes = { ArgeoTypes.ARGEO_USER_PROFILE }; - // jcrProfileListener = new JcrProfileListener(); - // noLocal is used so that we are not notified when we modify JCR - // from LDAP - // nodeSession - // .getWorkspace() - // .getObservationManager() - // .addEventListener(jcrProfileListener, - // Event.PROPERTY_CHANGED | Event.NODE_ADDED, "/", - // true, null, nodeTypes, true); - } catch (Exception e) { - JcrUtils.logoutQuietly(nodeSession); - throw new ArgeoException("Cannot initialize LDAP/JCR synchronizer", - e); - } - } - - public void destroy() { - // JcrUtils.removeListenerQuietly(nodeSession, jcrProfileListener); - JcrUtils.logoutQuietly(nodeSession); - // try { - // rawLdapTemplate.executeReadOnly(new ContextExecutor() { - // public Object executeWithContext(DirContext ctx) - // throws NamingException { - // EventDirContext ectx = (EventDirContext) ctx.lookup(""); - // ectx.removeNamingListener(ldapUserListener); - // return null; - // } - // }); - // } catch (Exception e) { - // // silent (LDAP server may have been shutdown already) - // if (log.isTraceEnabled()) - // log.trace("Cannot remove LDAP listener", e); - // } - } - - /* - * LDAP TO JCR - */ - /** Full synchronization between LDAP and JCR. LDAP has priority. */ - protected void synchronize() { - try { - Name userBaseName = new DistinguishedName(userBase); - // TODO subtree search? - @SuppressWarnings("unchecked") - List userPaths = (List) ldapTemplate.listBindings( - userBaseName, new ContextMapper() { - public Object mapFromContext(Object ctxObj) { - try { - return mapLdapToJcr((DirContextAdapter) ctxObj); - } catch (Exception e) { - // do not break process because of error - log.error( - "Could not LDAP->JCR synchronize user " - + ctxObj, e); - return null; - } - } - }); - - // create accounts which are not in LDAP - Query query = nodeSession - .getWorkspace() - .getQueryManager() - .createQuery( - "select * from [" + ArgeoTypes.ARGEO_USER_PROFILE - + "]", Query.JCR_SQL2); - NodeIterator it = query.execute().getNodes(); - while (it.hasNext()) { - Node userProfile = it.nextNode(); - String path = userProfile.getPath(); - try { - if (!userPaths.contains(path)) { - String username = userProfile - .getProperty(ARGEO_USER_ID).getString(); - // GrantedAuthority[] authorities = {new - // GrantedAuthorityImpl(defaultUserRole)}; - GrantedAuthority[] authorities = {}; - JcrUserDetails userDetails = new JcrUserDetails( - userProfile, username, authorities); - String dn = createLdapUser(userDetails); - log.warn("Created ldap entry '" + dn + "' for user '" - + username + "'"); - - // if(!userProfile.getProperty(ARGEO_ENABLED).getBoolean()){ - // continue profiles; - // } - // - // log.warn("Path " - // + path - // + " not found in LDAP, disabling user " - // + userProfile.getProperty(ArgeoNames.ARGEO_USER_ID) - // .getString()); - - // Temporary hack to repair previous behaviour - if (!userProfile.getProperty(ARGEO_ENABLED) - .getBoolean()) { - VersionManager versionManager = nodeSession - .getWorkspace().getVersionManager(); - versionManager.checkout(userProfile.getPath()); - userProfile.setProperty(ArgeoNames.ARGEO_ENABLED, - true); - nodeSession.save(); - versionManager.checkin(userProfile.getPath()); - } - } - } catch (Exception e) { - log.error("Cannot process " + path, e); - } - } - } catch (Exception e) { - JcrUtils.discardQuietly(nodeSession); - log.error("Cannot synchronize LDAP and JCR", e); - // throw new ArgeoException("Cannot synchronize LDAP and JCR", e); - } - } - - private String createLdapUser(UserDetails user) { - DirContextAdapter ctx = new DirContextAdapter(); - mapUserToContext(user, ctx); - DistinguishedName dn = usernameMapper.buildDn(user.getUsername()); - ldapTemplate.bind(dn, ctx, null); - return dn.toString(); - } - - /** Called during authentication in order to retrieve user details */ - public UserDetails mapUserFromContext(final DirContextOperations ctx, - final String username, GrantedAuthority[] authorities) { - if (ctx == null) - throw new ArgeoException("No LDAP information for user " + username); - - String ldapUsername = ctx.getStringAttribute(usernameAttribute); - if (!ldapUsername.equals(username)) - throw new ArgeoException("Logged in with username " + username - + " but LDAP user is " + ldapUsername); - - Node userProfile = jcrSecurityModel.sync(nodeSession, username, - SecurityUtils.authoritiesToStringList(authorities)); - // JcrUserDetails.checkAccountStatus(userProfile); - - // password - SortedSet passwordAttributes = ctx - .getAttributeSortedStringSet(passwordAttribute); - String password; - if (passwordAttributes == null || passwordAttributes.size() == 0) { - //throw new ArgeoException("No password found for user " + username); - password = "NULL"; - } else { - byte[] arr = (byte[]) passwordAttributes.first(); - password = new String(arr); - // erase password - Arrays.fill(arr, (byte) 0); - } - - try { - return new JcrUserDetails(userProfile, password, authorities); - } catch (RepositoryException e) { - throw new ArgeoException("Cannot retrieve user details for " - + username, e); - } - } - - /** - * Writes an LDAP context to the JCR user profile. - * - * @return path to user profile - */ - protected synchronized String mapLdapToJcr(DirContextAdapter ctx) { - Session session = nodeSession; - try { - // process - String username = ctx.getStringAttribute(usernameAttribute); - - Node userProfile = jcrSecurityModel.sync(session, username, null); - Map modifications = new HashMap(); - for (String jcrProperty : propertyToAttributes.keySet()) - ldapToJcr(userProfile, jcrProperty, ctx, modifications); - - int modifCount = modifications.size(); - if (modifCount > 0) { - session.getWorkspace().getVersionManager() - .checkout(userProfile.getPath()); - for (String prop : modifications.keySet()) - userProfile.setProperty(prop, modifications.get(prop)); - JcrUtils.updateLastModified(userProfile); - session.save(); - session.getWorkspace().getVersionManager() - .checkin(userProfile.getPath()); - if (log.isDebugEnabled()) - log.debug("Mapped " + modifCount + " LDAP modification" - + (modifCount == 1 ? "" : "s") + " from " - + ctx.getDn() + " to " + userProfile); - } - return userProfile.getPath(); - } catch (Exception e) { - JcrUtils.discardQuietly(session); - throw new ArgeoException("Cannot synchronize JCR and LDAP", e); - } - } - - /** Maps an LDAP property to a JCR property */ - protected void ldapToJcr(Node userProfile, String jcrProperty, - DirContextOperations ctx, Map modifications) { - // TODO do we really need DirContextOperations? - try { - String ldapAttribute; - if (propertyToAttributes.containsKey(jcrProperty)) - ldapAttribute = propertyToAttributes.get(jcrProperty); - else - throw new ArgeoException( - "No LDAP attribute mapped for JCR proprty " - + jcrProperty); - - String value = ctx.getStringAttribute(ldapAttribute); - String jcrValue = userProfile.hasProperty(jcrProperty) ? userProfile - .getProperty(jcrProperty).getString() : null; - if (value != null && jcrValue != null) { - if (!value.equals(jcrValue)) - modifications.put(jcrProperty, value); - } else if (value != null && jcrValue == null) { - modifications.put(jcrProperty, value); - } else if (value == null && jcrValue != null) { - modifications.put(jcrProperty, value); - } - } catch (Exception e) { - throw new ArgeoException("Cannot map JCR property " + jcrProperty - + " from LDAP", e); - } - } - - /* - * JCR to LDAP - */ - - public void mapUserToContext(UserDetails user, final DirContextAdapter ctx) { - if (!(user instanceof JcrUserDetails)) - throw new ArgeoException("Unsupported user details: " - + user.getClass()); - - ctx.setAttributeValues("objectClass", userClasses); - ctx.setAttributeValue(usernameAttribute, user.getUsername()); - ctx.setAttributeValue(passwordAttribute, - encodePassword(user.getPassword())); - - final JcrUserDetails jcrUserDetails = (JcrUserDetails) user; - try { - Node userProfile = nodeSession - .getNode(jcrUserDetails.getHomePath()).getNode( - ARGEO_PROFILE); - for (String jcrProperty : propertyToAttributes.keySet()) { - if (userProfile.hasProperty(jcrProperty)) { - ModificationItem mi = jcrToLdap(jcrProperty, userProfile - .getProperty(jcrProperty).getString()); - if (mi != null) - ctx.setAttribute(mi.getAttribute()); - } - } - if (log.isTraceEnabled()) - log.trace("Mapped " + userProfile + " to " + ctx.getDn()); - } catch (RepositoryException e) { - throw new ArgeoException("Cannot synchronize JCR and LDAP", e); - } - - } - - /** Maps a JCR property to an LDAP property */ - protected ModificationItem jcrToLdap(String jcrProperty, String value) { - // TODO do we really need DirContextOperations? - try { - String ldapAttribute; - if (propertyToAttributes.containsKey(jcrProperty)) - ldapAttribute = propertyToAttributes.get(jcrProperty); - else - return null; - - // fix issue with empty 'sn' in LDAP - if (ldapAttribute.equals("sn") && (value.trim().equals(""))) - return null; - // fix issue with empty 'description' in LDAP - if (ldapAttribute.equals("description") && value.trim().equals("")) - return null; - BasicAttribute attr = new BasicAttribute( - propertyToAttributes.get(jcrProperty), value); - ModificationItem mi = new ModificationItem( - DirContext.REPLACE_ATTRIBUTE, attr); - return mi; - } catch (Exception e) { - throw new ArgeoException("Cannot map JCR property " + jcrProperty - + " from LDAP", e); - } - } - - /* - * UTILITIES - */ - protected String encodePassword(String password) { - if (!password.startsWith("{")) { - byte[] salt = new byte[16]; - random.nextBytes(salt); - return passwordEncoder.encodePassword(password, salt); - } else { - return password; - } - } - - private static Random createRandom() { - try { - return SecureRandom.getInstance("SHA1PRNG"); - } catch (NoSuchAlgorithmException e) { - return new Random(System.currentTimeMillis()); - } - } - - /* - * DEPENDENCY INJECTION - */ - - public void setLdapTemplate(LdapTemplate ldapTemplate) { - this.ldapTemplate = ldapTemplate; - } - - public void setRawLdapTemplate(LdapTemplate rawLdapTemplate) { - // this.rawLdapTemplate = rawLdapTemplate; - } - - public void setRepository(Repository repository) { - this.repository = repository; - } - - public void setUserBase(String userBase) { - this.userBase = userBase; - } - - public void setUsernameAttribute(String usernameAttribute) { - this.usernameAttribute = usernameAttribute; - } - - public void setPropertyToAttributes(Map propertyToAttributes) { - this.propertyToAttributes = propertyToAttributes; - } - - public void setUsernameMapper(LdapUsernameToDnMapper usernameMapper) { - this.usernameMapper = usernameMapper; - } - - public void setPasswordAttribute(String passwordAttribute) { - this.passwordAttribute = passwordAttribute; - } - - public void setUserClasses(String[] userClasses) { - this.userClasses = userClasses; - } - - public void setPasswordEncoder(PasswordEncoder passwordEncoder) { - this.passwordEncoder = passwordEncoder; - } - - public void setJcrSecurityModel(JcrSecurityModel jcrSecurityModel) { - this.jcrSecurityModel = jcrSecurityModel; - } - - /** Listen to LDAP */ - // class LdapUserListener implements ObjectChangeListener, - // NamespaceChangeListener, UnsolicitedNotificationListener { - // - // public void namingExceptionThrown(NamingExceptionEvent evt) { - // evt.getException().printStackTrace(); - // } - // - // public void objectChanged(NamingEvent evt) { - // Binding user = evt.getNewBinding(); - // // TODO find a way not to be called when JCR is the source of the - // // modification - // DirContextAdapter ctx = (DirContextAdapter) ldapTemplate - // .lookup(user.getName()); - // mapLdapToJcr(ctx); - // } - // - // public void objectAdded(NamingEvent evt) { - // Binding user = evt.getNewBinding(); - // DirContextAdapter ctx = (DirContextAdapter) ldapTemplate - // .lookup(user.getName()); - // mapLdapToJcr(ctx); - // } - // - // public void objectRemoved(NamingEvent evt) { - // if (log.isDebugEnabled()) - // log.debug(evt); - // } - // - // public void objectRenamed(NamingEvent evt) { - // if (log.isDebugEnabled()) - // log.debug(evt); - // } - // - // public void notificationReceived(UnsolicitedNotificationEvent evt) { - // UnsolicitedNotification notification = evt.getNotification(); - // NamingException ne = notification.getException(); - // String msg = "LDAP notification " + "ID=" + notification.getID() - // + ", referrals=" + notification.getReferrals(); - // if (ne != null) { - // if (log.isTraceEnabled()) - // log.trace(msg + ", exception= " + ne, ne); - // else - // log.warn(msg + ", exception= " + ne); - // } else if (log.isDebugEnabled()) { - // log.debug("Unsollicited LDAP notification " + msg); - // } - // } - // - // } - - /** Listen to JCR */ - // class JcrProfileListener implements EventListener { - // - // public void onEvent(EventIterator events) { - // try { - // final Map> modifications = new HashMap>(); - // while (events.hasNext()) { - // Event event = events.nextEvent(); - // try { - // if (Event.PROPERTY_CHANGED == event.getType()) { - // Property property = (Property) nodeSession - // .getItem(event.getPath()); - // String propertyName = property.getName(); - // Node userProfile = property.getParent(); - // String username = userProfile.getProperty( - // ARGEO_USER_ID).getString(); - // if (propertyToAttributes.containsKey(propertyName)) { - // Name name = usernameMapper.buildDn(username); - // if (!modifications.containsKey(name)) - // modifications.put(name, - // new ArrayList()); - // String value = property.getString(); - // ModificationItem mi = jcrToLdap(propertyName, - // value); - // if (mi != null) - // modifications.get(name).add(mi); - // } - // } else if (Event.NODE_ADDED == event.getType()) { - // Node userProfile = nodeSession.getNode(event - // .getPath()); - // String username = userProfile.getProperty( - // ARGEO_USER_ID).getString(); - // Name name = usernameMapper.buildDn(username); - // for (String propertyName : propertyToAttributes - // .keySet()) { - // if (!modifications.containsKey(name)) - // modifications.put(name, - // new ArrayList()); - // String value = userProfile.getProperty( - // propertyName).getString(); - // ModificationItem mi = jcrToLdap(propertyName, - // value); - // if (mi != null) - // modifications.get(name).add(mi); - // } - // } - // } catch (RepositoryException e) { - // throw new ArgeoException("Cannot process event " - // + event, e); - // } - // } - // - // for (Name name : modifications.keySet()) { - // List userModifs = modifications.get(name); - // int modifCount = userModifs.size(); - // ldapTemplate.modifyAttributes(name, userModifs - // .toArray(new ModificationItem[modifCount])); - // if (log.isDebugEnabled()) - // log.debug("Mapped " + modifCount + " JCR modification" - // + (modifCount == 1 ? "" : "s") + " to " + name); - // } - // } catch (Exception e) { - // // if (log.isDebugEnabled()) - // // e.printStackTrace(); - // throw new ArgeoException("Cannot process JCR events (" - // + e.getMessage() + ")", e); - // } - // } - // - // } -} diff --git a/org.argeo.security.ldap/src/main/java/org/argeo/security/ldap/jcr/JcrUserDetailsContextMapper.java b/org.argeo.security.ldap/src/main/java/org/argeo/security/ldap/jcr/JcrUserDetailsContextMapper.java deleted file mode 100644 index 87973d9bd..000000000 --- a/org.argeo.security.ldap/src/main/java/org/argeo/security/ldap/jcr/JcrUserDetailsContextMapper.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright (C) 2007-2012 Argeo GmbH - * - * 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.argeo.security.ldap.jcr; - -import java.util.UUID; - -import javax.jcr.Node; -import javax.jcr.Repository; -import javax.jcr.RepositoryException; -import javax.jcr.Session; - -import org.argeo.ArgeoException; -import org.argeo.jcr.ArgeoNames; -import org.argeo.jcr.JcrUtils; -import org.argeo.jcr.UserJcrUtils; -import org.argeo.security.jcr.JcrUserDetails; -import org.springframework.ldap.core.DirContextAdapter; -import org.springframework.ldap.core.DirContextOperations; -import org.springframework.security.GrantedAuthority; -import org.springframework.security.userdetails.UserDetails; -import org.springframework.security.userdetails.ldap.UserDetailsContextMapper; - -/** @deprecated Read only mapping from LDAP to user details */ -@Deprecated -public class JcrUserDetailsContextMapper implements UserDetailsContextMapper, - ArgeoNames { - /** Admin session on the security workspace */ - private Session securitySession; - private Repository repository; - private String securityWorkspace = "security"; - - public void init() { - try { - securitySession = repository.login(securityWorkspace); - } catch (RepositoryException e) { - JcrUtils.logoutQuietly(securitySession); - throw new ArgeoException( - "Cannot initialize LDAP/JCR user details context mapper", e); - } - } - - public void destroy() { - JcrUtils.logoutQuietly(securitySession); - } - - /** Called during authentication in order to retrieve user details */ - public UserDetails mapUserFromContext(final DirContextOperations ctx, - final String username, GrantedAuthority[] authorities) { - if (ctx == null) - throw new ArgeoException("No LDAP information for user " + username); - Node userHome = UserJcrUtils.getUserHome(securitySession, username); - if (userHome == null) - throw new ArgeoException("No JCR information for user " + username); - - // password - // SortedSet passwordAttributes = ctx - // .getAttributeSortedStringSet(passwordAttribute); - // String password; - // if (passwordAttributes == null || passwordAttributes.size() == 0) { - // throw new ArgeoException("No password found for user " + username); - // } else { - // byte[] arr = (byte[]) passwordAttributes.first(); - // password = new String(arr); - // // erase password - // Arrays.fill(arr, (byte) 0); - // } - - try { - // we don't have access to password, so let's not pretend - String password = UUID.randomUUID().toString(); - return new JcrUserDetails(userHome.getNode(ARGEO_PROFILE), - password, authorities); - } catch (RepositoryException e) { - throw new ArgeoException("Cannot retrieve user details for " - + username, e); - } - } - - public void mapUserToContext(UserDetails user, final DirContextAdapter ctx) { - throw new UnsupportedOperationException("LDAP access is read-only"); - } - -} diff --git a/org.argeo.security.ldap/src/org/argeo/security/ldap/ArgeoLdapShaPasswordEncoder.java b/org.argeo.security.ldap/src/org/argeo/security/ldap/ArgeoLdapShaPasswordEncoder.java new file mode 100644 index 000000000..ea22ef350 --- /dev/null +++ b/org.argeo.security.ldap/src/org/argeo/security/ldap/ArgeoLdapShaPasswordEncoder.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2007-2012 Argeo GmbH + * + * 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.argeo.security.ldap; + +import org.springframework.security.providers.ldap.authenticator.LdapShaPasswordEncoder; + +/** + * {@link LdapShaPasswordEncoder} allowing to configure the usage of salt (APache + * Directory Server 1.0 does not support bind with SSHA) + */ +public class ArgeoLdapShaPasswordEncoder extends LdapShaPasswordEncoder { + private Boolean useSalt = true; + + @Override + public String encodePassword(String rawPass, Object salt) { + return super.encodePassword(rawPass, useSalt ? salt : null); + } + + public void setUseSalt(Boolean useSalt) { + this.useSalt = useSalt; + } + +} diff --git a/org.argeo.security.ldap/src/org/argeo/security/ldap/ArgeoLdapUserDetailsManager.java b/org.argeo.security.ldap/src/org/argeo/security/ldap/ArgeoLdapUserDetailsManager.java new file mode 100644 index 000000000..0c7368f1f --- /dev/null +++ b/org.argeo.security.ldap/src/org/argeo/security/ldap/ArgeoLdapUserDetailsManager.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2007-2012 Argeo GmbH + * + * 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.argeo.security.ldap; + +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Random; +import java.util.Set; +import java.util.TreeSet; + +import org.argeo.ArgeoException; +import org.argeo.security.UserAdminService; +import org.springframework.ldap.core.ContextSource; +import org.springframework.security.Authentication; +import org.springframework.security.GrantedAuthority; +import org.springframework.security.context.SecurityContextHolder; +import org.springframework.security.providers.encoding.PasswordEncoder; +import org.springframework.security.userdetails.UserDetails; +import org.springframework.security.userdetails.ldap.LdapUserDetailsManager; + +/** Extends {@link LdapUserDetailsManager} by adding password encoding support. */ +public class ArgeoLdapUserDetailsManager extends LdapUserDetailsManager + implements UserAdminService { + private String superUsername = "root"; + private ArgeoUserAdminDaoLdap userAdminDao; + private PasswordEncoder passwordEncoder; + private final Random random; + + public ArgeoLdapUserDetailsManager(ContextSource contextSource) { + super(contextSource); + this.random = createRandom(); + } + + private static Random createRandom() { + try { + return SecureRandom.getInstance("SHA1PRNG"); + } catch (NoSuchAlgorithmException e) { + return new Random(System.currentTimeMillis()); + } + } + + @Override + public void changePassword(String oldPassword, String newPassword) { + Authentication authentication = SecurityContextHolder.getContext() + .getAuthentication(); + if (authentication == null) + throw new ArgeoException( + "Cannot change password without authentication"); + String username = authentication.getName(); + UserDetails userDetails = loadUserByUsername(username); + String currentPassword = userDetails.getPassword(); + if (currentPassword == null) + throw new ArgeoException("Cannot access current password"); + if (!passwordEncoder + .isPasswordValid(currentPassword, oldPassword, null)) + throw new ArgeoException("Old password invalid"); + // Spring Security LDAP 2.0 is buggy when used with OpenLDAP and called + // with oldPassword argument + super.changePassword(null, encodePassword(newPassword)); + } + + public void newRole(String role) { + userAdminDao.createRole(role, superUsername); + } + + public void synchronize() { + for (String username : userAdminDao.listUsers()) + loadUserByUsername(username); + // TODO: find a way to remove from JCR + } + + public void deleteRole(String role) { + userAdminDao.deleteRole(role); + } + + public Set listUsers() { + return userAdminDao.listUsers(); + } + + public Set listUsersInRole(String role) { + Set lst = new TreeSet( + userAdminDao.listUsersInRole(role)); + Iterator it = lst.iterator(); + while (it.hasNext()) { + if (it.next().equals(superUsername)) { + it.remove(); + break; + } + } + return lst; + } + + public List listUserRoles(String username) { + UserDetails userDetails = loadUserByUsername(username); + List roles = new ArrayList(); + for (GrantedAuthority ga : userDetails.getAuthorities()) { + roles.add(ga.getAuthority()); + } + return Collections.unmodifiableList(roles); + } + + public Set listEditableRoles() { + return userAdminDao.listEditableRoles(); + } + + protected String encodePassword(String password) { + if (!password.startsWith("{")) { + byte[] salt = new byte[16]; + random.nextBytes(salt); + return passwordEncoder.encodePassword(password, salt); + } else { + return password; + } + } + + public void setPasswordEncoder(PasswordEncoder passwordEncoder) { + this.passwordEncoder = passwordEncoder; + } + + public void setSuperUsername(String superUsername) { + this.superUsername = superUsername; + } + + public void setUserAdminDao(ArgeoUserAdminDaoLdap userAdminDao) { + this.userAdminDao = userAdminDao; + } + +} diff --git a/org.argeo.security.ldap/src/org/argeo/security/ldap/ArgeoUserAdminDaoLdap.java b/org.argeo.security.ldap/src/org/argeo/security/ldap/ArgeoUserAdminDaoLdap.java new file mode 100644 index 000000000..37d2a06a0 --- /dev/null +++ b/org.argeo.security.ldap/src/org/argeo/security/ldap/ArgeoUserAdminDaoLdap.java @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2007-2012 Argeo GmbH + * + * 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.argeo.security.ldap; + +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +import javax.naming.Name; +import javax.naming.NamingException; +import javax.naming.directory.DirContext; + +import org.springframework.ldap.core.ContextExecutor; +import org.springframework.ldap.core.ContextMapper; +import org.springframework.ldap.core.DirContextAdapter; +import org.springframework.ldap.core.DistinguishedName; +import org.springframework.ldap.core.LdapTemplate; +import org.springframework.ldap.core.support.BaseLdapPathContextSource; +import org.springframework.security.ldap.LdapUsernameToDnMapper; +import org.springframework.security.ldap.LdapUtils; + +/** + * Wraps low-level LDAP operation on user and roles, used by + * {@link ArgeoLdapUserDetailsManager} + */ +public class ArgeoUserAdminDaoLdap { + private String userBase; + private String usernameAttribute; + private String groupBase; + private String[] groupClasses; + + private String groupRoleAttribute; + private String groupMemberAttribute; + private String defaultRole; + private String rolePrefix; + + private final LdapTemplate ldapTemplate; + private LdapUsernameToDnMapper usernameMapper; + + /** + * Standard constructor, using the LDAP context source shared with Spring + * Security components. + */ + public ArgeoUserAdminDaoLdap(BaseLdapPathContextSource contextSource) { + this.ldapTemplate = new LdapTemplate(contextSource); + } + + @SuppressWarnings("unchecked") + public synchronized Set listUsers() { + List usernames = (List) ldapTemplate.listBindings( + new DistinguishedName(userBase), new ContextMapper() { + public Object mapFromContext(Object ctxArg) { + DirContextAdapter ctx = (DirContextAdapter) ctxArg; + return ctx.getStringAttribute(usernameAttribute); + } + }); + + return Collections + .unmodifiableSortedSet(new TreeSet(usernames)); + } + + @SuppressWarnings("unchecked") + public Set listEditableRoles() { + return Collections.unmodifiableSortedSet(new TreeSet( + ldapTemplate.listBindings(groupBase, new ContextMapper() { + public Object mapFromContext(Object ctxArg) { + String groupName = ((DirContextAdapter) ctxArg) + .getStringAttribute(groupRoleAttribute); + String roleName = convertGroupToRole(groupName); + return roleName; + } + }))); + } + + @SuppressWarnings("unchecked") + public Set listUsersInRole(String role) { + return (Set) ldapTemplate.lookup( + buildGroupDn(convertRoleToGroup(role)), new ContextMapper() { + public Object mapFromContext(Object ctxArg) { + DirContextAdapter ctx = (DirContextAdapter) ctxArg; + String[] userDns = ctx + .getStringAttributes(groupMemberAttribute); + TreeSet set = new TreeSet(); + for (String userDn : userDns) { + DistinguishedName dn = new DistinguishedName(userDn); + String username = dn.getValue(usernameAttribute); + set.add(username); + } + return Collections.unmodifiableSortedSet(set); + } + }); + } + + public void createRole(String role, final String superuserName) { + String group = convertRoleToGroup(role); + DistinguishedName superuserDn = (DistinguishedName) ldapTemplate + .executeReadWrite(new ContextExecutor() { + public Object executeWithContext(DirContext ctx) + throws NamingException { + return LdapUtils.getFullDn( + usernameMapper.buildDn(superuserName), ctx); + } + }); + + Name groupDn = buildGroupDn(group); + DirContextAdapter context = new DirContextAdapter(); + context.setAttributeValues("objectClass", groupClasses); + context.setAttributeValue("cn", group); + // Add superuser because cannot create empty group + context.setAttributeValue(groupMemberAttribute, superuserDn.toString()); + ldapTemplate.bind(groupDn, context, null); + } + + public void deleteRole(String role) { + String group = convertRoleToGroup(role); + Name dn = buildGroupDn(group); + ldapTemplate.unbind(dn); + } + + /** Maps a role (ROLE_XXX) to the related LDAP group (xxx) */ + protected String convertRoleToGroup(String role) { + String group = role; + if (group.startsWith(rolePrefix)) { + group = group.substring(rolePrefix.length()); + group = group.toLowerCase(); + } + return group; + } + + /** Maps anLDAP group (xxx) to the related role (ROLE_XXX) */ + protected String convertGroupToRole(String groupName) { + groupName = groupName.toUpperCase(); + + return rolePrefix + groupName; + } + + protected Name buildGroupDn(String name) { + return new DistinguishedName(groupRoleAttribute + "=" + name + "," + + groupBase); + } + + public void setUserBase(String userBase) { + this.userBase = userBase; + } + + public void setUsernameAttribute(String usernameAttribute) { + this.usernameAttribute = usernameAttribute; + } + + public void setGroupBase(String groupBase) { + this.groupBase = groupBase; + } + + public void setGroupRoleAttribute(String groupRoleAttributeName) { + this.groupRoleAttribute = groupRoleAttributeName; + } + + public void setGroupMemberAttribute(String groupMemberAttributeName) { + this.groupMemberAttribute = groupMemberAttributeName; + } + + public void setDefaultRole(String defaultRole) { + this.defaultRole = defaultRole; + } + + public void setRolePrefix(String rolePrefix) { + this.rolePrefix = rolePrefix; + } + + public void setUsernameMapper(LdapUsernameToDnMapper usernameMapper) { + this.usernameMapper = usernameMapper; + } + + public String getDefaultRole() { + return defaultRole; + } + + public void setGroupClasses(String[] groupClasses) { + this.groupClasses = groupClasses; + } +} diff --git a/org.argeo.security.ldap/src/org/argeo/security/ldap/jcr/JcrLdapSynchronizer.java b/org.argeo.security.ldap/src/org/argeo/security/ldap/jcr/JcrLdapSynchronizer.java new file mode 100644 index 000000000..3e9e2cbfa --- /dev/null +++ b/org.argeo.security.ldap/src/org/argeo/security/ldap/jcr/JcrLdapSynchronizer.java @@ -0,0 +1,619 @@ +/* + * Copyright (C) 2007-2012 Argeo GmbH + * + * 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.argeo.security.ldap.jcr; + +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.SortedSet; +import java.util.UUID; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.query.Query; +import javax.jcr.version.VersionManager; +import javax.naming.Name; +import javax.naming.directory.BasicAttribute; +import javax.naming.directory.DirContext; +import javax.naming.directory.ModificationItem; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.argeo.ArgeoException; +import org.argeo.jcr.ArgeoNames; +import org.argeo.jcr.ArgeoTypes; +import org.argeo.jcr.JcrUtils; +import org.argeo.security.SecurityUtils; +import org.argeo.security.jcr.JcrSecurityModel; +import org.argeo.security.jcr.JcrUserDetails; +import org.argeo.security.jcr.SimpleJcrSecurityModel; +import org.springframework.ldap.core.ContextMapper; +import org.springframework.ldap.core.DirContextAdapter; +import org.springframework.ldap.core.DirContextOperations; +import org.springframework.ldap.core.DistinguishedName; +import org.springframework.ldap.core.LdapTemplate; +import org.springframework.security.GrantedAuthority; +import org.springframework.security.ldap.LdapUsernameToDnMapper; +import org.springframework.security.providers.encoding.PasswordEncoder; +import org.springframework.security.userdetails.UserDetails; +import org.springframework.security.userdetails.ldap.UserDetailsContextMapper; + +/** Makes sure that LDAP and JCR are in line. */ +public class JcrLdapSynchronizer implements UserDetailsContextMapper, + ArgeoNames { + private final static Log log = LogFactory.getLog(JcrLdapSynchronizer.class); + + // LDAP + private LdapTemplate ldapTemplate; + /** + * LDAP template whose context source has an object factory set to null. see + * this + */ + // private LdapTemplate rawLdapTemplate; + + private String userBase; + private String usernameAttribute; + private String passwordAttribute; + private String[] userClasses; + // private String defaultUserRole ="ROLE_USER"; + + // private NamingListener ldapUserListener; + // private SearchControls subTreeSearchControls; + private LdapUsernameToDnMapper usernameMapper; + + private PasswordEncoder passwordEncoder; + private final Random random; + + // JCR + /** Admin session on the main workspace */ + private Session nodeSession; + private Repository repository; + + // private JcrProfileListener jcrProfileListener; + private JcrSecurityModel jcrSecurityModel = new SimpleJcrSecurityModel(); + + // Mapping + private Map propertyToAttributes = new HashMap(); + + public JcrLdapSynchronizer() { + random = createRandom(); + } + + public void init() { + try { + nodeSession = repository.login(); + + // TODO put this in a different thread, and poll the LDAP server + // until it is up + try { + synchronize(); + + // LDAP + // subTreeSearchControls = new SearchControls(); + // subTreeSearchControls + // .setSearchScope(SearchControls.SUBTREE_SCOPE); + // LDAP listener + // ldapUserListener = new LdapUserListener(); + // rawLdapTemplate.executeReadOnly(new ContextExecutor() { + // public Object executeWithContext(DirContext ctx) + // throws NamingException { + // EventDirContext ectx = (EventDirContext) ctx.lookup(""); + // ectx.addNamingListener(userBase, "(" + // + usernameAttribute + "=*)", + // subTreeSearchControls, ldapUserListener); + // return null; + // } + // }); + } catch (Exception e) { + log.error("Could not synchronize and listen to LDAP," + + " probably because the LDAP server is not available." + + " Restart the system as soon as possible.", e); + } + + // JCR + // String[] nodeTypes = { ArgeoTypes.ARGEO_USER_PROFILE }; + // jcrProfileListener = new JcrProfileListener(); + // noLocal is used so that we are not notified when we modify JCR + // from LDAP + // nodeSession + // .getWorkspace() + // .getObservationManager() + // .addEventListener(jcrProfileListener, + // Event.PROPERTY_CHANGED | Event.NODE_ADDED, "/", + // true, null, nodeTypes, true); + } catch (Exception e) { + JcrUtils.logoutQuietly(nodeSession); + throw new ArgeoException("Cannot initialize LDAP/JCR synchronizer", + e); + } + } + + public void destroy() { + // JcrUtils.removeListenerQuietly(nodeSession, jcrProfileListener); + JcrUtils.logoutQuietly(nodeSession); + // try { + // rawLdapTemplate.executeReadOnly(new ContextExecutor() { + // public Object executeWithContext(DirContext ctx) + // throws NamingException { + // EventDirContext ectx = (EventDirContext) ctx.lookup(""); + // ectx.removeNamingListener(ldapUserListener); + // return null; + // } + // }); + // } catch (Exception e) { + // // silent (LDAP server may have been shutdown already) + // if (log.isTraceEnabled()) + // log.trace("Cannot remove LDAP listener", e); + // } + } + + /* + * LDAP TO JCR + */ + /** Full synchronization between LDAP and JCR. LDAP has priority. */ + protected void synchronize() { + try { + Name userBaseName = new DistinguishedName(userBase); + // TODO subtree search? + @SuppressWarnings("unchecked") + List userPaths = (List) ldapTemplate.listBindings( + userBaseName, new ContextMapper() { + public Object mapFromContext(Object ctxObj) { + try { + return mapLdapToJcr((DirContextAdapter) ctxObj); + } catch (Exception e) { + // do not break process because of error + log.error( + "Could not LDAP->JCR synchronize user " + + ctxObj, e); + return null; + } + } + }); + + // create accounts which are not in LDAP + Query query = nodeSession + .getWorkspace() + .getQueryManager() + .createQuery( + "select * from [" + ArgeoTypes.ARGEO_USER_PROFILE + + "]", Query.JCR_SQL2); + NodeIterator it = query.execute().getNodes(); + while (it.hasNext()) { + Node userProfile = it.nextNode(); + String path = userProfile.getPath(); + try { + if (!userPaths.contains(path)) { + String username = userProfile + .getProperty(ARGEO_USER_ID).getString(); + // GrantedAuthority[] authorities = {new + // GrantedAuthorityImpl(defaultUserRole)}; + GrantedAuthority[] authorities = {}; + JcrUserDetails userDetails = new JcrUserDetails( + userProfile, username, authorities); + String dn = createLdapUser(userDetails); + log.warn("Created ldap entry '" + dn + "' for user '" + + username + "'"); + + // if(!userProfile.getProperty(ARGEO_ENABLED).getBoolean()){ + // continue profiles; + // } + // + // log.warn("Path " + // + path + // + " not found in LDAP, disabling user " + // + userProfile.getProperty(ArgeoNames.ARGEO_USER_ID) + // .getString()); + + // Temporary hack to repair previous behaviour + if (!userProfile.getProperty(ARGEO_ENABLED) + .getBoolean()) { + VersionManager versionManager = nodeSession + .getWorkspace().getVersionManager(); + versionManager.checkout(userProfile.getPath()); + userProfile.setProperty(ArgeoNames.ARGEO_ENABLED, + true); + nodeSession.save(); + versionManager.checkin(userProfile.getPath()); + } + } + } catch (Exception e) { + log.error("Cannot process " + path, e); + } + } + } catch (Exception e) { + JcrUtils.discardQuietly(nodeSession); + log.error("Cannot synchronize LDAP and JCR", e); + // throw new ArgeoException("Cannot synchronize LDAP and JCR", e); + } + } + + private String createLdapUser(UserDetails user) { + DirContextAdapter ctx = new DirContextAdapter(); + mapUserToContext(user, ctx); + DistinguishedName dn = usernameMapper.buildDn(user.getUsername()); + ldapTemplate.bind(dn, ctx, null); + return dn.toString(); + } + + /** Called during authentication in order to retrieve user details */ + public UserDetails mapUserFromContext(final DirContextOperations ctx, + final String username, GrantedAuthority[] authorities) { + if (ctx == null) + throw new ArgeoException("No LDAP information for user " + username); + + String ldapUsername = ctx.getStringAttribute(usernameAttribute); + if (!ldapUsername.equals(username)) + throw new ArgeoException("Logged in with username " + username + + " but LDAP user is " + ldapUsername); + + Node userProfile = jcrSecurityModel.sync(nodeSession, username, + SecurityUtils.authoritiesToStringList(authorities)); + // JcrUserDetails.checkAccountStatus(userProfile); + + // password + SortedSet passwordAttributes = ctx + .getAttributeSortedStringSet(passwordAttribute); + String password; + if (passwordAttributes == null || passwordAttributes.size() == 0) { + //throw new ArgeoException("No password found for user " + username); + password = "NULL"; + } else { + byte[] arr = (byte[]) passwordAttributes.first(); + password = new String(arr); + // erase password + Arrays.fill(arr, (byte) 0); + } + + try { + return new JcrUserDetails(userProfile, password, authorities); + } catch (RepositoryException e) { + throw new ArgeoException("Cannot retrieve user details for " + + username, e); + } + } + + /** + * Writes an LDAP context to the JCR user profile. + * + * @return path to user profile + */ + protected synchronized String mapLdapToJcr(DirContextAdapter ctx) { + Session session = nodeSession; + try { + // process + String username = ctx.getStringAttribute(usernameAttribute); + + Node userProfile = jcrSecurityModel.sync(session, username, null); + Map modifications = new HashMap(); + for (String jcrProperty : propertyToAttributes.keySet()) + ldapToJcr(userProfile, jcrProperty, ctx, modifications); + + int modifCount = modifications.size(); + if (modifCount > 0) { + session.getWorkspace().getVersionManager() + .checkout(userProfile.getPath()); + for (String prop : modifications.keySet()) + userProfile.setProperty(prop, modifications.get(prop)); + JcrUtils.updateLastModified(userProfile); + session.save(); + session.getWorkspace().getVersionManager() + .checkin(userProfile.getPath()); + if (log.isDebugEnabled()) + log.debug("Mapped " + modifCount + " LDAP modification" + + (modifCount == 1 ? "" : "s") + " from " + + ctx.getDn() + " to " + userProfile); + } + return userProfile.getPath(); + } catch (Exception e) { + JcrUtils.discardQuietly(session); + throw new ArgeoException("Cannot synchronize JCR and LDAP", e); + } + } + + /** Maps an LDAP property to a JCR property */ + protected void ldapToJcr(Node userProfile, String jcrProperty, + DirContextOperations ctx, Map modifications) { + // TODO do we really need DirContextOperations? + try { + String ldapAttribute; + if (propertyToAttributes.containsKey(jcrProperty)) + ldapAttribute = propertyToAttributes.get(jcrProperty); + else + throw new ArgeoException( + "No LDAP attribute mapped for JCR proprty " + + jcrProperty); + + String value = ctx.getStringAttribute(ldapAttribute); + String jcrValue = userProfile.hasProperty(jcrProperty) ? userProfile + .getProperty(jcrProperty).getString() : null; + if (value != null && jcrValue != null) { + if (!value.equals(jcrValue)) + modifications.put(jcrProperty, value); + } else if (value != null && jcrValue == null) { + modifications.put(jcrProperty, value); + } else if (value == null && jcrValue != null) { + modifications.put(jcrProperty, value); + } + } catch (Exception e) { + throw new ArgeoException("Cannot map JCR property " + jcrProperty + + " from LDAP", e); + } + } + + /* + * JCR to LDAP + */ + + public void mapUserToContext(UserDetails user, final DirContextAdapter ctx) { + if (!(user instanceof JcrUserDetails)) + throw new ArgeoException("Unsupported user details: " + + user.getClass()); + + ctx.setAttributeValues("objectClass", userClasses); + ctx.setAttributeValue(usernameAttribute, user.getUsername()); + ctx.setAttributeValue(passwordAttribute, + encodePassword(user.getPassword())); + + final JcrUserDetails jcrUserDetails = (JcrUserDetails) user; + try { + Node userProfile = nodeSession + .getNode(jcrUserDetails.getHomePath()).getNode( + ARGEO_PROFILE); + for (String jcrProperty : propertyToAttributes.keySet()) { + if (userProfile.hasProperty(jcrProperty)) { + ModificationItem mi = jcrToLdap(jcrProperty, userProfile + .getProperty(jcrProperty).getString()); + if (mi != null) + ctx.setAttribute(mi.getAttribute()); + } + } + if (log.isTraceEnabled()) + log.trace("Mapped " + userProfile + " to " + ctx.getDn()); + } catch (RepositoryException e) { + throw new ArgeoException("Cannot synchronize JCR and LDAP", e); + } + + } + + /** Maps a JCR property to an LDAP property */ + protected ModificationItem jcrToLdap(String jcrProperty, String value) { + // TODO do we really need DirContextOperations? + try { + String ldapAttribute; + if (propertyToAttributes.containsKey(jcrProperty)) + ldapAttribute = propertyToAttributes.get(jcrProperty); + else + return null; + + // fix issue with empty 'sn' in LDAP + if (ldapAttribute.equals("sn") && (value.trim().equals(""))) + return null; + // fix issue with empty 'description' in LDAP + if (ldapAttribute.equals("description") && value.trim().equals("")) + return null; + BasicAttribute attr = new BasicAttribute( + propertyToAttributes.get(jcrProperty), value); + ModificationItem mi = new ModificationItem( + DirContext.REPLACE_ATTRIBUTE, attr); + return mi; + } catch (Exception e) { + throw new ArgeoException("Cannot map JCR property " + jcrProperty + + " from LDAP", e); + } + } + + /* + * UTILITIES + */ + protected String encodePassword(String password) { + if (!password.startsWith("{")) { + byte[] salt = new byte[16]; + random.nextBytes(salt); + return passwordEncoder.encodePassword(password, salt); + } else { + return password; + } + } + + private static Random createRandom() { + try { + return SecureRandom.getInstance("SHA1PRNG"); + } catch (NoSuchAlgorithmException e) { + return new Random(System.currentTimeMillis()); + } + } + + /* + * DEPENDENCY INJECTION + */ + + public void setLdapTemplate(LdapTemplate ldapTemplate) { + this.ldapTemplate = ldapTemplate; + } + + public void setRawLdapTemplate(LdapTemplate rawLdapTemplate) { + // this.rawLdapTemplate = rawLdapTemplate; + } + + public void setRepository(Repository repository) { + this.repository = repository; + } + + public void setUserBase(String userBase) { + this.userBase = userBase; + } + + public void setUsernameAttribute(String usernameAttribute) { + this.usernameAttribute = usernameAttribute; + } + + public void setPropertyToAttributes(Map propertyToAttributes) { + this.propertyToAttributes = propertyToAttributes; + } + + public void setUsernameMapper(LdapUsernameToDnMapper usernameMapper) { + this.usernameMapper = usernameMapper; + } + + public void setPasswordAttribute(String passwordAttribute) { + this.passwordAttribute = passwordAttribute; + } + + public void setUserClasses(String[] userClasses) { + this.userClasses = userClasses; + } + + public void setPasswordEncoder(PasswordEncoder passwordEncoder) { + this.passwordEncoder = passwordEncoder; + } + + public void setJcrSecurityModel(JcrSecurityModel jcrSecurityModel) { + this.jcrSecurityModel = jcrSecurityModel; + } + + /** Listen to LDAP */ + // class LdapUserListener implements ObjectChangeListener, + // NamespaceChangeListener, UnsolicitedNotificationListener { + // + // public void namingExceptionThrown(NamingExceptionEvent evt) { + // evt.getException().printStackTrace(); + // } + // + // public void objectChanged(NamingEvent evt) { + // Binding user = evt.getNewBinding(); + // // TODO find a way not to be called when JCR is the source of the + // // modification + // DirContextAdapter ctx = (DirContextAdapter) ldapTemplate + // .lookup(user.getName()); + // mapLdapToJcr(ctx); + // } + // + // public void objectAdded(NamingEvent evt) { + // Binding user = evt.getNewBinding(); + // DirContextAdapter ctx = (DirContextAdapter) ldapTemplate + // .lookup(user.getName()); + // mapLdapToJcr(ctx); + // } + // + // public void objectRemoved(NamingEvent evt) { + // if (log.isDebugEnabled()) + // log.debug(evt); + // } + // + // public void objectRenamed(NamingEvent evt) { + // if (log.isDebugEnabled()) + // log.debug(evt); + // } + // + // public void notificationReceived(UnsolicitedNotificationEvent evt) { + // UnsolicitedNotification notification = evt.getNotification(); + // NamingException ne = notification.getException(); + // String msg = "LDAP notification " + "ID=" + notification.getID() + // + ", referrals=" + notification.getReferrals(); + // if (ne != null) { + // if (log.isTraceEnabled()) + // log.trace(msg + ", exception= " + ne, ne); + // else + // log.warn(msg + ", exception= " + ne); + // } else if (log.isDebugEnabled()) { + // log.debug("Unsollicited LDAP notification " + msg); + // } + // } + // + // } + + /** Listen to JCR */ + // class JcrProfileListener implements EventListener { + // + // public void onEvent(EventIterator events) { + // try { + // final Map> modifications = new HashMap>(); + // while (events.hasNext()) { + // Event event = events.nextEvent(); + // try { + // if (Event.PROPERTY_CHANGED == event.getType()) { + // Property property = (Property) nodeSession + // .getItem(event.getPath()); + // String propertyName = property.getName(); + // Node userProfile = property.getParent(); + // String username = userProfile.getProperty( + // ARGEO_USER_ID).getString(); + // if (propertyToAttributes.containsKey(propertyName)) { + // Name name = usernameMapper.buildDn(username); + // if (!modifications.containsKey(name)) + // modifications.put(name, + // new ArrayList()); + // String value = property.getString(); + // ModificationItem mi = jcrToLdap(propertyName, + // value); + // if (mi != null) + // modifications.get(name).add(mi); + // } + // } else if (Event.NODE_ADDED == event.getType()) { + // Node userProfile = nodeSession.getNode(event + // .getPath()); + // String username = userProfile.getProperty( + // ARGEO_USER_ID).getString(); + // Name name = usernameMapper.buildDn(username); + // for (String propertyName : propertyToAttributes + // .keySet()) { + // if (!modifications.containsKey(name)) + // modifications.put(name, + // new ArrayList()); + // String value = userProfile.getProperty( + // propertyName).getString(); + // ModificationItem mi = jcrToLdap(propertyName, + // value); + // if (mi != null) + // modifications.get(name).add(mi); + // } + // } + // } catch (RepositoryException e) { + // throw new ArgeoException("Cannot process event " + // + event, e); + // } + // } + // + // for (Name name : modifications.keySet()) { + // List userModifs = modifications.get(name); + // int modifCount = userModifs.size(); + // ldapTemplate.modifyAttributes(name, userModifs + // .toArray(new ModificationItem[modifCount])); + // if (log.isDebugEnabled()) + // log.debug("Mapped " + modifCount + " JCR modification" + // + (modifCount == 1 ? "" : "s") + " to " + name); + // } + // } catch (Exception e) { + // // if (log.isDebugEnabled()) + // // e.printStackTrace(); + // throw new ArgeoException("Cannot process JCR events (" + // + e.getMessage() + ")", e); + // } + // } + // + // } +} diff --git a/org.argeo.security.ldap/src/org/argeo/security/ldap/jcr/JcrUserDetailsContextMapper.java b/org.argeo.security.ldap/src/org/argeo/security/ldap/jcr/JcrUserDetailsContextMapper.java new file mode 100644 index 000000000..87973d9bd --- /dev/null +++ b/org.argeo.security.ldap/src/org/argeo/security/ldap/jcr/JcrUserDetailsContextMapper.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2007-2012 Argeo GmbH + * + * 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.argeo.security.ldap.jcr; + +import java.util.UUID; + +import javax.jcr.Node; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.argeo.ArgeoException; +import org.argeo.jcr.ArgeoNames; +import org.argeo.jcr.JcrUtils; +import org.argeo.jcr.UserJcrUtils; +import org.argeo.security.jcr.JcrUserDetails; +import org.springframework.ldap.core.DirContextAdapter; +import org.springframework.ldap.core.DirContextOperations; +import org.springframework.security.GrantedAuthority; +import org.springframework.security.userdetails.UserDetails; +import org.springframework.security.userdetails.ldap.UserDetailsContextMapper; + +/** @deprecated Read only mapping from LDAP to user details */ +@Deprecated +public class JcrUserDetailsContextMapper implements UserDetailsContextMapper, + ArgeoNames { + /** Admin session on the security workspace */ + private Session securitySession; + private Repository repository; + private String securityWorkspace = "security"; + + public void init() { + try { + securitySession = repository.login(securityWorkspace); + } catch (RepositoryException e) { + JcrUtils.logoutQuietly(securitySession); + throw new ArgeoException( + "Cannot initialize LDAP/JCR user details context mapper", e); + } + } + + public void destroy() { + JcrUtils.logoutQuietly(securitySession); + } + + /** Called during authentication in order to retrieve user details */ + public UserDetails mapUserFromContext(final DirContextOperations ctx, + final String username, GrantedAuthority[] authorities) { + if (ctx == null) + throw new ArgeoException("No LDAP information for user " + username); + Node userHome = UserJcrUtils.getUserHome(securitySession, username); + if (userHome == null) + throw new ArgeoException("No JCR information for user " + username); + + // password + // SortedSet passwordAttributes = ctx + // .getAttributeSortedStringSet(passwordAttribute); + // String password; + // if (passwordAttributes == null || passwordAttributes.size() == 0) { + // throw new ArgeoException("No password found for user " + username); + // } else { + // byte[] arr = (byte[]) passwordAttributes.first(); + // password = new String(arr); + // // erase password + // Arrays.fill(arr, (byte) 0); + // } + + try { + // we don't have access to password, so let's not pretend + String password = UUID.randomUUID().toString(); + return new JcrUserDetails(userHome.getNode(ARGEO_PROFILE), + password, authorities); + } catch (RepositoryException e) { + throw new ArgeoException("Cannot retrieve user details for " + + username, e); + } + } + + public void mapUserToContext(UserDetails user, final DirContextAdapter ctx) { + throw new UnsupportedOperationException("LDAP access is read-only"); + } + +} diff --git a/org.argeo.security.mvc/.classpath b/org.argeo.security.mvc/.classpath index ff41fbb4d..d2953a684 100644 --- a/org.argeo.security.mvc/.classpath +++ b/org.argeo.security.mvc/.classpath @@ -1,7 +1,9 @@ - - - - + + + + diff --git a/org.argeo.security.mvc/bnd.bnd b/org.argeo.security.mvc/bnd.bnd new file mode 100644 index 000000000..630bdd797 --- /dev/null +++ b/org.argeo.security.mvc/bnd.bnd @@ -0,0 +1 @@ +Import-Package: javax.servlet,* diff --git a/org.argeo.security.mvc/build.properties b/org.argeo.security.mvc/build.properties index a740a346d..1414da572 100644 --- a/org.argeo.security.mvc/build.properties +++ b/org.argeo.security.mvc/build.properties @@ -1,2 +1,2 @@ additional.bundles = org.springframework.beans -source.. = src/main/java/ +source.. = src/ diff --git a/org.argeo.security.mvc/pom.xml b/org.argeo.security.mvc/pom.xml index dcee8b322..4fe9ca336 100644 --- a/org.argeo.security.mvc/pom.xml +++ b/org.argeo.security.mvc/pom.xml @@ -1,4 +1,6 @@ - + + 4.0.0 org.argeo.commons @@ -8,35 +10,6 @@ org.argeo.security.mvc Commons Security MVC - - - - org.apache.maven.plugins - maven-compiler-plugin - - - org.apache.maven.plugins - maven-source-plugin - - - org.apache.maven.plugins - maven-jar-plugin - - - org.apache.felix - maven-bundle-plugin - - - - - org.argeo.security.mvc.* - - *,javax.servlet - - - - - diff --git a/org.argeo.security.mvc/src/main/java/org/argeo/security/mvc/ArgeoRememberMeServices.java b/org.argeo.security.mvc/src/main/java/org/argeo/security/mvc/ArgeoRememberMeServices.java deleted file mode 100644 index fde9f3034..000000000 --- a/org.argeo.security.mvc/src/main/java/org/argeo/security/mvc/ArgeoRememberMeServices.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (C) 2007-2012 Mathieu Baudier - * - * 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.argeo.security.mvc; - -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.springframework.security.ui.rememberme.TokenBasedRememberMeServices; - -public class ArgeoRememberMeServices extends TokenBasedRememberMeServices { - public final static String DEFAULT_COOKIE_NAME = "ARGEO_SECURITY"; - - public ArgeoRememberMeServices() { - setCookieName(DEFAULT_COOKIE_NAME); - } - - /** - * Sets a "cancel cookie" (with maxAge = 0) on the response to disable - * persistent logins. - * - * @param request - * @param response - */ - protected void cancelCookie(HttpServletRequest request, - HttpServletResponse response) { - Cookie cookie = new Cookie(getCookieName(), null); - cookie.setMaxAge(0); - cookie.setPath("/"); - - response.addCookie(cookie); - } - - /** - * Sets the cookie on the response - * - * @param tokens - * the tokens which will be encoded to make the cookie value. - * @param maxAge - * the value passed to {@link Cookie#setMaxAge(int)} - * @param request - * the request - * @param response - * the response to add the cookie to. - */ - protected void setCookie(String[] tokens, int maxAge, - HttpServletRequest request, HttpServletResponse response) { - String cookieValue = encodeCookie(tokens); - Cookie cookie = new Cookie(getCookieName(), cookieValue); - cookie.setMaxAge(maxAge); - cookie.setPath("/"); - response.addCookie(cookie); - } - -} diff --git a/org.argeo.security.mvc/src/main/java/org/argeo/security/mvc/ArgeoUserInterceptor.java b/org.argeo.security.mvc/src/main/java/org/argeo/security/mvc/ArgeoUserInterceptor.java deleted file mode 100644 index fd83e9f73..000000000 --- a/org.argeo.security.mvc/src/main/java/org/argeo/security/mvc/ArgeoUserInterceptor.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (C) 2007-2012 Mathieu Baudier - * - * 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.argeo.security.mvc; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.argeo.security.UserAdminService; -import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; - -/** Add the current argeo user as an attribute to the request. */ -public class ArgeoUserInterceptor extends HandlerInterceptorAdapter { - private UserAdminService securityService; - - @Override - public boolean preHandle(HttpServletRequest request, - HttpServletResponse response, Object handler) throws Exception { - //request.setAttribute("argeoUser", securityService.getCurrentUser()); - return super.preHandle(request, response, handler); - } - - public void setSecurityService(UserAdminService securityService) { - this.securityService = securityService; - } - -} diff --git a/org.argeo.security.mvc/src/main/java/org/argeo/security/mvc/UsersRolesController.java b/org.argeo.security.mvc/src/main/java/org/argeo/security/mvc/UsersRolesController.java deleted file mode 100644 index 185d376a6..000000000 --- a/org.argeo.security.mvc/src/main/java/org/argeo/security/mvc/UsersRolesController.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright (C) 2007-2012 Mathieu Baudier - * - * 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.argeo.security.mvc; - -import org.argeo.server.mvc.MvcConstants; -import org.springframework.stereotype.Controller; - -@Controller -public class UsersRolesController implements MvcConstants { -// private ArgeoSecurityService securityService; -// private Deserializer userDeserializer = null; - - /* USER */ - -// @RequestMapping("/getCredentials.*") -// @ModelAttribute("user") -// public ArgeoUser getCredentials() { -// ArgeoUser argeoUser = securityService.getCurrentUser(); -// if (argeoUser == null) -// return new SimpleArgeoUser(); -// else -// return argeoUser; -// } -// -// @RequestMapping("/getUsersList.*") -// @ModelAttribute("users") -// public Set getUsersList() { -// return securityService.listUsers(); -// } -// -// @RequestMapping("/userExists.*") -// public BooleanAnswer userExists(@RequestParam("username") String username) { -// return new BooleanAnswer(securityService.userExists(username)); -// } -// -// @RequestMapping("/createUser.*") -// @ModelAttribute("user") -// public ArgeoUser createUser(Reader reader) { -// ArgeoUser user = userDeserializer.deserialize(reader, -// SimpleArgeoUser.class); -// securityService.newUser(user); -// return securityService.getUser(user.getUsername()); -// } -// -// @RequestMapping("/updateUser.*") -// @ModelAttribute("user") -// public ArgeoUser updateUser(Reader reader) { -// ArgeoUser user = userDeserializer.deserialize(reader, -// SimpleArgeoUser.class); -// securityService.updateUser(user); -// return securityService.getUser(user.getUsername()); -// } -// -// @RequestMapping("/updateUserSelf.*") -// @ModelAttribute("user") -// /** Will only update the user natures.*/ -// public ArgeoUser updateUserSelf(Reader reader) { -// ArgeoUser user = securityService.getCurrentUser(); -// ArgeoUser userForNatures = userDeserializer.deserialize(reader, -// SimpleArgeoUser.class); -// user.updateUserNatures(userForNatures.getUserNatures()); -// securityService.updateUser(user); -// return securityService.getUser(user.getUsername()); -// } -// -// @RequestMapping("/deleteUser.*") -// public ServerAnswer deleteUser(@RequestParam("username") String username) { -// securityService.deleteUser(username); -// return ServerAnswer.ok("User " + username + " deleted"); -// } -// -// @RequestMapping("/getUserDetails.*") -// @ModelAttribute("user") -// public ArgeoUser getUserDetails(@RequestParam("username") String username) { -// return securityService.getUser(username); -// } - - /* ROLE */ -// @RequestMapping("/getRolesList.*") -// @ModelAttribute("roles") -// public Set getEditableRolesList() { -// return securityService.listEditableRoles(); -// } -// -// @RequestMapping("/createRole.*") -// public ServerAnswer createRole(@RequestParam("role") String role) { -// securityService.newRole(role); -// return ServerAnswer.ok("Role " + role + " created"); -// } -// -// @RequestMapping("/deleteRole.*") -// public ServerAnswer deleteRole(@RequestParam("role") String role) { -// securityService.deleteRole(role); -// return ServerAnswer.ok("Role " + role + " deleted"); -// } -// -// @RequestMapping("/updateUserPassword.*") -// public ServerAnswer updateUserPassword( -// @RequestParam("username") String username, -// @RequestParam("password") String password) { -// securityService.updateUserPassword(username, password); -// return ServerAnswer.ok("Password updated for user " + username); -// } -// -// @RequestMapping("/updatePassword.*") -// public ServerAnswer updatePassword( -// @RequestParam("oldPassword") String oldPassword, -// @RequestParam("password") String password) { -// securityService.updateCurrentUserPassword(oldPassword, password); -// return ServerAnswer.ok("Password updated"); -// } -// -// public void setUserDeserializer(Deserializer userDeserializer) { -// this.userDeserializer = userDeserializer; -// } -// -// public void setSecurityService(ArgeoSecurityService securityService) { -// this.securityService = securityService; -// } - -} diff --git a/org.argeo.security.mvc/src/org/argeo/security/mvc/ArgeoRememberMeServices.java b/org.argeo.security.mvc/src/org/argeo/security/mvc/ArgeoRememberMeServices.java new file mode 100644 index 000000000..fde9f3034 --- /dev/null +++ b/org.argeo.security.mvc/src/org/argeo/security/mvc/ArgeoRememberMeServices.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2007-2012 Mathieu Baudier + * + * 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.argeo.security.mvc; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.ui.rememberme.TokenBasedRememberMeServices; + +public class ArgeoRememberMeServices extends TokenBasedRememberMeServices { + public final static String DEFAULT_COOKIE_NAME = "ARGEO_SECURITY"; + + public ArgeoRememberMeServices() { + setCookieName(DEFAULT_COOKIE_NAME); + } + + /** + * Sets a "cancel cookie" (with maxAge = 0) on the response to disable + * persistent logins. + * + * @param request + * @param response + */ + protected void cancelCookie(HttpServletRequest request, + HttpServletResponse response) { + Cookie cookie = new Cookie(getCookieName(), null); + cookie.setMaxAge(0); + cookie.setPath("/"); + + response.addCookie(cookie); + } + + /** + * Sets the cookie on the response + * + * @param tokens + * the tokens which will be encoded to make the cookie value. + * @param maxAge + * the value passed to {@link Cookie#setMaxAge(int)} + * @param request + * the request + * @param response + * the response to add the cookie to. + */ + protected void setCookie(String[] tokens, int maxAge, + HttpServletRequest request, HttpServletResponse response) { + String cookieValue = encodeCookie(tokens); + Cookie cookie = new Cookie(getCookieName(), cookieValue); + cookie.setMaxAge(maxAge); + cookie.setPath("/"); + response.addCookie(cookie); + } + +} diff --git a/org.argeo.security.mvc/src/org/argeo/security/mvc/ArgeoUserInterceptor.java b/org.argeo.security.mvc/src/org/argeo/security/mvc/ArgeoUserInterceptor.java new file mode 100644 index 000000000..fd83e9f73 --- /dev/null +++ b/org.argeo.security.mvc/src/org/argeo/security/mvc/ArgeoUserInterceptor.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2007-2012 Mathieu Baudier + * + * 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.argeo.security.mvc; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.argeo.security.UserAdminService; +import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; + +/** Add the current argeo user as an attribute to the request. */ +public class ArgeoUserInterceptor extends HandlerInterceptorAdapter { + private UserAdminService securityService; + + @Override + public boolean preHandle(HttpServletRequest request, + HttpServletResponse response, Object handler) throws Exception { + //request.setAttribute("argeoUser", securityService.getCurrentUser()); + return super.preHandle(request, response, handler); + } + + public void setSecurityService(UserAdminService securityService) { + this.securityService = securityService; + } + +} diff --git a/org.argeo.security.mvc/src/org/argeo/security/mvc/UsersRolesController.java b/org.argeo.security.mvc/src/org/argeo/security/mvc/UsersRolesController.java new file mode 100644 index 000000000..185d376a6 --- /dev/null +++ b/org.argeo.security.mvc/src/org/argeo/security/mvc/UsersRolesController.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2007-2012 Mathieu Baudier + * + * 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.argeo.security.mvc; + +import org.argeo.server.mvc.MvcConstants; +import org.springframework.stereotype.Controller; + +@Controller +public class UsersRolesController implements MvcConstants { +// private ArgeoSecurityService securityService; +// private Deserializer userDeserializer = null; + + /* USER */ + +// @RequestMapping("/getCredentials.*") +// @ModelAttribute("user") +// public ArgeoUser getCredentials() { +// ArgeoUser argeoUser = securityService.getCurrentUser(); +// if (argeoUser == null) +// return new SimpleArgeoUser(); +// else +// return argeoUser; +// } +// +// @RequestMapping("/getUsersList.*") +// @ModelAttribute("users") +// public Set getUsersList() { +// return securityService.listUsers(); +// } +// +// @RequestMapping("/userExists.*") +// public BooleanAnswer userExists(@RequestParam("username") String username) { +// return new BooleanAnswer(securityService.userExists(username)); +// } +// +// @RequestMapping("/createUser.*") +// @ModelAttribute("user") +// public ArgeoUser createUser(Reader reader) { +// ArgeoUser user = userDeserializer.deserialize(reader, +// SimpleArgeoUser.class); +// securityService.newUser(user); +// return securityService.getUser(user.getUsername()); +// } +// +// @RequestMapping("/updateUser.*") +// @ModelAttribute("user") +// public ArgeoUser updateUser(Reader reader) { +// ArgeoUser user = userDeserializer.deserialize(reader, +// SimpleArgeoUser.class); +// securityService.updateUser(user); +// return securityService.getUser(user.getUsername()); +// } +// +// @RequestMapping("/updateUserSelf.*") +// @ModelAttribute("user") +// /** Will only update the user natures.*/ +// public ArgeoUser updateUserSelf(Reader reader) { +// ArgeoUser user = securityService.getCurrentUser(); +// ArgeoUser userForNatures = userDeserializer.deserialize(reader, +// SimpleArgeoUser.class); +// user.updateUserNatures(userForNatures.getUserNatures()); +// securityService.updateUser(user); +// return securityService.getUser(user.getUsername()); +// } +// +// @RequestMapping("/deleteUser.*") +// public ServerAnswer deleteUser(@RequestParam("username") String username) { +// securityService.deleteUser(username); +// return ServerAnswer.ok("User " + username + " deleted"); +// } +// +// @RequestMapping("/getUserDetails.*") +// @ModelAttribute("user") +// public ArgeoUser getUserDetails(@RequestParam("username") String username) { +// return securityService.getUser(username); +// } + + /* ROLE */ +// @RequestMapping("/getRolesList.*") +// @ModelAttribute("roles") +// public Set getEditableRolesList() { +// return securityService.listEditableRoles(); +// } +// +// @RequestMapping("/createRole.*") +// public ServerAnswer createRole(@RequestParam("role") String role) { +// securityService.newRole(role); +// return ServerAnswer.ok("Role " + role + " created"); +// } +// +// @RequestMapping("/deleteRole.*") +// public ServerAnswer deleteRole(@RequestParam("role") String role) { +// securityService.deleteRole(role); +// return ServerAnswer.ok("Role " + role + " deleted"); +// } +// +// @RequestMapping("/updateUserPassword.*") +// public ServerAnswer updateUserPassword( +// @RequestParam("username") String username, +// @RequestParam("password") String password) { +// securityService.updateUserPassword(username, password); +// return ServerAnswer.ok("Password updated for user " + username); +// } +// +// @RequestMapping("/updatePassword.*") +// public ServerAnswer updatePassword( +// @RequestParam("oldPassword") String oldPassword, +// @RequestParam("password") String password) { +// securityService.updateCurrentUserPassword(oldPassword, password); +// return ServerAnswer.ok("Password updated"); +// } +// +// public void setUserDeserializer(Deserializer userDeserializer) { +// this.userDeserializer = userDeserializer; +// } +// +// public void setSecurityService(ArgeoSecurityService securityService) { +// this.securityService = securityService; +// } + +}