Integrate LDAP and JCR
authorMathieu Baudier <mbaudier@argeo.org>
Fri, 18 Mar 2011 11:03:39 +0000 (11:03 +0000)
committerMathieu Baudier <mbaudier@argeo.org>
Fri, 18 Mar 2011 11:03:39 +0000 (11:03 +0000)
git-svn-id: https://svn.argeo.org/commons/trunk@4320 4cfe0d0a-d680-48aa-b62c-e0a02a3f76cc

14 files changed:
demo/argeo-node-web.properties
security/modules/org.argeo.security.dao.ldap/META-INF/MANIFEST.MF
security/modules/org.argeo.security.dao.ldap/META-INF/spring/jcr.xml [new file with mode: 0644]
security/modules/org.argeo.security.dao.ldap/META-INF/spring/ldap-osgi.xml
security/modules/org.argeo.security.dao.ldap/META-INF/spring/ldap.xml
security/modules/org.argeo.security.services/META-INF/spring/osgi.xml
security/runtime/org.argeo.security.core/src/main/java/org/argeo/security/core/KeyBasedSystemExecutionService.java
security/runtime/org.argeo.security.core/src/main/java/org/argeo/security/jcr/JcrAuthenticationProvider.java
security/runtime/org.argeo.security.ldap/src/main/java/org/argeo/security/ldap/jcr/JcrUserDetailsContextMapper.java
server/modules/org.argeo.node.repo.jackrabbit/META-INF/spring/noderepo-osgi.xml
server/modules/org.argeo.node.repo.jackrabbit/META-INF/spring/noderepo.xml
server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/ArgeoJcrConstants.java
server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/ArgeoNames.java
server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/JcrUtils.java

index 726f225c305ba75ca81a18f4201a127a6653cb3c..7cbe55435482ee5013b2ff2f0240ede42550a55c 100644 (file)
@@ -5,7 +5,6 @@ org.argeo.node.repo.jackrabbit,\
 org.argeo.security.dao.ldap,\
 org.argeo.security.services,\
 org.argeo.security.services.admin,\
-com.springsource.javax.servlet,\
 org.argeo.security.equinox,\
 org.eclipse.core.runtime,\
 org.eclipse.equinox.common,\
index 85bbf4b21783e2ab2813061526773b4406c80f0c..d377808d80685b8fd24c4184df59520bb86626bd 100644 (file)
@@ -1,8 +1,11 @@
 Bundle-SymbolicName: org.argeo.security.dao.ldap
 Bundle-Version: 0.2.3.SNAPSHOT
 Import-Package: com.sun.jndi.ldap;resolution:=optional,
+ javax.jcr;version="[2.0.0,3.0.0)",
+ org.argeo.jcr,
  org.argeo.security,
  org.argeo.security.ldap,
+ org.argeo.security.ldap.jcr,
  org.argeo.security.ldap.nature,
  org.argeo.security.nature,
  org.springframework.beans.factory.config,
