You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jspwiki.apache.org by aj...@apache.org on 2009/08/08 19:53:28 UTC

svn commit: r802426 - in /incubator/jspwiki/trunk: ./ etc/ etc/ldap/ src/WebContent/WEB-INF/lib/ src/java/org/apache/wiki/ src/java/org/apache/wiki/auth/ src/java/org/apache/wiki/auth/authorize/ src/java/org/apache/wiki/auth/login/ tests/java/org/apach...

Author: ajaquith
Date: Sat Aug  8 17:53:27 2009
New Revision: 802426

URL: http://svn.apache.org/viewvc?rev=802426&view=rev
Log:
Checked in revised LdapLoginModule and LdapAuthorizer. Notable new features include provisional support for Active Directory, which "mostly" works. I also added the ability to bind to LDAP with a specified identity. To to this, we use a new feature of freshcookies-security called a "keychain" that stores the binding DN in an password-locked, triple-DES encrypted file. The keychain is available via AuthenticationManager.getKeychain() and can be used for storage of anything sensitive. The keychain is NOT required to be unlocked at startup time (although obviously LDAP binds will fail). We will provide the ability for the admin to unlock the keychain via the web UI in a future build.

Added:
    incubator/jspwiki/trunk/src/WebContent/WEB-INF/lib/freshcookies-security-0.61.jar   (with props)
Removed:
    incubator/jspwiki/trunk/src/WebContent/WEB-INF/lib/freshcookies-security-0.60.jar
Modified:
    incubator/jspwiki/trunk/ChangeLog
    incubator/jspwiki/trunk/etc/jspwiki.properties.tmpl
    incubator/jspwiki/trunk/etc/ldap/README
    incubator/jspwiki/trunk/etc/ldap/slapd.conf
    incubator/jspwiki/trunk/etc/ldap/test.ldif
    incubator/jspwiki/trunk/src/java/org/apache/wiki/Release.java
    incubator/jspwiki/trunk/src/java/org/apache/wiki/auth/AuthenticationManager.java
    incubator/jspwiki/trunk/src/java/org/apache/wiki/auth/authorize/LdapAuthorizer.java
    incubator/jspwiki/trunk/src/java/org/apache/wiki/auth/login/LdapLoginModule.java
    incubator/jspwiki/trunk/tests/java/org/apache/wiki/auth/AllTests.java
    incubator/jspwiki/trunk/tests/java/org/apache/wiki/auth/authorize/LdapAuthorizerTest.java
    incubator/jspwiki/trunk/tests/java/org/apache/wiki/auth/login/LdapLoginModuleTest.java

Modified: incubator/jspwiki/trunk/ChangeLog
URL: http://svn.apache.org/viewvc/incubator/jspwiki/trunk/ChangeLog?rev=802426&r1=802425&r2=802426&view=diff
==============================================================================
--- incubator/jspwiki/trunk/ChangeLog (original)
+++ incubator/jspwiki/trunk/ChangeLog Sat Aug  8 17:53:27 2009
@@ -1,3 +1,19 @@
+2009-08-08 Andrew Jaquith <ajaquith AT apache DOT org>
+
+        * 3.0.0-svn-139
+
+        * Checked in revised LdapLoginModule and LdapAuthorizer. Notable new
+        features include provisional support for Active Directory, which
+        "mostly" works. I also added the ability to bind to LDAP with a
+        specified identity. To to this, we use a new feature of
+        freshcookies-security called a "keychain" that stores the binding
+        DN in an password-locked, triple-DES encrypted file. The keychain
+        is available via AuthenticationManager.getKeychain() and can be
+        used for storage of anything sensitive. The keychain is NOT required
+        to be unlocked at startup time (although obviously LDAP binds will
+        fail). We will provide the ability for the admin to unlock the
+        keychain via the web UI in a future build.  
+
 2009-07-29 Harry Metske <me...@apache.org>
 
         * 3.0.0-svn-138

Modified: incubator/jspwiki/trunk/etc/jspwiki.properties.tmpl
URL: http://svn.apache.org/viewvc/incubator/jspwiki/trunk/etc/jspwiki.properties.tmpl?rev=802426&r1=802425&r2=802426&view=diff
==============================================================================
--- incubator/jspwiki/trunk/etc/jspwiki.properties.tmpl (original)
+++ incubator/jspwiki/trunk/etc/jspwiki.properties.tmpl Sat Aug  8 17:53:27 2009
@@ -468,9 +468,13 @@
 #  etc. should be replaced with the actual parameter names. The parameter names and
 # values will be loaded to a Map and passed to the LoginModule as the 'options' parameter
 # when its initialize() method is called. The default UserDatabaseLoginModule class does
-# not need any options. The example below is for LdapLoginModule.
+# not need any options. The example below is for LdapLoginModule, configured for OpenLDAP.
+#
+#jspwiki.loginModule.options.ldap.authentication = DIGEST-MD5
 #jspwiki.loginModule.options.ldap.connectionURL = ldap://127.0.0.1:4890/
-#jspwiki.loginModule.options.ldap.userPattern = uid={0},ou=people,dc=jspwiki,dc=org
+#jspwiki.loginModule.options.ldap.loginIdPattern = uid\={0},ou\=people,dc\=jspwiki,dc\=org
+#jspwiki.loginModule.options.ldap.userBase = ou\=people,dc\=jspwiki,dc\=org
+#jspwiki.loginModule.options.ldap.userPattern = (&(objectClass\=inetOrgPerson)(uid\={0}))
 #jspwiki.loginModule.options.ldap.ssl = false
 
 # 
@@ -528,6 +532,19 @@
 
 jspwiki.authorizer = org.apache.wiki.auth.authorize.WebContainerAuthorizer
 
+# In addition to the built-in web container authorizer, JSPWiki also supplies
+# an Authorizer that works with LDAP. If the LdapAuthorizer is specified,
+# these settings used to describe how roles are looked up. The connection URL is
+# the same specified above in Custom Authentication. The 'role base' supplies
+# the DN containing role entries.
+#
+#jspwiki.authorizer = org.apache.wiki.auth.authorize.WebContainerAuthorizer
+#jspwiki.ldap.roleBase = ou\=roles,dc\=jspwiki,dc\=org
+#jspwiki.ldap.rolePattern = (&(objectClass\=groupOfUniqueNames)(cn\={0})(uniqueMember\={1}))
+#jspwiki.ldap.bindDN = cn\=Manager,dc\=jspwiki,dc\=org
+#jspwiki.ldap.bindPasswordAlias = ldap.ManagerDN
+
+
 #  B) GROUPS
 #  As an additional source of authorization, users can belong to discretionary
 #  "wiki groups" that the users manage themselves. Wiki groups are stored in a

Modified: incubator/jspwiki/trunk/etc/ldap/README
URL: http://svn.apache.org/viewvc/incubator/jspwiki/trunk/etc/ldap/README?rev=802426&r1=802425&r2=802426&view=diff
==============================================================================
--- incubator/jspwiki/trunk/etc/ldap/README (original)
+++ incubator/jspwiki/trunk/etc/ldap/README Sat Aug  8 17:53:27 2009
@@ -2,8 +2,8 @@
 the LdapAuthorizerTest and LdapLoginModuleTest unit tests. It assumes that OpenLDAP
 is installed on your system and that config files are stored in /etc/openldap.
 OpenLDAP runs on most Unix-like systems. Mac OS X 10.5 Leopard has OpenLDAP installed
-out of the box, and this is the setup this document was written to work with.
-Your mileage may vary.
+out of the box. The out-of-the-box OS X setup was what this document was written
+to work with. Your mileage may vary.
 
 First-time setup
 1) copy ldap.conf to /etc/openldap

Modified: incubator/jspwiki/trunk/etc/ldap/slapd.conf
URL: http://svn.apache.org/viewvc/incubator/jspwiki/trunk/etc/ldap/slapd.conf?rev=802426&r1=802425&r2=802426&view=diff
==============================================================================
--- incubator/jspwiki/trunk/etc/ldap/slapd.conf (original)
+++ incubator/jspwiki/trunk/etc/ldap/slapd.conf Sat Aug  8 17:53:27 2009
@@ -1,14 +1,36 @@
-include   /private/etc/openldap/schema/core.schema
-include   /private/etc/openldap/schema/cosine.schema
-include   /private/etc/openldap/schema/inetorgperson.schema
-include   /private/etc/openldap/schema/nis.schema
-pidfile   /Users/arj/workspace/ldap/run/slapd.pid
-argsfile  /Users/arj/workspace/ldap/run/slapd.args
-database  bdb
-suffix    "dc=jspwiki,dc=org"
-rootdn    "cn=Manager,dc=jspwiki,dc=org"
-# Password for next line is 'test'
-rootpw    {SSHA}RJMzbtilrHxvREvaJP7eTBZzSN1Za73l
-directory	/Users/arj/workspace/ldap/data
-index     objectClass,uid,uidNumber,gidNumber             eq
-index     cn,mail,surname,givenname                       eq,subinitial
\ No newline at end of file
+include       /private/etc/openldap/schema/core.schema
+include       /private/etc/openldap/schema/cosine.schema
+include       /private/etc/openldap/schema/inetorgperson.schema
+include       /private/etc/openldap/schema/nis.schema
+pidfile       /Users/arj/workspace/ldap/run/slapd.pid
+argsfile      /Users/arj/workspace/ldap/run/slapd.args
+database      bdb
+suffix        "dc=jspwiki,dc=org"
+# Use SASL so we can use DIGEST-MD5 authentication
+# authz-regexp
+#  uid=([^,]*),cn=gssapi,cn=auth
+#  uid=$1,ou=people,dc=jspwiki,dc=org  
+
+# Allow access to password for auth, and for changes by self
+access to attrs=userpassword
+  by self      write
+  by anonymous auth
+  by *         none
+
+# Allow anonymous access to people subtree
+access to dn.subtree="ou=people,dc=jspwiki,dc=org"
+  by * read
+
+# No anonymous binds for anything else; users can update own entries
+access to *
+  by self write
+  by users read
+  by * none
+
+# Password for root user is 'test'
+rootdn        "cn=Manager,dc=jspwiki,dc=org"
+rootpw        {SSHA}RJMzbtilrHxvREvaJP7eTBZzSN1Za73l
+
+directory	    /Users/arj/workspace/ldap/data
+index         objectClass,uid,uidNumber,gidNumber             eq
+index         cn,mail,surname,givenname                       eq,subinitial

Modified: incubator/jspwiki/trunk/etc/ldap/test.ldif
URL: http://svn.apache.org/viewvc/incubator/jspwiki/trunk/etc/ldap/test.ldif?rev=802426&r1=802425&r2=802426&view=diff
==============================================================================
--- incubator/jspwiki/trunk/etc/ldap/test.ldif (original)
+++ incubator/jspwiki/trunk/etc/ldap/test.ldif Sat Aug  8 17:53:27 2009
@@ -77,8 +77,9 @@
 objectClass: inetOrgPerson
 x500UniqueIdentifier: '1100101100011110011011001011100010110111110101110101101101111001110110110011110101101101110100111001110001111000101011011100010111001110010110011101101110111110010110010011000111100101101001100011110001110001111000011101111100001'B
 uid: Fred
+givenName: Fred
 sn: Flintstone
-cn: Fred Flintstone
+cn: Flintstone, Fred
 mail: fred@example.com
 userPassword: {SSHA}iDeE9dysPUE28SWd6yeIqiIj9sIVyiMM7VnMKQ==
 

Added: incubator/jspwiki/trunk/src/WebContent/WEB-INF/lib/freshcookies-security-0.61.jar
URL: http://svn.apache.org/viewvc/incubator/jspwiki/trunk/src/WebContent/WEB-INF/lib/freshcookies-security-0.61.jar?rev=802426&view=auto
==============================================================================
Binary file - no diff available.

Propchange: incubator/jspwiki/trunk/src/WebContent/WEB-INF/lib/freshcookies-security-0.61.jar
------------------------------------------------------------------------------
    svn:mime-type = application/octet-stream

Modified: incubator/jspwiki/trunk/src/java/org/apache/wiki/Release.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/trunk/src/java/org/apache/wiki/Release.java?rev=802426&r1=802425&r2=802426&view=diff
==============================================================================
--- incubator/jspwiki/trunk/src/java/org/apache/wiki/Release.java (original)
+++ incubator/jspwiki/trunk/src/java/org/apache/wiki/Release.java Sat Aug  8 17:53:27 2009
@@ -77,7 +77,7 @@
      *  <p>
      *  If the build identifier is empty, it is not added.
      */