diff --git a/security/modules/org.argeo.security.dao.ldap/META-INF/spring/jcr.xml b/security/modules/org.argeo.security.dao.ldap/META-INF/spring/jcr.xml
new file mode 100644 (file)
index 0000000..2a0a08c
--- /dev/null
@@ -0,0 +1,46 @@
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:security="http://www.springframework.org/schema/security"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:util="http://www.springframework.org/schema/util"
+       xsi:schemaLocation="http://www.springframework.org/schema/beans
+               http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
+               http://www.springframework.org/schema/security
+               http://www.springframework.org/schema/security/spring-security-2.0.4.xsd
+               http://www.springframework.org/schema/util
+               http://www.springframework.org/schema/util/spring-util-2.5.xsd">
+
+
+       <bean id="jcrUserDetailsContextMapper" class="org.argeo.security.ldap.jcr.JcrUserDetailsContextMapper">
+               <property name="systemExecutionService" ref="systemExecutionService" />
+               <property name="propertyToAttributes">
+                       <map>
+                               <entry value="cn">
+                                       <key>
+                                               <util:constant static-field="org.argeo.jcr.ArgeoNames.ARGEO_DISPLAY_NAME" />
+                                       </key>
+                               </entry>
+                               <entry value="givenName">
+                                       <key>
+                                               <util:constant static-field="org.argeo.jcr.ArgeoNames.ARGEO_FIRST_NAME" />
+                                       </key>
+                               </entry>
+                               <entry value="sn">
+                                       <key>
+                                               <util:constant static-field="org.argeo.jcr.ArgeoNames.ARGEO_LAST_NAME" />
+                                       </key>
+                               </entry>
+                               <entry value="mail">
+                                       <key>
+                                               <util:constant static-field="org.argeo.jcr.ArgeoNames.ARGEO_PRIMARY_EMAIL" />
+                                       </key>
+                               </entry>
+                               <entry value="o">
+                                       <key>
+                                               <util:constant
+                                                       static-field="org.argeo.jcr.ArgeoNames.ARGEO_PRIMARY_ORGANIZATION" />
+                                       </key>
+                               </entry>
+                       </map>
+               </property>
+
+       </bean>
+</beans>
index 3ae43c3307780e0bd0676038d6b7129c84dd3769..fcfa5fed17b8b2d0946c86145b092efe99495173 100644 (file)
@@ -9,6 +9,12 @@
        <!-- REFERENCES -->\r
        <list id="userNatureMappers" interface="org.argeo.security.ldap.UserNatureMapper"\r
                cardinality="0..N" />\r
+       <reference id="repositoryFactory" interface="javax.jcr.RepositoryFactory"\r
+               cardinality="0..1">\r
+               <listener ref="jcrUserDetailsContextMapper" bind-method="register"\r
+                       unbind-method="unregister" />\r
+       </reference>\r
+       <reference id="systemExecutionService" interface="org.argeo.security.SystemExecutionService" />\r
 \r
        <!-- SERVICES -->\r
        <service ref="authenticationProvider"\r
index fdf216444959cc4debf337431a249f3357e3774e..02ee71b1d44dfc562759038e39a385b65be16220 100644 (file)
@@ -30,9 +30,7 @@
                <constructor-arg>
                        <bean factory-bean="securityDao" factory-method="getAuthoritiesPopulator" />
                </constructor-arg>
-               <property name="userDetailsContextMapper">
-                       <bean factory-bean="securityDao" factory-method="getUserDetailsMapper" />
-               </property>
+               <property name="userDetailsContextMapper" ref="jcrUserDetailsContextMapper" />
        </bean>
 
        <bean id="securityDao" class="org.argeo.security.ldap.ArgeoSecurityDaoLdap">
index 2ee9bf3f569ef5ecd90d45d949f0a811bbcf55da..f83086a2b51653401a0ca1a55bfc77d2da1be3bc 100644 (file)
@@ -7,7 +7,8 @@
        http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">\r
 \r
        <!-- REFERENCES -->\r
-       <reference id="currentUserDao" interface="org.argeo.security.CurrentUserDao" />\r
+       <reference id="currentUserDao" interface="org.argeo.security.CurrentUserDao"\r
+               cardinality="0..1" />\r
 \r
        <list id="authenticationProviders"\r
                interface="org.springframework.security.providers.AuthenticationProvider"\r
index 2fc4388afbca2de6411e400bc7303ad2248caf38..01d760f12befc1ce88fa38eb37c4d308505c6a49 100644 (file)
@@ -39,11 +39,12 @@ public class KeyBasedSystemExecutionService implements SystemExecutionService {
                                                .authenticate(new InternalAuthentication(
                                                                systemAuthenticationKey));
                                securityContext.setAuthentication(auth);
-
-                               runnable.run();
-
-                               // remove the authentication
-                               securityContext.setAuthentication(null);
+                               try {
+                                       runnable.run();
+                               } finally {
+                                       // remove the authentication
+                                       securityContext.setAuthentication(null);
+                               }
                        }
                };
        }
index 844984a52b3c75400f1b12460712b52b0acf3c23..bfa51f5b12d11049d1aa7d33e6174e076fb7b6a8 100644 (file)
@@ -1,9 +1,6 @@
 package org.argeo.security.jcr;
 
-import java.util.ArrayList;
 import java.util.HashMap;
-import java.util.Iterator;
-import java.util.List;
 import java.util.Map;
 
 import javax.jcr.Node;
@@ -26,7 +23,7 @@ import org.springframework.security.providers.AuthenticationProvider;
 
 /** Connects to a JCR repository and delegate authentication to it. */
 public class JcrAuthenticationProvider implements AuthenticationProvider {
-       private List<RepositoryFactory> repositoryFactories = new ArrayList<RepositoryFactory>();
+       private RepositoryFactory repositoryFactory;
        private final String defaultHome;
        private final String userRole;
 
@@ -58,10 +55,7 @@ public class JcrAuthenticationProvider implements AuthenticationProvider {
                        parameters.put(ArgeoJcrConstants.JCR_REPOSITORY_URI, url);
 
                        Repository repository = null;
-                       for (Iterator<RepositoryFactory> it = repositoryFactories
-                                       .iterator(); it.hasNext();) {
-                               repository = it.next().getRepository(parameters);
-                       }
+                       repository = repositoryFactory.getRepository(parameters);
                        if (repository == null)
                                return null;
 
@@ -120,9 +114,14 @@ public class JcrAuthenticationProvider implements AuthenticationProvider {
                }
        }
 
-       public void setRepositoryFactories(
-                       List<RepositoryFactory> repositoryFactories) {
-               this.repositoryFactories = repositoryFactories;
+       public void register(RepositoryFactory repositoryFactory,
+                       Map<String, String> parameters) {
+               this.repositoryFactory = repositoryFactory;
+       }
+
+       public void unregister(RepositoryFactory repositoryFactory,
+                       Map<String, String> parameters) {
+               this.repositoryFactory = null;
        }
 
        public String getDefaultHome() {
index b5b77474cb2e40bc71c3a29d797640cc03e72f7e..86c788a448e5188f8afd6902188e955b492bf2fb 100644 (file)
 package org.argeo.security.ldap.jcr;
 
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.jcr.Node;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.RepositoryFactory;
 import javax.jcr.Session;
 
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.ArgeoException;
+import org.argeo.jcr.ArgeoJcrConstants;
+import org.argeo.jcr.ArgeoNames;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.security.SystemExecutionService;
+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;
 
+/**
+ * Maps LDAP attributes and JCR properties. This class is meant to be robust,
+ * checks of which values should be mandatory should be performed at a higher
+ * level.
+ */
 public class JcrUserDetailsContextMapper implements UserDetailsContextMapper {
-       private Session session;
+       private final static Log log = LogFactory
+                       .getLog(JcrUserDetailsContextMapper.class);
+
+       private Map<String, String> propertyToAttributes = new HashMap<String, String>();
+       private SystemExecutionService systemExecutionService;
+       private String homeBasePath = "/home";
+       private RepositoryFactory repositoryFactory;
+
+       public UserDetails mapUserFromContext(final DirContextOperations ctx,
+                       final String username, GrantedAuthority[] authorities) {
+               if (repositoryFactory == null)
+                       throw new ArgeoException("No JCR repository factory registered");
+               final String userHomePath = usernameToHomePath(username);
+               systemExecutionService.executeAsSystem(new Runnable() {
+                       public void run() {
+                               Session session = null;
+                               try {
+                                       Repository nodeRepo = JcrUtils.getRepositoryByAlias(
+                                                       repositoryFactory, ArgeoJcrConstants.ALIAS_NODE);
+                                       session = nodeRepo.login();
+                                       Node userProfile = JcrUtils.mkdirs(session, userHomePath
+                                                       + '/' + ArgeoNames.ARGEO_USER_PROFILE);
+                                       for (String jcrProperty : propertyToAttributes.keySet())
+                                               ldapToJcr(userProfile, jcrProperty, ctx);
+                                       session.save();
+                                       if (log.isDebugEnabled())
+                                               log.debug("Mapped " + ctx.getDn() + " to "
+                                                               + userProfile);
+                               } catch (RepositoryException e) {
+                                       throw new ArgeoException("Cannot synchronize JCR and LDAP",
+                                                       e);
+                               } finally {
+                                       session.logout();
+                               }
+                       }
+               });
 
-       public UserDetails mapUserFromContext(DirContextOperations ctx,
-                       String username, GrantedAuthority[] authority) {
-               // TODO Auto-generated method stub
-               return null;
+               // password
+               byte[] arr = (byte[]) ctx.getAttributeSortedStringSet("userPassword")
+                               .first();
+               JcrUserDetails userDetails = new JcrUserDetails(userHomePath, username,
+                               new String(arr), true, true, true, true, authorities);
+               Arrays.fill(arr, (byte) 0);
+               return userDetails;
        }
 
-       public void mapUserToContext(UserDetails user, DirContextAdapter ctx) {
-               // TODO Auto-generated method stub
+       public void mapUserToContext(UserDetails user, final DirContextAdapter ctx) {
+               if (!(user instanceof JcrUserDetails))
+                       throw new ArgeoException("Unsupported user details: "
+                                       + user.getClass());
+
+               ctx.setAttributeValues("objectClass", new String[] { "inetOrgPerson" });
+               ctx.setAttributeValue("uid", user.getUsername());
+               ctx.setAttributeValue("userPassword", user.getPassword());
+
+               final JcrUserDetails jcrUserDetails = (JcrUserDetails) user;
+               systemExecutionService.executeAsSystem(new Runnable() {
+                       public void run() {
+                               Session session = null;
+                               try {
+                                       Repository nodeRepo = JcrUtils.getRepositoryByAlias(
+                                                       repositoryFactory, ArgeoJcrConstants.ALIAS_NODE);
+                                       session = nodeRepo.login();
+                                       Node userProfile = session.getNode(jcrUserDetails
+                                                       .getHomePath()
+                                                       + '/'
+                                                       + ArgeoNames.ARGEO_USER_PROFILE);
+                                       for (String jcrProperty : propertyToAttributes.keySet())
+                                               jcrToLdap(userProfile, jcrProperty, ctx);
+                                       if (log.isDebugEnabled())
+                                               log.debug("Mapped " + userProfile + " to "
+                                                               + ctx.getDn());
+                               } catch (RepositoryException e) {
+                                       throw new ArgeoException("Cannot synchronize JCR and LDAP",
+                                                       e);
+                               } finally {
+                                       session.logout();
+                               }
+                       }
+               });
+       }
 
+       protected String usernameToHomePath(String username) {
+               return homeBasePath + '/' + JcrUtils.firstCharsToPath(username, 2)
+                               + '/' + username;
        }
 
+       protected void ldapToJcr(Node userProfile, String jcrProperty,
+                       DirContextOperations ctx) {
+               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);
+                       if (value == null)
+                               return;
+                       userProfile.setProperty(jcrProperty, value);
+               } catch (Exception e) {
+                       throw new ArgeoException("Cannot map JCR property " + jcrProperty
+                                       + " from LDAP", e);
+               }
+       }
+
+       protected void jcrToLdap(Node userProfile, String jcrProperty,
+                       DirContextOperations ctx) {
+               try {
+                       if (!userProfile.hasProperty(jcrProperty))
+                               return;
+                       String value = userProfile.getProperty(jcrProperty).getString();
+
+                       String ldapAttribute;
+                       if (propertyToAttributes.containsKey(jcrProperty))
+                               ldapAttribute = propertyToAttributes.get(jcrProperty);
+                       else
+                               throw new ArgeoException(
+                                               "No LDAP attribute mapped for JCR proprty "
+                                                               + jcrProperty);
+                       ctx.setAttributeValue(ldapAttribute, value);
+               } catch (Exception e) {
+                       throw new ArgeoException("Cannot map JCR property " + jcrProperty
+                                       + " from LDAP", e);
+               }
+       }
+
+       public void setPropertyToAttributes(Map<String, String> propertyToAttributes) {
+               this.propertyToAttributes = propertyToAttributes;
+       }
+
+       public void setSystemExecutionService(
+                       SystemExecutionService systemExecutionService) {
+               this.systemExecutionService = systemExecutionService;
+       }
+
+       public void setHomeBasePath(String homeBasePath) {
+               this.homeBasePath = homeBasePath;
+       }
+
+       public void register(RepositoryFactory repositoryFactory,
+                       Map<String, String> parameters) {
+               this.repositoryFactory = repositoryFactory;
+       }
+
+       public void unregister(RepositoryFactory repositoryFactory,
+                       Map<String, String> parameters) {
+               this.repositoryFactory = null;
+       }
 }
index 9721feebbddd113526dc8c2356675824244f1b69..f13839713779d7bba2bd50fe4fa8f4fd0dbb28e7 100644 (file)
        http://www.springframework.org/schema/util/spring-util-2.5.xsd">\r
 \r
        <!-- REFERENCE -->\r
-       <list id="repositoryFactories" interface="javax.jcr.RepositoryFactory"\r
-               cardinality="0..N" />\r
+       <reference id="repositoryFactory" interface="javax.jcr.RepositoryFactory"\r
+               cardinality="0..1">\r
+               <listener ref="jcrAuthenticationProvider" bind-method="register"\r
+                       unbind-method="unregister" />\r
+       </reference>\r
        <list id="repositories" interface="javax.jcr.Repository"\r
                cardinality="0..N">\r
                <listener ref="repositoryFactory" bind-method="register"\r
index a03a421b48014bac36b192524de45804a6e04fc5..ec8574efe5d7fb9244ed97ea2bdc2b28ba29df91 100644 (file)
@@ -33,9 +33,8 @@
                <property name="workspace" value="${argeo.node.repo.workspace}" />
        </bean>
 
-       <bean id="jcrAuthenticationProvider" class="org.argeo.security.jackrabbit.providers.JackrabbitAuthenticationProvider">
-               <property name="repositoryFactories" ref="repositoryFactories" />
-       </bean>
+       <bean id="jcrAuthenticationProvider"
+               class="org.argeo.security.jackrabbit.providers.JackrabbitAuthenticationProvider" />
 
        <bean id="repositoryFactory" class="org.argeo.jackrabbit.JackrabbitRepositoryFactory">
        </bean>
index 458246ef4ccd70b98c28ee5344007ee108f45323..5d5983c40e4051f505546dd8465f3b44b2641b82 100644 (file)
@@ -3,7 +3,11 @@ package org.argeo.jcr;
 /** JCR related constants */
 public interface ArgeoJcrConstants {
 
+       // parameters (typically for call to a RepositoryFactory)
        public final static String JCR_REPOSITORY_ALIAS = "argeo.jcr.repository.alias";
        public final static String JCR_REPOSITORY_URI = "argeo.jcr.repository.uri";
 
+       // standard aliases
+       public final static String ALIAS_NODE = "node";
+
 }
index fd215279811d2c8928340b7d7f5815158c182f66..4b5ffb8db78b24203d613149422da13c727abac2 100644 (file)
@@ -6,4 +6,12 @@ public interface ArgeoNames {
 
        public final static String ARGEO_URI = "argeo:uri";
        public final static String ARGEO_USER_ID = "argeo:userID";
+       
+       public final static String ARGEO_USER_PROFILE = "argeo:userProfile";
+       public final static String ARGEO_DISPLAY_NAME = "argeo:displayName";
+       public final static String ARGEO_FIRST_NAME = "argeo:firstName";
+       public final static String ARGEO_LAST_NAME = "argeo:lastName";
+       public final static String ARGEO_PRIMARY_EMAIL = "argeo:primaryEmail";
+       public final static String ARGEO_PRIMARY_ORGANIZATION = "argeo:primaryOrganization";
+
 }
index 8c6d6a78db2355377d0b0e0a7bf6789054178544..ad6ca0eddb10dd32d95eba289cd8c42bd4435c77 100644 (file)
@@ -23,6 +23,7 @@ import java.text.ParseException;
 import java.util.Calendar;
 import java.util.Date;
 import java.util.GregorianCalendar;
+import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
@@ -35,7 +36,9 @@ import javax.jcr.Node;
 import javax.jcr.NodeIterator;
 import javax.jcr.Property;
 import javax.jcr.PropertyIterator;
+import javax.jcr.Repository;
 import javax.jcr.RepositoryException;
+import javax.jcr.RepositoryFactory;
 import javax.jcr.Session;
 import javax.jcr.Value;
 import javax.jcr.nodetype.NodeType;
@@ -47,7 +50,7 @@ import org.apache.commons.logging.LogFactory;
 import org.argeo.ArgeoException;
 
 /** Utility methods to simplify common JCR operations. */
-public class JcrUtils {
+public class JcrUtils implements ArgeoJcrConstants {
        private final static Log log = LogFactory.getLog(JcrUtils.class);
 
        /**
@@ -219,7 +222,8 @@ public class JcrUtils {
        }
 
        /**
-        * Creates the nodes making path, if they don't exist.
+        * Creates the nodes making path, if they don't exist. This is up to the
+        * caller to save the session.
         */
        public static Node mkdirs(Session session, String path, String type,
                        String intermediaryNodeType, Boolean versioning) {
@@ -262,7 +266,7 @@ public class JcrUtils {
                                        currentNode = (Node) session.getItem(current.toString());
                                }
                        }
-                       session.save();
+                       // session.save();
                        return currentNode;
                } catch (RepositoryException e) {
                        throw new ArgeoException("Cannot mkdirs " + path, e);
@@ -573,4 +577,40 @@ public class JcrUtils {
                }
                return path.toString();
        }
+
+       /**
+        * Wraps the call to the repository factory based on parameter
+        * {@link ArgeoJcrConstants#JCR_REPOSITORY_ALIAS} in order to simplify it
+        * and protect against future API changes.
+        */
+       public static Repository getRepositoryByAlias(
+                       RepositoryFactory repositoryFactory, String alias) {
+               try {
+                       Map<String, String> parameters = new HashMap<String, String>();
+                       parameters.put(JCR_REPOSITORY_ALIAS, alias);
+                       return repositoryFactory.getRepository(parameters);
+               } catch (RepositoryException e) {
+                       throw new ArgeoException(
+                                       "Unexpected exception when trying to retrieve repository with alias "
+                                                       + alias, e);
+               }
+       }
+
+       /**
+        * Wraps the call to the repository factory based on parameter
+        * {@link ArgeoJcrConstants#JCR_REPOSITORY_URI} in order to simplify it and
+        * protect against future API changes.
+        */
+       public static Repository getRepositoryByUri(
+                       RepositoryFactory repositoryFactory, String uri) {
+               try {
+                       Map<String, String> parameters = new HashMap<String, String>();
+                       parameters.put(JCR_REPOSITORY_URI, uri);
+                       return repositoryFactory.getRepository(parameters);
+               } catch (RepositoryException e) {
+                       throw new ArgeoException(
+                                       "Unexpected exception when trying to retrieve repository with uri "
+                                                       + uri, e);
+               }
+       }
 }