-    public static final String     BUILD         = "138";
+    public static final String     BUILD         = "139";
 
     /**
      *  This is the generic version string you should use

Modified: incubator/jspwiki/trunk/src/java/org/apache/wiki/auth/AuthenticationManager.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/trunk/src/java/org/apache/wiki/auth/AuthenticationManager.java?rev=802426&r1=802425&r2=802426&view=diff
==============================================================================
--- incubator/jspwiki/trunk/src/java/org/apache/wiki/auth/AuthenticationManager.java (original)
+++ incubator/jspwiki/trunk/src/java/org/apache/wiki/auth/AuthenticationManager.java Sat Aug  8 17:53:27 2009
@@ -21,8 +21,11 @@
 package org.apache.wiki.auth;
 
 import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
 import java.net.MalformedURLException;
 import java.net.URL;
+import java.security.NoSuchAlgorithmException;
 import java.security.Principal;
 import java.util.*;
 
@@ -47,6 +50,7 @@
 import org.apache.wiki.log.LoggerFactory;
 import org.apache.wiki.util.TextUtil;
 import org.apache.wiki.util.TimedCounterList;
+import org.freshcookies.security.Keychain;
 
 
 /**
@@ -120,7 +124,8 @@
     protected Class<? extends LoginModule> m_loginModuleClass = UserDatabaseLoginModule.class;
     
     /** Options passed to {@link javax.security.auth.spi.LoginModule#initialize(Subject, CallbackHandler, Map, Map)}; 
-     * initialized by {@link #initialize(WikiEngine, Properties)}. */
+     * initialized by {@link #initialize(WikiEngine, Properties)}. It is initialized once when
+     * {@link #initialize(WikiEngine, Properties)} executes, and is unmodifiable. */
     protected Map<String,String> m_loginModuleOptions = new HashMap<String,String>();
 
     /** Just to provide compatibility with the old versions.  The same
@@ -156,6 +161,45 @@
     private TimedCounterList<String> m_lastLoginAttempts = new TimedCounterList<String>();
     
     /**
+     * The default path to the keychain.
+     */
+    protected static final String DEFAULT_KEYCHAIN_PATH = "keychain";
+    
+    /**
+     * The password for the keychain. If no password is specified, the default
+     * keychain password is used.
+     */
+    public static final String PROP_KEYCHAIN_PASSWORD = "jspwiki.keychainPassword";
+
+    /**
+     * The path to the keychain, if needed for loading. If a fully-qualified
+     * path is not specified, keychain will be assumed to be relative to
+     * WEB-INF/. If the path is not set at all, it will default to
+     * {@link #DEFAULT_KEYSTORE_PATH}.
+     */
+    public static final String PROP_KEYCHAIN_PATH = "jspwiki.keychainPath";
+    
+    /**
+     * Keeps a reference to the keychain.
+     */
+    private Keychain m_keychain = null;
+
+    /**
+     * Keeps a reference to the keychain path.
+     */
+    private String m_keychainPath = null;
+
+    /**
+     * Returns the Keychain object used to store passwords and keying materials.
+     * @return the keychain
+     * @since 3.0
+     */
+    public Keychain getKeychain()
+    {
+        return m_keychain;
+    }
+    
+    /**
      * Creates an AuthenticationManager instance for the given WikiEngine and
      * the specified set of properties. All initialization for the modules is
      * done here.
@@ -201,6 +245,31 @@
         
         // Initialize the LoginModule options
         initLoginModuleOptions( props );
+        
+        // Initialize the keychain
+        m_keychain = new Keychain();
+        String path = props.getProperty( PROP_KEYCHAIN_PATH );
+        path = (path == null || path.trim().length() == 0) ? null : path.trim();
+        if( path == null )
+        {
+            path = "WEB-INF/" + DEFAULT_KEYCHAIN_PATH;
+        }
+        else
+        {
+            File filePath = new File( path );
+            if ( filePath.getParent() == null )
+            {
+                path = "WEB-INF/" + path;
+            }
+        }
+        m_keychainPath = path;
+
+        // Unlock the keychain if password was supplied.
+        String password = props.getProperty( PROP_KEYCHAIN_PASSWORD );
+        if( password != null )
+        {
+            initKeychain( password );
+        }
     }
 
     /**
@@ -569,6 +638,26 @@
     }
 
     /**
+     * Attempts to unlock the keychain using the supplied password. This method
+     * is generally called after JSPWiki starts up by a user-triggered event,
+     * for example an administration page. If the keychain has already been
+     * unlocked, this method does nothing and returns immediately.
+     * 
+     * @param password the password or PIN that protects the underlying keychain
+     */
+    public void unlockKeychain( String password ) throws WikiSecurityException
+    {
+        if( !m_keychain.isLoaded() )
+        {
+            initKeychain( password );
+        }
+        if( !m_keychain.isLoaded() )
+        {
+            throw new WikiSecurityException( "Wrong password." );
+        }
+    }
+    
+    /**
      * Instantiates and executes a single JAAS
      * {@link javax.security.auth.spi.LoginModule}, and returns a Set of
      * Principals that results from a successful login. The LoginModule is instantiated,
@@ -754,6 +843,7 @@
      */
     private void initLoginModuleOptions(Properties props)
     {
+        Map<String,String> options = new HashMap<String,String>();
         for ( Object key : props.keySet() )
         {
             String propName = key.toString();
@@ -770,10 +860,11 @@
                     {
                         throw new IllegalArgumentException( "JAAS LoginModule key " + propName + " cannot be specified twice!" );
                     }
-                    m_loginModuleOptions.put( optionKey, optionValue );
+                    options.put( optionKey, optionValue );
                 }
             }
         }
+        m_loginModuleOptions = Collections.unmodifiableMap( options );
     }
     
     /**
@@ -818,5 +909,48 @@
             }
         }
     }
+    
+    /**
+     * Loads the keychain, using a supplied password to unlock it.
+     * 
+     * @param password the password to unlock the keychain. If <code>null</code>
+     *            the keychain will be loaded without a password.
+     * @throws WikiSecurityException if the keychain could not be located, or if
+     *             the password is incorrect
+     */
+    private void initKeychain( String password ) throws WikiSecurityException
+    {
+        // If the path was supplied, verify that it exists
+        InputStream stream = null;
+        log.debug( "Loading keychain from path " + m_keychainPath );
+        if( m_engine.getServletContext() == null )
+        {
+            ClassLoader cl = WebContainerAuthorizer.class.getClassLoader();
+            stream = cl.getResourceAsStream( m_keychainPath );
+        }
+        else
+        {
+            stream = m_engine.getServletContext().getResourceAsStream( "/" + m_keychainPath );
+        }
+        if( stream == null )
+        {
+            throw new WikiSecurityException( "Unable to find keychain " + m_keychainPath + "." );
+        }
 
+        // Load the keychain
+        char[] passwordChars = password == null ? null : password.toCharArray();
+        try
+        {
+            m_keychain.load( stream, passwordChars );
+        }
+        catch( NoSuchAlgorithmException e )
+        {
+            e.printStackTrace();
+        }
+        catch( IOException e )
+        {
+            // Wrong password! That is ok for now.
+            log.info( "Keychain could not be unlocked; wrong password or PIN." );
+        }
+    }
 }

Modified: incubator/jspwiki/trunk/src/java/org/apache/wiki/auth/authorize/LdapAuthorizer.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/trunk/src/java/org/apache/wiki/auth/authorize/LdapAuthorizer.java?rev=802426&r1=802425&r2=802426&view=diff
==============================================================================
--- incubator/jspwiki/trunk/src/java/org/apache/wiki/auth/authorize/LdapAuthorizer.java (original)
+++ incubator/jspwiki/trunk/src/java/org/apache/wiki/auth/authorize/LdapAuthorizer.java Sat Aug  8 17:53:27 2009
@@ -1,7 +1,12 @@
 package org.apache.wiki.auth.authorize;
 
+import java.security.KeyStore;
+import java.security.KeyStoreException;
 import java.security.Principal;
-import java.util.*;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.Properties;
+import java.util.Set;
 
 import javax.naming.Context;
 import javax.naming.NamingEnumeration;
@@ -18,17 +23,35 @@
 import org.apache.wiki.log.Logger;
 import org.apache.wiki.log.LoggerFactory;
 import org.apache.wiki.util.TextUtil;
+import org.freshcookies.security.Keychain;
 
 /**
  * Authorizer whose Roles are supplied by LDAP groups.
  */
 public class LdapAuthorizer implements Authorizer
 {
-    private Hashtable<String, String> m_env;
+    private Hashtable<String, String> m_jndiEnv;
 
     private String m_roleBase = null;
 
-    private String m_userPattern = null;
+    /**
+     * Finds all roles; based on m_rolePattern
+     */
+    private String m_roleFinder = null;
+
+    private String m_authentication = null;
+
+    private Keychain m_keychain = null;
+
+    private String m_connectionUrl = null;
+
+    private String m_ssl = null;
+
+    private String m_rolePattern = null;
+
+    private String m_userLoginIdPattern = null;
+
+    private String m_bindDN = null;
 
     /**
      * {@inheritDoc}
@@ -37,7 +60,7 @@
     {
         try
         {
-            DirContext ctx = new InitialLdapContext( m_env, null );
+            DirContext ctx = new InitialLdapContext( m_jndiEnv, null );
             SearchControls searchControls = new SearchControls();
             searchControls.setReturningAttributes( new String[0] );
             NamingEnumeration<SearchResult> roles = ctx.search( m_roleBase, "(cn=" + role + ")", searchControls );
@@ -61,10 +84,10 @@
         Set<Role> foundRoles = new HashSet<Role>();
         try
         {
-            DirContext ctx = new InitialLdapContext( m_env, null );
+            DirContext ctx = new InitialLdapContext( m_jndiEnv, null );
             SearchControls searchControls = new SearchControls();
             searchControls.setReturningAttributes( new String[] { "cn" } );
-            NamingEnumeration<SearchResult> roles = ctx.search( m_roleBase, "(objectClass=groupOfUniqueNames)", searchControls );
+            NamingEnumeration<SearchResult> roles = ctx.search( m_roleBase, m_roleFinder, searchControls );
             while ( roles.hasMore() )
             {
                 SearchResult foundRole = roles.next();
@@ -82,6 +105,16 @@
     private static final Logger log = LoggerFactory.getLogger( LdapAuthorizer.class );
 
     /**
+     * Property that specifies the JNDI authentication type. Valid values are
+     * the same as those for {@link Context#SECURITY_AUTHENTICATION}:
+     * <code>none</code>, <code>simple</code>, <code>strong</code> or
+     * <code>DIGEST-MD5</code>. The default is <code>simple</code> if SSL is
+     * specified, and <code>DIGEST-MD5</code> otherwise. This property is also
+     * used by {@link org.apache.wiki.auth.login.LdapLoginModule}.
+     */
+    protected static final String PROPERTY_AUTHENTICATION = "jspwiki.loginModule.options.ldap.authentication";
+
+    /**
      * Property that supplies the connection URL for the LDAP server, e.g.
      * <code>ldap://127.0.0.1:4890/</code>. This property is also used by
      * {@link org.apache.wiki.auth.login.LdapLoginModule}.
@@ -89,6 +122,26 @@
     protected static final String PROPERTY_CONNECTION_URL = "jspwiki.loginModule.options.ldap.connectionURL";
 
     /**
+     * Property that supplies the DN used to bind to the directory when looking
+     * up users and roles.
+     */
+    protected static final String PROPERTY_BIND_DN = "jspwiki.ldap.bindDN";
+
+    /**
+     * Property that supplies the base DN where roles are contained, e.g.
+     * <code>ou=roles,dc=jspwiki,dc=org</code>.
+     */
+    protected static final String PROPERTY_ROLE_BASE = "jspwiki.ldap.roleBase";
+
+    /**
+     * Property that supplies the pattern for finding users within the role
+     * base, e.g.
+     * <code>(&(objectClass=groupOfUniqueNames)(cn={0})(uniqueMember={1}))</code>
+     * .
+     */
+    protected static final String PROPERTY_ROLE_PATTERN = "jspwiki.ldap.rolePattern";
+
+    /**
      * Property that indicates whether to use SSL for connecting to the LDAP
      * server. This property is also used by
      * {@link org.apache.wiki.auth.login.LdapLoginModule}.
@@ -96,71 +149,65 @@
     protected static final String PROPERTY_SSL = "jspwiki.loginModule.options.ldap.ssl";
 
     /**
-     * Property that supplies the DN pattern for finding users, e.g.
-     * <code>uid={0},ou=people,dc=jspwiki,dc=org</code> This property is also
+     * Property that specifies the pattern for the username used to log in to
+     * the LDAP server. Usually this is a full DN, for example
+     * <code>uid={0},ou=people,dc=jspwiki,dc=org</code>. However, sometimes (as
+     * with Active Directory 2003 and later) only the userid is used, in which
+     * case the principal will simply be <code>{0}</code>. This property is also
      * used by {@link org.apache.wiki.auth.login.LdapLoginModule}.
      */
-    protected static final String PROPERTY_USER_PATTERN = "jspwiki.loginModule.options.ldap.userPattern";
+    protected static final String PROPERTY_LOGIN_ID_PATTERN = "jspwiki.loginModule.options.ldap.userPattern";
 
-    /**
-     * Property that supplies the DN pattern for finding roles, e.g.
-     * <code>ou=roles,dc=jspwiki,dc=org</code>
-     */
-    protected static final String PROPERTY_ROLE_BASE = "jspwiki.ldap.roleBase";
+    private static final String[] REQUIRED_PROPERTIES = new String[] { PROPERTY_CONNECTION_URL, PROPERTY_LOGIN_ID_PATTERN,
+                                                                      PROPERTY_ROLE_BASE, PROPERTY_ROLE_PATTERN };
+
+    protected static final String KEYCHAIN_BIND_DN_ENTRY = "LdapAuthorizer.BindDN.Password";
 
     /**
      * {@inheritDoc}
      */
     public void initialize( WikiEngine engine, Properties props ) throws WikiSecurityException
     {
-        Hashtable<String, String> env = new Hashtable<String, String>();
-        env.put( Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory" );
-        env.put( Context.SECURITY_AUTHENTICATION, "none" );
-
-        // LDAP server to search
-        String option = (String) props.get( PROPERTY_CONNECTION_URL );
-        if( option != null && option.trim().length() > 0 )
-        {
-            env.put( Context.PROVIDER_URL, option.trim() );
-        }
-
-        // Use SSL?
-        option = (String) props.get( PROPERTY_SSL );
-        boolean ssl = TextUtil.isPositive( option );
-        env.put( Context.SECURITY_PROTOCOL, ssl ? "ssl" : "none" );
-        m_env = env;
-
-        if( log.isDebugEnabled() )
+        // Make sure all required properties are here
+        for( String prop : REQUIRED_PROPERTIES )
         {
-            log.debug( "Built JNDI environment for LDAP search.", m_env );
+            if( !props.containsKey( prop ) )
+            {
+                throw new WikiSecurityException( "Property " + prop + " is required!" );
+            }
         }
 
-        // DN pattern for finding users
-        option = (String) props.get( PROPERTY_USER_PATTERN );
-        if( option != null && option.trim().length() > 0 )
+        // Figure out LDAP environment settings
+        m_connectionUrl = props.getProperty( PROPERTY_CONNECTION_URL ).trim();
+        m_userLoginIdPattern = props.getProperty( PROPERTY_LOGIN_ID_PATTERN ).trim();
+        m_roleBase = props.getProperty( PROPERTY_ROLE_BASE ).trim();
+        m_rolePattern = props.getProperty( PROPERTY_ROLE_PATTERN ).trim();
+        m_roleFinder = m_rolePattern.replaceAll( "\\{[0-1]\\}", "\\*" );
+        m_keychain = engine.getAuthenticationManager().getKeychain();
+
+        // Figure out optional properties
+        String ssl = (String) props.get( PROPERTY_SSL );
+        m_ssl = (ssl != null && TextUtil.isPositive( ssl )) ? "ssl" : "none";
+        String authentication = (String) props.get( PROPERTY_AUTHENTICATION );
+        if( authentication == null || authentication.length() == 0 )
         {
-            m_userPattern = option.trim();
+            m_authentication = "ssl".equals( m_ssl ) ? "simple" : "DIGEST-MD5";
         }
         else
         {
-            throw new WikiSecurityException( PROPERTY_USER_PATTERN + " not supplied." );
+            m_authentication = authentication;
         }
-
-        // DN pattern for finding roles
-        option = (String) props.get( PROPERTY_ROLE_BASE );
-        if( option != null && option.trim().length() > 0 )
-        {
-            m_roleBase = option.trim();
-        }
-        else
+        String bindDN = props.getProperty( PROPERTY_BIND_DN );
+        if( bindDN != null && bindDN.length() > 0 )
         {
-            throw new WikiSecurityException( PROPERTY_ROLE_BASE + " not supplied." );
+            m_bindDN = bindDN.trim();
         }
-        
+
         // Do a quick connection test, and fail-fast if needed
+        buildJndiEnvironment();
         try
         {
-            new InitialLdapContext( m_env, null );
+            new InitialLdapContext( m_jndiEnv, null );
         }
         catch( NamingException e )
         {
@@ -168,6 +215,43 @@
         }
     }
 
+    private void buildJndiEnvironment() throws WikiSecurityException
+    {
+        Hashtable<String, String> env = new Hashtable<String, String>();
+        env.put( Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory" );
+        env.put( Context.PROVIDER_URL, m_connectionUrl );
+        env.put( Context.SECURITY_PROTOCOL, m_ssl );
+        env.put( Context.SECURITY_AUTHENTICATION, m_authentication );
+
+        // If we need and Bind DN and Keychain is loaded, get the bind DN and
+        // password
+        if( m_bindDN != null && m_keychain.isLoaded() )
+        {
+            try
+            {
+                KeyStore.Entry password = m_keychain.getEntry( KEYCHAIN_BIND_DN_ENTRY );
+                if( password instanceof Keychain.Password )
+                {
+                    env.put( Context.SECURITY_PRINCIPAL, m_bindDN );
+                    env.put( Context.SECURITY_CREDENTIALS, ((Keychain.Password) password).getPassword() );
+                }
+            }
+            catch( KeyStoreException e )
+            {
+                e.printStackTrace();
+                throw new WikiSecurityException( "Could not build JNDI environment. ", e );
+            }
+        }
+
+        // Spill all the information if debugging
+        if( log.isDebugEnabled() )
+        {
+            log.debug( "Built JNDI environment for LDAP login.", env );
+        }
+
+        m_jndiEnv = env;
+    }
+
     /**
      * {@inheritDoc}
      * <p>
@@ -176,7 +260,7 @@
      * an LDAP group contained in the role-base DN. The user DN is constructed
      * from the user Principal, where the <code>uid</code> of the user's DN is
      * the Principal name, and the rest of the DN is determined by
-     * {@link #PROPERTY_USER_PATTERN}.
+     * {@link #PROPERTY_LOGIN_ID_PATTERN}.
      * </p>
      * <p>
      * To make an accurate search, Principals that are of type {@link Role},
@@ -211,15 +295,17 @@
     {
         // Build DN
         String uid = session.getLoginPrincipal().getName();
-        String dn = m_userPattern.replace( "{0}", uid ).trim();
+        String dn = m_userLoginIdPattern.replace( "{0}", uid ).trim();
         dn = dn.replace( "=", "\\3D" );
 
         try
         {
-            DirContext ctx = new InitialLdapContext( m_env, null );
+            DirContext ctx = new InitialLdapContext( m_jndiEnv, null );
             SearchControls searchControls = new SearchControls();
+            searchControls.setSearchScope( SearchControls.SUBTREE_SCOPE );
             searchControls.setReturningAttributes( new String[0] );
-            String filter = "(&(objectClass=groupOfUniqueNames)(cn=" + role.getName() + ")(uniqueMember=" + dn + "))";
+            String filter = m_rolePattern.replace( "{0}", role.getName() );
+            filter = filter.replace( "{1}", dn );
             NamingEnumeration<SearchResult> roles = ctx.search( m_roleBase, filter, searchControls );
             return roles.hasMore();
         }

Modified: incubator/jspwiki/trunk/src/java/org/apache/wiki/auth/login/LdapLoginModule.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/trunk/src/java/org/apache/wiki/auth/login/LdapLoginModule.java?rev=802426&r1=802425&r2=802426&view=diff
==============================================================================
--- incubator/jspwiki/trunk/src/java/org/apache/wiki/auth/login/LdapLoginModule.java (original)
+++ incubator/jspwiki/trunk/src/java/org/apache/wiki/auth/login/LdapLoginModule.java Sat Aug  8 17:53:27 2009
@@ -22,10 +22,17 @@
 
 import java.io.IOException;
 import java.security.Principal;
-import java.util.*;
+import java.util.Hashtable;
+import java.util.Map;
 
-import javax.naming.*;
-import javax.naming.directory.*;
+import javax.naming.AuthenticationException;
+import javax.naming.Context;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.DirContext;
+import javax.naming.directory.SearchControls;
+import javax.naming.directory.SearchResult;
 import javax.naming.ldap.InitialLdapContext;
 import javax.security.auth.callback.Callback;
 import javax.security.auth.callback.NameCallback;
@@ -35,7 +42,6 @@
 import javax.security.auth.login.LoginException;
 
 import org.apache.wiki.auth.WikiPrincipal;
-import org.apache.wiki.auth.authorize.Role;
 import org.apache.wiki.i18n.InternationalizationManager;
 import org.apache.wiki.log.Logger;
 import org.apache.wiki.log.LoggerFactory;
@@ -50,20 +56,40 @@
  * the following key/value pairs:
  * </p>
  * <ul>
- * <li>{@link #OPTIONS_CONNECTION_URL} - the connection string for the LDAP
- * server, for example <code>ldap://ldap.jspwiki.org:389/</code></li>
- * <li>{@link #OPTIONS_USER_PATTERN} - the pattern used for forming the
- * distinguished name for the user. The user ID supplied during the login will
- * be substituted into the <code>{0}</code> token in this pattern, if it
- * contains one. For example, if the user pattern is
- * <code>uid={0},ou=people,dc=jspwiki,dc=org</code> and the user name supplied
- * during login is <code>fflintstone</code>, the DN that will be used for
- * authentication will be
- * <code>uid=fflintstone,ou=people,dc=jspwiki,dc=org</code></li>
- * <li>{@link #OPTIONS_SSL} - Optional parameter that specifies whether to use
+ * <li>{@link #OPTION_CONNECTION_URL} - the connection string for the LDAP
+ * server, for example <code>ldap://ldap.jspwiki.org:389/</code>.</li>
+ * <li>{@link #OPTION_LOGIN_ID_PATTERN} - a string pattern indicating how the
+ * login id should be formatted into a credential the LDAP server will
+ * understand. The exact credential pattern varies by LDAP server. OpenLDAP
+ * expects login IDs that match a distinguished name. Active Directory, on the
+ * other hand, requires just the "short" login ID that is not in DN format. The
+ * user ID supplied during the login will be substituted into the
+ * <code>{0}</code> token in this pattern. Valid examples of login ID patterns
+ * include <code>uid={0},ou=users,dc=jspwiki,dc=org</code> (for OpenLDAP) and
+ * <code>{0}</code> (for Active Directory).</li>
+ * <li>{@link #OPTION_USER_BASE} - the distinguished name of the base location
+ * where user objects are located. This is generally an organizational unit (OU)
+ * DN, such as <code>ou=people,dc=jspwiki,dc=org</code>. The user base and all
+ * of its subtrees will be searched.</li>
+ * <li>{@link #OPTION_USER_PATTERN} - an RFC 2254 search filter string used for
+ * locating the actual user object within the user base. The user ID supplied
+ * during the login will be substituted into the <code>{0}</code> token in this
+ * pattern, if it contains one. Only the first match will be selected, so it is
+ * important that this pattern selects unique objects. For example, if the user
+ * pattern is <code>(&(objectClass=inetOrgPerson)(uid={0}))</code> and the user
+ * name supplied during login is <code>fflintstone</code>, the the first object
+ * within {@link #OPTION_USER_BASE} that matches the filter
+ * <code>(&(objectClass=inetOrgPerson)(uid={0}))</code> will be selected.</li>
+ * <li>{@link #OPTION_SSL} - Optional parameter that specifies whether to use
  * SSL when connecting to the LDAP server. Values like <code>true</code> or
  * <code>on</code> indicate that SSL should be used. If this parameter is not
  * supplied, SSL will not be used.</li>
+ * <li>{@link #OPTION_AUTHENTICATION} - Optional parameter that specifies the
+ * type of authentication method to be used. Valid values include
+ * <code>simple</code> for plaintext username/password, and
+ * <code>DIGEST-MD5</code> for digested passwords. Note that if SSL is not used,
+ * for safety reasons this method will default to <code>DIGEST-MD5</code> to
+ * prevent password interception.</li>
  * </ul>
  * <p>
  * If this LoginModule is used with a system-wide JAAS configuration (with or
@@ -73,9 +99,9 @@
  * <code>jspwiki.properties</code>. In this case, each option key must be
  * prefixed by <code>jspwiki.loginModule.options.</code> to indicate that the
  * values are, in fact, JAAS configuration items. Thus, the option
- * {@link #OPTIONS_CONNECTION_URL} would be configured in
+ * {@link #OPTION_CONNECTION_URL} would be configured in
  * <code>jspwiki.properties</code> using the key/value pair
- * <code>jspwiki.loginModule.options.{@value #OPTIONS_CONNECTION_URL}
+ * <code>jspwiki.loginModule.options.{@value #OPTION_CONNECTION_URL}
  * = ldap://ldap.jspwiki.org:389/</code>.
  * </p>
  * 
@@ -91,25 +117,64 @@
     private static final Principal[] NO_PRINCIPALS = new Principal[0];
 
     /**
-     * JAAS option that supplies the connection URL for the LDAP server, e.g.
-     * ldap://127.0.0.1:4890/
+     * JAAS option that specifies the JNDI authentication type. Valid values are
+     * the same as those for {@link Context#SECURITY_AUTHENTICATION}:
+     * <code>none</code>, <code>simple</code>, <code>strong</code> or
+     * <code>DIGEST-MD5</code>. The default is <code>simple</code> if SSL
+     * is specified, and <code>DIGEST-MD5</code> otherwise.
      */
-    protected static final String OPTIONS_CONNECTION_URL = "ldap.connectionURL";
+    protected static final String OPTION_AUTHENTICATION = "ldap.authentication";
 
     /**
-     * JAAS option that supplies the DN pattern for finding users, e.g.
-     * uid={0},ou=people,dc=jspwiki,dc=org
+     * JAAS option that specifies the JNDI connection URL for the LDAP server,
+     * <em>e.g.,</em> <code>ldap://127.0.0.1:4890/</code>
      */
-    protected static final String OPTIONS_USER_PATTERN = "ldap.userPattern";
+    protected static final String OPTION_CONNECTION_URL = "ldap.connectionURL";
 
     /**
-     * JAAS option that indicates whether to use SSL for connecting to the LDAP
-     * server.
+     * JAAS option that specifies the base DN where users are contained,
+     * <em>e.g.,</em> <code>ou=people,dc=jspwiki,dc=org</code>. This DN is
+     * searched recursively.
      */
-    protected static final String OPTIONS_SSL = "ldap.ssl";
+    protected static final String OPTION_USER_BASE = "ldap.userBase";
+
+    /**
+     * JAAS option that specifies the DN pattern for finding users within the
+     * user base, <em>e.g.,</em>
+     * <code>(&(objectClass=inetOrgPerson)(uid={0}))</code>
+     */
+    protected static final String OPTION_USER_PATTERN = "ldap.userPattern";
+
+    /**
+     * JAAS option specifies the pattern for the username used to log in to the
+     * LDAP server. Usually this is a full DN, for example
+     * <code>uid={0},ou=people,dc=jspwiki,dc=org</code>. However, sometimes (as
+     * with Active Directory 2003 and later) only the userid is used, in which
+     * case the principal will simply be <code>{0}</code>.
+     */
+    protected static final String OPTION_LOGIN_ID_PATTERN = "ldap.loginIdPattern";
+
+    /**
+     * JAAS option specifies that indicates whether to use SSL for connecting to
+     * the LDAP server.
+     */
+    protected static final String OPTION_SSL = "ldap.ssl";
+
+    private static final String[] REQUIRED_OPTIONS = new String[] { OPTION_AUTHENTICATION, OPTION_CONNECTION_URL,
+                                                                   OPTION_LOGIN_ID_PATTERN, OPTION_USER_BASE,
+                                                                   OPTION_USER_PATTERN };
 
     public boolean login() throws LoginException
     {
+        // Make sure all required properties are here
+        for( String option : REQUIRED_OPTIONS )
+        {
+            if( !m_options.containsKey( option ) )
+            {
+                throw new LoginException( "Option " + option + " is required!" );
+            }
+        }
+
         // Retrieve the essential callbacks: username and password
         String username;
         String password;
@@ -140,18 +205,18 @@
         {
             // Log in
             Hashtable<String, String> env = buildJndiEnvironment( username, password );
-            String dn = env.get( Context.SECURITY_PRINCIPAL );
+            String loginId = env.get( Context.SECURITY_PRINCIPAL );
             DirContext ctx = new InitialLdapContext( env, null );
 
             // If login succeeds, commit the login principal
             m_principals.add( new WikiPrincipal( username, WikiPrincipal.LOGIN_NAME ) );
             if( log.isDebugEnabled() )
             {
-                log.debug( "Logged in user " + username + " with LDAP DN " + dn );
+                log.debug( "Logged in user " + username + " with LDAP DN " + loginId );
             }
 
             // Also look up the full name (and make the wiki name out of it)
-            Principal[] principals = extractNamePrincipals( ctx, dn );
+            Principal[] principals = extractNamePrincipals( ctx, username );
             for( Principal principal : principals )
             {
                 m_principals.add( principal );
@@ -176,8 +241,7 @@
      * server. The hashtable is built using information extracted from the
      * options map supplied to the LoginModule via
      * {@link #initialize(javax.security.auth.Subject, javax.security.auth.callback.CallbackHandler, Map, Map)}
-     * . The options map supplies the LDAP connection URL and SSL handling flag;
-     * the username and password parameters supply the LDAP credentials.
+     * . The username and password parameters supply the LDAP credentials.
      * 
      * @param username the user's distinguished name (DN), used for
      *            authentication
@@ -188,29 +252,31 @@
     {
         Hashtable<String, String> env = new Hashtable<String, String>();
         env.put( Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory" );
-        env.put( Context.SECURITY_AUTHENTICATION, "simple" );
-        env.put( Context.SECURITY_CREDENTIALS, password );
-
-        // LDAP server to search
-        String option = (String) m_options.get( OPTIONS_CONNECTION_URL );
-        if( option != null && option.trim().length() > 0 )
-        {
-            env.put( Context.PROVIDER_URL, option.trim() );
-        }
 
-        // DN pattern for finding users
-        option = (String) m_options.get( OPTIONS_USER_PATTERN );
-        if( option != null && option.trim().length() > 0 )
-        {
-            option = option.replace( "{0}", username ).trim();
-            env.put( Context.SECURITY_PRINCIPAL, option );
-        }
+        // LDAP server to authenticate to
+        String option = (String) m_options.get( OPTION_CONNECTION_URL );
+        env.put( Context.PROVIDER_URL, option );
+
+        // Compose the username (credentials) and password
+        option = (String) m_options.get( OPTION_LOGIN_ID_PATTERN );
+        option = option.replace( "{0}", username );
+        env.put( Context.SECURITY_PRINCIPAL, option );
+        env.put( Context.SECURITY_CREDENTIALS, password );
 
         // Use SSL?
-        option = (String) m_options.get( OPTIONS_SSL );
+        option = (String) m_options.get( OPTION_SSL );
         boolean ssl = TextUtil.isPositive( option );
         env.put( Context.SECURITY_PROTOCOL, ssl ? "ssl" : "none" );
 
+        // Authentication type (simple, DIGEST-MD5, etc)
+        option = (String) m_options.get( OPTION_AUTHENTICATION );
+        if( option == null )
+        {
+            option = ssl ? "simple" : "DIGEST-MD5";
+        }
+        env.put( Context.SECURITY_AUTHENTICATION, option );
+
+        // Spill all the information if debugging
         if( log.isDebugEnabled() )
         {
             log.debug( "Built JNDI environment for LDAP login.", env );
@@ -220,75 +286,65 @@
     }
 
     /**
-     * Looks up the user at a supplied DN and returns an array of WikiPrincipals
-     * whose values are equal to the node's common name (cn) attribute, and a
-     * trimmed version without spaces. If no user is found this method will
-     * return a zero-length array.
+     * Looks up the user in the user base and returns an array of WikiPrincipals
+     * representing the full name and wiki name. The full name will be equal to
+     * the user object's first name (givenName) + last name (sn) attributes,
+     * separated by a space, <em>or</em> the user object's common name (cn)
+     * attribute. The wiki name is simply the full name with all spaces removed.
+     * If no user is found this method will return a zero-length array.
      * 
      * @param ctx the previously initialized LDAP context
-     * @param dn the distinguished name
-     * @return the principal
+     * @param username the user name
+     * @return the principals
      * @throws NamingException if anything goes wrong for any reason
      */
-    protected Principal[] extractNamePrincipals( DirContext ctx, String dn ) throws NamingException
+    protected Principal[] extractNamePrincipals( DirContext ctx, String username ) throws NamingException
     {
-        // Look up the user in the current LDAP context
-        Attributes attributes = ctx.getAttributes( dn, new String[] { "cn" } );
-        Attribute attribute = attributes.get( "cn" );
-        if( attribute != null && attribute.size() > 0 )
-        {
-            String fullName = attribute.get( 0 ).toString();
-            Principal[] principals = new Principal[2];
-            
+        // Create the search scope
+        String userBase = (String) m_options.get( OPTION_USER_BASE );
+        String userFinder = (String) m_options.get( OPTION_USER_PATTERN );
+        userFinder = userFinder.replace( "{0}", username );
+        SearchControls searchControls = new SearchControls();
+        searchControls.setSearchScope( SearchControls.SUBTREE_SCOPE );
+        searchControls.setReturningAttributes( new String[] { "cn", "givenName", "sn" } );
+
+        // Find the user
+        NamingEnumeration<SearchResult> users = ctx.search( userBase, userFinder, searchControls );
+        if( users.hasMore() )
+        {
+            // Look up the user in the current LDAP context
+            Attributes attributes = users.next().getAttributes();
+
+            // Figure out name to use. Prefer piecing together the first + last
+            // names to CN
+            boolean hasCommonName = attributes.get( "cn" ) != null;
+            boolean hasSurname = attributes.get( "sn" ) != null;
+            boolean hasGivenName = attributes.get( "givenName" ) != null;
+
+            String fullName = null;
+            if( hasGivenName && hasSurname )
+            {
+                fullName = attributes.get( "givenName" ).get( 0 ) + " " + attributes.get( "sn" ).get( 0 );
+            }
+            else if( hasCommonName )
+            {
+                fullName = attributes.get( "cn" ).get( 0 ).toString();
+            }
+            else
+            {
+                throw new NamingException( "User " + username + " did not have a givenName+sn or cn" );
+            }
+
+            // Build the wiki principals
             // FIXME: This should be sanitized better.
+            Principal[] principals = new Principal[2];
             String wikiName = fullName.indexOf( ' ' ) == -1 ? fullName : fullName.replace( " ", "" );
-            
             principals[0] = new WikiPrincipal( fullName, WikiPrincipal.FULL_NAME );
             principals[1] = new WikiPrincipal( wikiName, WikiPrincipal.WIKI_NAME );
             return principals;
         }
+
         return NO_PRINCIPALS;
     }
 
-    protected Map<Role, Set<Principal>> getRoles( String ldapUrl )
-    {
-        String roleSearch = "ou=roles,dc=jspwiki,dc=org";
-        Map<Role, Set<Principal>> roleMap = new HashMap<Role, Set<Principal>>();
-        try
-        {
-            InitialContext iCtx = new InitialContext();
-            DirContext ctx = (DirContext) iCtx.lookup( ldapUrl );
-            SearchControls controls = new SearchControls();
-            NamingEnumeration<SearchResult> ne = ctx.search( roleSearch, "(objectClass=groupOfUniqueNames)", controls );
-            while ( ne.hasMore() )
-            {
-                SearchResult result = ne.next();
-                Attributes attributes = result.getAttributes();
-
-                // Get role name
-                String role = attributes.get( "cn" ).get().toString();
-
-                // Build role membership
-                Attribute attribute = attributes.get( "uniqueMember" );
-                Set<Principal> members = new HashSet<Principal>();
-                if( attribute != null )
-                {
-                    for( int i = 0; i < attribute.size(); i++ )
-                    {
-                        Principal[] principals = extractNamePrincipals( ctx, attribute.get( i ).toString() );
-                        for( Principal principal : principals )
-                        {
-                            members.add( principal );
-                        }
-                    }
-                }
-                roleMap.put( new Role( role ), members );
-            }
-        }
-        catch( NamingException e )
-        {
-            e.printStackTrace();
-        }
-        return roleMap;
-    }
 }

Modified: incubator/jspwiki/trunk/tests/java/org/apache/wiki/auth/AllTests.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/trunk/tests/java/org/apache/wiki/auth/AllTests.java?rev=802426&r1=802425&r2=802426&view=diff
==============================================================================
--- incubator/jspwiki/trunk/tests/java/org/apache/wiki/auth/AllTests.java (original)
+++ incubator/jspwiki/trunk/tests/java/org/apache/wiki/auth/AllTests.java Sat Aug  8 17:53:27 2009
@@ -44,7 +44,7 @@
         suite.addTest( org.apache.wiki.auth.login.AllTests.suite() );
         suite.addTest( org.apache.wiki.auth.permissions.AllTests.suite() );
         suite.addTest( org.apache.wiki.auth.user.AllTests.suite() );
-        suite.addTestSuite( org.apache.wiki.auth.UserManagerTest.class );
+        suite.addTestSuite( UserManagerTest.class );
         
         return suite;
     }

Modified: incubator/jspwiki/trunk/tests/java/org/apache/wiki/auth/authorize/LdapAuthorizerTest.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/trunk/tests/java/org/apache/wiki/auth/authorize/LdapAuthorizerTest.java?rev=802426&r1=802425&r2=802426&view=diff
==============================================================================
--- incubator/jspwiki/trunk/tests/java/org/apache/wiki/auth/authorize/LdapAuthorizerTest.java (original)
+++ incubator/jspwiki/trunk/tests/java/org/apache/wiki/auth/authorize/LdapAuthorizerTest.java Sat Aug  8 17:53:27 2009
@@ -20,6 +20,9 @@
  */
 package org.apache.wiki.auth.authorize;
 
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.OutputStream;
 import java.security.Principal;
 import java.util.HashMap;
 import java.util.Map;
@@ -29,8 +32,10 @@
 
 import org.apache.wiki.TestEngine;
 import org.apache.wiki.WikiSession;
+import org.apache.wiki.auth.AuthenticationManager;
 import org.apache.wiki.auth.AuthorizationManager;
 import org.apache.wiki.auth.Authorizer;
+import org.freshcookies.security.Keychain;
 
 /**
  * @author Andrew Jaquith
@@ -39,9 +44,25 @@
 {
     private Map<String, String> m_options;
 
-    protected void setUp()
+    protected void setUp() throws Exception
     {
         m_options = new HashMap<String, String>();
+        m_options.put( LdapAuthorizer.PROPERTY_CONNECTION_URL, "ldap://127.0.0.1:4890" );
+        m_options.put( LdapAuthorizer.PROPERTY_LOGIN_ID_PATTERN, "uid={0},ou=people,dc=jspwiki,dc=org" );
+        m_options.put( LdapAuthorizer.PROPERTY_ROLE_BASE, "ou=roles,dc=jspwiki,dc=org" );
+        m_options.put( LdapAuthorizer.PROPERTY_ROLE_PATTERN, "(&(objectClass=groupOfUniqueNames)(cn={0})(uniqueMember={1}))" );
+        m_options.put( LdapAuthorizer.PROPERTY_SSL, "false" );
+        m_options.put( LdapAuthorizer.PROPERTY_AUTHENTICATION, "simple" );
+        m_options.put( LdapAuthorizer.PROPERTY_BIND_DN, "uid=Fred,ou=people,dc=jspwiki,dc=org" );
+        
+        // Create the Keychain
+        Keychain keychain = new Keychain();
+        keychain.load( null, "keychain-password".toCharArray() );
+        Keychain.Password password = new Keychain.Password( "password" );
+        keychain.setEntry( LdapAuthorizer.KEYCHAIN_BIND_DN_ENTRY, password );
+        File file = new File("tests/etc/WEB-INF/test-keychain" );
+        OutputStream stream = new FileOutputStream( file );
+        keychain.store( stream, "keychain-password".toCharArray() );
     }
 
     /**
@@ -53,15 +74,14 @@
         props.load( TestEngine.findTestProperties() );
         props.putAll( config );
         props.put( AuthorizationManager.PROP_AUTHORIZER, LdapAuthorizer.class.getCanonicalName() );
+        props.put( AuthenticationManager.PROP_KEYCHAIN_PATH, "test-keychain" );
+        props.put( AuthenticationManager.PROP_KEYCHAIN_PASSWORD, "keychain-password" );
         TestEngine engine = new TestEngine( props );
         return engine;
     }
 
     public void testGetRoles() throws Exception
     {
-        m_options.put( LdapAuthorizer.PROPERTY_CONNECTION_URL, "ldap://127.0.0.1:4890" );
-        m_options.put( LdapAuthorizer.PROPERTY_ROLE_BASE, "ou=roles,dc=jspwiki,dc=org" );
-        m_options.put( LdapAuthorizer.PROPERTY_USER_PATTERN, "uid={0},ou=people,dc=jspwiki,dc=org" );
         Authorizer authorizer = createEngine( m_options ).getAuthorizationManager().getAuthorizer();
 
         // LDAP should return just 2 roles, Admin and Role1
@@ -75,9 +95,6 @@
 
     public void testFindRole() throws Exception
     {
-        m_options.put( LdapAuthorizer.PROPERTY_CONNECTION_URL, "ldap://127.0.0.1:4890" );
-        m_options.put( LdapAuthorizer.PROPERTY_ROLE_BASE, "ou=roles,dc=jspwiki,dc=org" );
-        m_options.put( LdapAuthorizer.PROPERTY_USER_PATTERN, "uid={0},ou=people,dc=jspwiki,dc=org" );
         Authorizer authorizer = createEngine( m_options ).getAuthorizationManager().getAuthorizer();
         
         // We should be able to find roles Admin and Role1
@@ -90,9 +107,6 @@
 
     public void testIsUserInRole() throws Exception
     {
-        m_options.put( LdapAuthorizer.PROPERTY_CONNECTION_URL, "ldap://127.0.0.1:4890" );
-        m_options.put( LdapAuthorizer.PROPERTY_ROLE_BASE, "ou=roles,dc=jspwiki,dc=org" );
-        m_options.put( LdapAuthorizer.PROPERTY_USER_PATTERN, "uid={0},ou=people,dc=jspwiki,dc=org" );
         TestEngine engine = createEngine( m_options );
         Authorizer authorizer = engine.getAuthorizationManager().getAuthorizer();
         Role admin = new Role( "Admin" );

Modified: incubator/jspwiki/trunk/tests/java/org/apache/wiki/auth/login/LdapLoginModuleTest.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/trunk/tests/java/org/apache/wiki/auth/login/LdapLoginModuleTest.java?rev=802426&r1=802425&r2=802426&view=diff
==============================================================================
--- incubator/jspwiki/trunk/tests/java/org/apache/wiki/auth/login/LdapLoginModuleTest.java (original)
+++ incubator/jspwiki/trunk/tests/java/org/apache/wiki/auth/login/LdapLoginModuleTest.java Sat Aug  8 17:53:27 2009
@@ -40,12 +40,15 @@
  */
 public class LdapLoginModuleTest extends TestCase
 {
-    static final Map<String,String> m_options;
+    private Map<String,String> m_options = null;
     
-    static {
+    public void setUp() {
         m_options = new HashMap<String, String>();
-        m_options.put( LdapLoginModule.OPTIONS_CONNECTION_URL, "ldap://127.0.0.1:4890" );
-        m_options.put( LdapLoginModule.OPTIONS_USER_PATTERN, "uid={0},ou=people,dc=jspwiki,dc=org" );
+        m_options.put( LdapLoginModule.OPTION_CONNECTION_URL, "ldap://127.0.0.1:4890" );
+        m_options.put( LdapLoginModule.OPTION_LOGIN_ID_PATTERN, "uid={0},ou=people,dc=jspwiki,dc=org" );
+        m_options.put( LdapLoginModule.OPTION_USER_BASE, "ou=people,dc=jspwiki,dc=org" );
+        m_options.put( LdapLoginModule.OPTION_USER_PATTERN, "(&(objectClass=inetOrgPerson)(uid={0}))" );
+        m_options.put( LdapLoginModule.OPTION_AUTHENTICATION, "simple" );
     }
 
     public final void testLoginNonExistentUser() throws Exception
@@ -93,6 +96,61 @@
         assertFalse( principals.contains( Role.ALL ) );
     }
 
+    public final void testLoginFullname() throws Exception
+    {
+        // Login with a user that has both a surname and given name
+        Subject subject = new Subject();
+        CallbackHandler handler = new WikiCallbackHandler( null, null, "Fred", "password" );
+        LoginModule module = new LdapLoginModule();
+        module.initialize( subject, handler, new HashMap<String, Object>(), m_options );
+        module.login();
+        module.commit();
+        
+        // Successful login will inject the usual LoginPrincipal
+        Set<Principal> principals = subject.getPrincipals();
+        assertEquals( 3, principals.size() );
+        assertTrue( principals.contains( new WikiPrincipal( "Fred", WikiPrincipal.LOGIN_NAME ) ) );
+        
+        // PLUS, in this case only, principals for Wiki Name and Full Name
+        // NOTE that because Fred has a first name + last name, this is preferred
+        // to the common name of "Flintstone, Fred"
+        assertTrue( principals.contains( new WikiPrincipal( "Fred Flintstone", WikiPrincipal.FULL_NAME ) ) );
+        assertTrue( principals.contains( new WikiPrincipal( "FredFlintstone", WikiPrincipal.WIKI_NAME ) ) );
+    }
+    
+    public static final void main( String... args ) throws Exception
+    {
+        LdapLoginModuleTest t = new LdapLoginModuleTest();
+
+        t.m_options.clear();
+        t.m_options.put( LdapLoginModule.OPTION_AUTHENTICATION, "DIGEST-MD5" );
+        t.m_options.put( LdapLoginModule.OPTION_CONNECTION_URL, "ldap://camb-dc01.forrester.loc:389" );
+        t.m_options.put( LdapLoginModule.OPTION_LOGIN_ID_PATTERN, "(uid={0})" );
+        t.m_options.put( LdapLoginModule.OPTION_USER_BASE, "OU=users,OU=Cambridge,OU=Office Locations,OU=forrester,DC=forrester,DC=loc" );
+        t.m_options.put( LdapLoginModule.OPTION_USER_PATTERN, "(&(objectClass=person)(mailNickname={0}))" );
+        
+        // Login with a user that IS in the database
+        Subject subject = new Subject();
+        CallbackHandler handler = new WikiCallbackHandler( null, null, "ajaquith", "****" );
+        LoginModule module = new LdapLoginModule();
+        module.initialize( subject, handler, new HashMap<String, Object>(), t.m_options );
+        module.login();
+        module.commit();
+        
+        // Successful login will inject the usual LoginPrincipal
+        Set<Principal> principals = subject.getPrincipals();
+        assertEquals( 3, principals.size() );
+        assertTrue( principals.contains( new WikiPrincipal( "ajaquith", WikiPrincipal.LOGIN_NAME ) ) );
+        
+        // PLUS, in this case only, principals for Wiki Name and Full Name
+        assertTrue( principals.contains( new WikiPrincipal( "Andrew Jaquith", WikiPrincipal.FULL_NAME ) ) );
+        assertTrue( principals.contains( new WikiPrincipal( "AndrewJaquith", WikiPrincipal.WIKI_NAME ) ) );
+        
+        // AuthenticationManager, NOT the LoginModule, adds the Role principals
+        assertFalse( principals.contains( Role.AUTHENTICATED ) );
+        assertFalse( principals.contains( Role.ALL ) );
+    }
+    
     public final void testLogout() throws Exception
     {
         Subject subject = new Subject();