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/15 13:53:38 UTC

svn commit: r804457 [1/2] - in /incubator/jspwiki/trunk: ./ etc/ etc/ldap/ src/WebContent/templates/default/ 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/ src/java...

Author: ajaquith
Date: Sat Aug 15 11:53:37 2009
New Revision: 804457

URL: http://svn.apache.org/viewvc?rev=804457&view=rev
Log:
Checked in LdapUserDatabase, and refactorings to other LDAP classes. The LdapUserDatabase is read-only. ProfileTab will respect this will print, rather than allow edits to, user profile information as a result. Any UserDatabase can now be flagged as "read-only" (via jspwiki.properties) to achieve the same effect.

Added:
    incubator/jspwiki/trunk/src/java/org/apache/wiki/auth/LdapConfig.java
    incubator/jspwiki/trunk/src/java/org/apache/wiki/auth/user/LdapUserDatabase.java
    incubator/jspwiki/trunk/tests/java/org/apache/wiki/auth/user/LdapUserDatabaseTest.java
Modified:
    incubator/jspwiki/trunk/ChangeLog
    incubator/jspwiki/trunk/etc/jspwiki.properties.tmpl
    incubator/jspwiki/trunk/etc/ldap/test.ldif
    incubator/jspwiki/trunk/src/WebContent/templates/default/AttachmentTab.jsp
    incubator/jspwiki/trunk/src/WebContent/templates/default/ProfileTab.jsp
    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/UserManager.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/src/java/org/apache/wiki/auth/user/UserDatabase.java
    incubator/jspwiki/trunk/src/java/org/apache/wiki/tags/UserProfileTag.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
    incubator/jspwiki/trunk/tests/java/org/apache/wiki/auth/user/AllTests.java

Modified: incubator/jspwiki/trunk/ChangeLog
URL: http://svn.apache.org/viewvc/incubator/jspwiki/trunk/ChangeLog?rev=804457&r1=804456&r2=804457&view=diff
==============================================================================
--- incubator/jspwiki/trunk/ChangeLog (original)
+++ incubator/jspwiki/trunk/ChangeLog Sat Aug 15 11:53:37 2009
@@ -1,5 +1,16 @@
 2009-08-08 Andrew Jaquith <ajaquith AT apache DOT org>
 
+        * 3.0.0-svn-140
+
+        * Checked in LdapUserDatabase, and refactorings to other LDAP
+        classes. The LdapUserDatabase is read-only. ProfileTab will
+        respect this will print, rather than allow edits to, user
+        profile information as a result. Any UserDatabase can now be
+        flagged as "read-only" (via jspwiki.properties) to achieve
+        the same effect. 
+
+2009-08-08 Andrew Jaquith <ajaquith AT apache DOT org>
+
         * 3.0.0-svn-139
 
         * Checked in revised LdapLoginModule and LdapAuthorizer. Notable new

Modified: incubator/jspwiki/trunk/etc/jspwiki.properties.tmpl
URL: http://svn.apache.org/viewvc/incubator/jspwiki/trunk/etc/jspwiki.properties.tmpl?rev=804457&r1=804456&r2=804457&view=diff
==============================================================================
--- incubator/jspwiki/trunk/etc/jspwiki.properties.tmpl (original)
+++ incubator/jspwiki/trunk/etc/jspwiki.properties.tmpl Sat Aug 15 11:53:37 2009
@@ -434,6 +434,17 @@
 #  want to modify the security policy file WEB-INF/jspwiki.policy. See
 #  the policy file for more details.
 #
+#  KEYCHAIN
+#
+#  Where secrets are kept. The default location is WEB-INF/keychain
+#jspwiki.keychainPath = keychain
+
+#
+#  The keychain password, in plain text. If omitted, the keychain will
+#  remain locked after startup. It is safer to omit the password.
+#jspwiki.keychainPassword = myPlaintextPassword
+
+#
 #  AUTHENTICATION
 #
 #  For authentication, JSPWiki uses JAAS (Java Authentication and Authorization
@@ -539,11 +550,9 @@
 # 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
-
+#ldap.roleBase = ou\=roles,dc\=jspwiki,dc\=org
+#ldap.isInRolePattern = (&(objectClass\=groupOfUniqueNames)(cn\={0})(uniqueMember\={1}))
+#ldap.bindDN = cn\=Manager,dc\=jspwiki,dc\=org
 
 #  B) GROUPS
 #  As an additional source of authorization, users can belong to discretionary
@@ -593,14 +602,10 @@
 
 #jspwiki.userdatabase = org.apache.wiki.auth.user.JDBCUserDatabase
 
-#  If your JSPWiki user database shares login information with your
-#  web container's authentication realm, you can configure JSPWiki to
-#  add container users. At present, this only works with JDBCUserDatabase,
-#  and only if you've configured your web container to use a database
-#  with compatible columns and tables. If you don't know what this means,
-#  then leave this property set to FALSE (the default).
+#  If your user database is read-only, set this property to true.
+#  LdapUserDatabase, for example, should almost always be read-only.
 
-#jspwiki.userdatabase.isSharedWithContainer = false
+jspwiki.userdatabase.readOnlyProfiles = false
 
 #  ACCESS CONTROL LISTS
 #  Last but not least, JSPWiki needs a way of reading and persisting page

Modified: incubator/jspwiki/trunk/etc/ldap/test.ldif
URL: http://svn.apache.org/viewvc/incubator/jspwiki/trunk/etc/ldap/test.ldif?rev=804457&r1=804456&r2=804457&view=diff
==============================================================================
--- incubator/jspwiki/trunk/etc/ldap/test.ldif (original)
+++ incubator/jspwiki/trunk/etc/ldap/test.ldif Sat Aug 15 11:53:37 2009
@@ -89,7 +89,7 @@
 x500UniqueIdentifier: '110000111001011000011000011001111010111001011101101011011100101100001110010111001001011011101001100010110110110100101101110001011001011101101100101011011100101110001100001110101110001111000110100110000111000111100110110001110001'B
 uid: Biff
 sn: Biff
-cn: biff
+cn: Biff
 mail: biff@example.com
 userPassword: {SSHA}xKAIienaZZHhKTGCNv5Li6lzeemaSs6ZYXTHFQ==
 

Modified: incubator/jspwiki/trunk/src/WebContent/templates/default/AttachmentTab.jsp
URL: http://svn.apache.org/viewvc/incubator/jspwiki/trunk/src/WebContent/templates/default/AttachmentTab.jsp?rev=804457&r1=804456&r2=804457&view=diff
==============================================================================
--- incubator/jspwiki/trunk/src/WebContent/templates/default/AttachmentTab.jsp (original)
+++ incubator/jspwiki/trunk/src/WebContent/templates/default/AttachmentTab.jsp Sat Aug 15 11:53:37 2009
@@ -26,6 +26,7 @@
 <%@ page import="org.apache.wiki.action.WikiContextFactory" %>
 <%@ page import="org.apache.wiki.util.TextUtil" %>
 <%@ page import="org.apache.wiki.api.WikiPage" %>
+<%@ page errorPage="/Error.jsp" %>
 <%
   int MAXATTACHNAMELENGTH = 30;
   WikiContext c = WikiContextFactory.findContext( pageContext );

Modified: incubator/jspwiki/trunk/src/WebContent/templates/default/ProfileTab.jsp
URL: http://svn.apache.org/viewvc/incubator/jspwiki/trunk/src/WebContent/templates/default/ProfileTab.jsp?rev=804457&r1=804456&r2=804457&view=diff
==============================================================================
--- incubator/jspwiki/trunk/src/WebContent/templates/default/ProfileTab.jsp (original)
+++ incubator/jspwiki/trunk/src/WebContent/templates/default/ProfileTab.jsp Sat Aug 15 11:53:37 2009
@@ -82,8 +82,13 @@
      <tr>
        <td><s:label for="profile.fullname" /></td>
        <td>
-         <s:text name="profile.fullname" id="fullname" size="20"><wiki:UserProfile property="fullname" /></s:text>
-          <s:errors field="profile.fullname" />
+         <wiki:UserProfile property="canChangeFullname">
+           <s:text name="profile.fullname" id="fullname" size="20"><wiki:UserProfile property="fullname" /></s:text>
+           <s:errors field="profile.fullname" />
+         </wiki:UserProfile>
+         <wiki:UserProfile property="!canChangeFullname">
+           <wiki:UserProfile property="fullname" />
+         </wiki:UserProfile>
          <div class="formhelp"><fmt:message key="prefs.fullname.description" /></div>
        </td>
      </tr>
@@ -92,8 +97,13 @@
      <tr>
        <td><s:label for="profile.email" name="email" /></td>
        <td>
-         <s:text name="profile.email" id="email" size="20"><wiki:UserProfile property="email" /></s:text>
-         <s:errors field="profile.email" />
+         <wiki:UserProfile property="canChangeEmail">
+           <s:text name="profile.email" id="email" size="20"><wiki:UserProfile property="email" /></s:text>
+           <s:errors field="profile.email" />
+         </wiki:UserProfile>
+         <wiki:UserProfile property="!canChangeEmail">
+           <wiki:UserProfile property="email" />
+         </wiki:UserProfile>
          <div class="formhelp"><fmt:message key="prefs.email.description" /></div>
        </td>
      </tr>

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=804457&r1=804456&r2=804457&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 15 11:53:37 2009
@@ -77,7 +77,7 @@
      *  <p>
      *  If the build identifier is empty, it is not added.
      */
-    public static final String     BUILD         = "139";
+    public static final String     BUILD         = "140";
 
     /**
      *  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=804457&r1=804456&r2=804457&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 15 11:53:37 2009
@@ -21,6 +21,7 @@
 package org.apache.wiki.auth;
 
 import java.io.File;
+import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.MalformedURLException;
@@ -109,13 +110,13 @@
     protected static final Logger              log                 = LoggerFactory.getLogger( AuthenticationManager.class );
 
     /** Prefix for LoginModule options key/value pairs. */
-    protected static final String                 PREFIX_LOGIN_MODULE_OPTIONS = "jspwiki.loginModule.options.";
+    public static final String                 PREFIX_LOGIN_MODULE_OPTIONS = "jspwiki.loginModule.options.";
 
     /** If this jspwiki.properties property is <code>true</code>, allow cookies to be used to assert identities. */
     protected static final String                 PROP_ALLOW_COOKIE_ASSERTIONS = "jspwiki.cookieAssertions";
 
     /** The {@link javax.security.auth.spi.LoginModule} to use for custom authentication. */
-    protected static final String                 PROP_LOGIN_MODULE = "jspwiki.loginModule.class";
+    public static final String                 PROP_LOGIN_MODULE = "jspwiki.loginModule.class";
     
     /** Empty Map passed to JAAS {@link #doJAASLogin(Class, CallbackHandler, Map)} method. */
     protected static final Map<String,String> EMPTY_MAP = Collections.unmodifiableMap( new HashMap<String,String>() );
@@ -211,6 +212,32 @@
     public final void initialize( WikiEngine engine, Properties props ) throws WikiException
     {
         m_engine = engine;
+
+        // 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 );
+        }
+
         m_storeIPAddress = TextUtil.getBooleanProperty( props, PROP_STOREIPADDRESS, m_storeIPAddress );
 
         // Should J2SE policies be used for authorization?
@@ -245,31 +272,6 @@
         
         // 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 );
-        }
     }
 
     /**
@@ -776,13 +778,24 @@
     }
 
     /**
-     * Returns the first Principal in a set that isn't a {@link org.apache.wiki.auth.authorize.Role} or
+     * Returns the first Principal in a set of type
+     * {@link WikiPrincipal#LOGIN_NAME}, or failing that, the first one that
+     * isn't a {@link org.apache.wiki.auth.authorize.Role} or
      * {@link org.apache.wiki.auth.GroupPrincipal}.
+     * 
      * @param principals the principal set
      * @return the login principal
      */
     protected Principal getLoginPrincipal(Set<Principal> principals)
     {
+        for ( Principal principal : principals )
+        {
+            if ( principal instanceof WikiPrincipal
+                &&((WikiPrincipal)principal).getType() == WikiPrincipal.LOGIN_NAME )
+            {
+                return principal;
+            }
+        }
         for (Principal principal: principals )
         {
             if ( isUserPrincipal( principal ) )
@@ -932,6 +945,21 @@
         {
             stream = m_engine.getServletContext().getResourceAsStream( "/" + m_keychainPath );
         }
+
+        // If we can't get it from classloader, see if it's an absolute path
+        if ( stream == null )
+        {
+            File file = new File( m_keychainPath);
+            try
+            {
+                if ( file.isAbsolute() && file.exists() )
+                {
+                    stream = new FileInputStream( file );
+                }
+            }
+            catch ( Exception e ) { }
+        }
+
         if( stream == null )
         {
             throw new WikiSecurityException( "Unable to find keychain " + m_keychainPath + "." );

Added: incubator/jspwiki/trunk/src/java/org/apache/wiki/auth/LdapConfig.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/trunk/src/java/org/apache/wiki/auth/LdapConfig.java?rev=804457&view=auto
==============================================================================
--- incubator/jspwiki/trunk/src/java/org/apache/wiki/auth/LdapConfig.java (added)
+++ incubator/jspwiki/trunk/src/java/org/apache/wiki/auth/LdapConfig.java Sat Aug 15 11:53:37 2009
@@ -0,0 +1,381 @@
+package org.apache.wiki.auth;
+
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.util.*;
+
+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 org.apache.wiki.util.TextUtil;
+import org.freshcookies.security.Keychain;
+
+/**
+ * Immutable holder for configuration of LDAP-related security modules.
+ */
+public class LdapConfig
+{
+    public static final Map<String,String> ACTIVE_DIRECTORY_CONFIG;
+    
+    public static final Map<String,String> OPEN_LDAP_CONFIG;
+
+    public static final String KEYCHAIN_BIND_DN_ENTRY = "ldap.bindDNPassword";
+    
+    /**
+     * Property that indicates what LDAP server configuration to use. Valid values include
+     * <code>ad</code> for Active Directory and <code>openldap</code> for OpenLDAP.
+     * If this value is set, the default settings for these configurations will be loaded.
+     */
+    public static final String PROPERTY_CONFIG = "ldap.config";
+    
+    /**
+     * 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}.
+     */
+    public static final String PROPERTY_SSL = "ldap.ssl";
+
+    /**
+     * Property that supplies the pattern for finding roles within the role base, e.g.
+     * <code>(&(objectClass=groupOfUniqueNames)(cn={0}))</code>
+     * .
+     */
+    public static final String PROPERTY_ROLE_PATTERN = "ldap.rolePattern";
+    
+    /**
+     * Property that supplies the pattern for finding users within the role base
+     * that possess a given role, e.g.
+     * <code>(&(objectClass=groupOfUniqueNames)(cn={0})(uniqueMember={1}))</code>
+     * .
+     */
+    public static final String PROPERTY_IS_IN_ROLE_PATTERN = "ldap.isInRolePattern";
+
+    /**
+     * Property that supplies the DN used to bind to the directory when looking
+     * up users and roles.
+     */
+    public static final String PROPERTY_BIND_DN = "ldap.bindDN";
+
+    /**
+     * 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}.
+     */
+    public static final String PROPERTY_CONNECTION_URL = "ldap.connectionURL";
+
+    /**
+     * 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}.
+     */
+    public static final String PROPERTY_AUTHENTICATION = "ldap.authentication";
+    
+    /**
+     * Property that specifies the login name attribute for user objects.
+     * By default, this is <code>uid</code>.
+     */
+    public static final String PROPERTY_USER_LOGIN_NAME_ATTRIBUTE = "ldap.user.loginName";
+    
+    /**
+     * Property 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.
+     */
+    public static final String PROPERTY_USER_BASE = "ldap.userBase";
+    
+    /**
+     * Property that specifies the DN pattern for finding users within the
+     * user base, <em>e.g.,</em>
+     * <code>(&(objectClass=inetOrgPerson)(uid={0}))</code>
+     */
+    public static final String PROPERTY_USER_PATTERN = "ldap.userPattern";
+
+    
+    /**
+     * Property that specifies the pattern for the username used to log in to the
+     * LDAP server. This pattern maps the username supplied at login time by the
+     * user to a username format the LDAP server can recognized. Usually this is
+     * a pattern that produces 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>. The default value
+     * if not supplied is <code>{0}</code>.
+     */
+    public static final String PROPERTY_LOGIN_ID_PATTERN = "ldap.loginIdPattern";
+    
+    public static final String PROPERTY_USER_OBJECT_CLASS = "ldap.user.objectClass";
+    
+    /**
+     * Property that supplies the base DN where roles are contained, e.g.
+     * <code>ou=roles,dc=jspwiki,dc=org</code>.
+     */
+    public static final String PROPERTY_ROLE_BASE = "ldap.roleBase";
+
+    public final String connectionUrl;
+
+    public final String roleBase;
+    
+    public final String rolePattern;
+
+    public final String isInRolePattern;
+
+    public final String bindDN;
+
+    public final String ssl;
+
+    public final String authentication;
+    
+    public final String userBase;
+    
+    public final String loginIdPattern;
+    
+    public final String userPattern;
+    
+    public final String userLoginNameAttribute;
+
+    public final String userObjectClass;
+    
+    private final Set<String> m_configured = new HashSet<String>();
+
+    private Keychain m_keychain;
+    
+    static
+    {
+        // Active Directory 2000+ defaults
+        Map<String,String> options = new HashMap<String,String>();
+        options.put( PROPERTY_IS_IN_ROLE_PATTERN, "(&(objectClass=group)(cn={0})(member={1}))" );
+        options.put( PROPERTY_LOGIN_ID_PATTERN, "{0}" );
+        options.put( PROPERTY_ROLE_PATTERN, "(&(objectClass=group)(cn={0}))" );
+        options.put( PROPERTY_USER_LOGIN_NAME_ATTRIBUTE, "sAMAccountName" );
+        options.put( PROPERTY_USER_OBJECT_CLASS, "person" );
+        options.put( PROPERTY_USER_PATTERN, "(&(objectClass=person)(sAMAccountName={0}))" );
+        ACTIVE_DIRECTORY_CONFIG = Collections.unmodifiableMap( options );
+        
+        // OpenLDAP defaults
+        options = new HashMap<String,String>();
+        options.put( PROPERTY_IS_IN_ROLE_PATTERN, "(&(&(objectClass=groupOfUniqueNames)(cn={0}))(uniqueMember={1}))" );
+        options.put( PROPERTY_ROLE_PATTERN, "(&(objectClass=groupOfUniqueNames)(cn={0}))" );
+        options.put( PROPERTY_USER_LOGIN_NAME_ATTRIBUTE, "uid" );
+        options.put( PROPERTY_USER_OBJECT_CLASS, "inetOrgPerson" );
+        options.put( PROPERTY_USER_PATTERN, "(&(objectClass=inetOrgPerson)(uid={0}))" );
+        OPEN_LDAP_CONFIG = Collections.unmodifiableMap( options );
+    }
+
+    private String getProperty( Map<? extends Object,? extends Object> props, String property, String defaultValue )
+    {
+        String shortProperty = property;
+        String longProperty = AuthenticationManager.PREFIX_LOGIN_MODULE_OPTIONS + property;
+        String value = (String)props.get( property );
+        if ( value == null )
+        {
+            property = longProperty;
+            value = (String)props.get( property );
+        }
+        if( value != null && value.length() > 0 )
+        {
+            m_configured.add( shortProperty );
+            m_configured.add( longProperty );
+            return props.get( property ).toString().trim();
+        }
+        return defaultValue;
+    }
+    
+    public String getBindDNPassword() throws KeyStoreException
+    {
+        if( m_keychain == null )
+        {
+            throw new KeyStoreException( "LdapConfig was initialized without a keychain!" );
+        }
+        KeyStore.Entry password = m_keychain.getEntry( LdapConfig.KEYCHAIN_BIND_DN_ENTRY );
+        if( password instanceof Keychain.Password )
+        {
+            return ((Keychain.Password) password).getPassword();
+        }
+        return null;
+    }
+
+    public static LdapConfig getInstance( Keychain keychain, Map<? extends Object,? extends Object> props, String[] requiredProperties )
+    {
+        return new LdapConfig( keychain, props, requiredProperties );
+    }
+
+    private LdapConfig( Keychain keychain, Map<? extends Object,? extends Object> props, String[] requiredProperties )
+    {
+        // Basic connection properties
+        connectionUrl = getProperty( props, PROPERTY_CONNECTION_URL, null );
+
+        // Binding DN properties
+        m_keychain = keychain;
+        bindDN = getProperty( props, PROPERTY_BIND_DN, null );
+        
+        // User lookup properties
+        userBase = getProperty( props, PROPERTY_USER_BASE, null );
+        userPattern = getProperty( props, PROPERTY_USER_PATTERN, null );
+
+        // Role lookup properties
+        roleBase = getProperty( props, PROPERTY_ROLE_BASE, null );
+        rolePattern = getProperty( props, PROPERTY_ROLE_PATTERN, null );
+        isInRolePattern = getProperty( props, PROPERTY_IS_IN_ROLE_PATTERN, null );
+
+        // Optional security properties
+        String parsedSsl = getProperty( props, PROPERTY_SSL, null );
+        ssl = (parsedSsl != null && TextUtil.isPositive( parsedSsl )) ? "ssl" : "none";
+        String parsedAuthentication = getProperty( props, PROPERTY_AUTHENTICATION, null );
+        if( parsedAuthentication == null )
+        {
+            authentication = "ssl".equals( ssl ) ? "simple" : "DIGEST-MD5";
+        }
+        else
+        {
+            authentication = parsedAuthentication;
+        }
+        loginIdPattern = getProperty( props, PROPERTY_LOGIN_ID_PATTERN, "{0}" );
+
+        // Optional user object attributes
+        userObjectClass = getProperty( props, PROPERTY_USER_OBJECT_CLASS, "inetOrgPerson" );
+        userLoginNameAttribute = getProperty( props, PROPERTY_USER_LOGIN_NAME_ATTRIBUTE, "uid" );
+
+        // Validate everything
+        for( String property : requiredProperties )
+        {
+            if( !m_configured.contains( property ) )
+            {
+                throw new IllegalArgumentException( "Property " + property + " is required!" );
+            }
+        }
+    }
+    
+    public Hashtable<String,String> newJndiEnvironment() throws NamingException
+    {
+        // If we need a Bind DN and Keychain is loaded, get the bind DN and password
+        String username = bindDN;
+        String password = null;
+        if( username != null )
+        {
+            try
+            {
+                password = getBindDNPassword();
+            }
+            catch( KeyStoreException e )
+            {
+                e.printStackTrace();
+                throw new NamingException( "Could not build JNDI environment: " + e.getMessage() );
+            }
+        }
+        return newJndiEnvironment( username, password );
+    }
+    
+    /**
+     * Builds a JNDI environment hashtable for authenticating to the LDAP
+     * 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 username and password parameters supply the LDAP credentials.
+     * 
+     * @param username the user's distinguished name (DN), used for
+     *            authentication
+     * @param password the password
+     * @return the constructed hash table
+     */
+    public Hashtable<String,String> newJndiEnvironment( String username, String password )
+    {
+        Hashtable<String, String> env = new Hashtable<String, String>();
+        env.put( Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory" );
+
+        // LDAP server to authenticate to
+        env.put( Context.PROVIDER_URL, connectionUrl );
+
+        // Add credentials if supplied
+        if ( username != null )
+        {
+            env.put( Context.SECURITY_PRINCIPAL, username );
+            env.put( Context.SECURITY_CREDENTIALS, password );
+        }
+        else
+        {
+            
+        }
+
+        // Use SSL?
+        env.put( Context.SECURITY_PROTOCOL, ssl );
+
+        // Authentication type (simple, DIGEST-MD5, etc)
+        env.put( Context.SECURITY_AUTHENTICATION, authentication );
+
+        return env;
+   }
+    
+    public static String getFullName( Attributes attributes ) throws NamingException
+    {
+        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 did not have a givenName+sn or cn." );
+        }
+        return fullName;
+    }
+
+    private final Map<String, String> m_userDns = new HashMap<String, String>();
+
+    private static final SearchControls SEARCH_CONTROLS;
+    
+    static
+    {
+        SEARCH_CONTROLS = new SearchControls();
+        SEARCH_CONTROLS.setSearchScope( SearchControls.SUBTREE_SCOPE );
+    }
+    
+    public String getUserDn( String loginName ) throws NamingException
+    {
+        String dn = m_userDns.get( loginName );
+        if( dn == null )
+        {
+            loginName = sanitizeDn( loginName );
+            String userFinder = userPattern.replace( "{0}", loginName );
+            Hashtable<String, String> env = newJndiEnvironment();
+
+            // Find the user
+            DirContext ctx = new InitialLdapContext( env, null );
+            NamingEnumeration<SearchResult> users = ctx.search( userBase, userFinder, SEARCH_CONTROLS );
+            if( users.hasMore() )
+            {
+                dn = users.next().getNameInNamespace();
+                m_userDns.put( loginName, dn );
+            }
+        }
+        return dn;
+    }
+
+    /**
+     * See http://blogs.sun.com/shankar/entry/what_is_ldap_injection
+     * 
+     * @param index
+     * @return
+     */
+    private String sanitizeDn( String index )
+    {
+        index = index.replace( " ", " " );
+        return index.replace( "=", "\\3D" );
+    }
+}

Modified: incubator/jspwiki/trunk/src/java/org/apache/wiki/auth/UserManager.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/trunk/src/java/org/apache/wiki/auth/UserManager.java?rev=804457&r1=804456&r2=804457&view=diff
==============================================================================
--- incubator/jspwiki/trunk/src/java/org/apache/wiki/auth/UserManager.java (original)
+++ incubator/jspwiki/trunk/src/java/org/apache/wiki/auth/UserManager.java Sat Aug 15 11:53:37 2009
@@ -52,6 +52,7 @@
 import org.apache.wiki.ui.InputValidator;
 import org.apache.wiki.util.ClassUtil;
 import org.apache.wiki.util.MailUtil;
+import org.apache.wiki.util.TextUtil;
 import org.apache.wiki.workflow.*;
 
 
@@ -63,6 +64,8 @@
  */
 public final class UserManager
 {
+    public static final String PROP_READ_ONLY_PROFILES = "jspwiki.userdatabase.readOnlyProfiles";
+
     private static final String USERDATABASE_PACKAGE = "org.apache.wiki.auth.user";
     private static final String SESSION_MESSAGES = "profile";
     private static final String PARAM_EMAIL = "email";
@@ -73,6 +76,8 @@
 
     private WikiEngine m_engine;
 
+    private boolean m_readOnlyProfiles = false;
+
     private static Logger log = LoggerFactory.getLogger(UserManager.class);
 
     /** Message key for the "save profile" message. */
@@ -86,8 +91,6 @@
     protected static final String PREFS_FULL_NAME           = "prefs.fullname";
     protected static final String PREFS_EMAIL               = "prefs.email";
 
-    // private static final String  PROP_ACLMANAGER     = "jspwiki.aclManager";
-
     /** Associates wiki sessions with profiles */
     private final Map<WikiSession,UserProfile> m_profiles = new WeakHashMap<WikiSession,UserProfile>();
 
@@ -115,8 +118,9 @@
 
         m_useJAAS = AuthenticationManager.SECURITY_JAAS.equals( props.getProperty(AuthenticationManager.PROP_SECURITY, AuthenticationManager.SECURITY_JAAS ) );
 
-        // Attach the PageManager as a listener
-        // TODO: it would be better if we did this in PageManager directly
+        m_readOnlyProfiles = TextUtil.isPositive( props.getProperty( PROP_READ_ONLY_PROFILES ) );
+
+        // Attach the ContentManager as a listener
         addWikiEventListener( engine.getContentManager() );
 
         JSONRPCManager.registerGlobalObject( "users", new JSONUserModule(this), new AllPermission(null) );
@@ -249,6 +253,17 @@
     }
 
     /**
+     * Returns <code>true</code> if all user profiles returned by the back-end database are
+     * read-only. Read-only behavior can be forced by setting the <code>jspwiki.properties</code>
+     * property {@link #PROP_READ_ONLY_PROFILES}.
+     * @return the result
+     */
+    public boolean isReadOnly()
+    {
+        return m_readOnlyProfiles;
+    }
+
+    /**
      * <p>
      * Saves the {@link org.apache.wiki.auth.user.UserProfile}for the user in
      * a wiki session. This method verifies that a user profile to be saved

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=804457&r1=804456&r2=804457&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 15 11:53:37 2009
@@ -1,14 +1,11 @@
 package org.apache.wiki.auth.authorize;
 
-import java.security.KeyStore;
-import java.security.KeyStoreException;
 import java.security.Principal;
 import java.util.HashSet;
 import java.util.Hashtable;
 import java.util.Properties;
 import java.util.Set;
 
-import javax.naming.Context;
 import javax.naming.NamingEnumeration;
 import javax.naming.NamingException;
 import javax.naming.directory.DirContext;
@@ -19,39 +16,39 @@
 import org.apache.wiki.WikiEngine;
 import org.apache.wiki.WikiSession;
 import org.apache.wiki.auth.Authorizer;
+import org.apache.wiki.auth.LdapConfig;
 import org.apache.wiki.auth.WikiSecurityException;
-import org.apache.wiki.log.Logger;
-import org.apache.wiki.log.LoggerFactory;
-import org.apache.wiki.util.TextUtil;
-import org.freshcookies.security.Keychain;
+import org.apache.wiki.auth.login.LdapLoginModule;
 
 /**
- * Authorizer whose Roles are supplied by LDAP groups.
+ * <p>
+ * Authorizer whose Roles are supplied by LDAP groups. This Authorizer requires
+ * that {@link LdapLoginModule} be used for authentication. This can be done
+ * either as part of the web container authentication configuration or (more
+ * likely) as part of JSPWiki's own native authentication configuration.
+ * </p>
+ * <p>
+ * When {@link #initialize(WikiEngine, Properties)} executes, a new instance of
+ * {@link org.apache.wiki.auth.LdapConfig} is created and configured based on
+ * the settings in <code>jspwiki.properties</code>. The properties that are
+ * required in order for LdapAuthorizer to function correctly are
+ * {@link LdapConfig#PROPERTY_CONNECTION_URL},
+ * {@link LdapConfig#PROPERTY_ROLE_BASE},
+ * {@link LdapConfig#PROPERTY_ROLE_PATTERN} and
+ * {@link LdapConfig#PROPERTY_IS_IN_ROLE_PATTERN}. Additional properties that
+ * can be set include {@link LdapConfig#PROPERTY_BIND_DN},
+ * {@link LdapConfig#PROPERTY_AUTHENTICATION} and
+ * {@link LdapConfig#PROPERTY_SSL}. See the documentation for that LdapConfig
+ * for more details.
+ * </p>
  */
 public class LdapAuthorizer implements Authorizer
 {
     private Hashtable<String, String> m_jndiEnv;
 
-    private String m_roleBase = null;
+    private LdapConfig m_cfg = 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;
+    private String m_allRoleFinder = null;
 
     /**
      * {@inheritDoc}
@@ -61,9 +58,9 @@
         try
         {
             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 );
+            String roleFinder = m_cfg.rolePattern;
+            roleFinder = roleFinder.replace( "{0}", role );
+            NamingEnumeration<SearchResult> roles = ctx.search( m_cfg.roleBase, roleFinder, SEARCH_CONTROLS );
             if( roles.hasMore() )
             {
                 return new Role( role );
@@ -85,9 +82,7 @@
         try
         {
             DirContext ctx = new InitialLdapContext( m_jndiEnv, null );
-            SearchControls searchControls = new SearchControls();
-            searchControls.setReturningAttributes( new String[] { "cn" } );
-            NamingEnumeration<SearchResult> roles = ctx.search( m_roleBase, m_roleFinder, searchControls );
+            NamingEnumeration<SearchResult> roles = ctx.search( m_cfg.roleBase, m_allRoleFinder, SEARCH_CONTROLS );
             while ( roles.hasMore() )
             {
                 SearchResult foundRole = roles.next();
@@ -102,111 +97,31 @@
         return foundRoles.toArray( new Role[foundRoles.size()] );
     }
 
-    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}.
-     */
-    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}.
-     */
-    protected static final String PROPERTY_SSL = "jspwiki.loginModule.options.ldap.ssl";
-
-    /**
-     * 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_LOGIN_ID_PATTERN = "jspwiki.loginModule.options.ldap.userPattern";
+    private static final String[] REQUIRED_PROPERTIES = new String[] { LdapConfig.PROPERTY_CONNECTION_URL,
+                                                                      LdapConfig.PROPERTY_ROLE_BASE,
+                                                                      LdapConfig.PROPERTY_ROLE_PATTERN,
+                                                                      LdapConfig.PROPERTY_IS_IN_ROLE_PATTERN };
 
-    private static final String[] REQUIRED_PROPERTIES = new String[] { PROPERTY_CONNECTION_URL, PROPERTY_LOGIN_ID_PATTERN,
-                                                                      PROPERTY_ROLE_BASE, PROPERTY_ROLE_PATTERN };
+    private static final SearchControls SEARCH_CONTROLS;
 
-    protected static final String KEYCHAIN_BIND_DN_ENTRY = "LdapAuthorizer.BindDN.Password";
+    static
+    {
+        SEARCH_CONTROLS = new SearchControls();
+        SEARCH_CONTROLS.setSearchScope( SearchControls.SUBTREE_SCOPE );
+    }
 
     /**
      * {@inheritDoc}
      */
     public void initialize( WikiEngine engine, Properties props ) throws WikiSecurityException
     {
-        // Make sure all required properties are here
-        for( String prop : REQUIRED_PROPERTIES )
-        {
-            if( !props.containsKey( prop ) )
-            {
-                throw new WikiSecurityException( "Property " + prop + " is required!" );
-            }
-        }
-
-        // 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_authentication = "ssl".equals( m_ssl ) ? "simple" : "DIGEST-MD5";
-        }
-        else
-        {
-            m_authentication = authentication;
-        }
-        String bindDN = props.getProperty( PROPERTY_BIND_DN );
-        if( bindDN != null && bindDN.length() > 0 )
-        {
-            m_bindDN = bindDN.trim();
-        }
+        m_cfg = LdapConfig.getInstance( engine.getAuthenticationManager().getKeychain(), props, REQUIRED_PROPERTIES );
+        m_allRoleFinder = m_cfg.rolePattern.replace( "{0}", "*" );
 
         // Do a quick connection test, and fail-fast if needed
-        buildJndiEnvironment();
         try
         {
+            m_jndiEnv = m_cfg.newJndiEnvironment();
             new InitialLdapContext( m_jndiEnv, null );
         }
         catch( NamingException e )
@@ -215,99 +130,52 @@
         }
     }
 
-    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>
-     * This implementation returns <code>true</code> when at least one of the
-     * user login Principals contained in the WikiSession's Subject belongs to
-     * 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_LOGIN_ID_PATTERN}.
-     * </p>
-     * <p>
-     * To make an accurate search, Principals that are of type {@link Role},
-     * {@link org.apache.wiki.auth.GroupPrincipal} are excluded from
-     * consideration. So are {@link org.apache.wiki.auth.WikiPrincipal} whose
-     * type are {@link org.apache.wiki.auth.WikiPrincipal#FULL_NAME} or
-     * {@link org.apache.wiki.auth.WikiPrincipal#WIKI_NAME}.
+     * This implementation returns <code>true</code> when the user login
+     * Principal contained in the WikiSession's Subject belongs to an LDAP group
+     * found in the role-base DN. The login Principal is assumed to be a valid
+     * DN. The scope searched is provided by
+     * {@link LdapConfig#PROPERTY_ROLE_BASE}, and the filter to match roles is
+     * provided by {@link LdapConfig#PROPERTY_IS_IN_ROLE_PATTERN}.
      * </p>
      * <p>
-     * For example, consider an LDAP user base of
-     * <code>ou=people,dc=jspwiki,dc=org</code>, and a WikiSession whose subject
-     * contains three user principals, the two built-in roles <code>ALL</code>
-     * and <code>AUTHENTICATED</code>, and a group principal
-     * <code>MyGroup</code>:
+     * For example, consider a WikiSession whose subject contains three user
+     * principals, the two built-in roles <code>ALL</code> and
+     * <code>AUTHENTICATED</code>, and a group principal <code>MyGroup</code>:
      * </p>
-     * <blockquote><code>WikiPrincipal.LOGIN_NAME "biggie.smalls"<br/>
+     * <blockquote>
+     * <code>WikiPrincipal.LOGIN_NAME "uid=biggie.smalls,ou=people,dc=jspwiki,dc=org"<br/>
      * WikiPrincipal.FULL_NAME "Biggie Smalls"<br/>
      * WikiPrincipal.WIKI_NAME "BiggieSmalls"<br/>
      * Role.ALL
      * Role.AUTHENTICATED
      * GroupPrincipal "MyGroup"</code></blockquote>
      * <p>
-     * In this case, only WikiPrincipal.LOGIN_NAME "biggie.smalls" would be
-     * examined. An LDAP search would be constructed that searched the LDAP role
-     * base for an object whose <code>objectClass</code> was of type
-     * <code>groupOfUniqueNames</code> and whose
-     * <code>uniqueMember<code> attribute contained the value
+     * In this case, the DN
+     * <code>uid=biggie.smalls,ou=people,dc=jspwiki,dc=org</code> would be
+     * examined for membership in an LDAP group whose common name matches
+     * <code>role</code>. Given an is-in-role pattern of
+     * <code>(&(objectClass=groupOfUniqueNames)(cn={0})(uniqueMember={1}))</code>
+     * , an LDAP search would be constructed to find objects whose
+     * <code>objectClass</code> was of type <code>groupOfUniqueNames</code> and
+     * whose <code>uniqueMember<code> attribute contained the value
      * <code>uid=biggie.smalls,ou=people,dc=jspwiki,dc=org</code>.
      * </p>
      */
     public boolean isUserInRole( WikiSession session, Principal role )
     {
-        // Build DN
-        String uid = session.getLoginPrincipal().getName();
-        String dn = m_userLoginIdPattern.replace( "{0}", uid ).trim();
-        dn = dn.replace( "=", "\\3D" );
-
+        String loginName = session.getLoginPrincipal().getName();
         try
         {
+            String dn = m_cfg.getUserDn( loginName );
             DirContext ctx = new InitialLdapContext( m_jndiEnv, null );
-            SearchControls searchControls = new SearchControls();
-            searchControls.setSearchScope( SearchControls.SUBTREE_SCOPE );
-            searchControls.setReturningAttributes( new String[0] );
-            String filter = m_rolePattern.replace( "{0}", role.getName() );
+            String filter = m_cfg.isInRolePattern.replace( "{0}", role.getName() );
             filter = filter.replace( "{1}", dn );
-            NamingEnumeration<SearchResult> roles = ctx.search( m_roleBase, filter, searchControls );
-            return roles.hasMore();
+            NamingEnumeration<SearchResult> roles = ctx.search( m_cfg.roleBase, filter, SEARCH_CONTROLS );
+            boolean isMember = roles.hasMore();
+            return isMember;
         }
         catch( NamingException e )
         {
@@ -315,5 +183,4 @@
         }
         return false;
     }
-
 }

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=804457&r1=804456&r2=804457&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 15 11:53:37 2009
@@ -23,16 +23,12 @@
 import java.io.IOException;
 import java.security.Principal;
 import java.util.Hashtable;
-import java.util.Map;
 
 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;
@@ -41,24 +37,37 @@
 import javax.security.auth.login.FailedLoginException;
 import javax.security.auth.login.LoginException;
 
+import org.apache.wiki.auth.LdapConfig;
 import org.apache.wiki.auth.WikiPrincipal;
 import org.apache.wiki.i18n.InternationalizationManager;
 import org.apache.wiki.log.Logger;
 import org.apache.wiki.log.LoggerFactory;
-import org.apache.wiki.util.TextUtil;
 
 /**
  * <p>
  * LoginModule that authenticates users against an LDAP server with the user's
  * supplied credentials. The authentication is performed by binding to the LDAP
- * server using the user's credentials, with or without SSL. To use this
- * LoginModule, callers must initialize it with a JAAS options Map that supplies
- * the following key/value pairs:
+ * server using the user's credentials, with or without SSL. After the user
+ * successfully authenticates, the user's Subject will be populated with three
+ * WikiPrincipals:
+ * </p>
+ * <ul>
+ * <li>a WikiPrincipal of type {@link WikiPrincipal#LOGIN_NAME} whose value is
+ * the distinguished name (DN) of the user.</li>
+ * <li>a WikiPrincipal of type {@link WikiPrincipal#FULL_NAME} whose value is
+ * set to either (first name + surname) or common name, with the former
+ * preferred.</li>
+ * <li>a WikiPrincipal of type {@link WikiPrincipal#WIKI_NAME} whose value is
+ * identical to the full name, but from which all whitespace is removed.</li>
+ * </ul>
+ * <p>
+ * To use this LoginModule, callers must initialize it with a JAAS options Map
+ * that supplies the following key/value pairs:
  * </p>
  * <ul>
  * <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
+ * <li>{@link #OPTION_LOGIN_ID_PATTERN} - optional 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
@@ -70,7 +79,9 @@
  * <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>
+ * of its subtrees will be searched. For directories that contain multiple OUs
+ * where users are located, use a higher-level base location (e.g.,
+ * <code>dc=jspwiki,dc=org</code>).</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
@@ -79,7 +90,9 @@
  * 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>
+ * <code>(&(objectClass=inetOrgPerson)(uid=fflintstone))</code> will be
+ * selected. A suitable value for this property that works with Active Directory
+ * 2000 and later is <code>(&(objectClass=person)(sAMAccountName={0}))</code>.</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
@@ -120,39 +133,42 @@
      * 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.
+     * <code>DIGEST-MD5</code>. The default is <code>simple</code> if SSL is
+     * specified, and <code>DIGEST-MD5</code> otherwise.
      */
-    protected static final String OPTION_AUTHENTICATION = "ldap.authentication";
+    public static final String OPTION_AUTHENTICATION = "ldap.authentication";
 
     /**
      * 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 OPTION_CONNECTION_URL = "ldap.connectionURL";
+    public static final String OPTION_CONNECTION_URL = "ldap.connectionURL";
 
     /**
      * 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 OPTION_USER_BASE = "ldap.userBase";
+    public static final String OPTION_USER_BASE = LdapConfig.PROPERTY_USER_BASE;
 
     /**
      * 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";
+    public static final String OPTION_USER_PATTERN = LdapConfig.PROPERTY_USER_PATTERN;
 
     /**
      * JAAS option specifies the pattern for the username used to log in to the
-     * LDAP server. Usually this is a full DN, for example
+     * LDAP server. This pattern maps the username supplied at login time by the
+     * user to a username format the LDAP server can recognized. Usually this is
+     * a pattern that produces 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>.
+     * case the principal will simply be <code>{0}</code>. The default value
+     * if not supplied is <code>{0}</code>.
      */
-    protected static final String OPTION_LOGIN_ID_PATTERN = "ldap.loginIdPattern";
+    public static final String OPTION_LOGIN_ID_PATTERN = LdapConfig.PROPERTY_LOGIN_ID_PATTERN;
 
     /**
      * JAAS option specifies that indicates whether to use SSL for connecting to
@@ -161,22 +177,18 @@
     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 };
+                                                                   OPTION_USER_BASE, OPTION_USER_PATTERN };
+
+    private LdapConfig m_cfg = null;
 
     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!" );
-            }
-        }
+        // Initialize the LDAP configuration
+        m_cfg = LdapConfig.getInstance( null, m_options, REQUIRED_OPTIONS );
 
         // Retrieve the essential callbacks: username and password
         String username;
+        String loginId;
         String password;
         NameCallback ncb = new NameCallback( "User name" );
         PasswordCallback pcb = new PasswordCallback( "Password", false );
@@ -184,7 +196,9 @@
         try
         {
             m_handler.handle( callbacks );
+            String userPattern = m_cfg.loginIdPattern;
             username = ncb.getName();
+            loginId = userPattern.replace( "{0}", username );
             password = new String( pcb.getPassword() );
         }
         catch( UnsupportedCallbackException e )
@@ -204,18 +218,16 @@
         try
         {
             // Log in
-            Hashtable<String, String> env = buildJndiEnvironment( username, password );
-            String loginId = env.get( Context.SECURITY_PRINCIPAL );
+            Hashtable<String, String> env = m_cfg.newJndiEnvironment( loginId, password );
             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 " + loginId );
             }
+            m_principals.add( new WikiPrincipal( username, WikiPrincipal.LOGIN_NAME ) );
 
-            // Also look up the full name (and make the wiki name out of it)
+            // Go and get the DN and common name (and make the wiki name out of
+            // it)
             Principal[] principals = extractNamePrincipals( ctx, username );
             for( Principal principal : principals )
             {
@@ -237,61 +249,13 @@
     }
 
     /**
-     * Builds a JNDI environment hashtable for authenticating to the LDAP
-     * 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 username and password parameters supply the LDAP credentials.
-     * 
-     * @param username the user's distinguished name (DN), used for
-     *            authentication
-     * @param password the password
-     * @return the constructed hash table
-     */
-    private Hashtable<String, String> buildJndiEnvironment( String username, String password )
-    {
-        Hashtable<String, String> env = new Hashtable<String, String>();
-        env.put( Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory" );
-
-        // 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( 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 );
-        }
-
-        return env;
-    }
-
-    /**
      * 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.
+     * 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 username the user name
@@ -300,44 +264,15 @@
      */
     protected Principal[] extractNamePrincipals( DirContext ctx, String username ) throws NamingException
     {
-        // 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() )
+        String dn = m_cfg.getUserDn( username );
+        Attributes attributes = ctx.getAttributes( dn );
+        if( attributes != null )
         {
-            // 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 fullName = LdapConfig.getFullName( attributes );
             String wikiName = fullName.indexOf( ' ' ) == -1 ? fullName : fullName.replace( " ", "" );
             principals[0] = new WikiPrincipal( fullName, WikiPrincipal.FULL_NAME );
             principals[1] = new WikiPrincipal( wikiName, WikiPrincipal.WIKI_NAME );

Added: incubator/jspwiki/trunk/src/java/org/apache/wiki/auth/user/LdapUserDatabase.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/trunk/src/java/org/apache/wiki/auth/user/LdapUserDatabase.java?rev=804457&view=auto
==============================================================================
--- incubator/jspwiki/trunk/src/java/org/apache/wiki/auth/user/LdapUserDatabase.java (added)
+++ incubator/jspwiki/trunk/src/java/org/apache/wiki/auth/user/LdapUserDatabase.java Sat Aug 15 11:53:37 2009
@@ -0,0 +1,214 @@
+package org.apache.wiki.auth.user;
+
+import java.security.Principal;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.Properties;
+import java.util.Set;
+
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.*;
+import javax.naming.ldap.InitialLdapContext;
+
+import org.apache.wiki.NoRequiredPropertyException;
+import org.apache.wiki.WikiEngine;
+import org.apache.wiki.auth.*;
+import org.apache.wiki.util.TextUtil;
+
+public class LdapUserDatabase extends AbstractUserDatabase
+{
+    /**
+     * LdapUserDatabase does not support this operation.
+     */
+    public void deleteByLoginName( String loginName ) throws NoSuchPrincipalException, WikiSecurityException
+    {
+        throw new WikiSecurityException( "Operation not supported" );
+    }
+
+    public UserProfile findByEmail( String index ) throws NoSuchPrincipalException
+    {
+        index = sanitize( index );
+        return findLdapUser( "(&(objectClass=" + m_cfg.userObjectClass + ")(mail=" + index + "))" );
+    }
+    
+    private UserProfile findLdapUser( String filter ) throws NoSuchPrincipalException
+    {
+        Hashtable<String, String> env = m_cfg.newJndiEnvironment( null, null );
+        try
+        {
+            DirContext ctx = new InitialLdapContext( env, null );
+            SearchControls searchControls = new SearchControls();
+            searchControls.setReturningAttributes( new String[] { m_cfg.userLoginNameAttribute, "cn", "givenName", "sn", "mail" } );
+            searchControls.setSearchScope( SearchControls.SUBTREE_SCOPE );
+            NamingEnumeration<SearchResult> results = ctx.search( m_cfg.userBase, filter, searchControls );
+            if( results.hasMore() )
+            {
+                SearchResult user = results.next();
+                Attributes attributes = user.getAttributes();
+                Attribute loginName = attributes.get( m_cfg.userLoginNameAttribute );
+                Attribute cn = attributes.get( "cn" );
+                Attribute mail = attributes.get( "mail" );
+                if ( loginName == null || cn == null || mail == null )
+                {
+                    throw new NamingException( "Malformed directory entry; missing cn, mail or uid." );
+                }
+                UserProfile p = newProfile();
+                p.setUid( user.getNameInNamespace() );
+                p.setLoginName( loginName.get( 0 ).toString() );
+                p.setFullname( LdapConfig.getFullName( user.getAttributes() ) );
+                p.setEmail( mail.get( 0 ).toString() );
+                return p;
+            }
+        }
+        catch( NamingException e )
+        {
+            throw new NoSuchPrincipalException( "Could not find object: " + e.getMessage() );
+        }
+        throw new NoSuchPrincipalException( "Could not find object." );
+    }
+
+    public UserProfile findByFullName( String index ) throws NoSuchPrincipalException
+    {
+        index = sanitize( index );
+        return findLdapUser( "(&(objectClass=" + m_cfg.userObjectClass + ")(cn=" + index + "))" );
+    }
+
+    public UserProfile findByLoginName( String index ) throws NoSuchPrincipalException
+    {
+        index = sanitize( index );
+        return findLdapUser( "(&(objectClass=" + m_cfg.userObjectClass + ")(" + m_cfg.userLoginNameAttribute + "=" + index + "))" );
+    }
+
+    public UserProfile findByUid( String uid ) throws NoSuchPrincipalException
+    {
+        String filter = "(objectClass=" + m_cfg.userObjectClass + ")";
+        Hashtable<String, String> env = m_cfg.newJndiEnvironment( null, null );
+        try
+        {
+            DirContext ctx = new InitialLdapContext( env, null );
+            SearchControls searchControls = new SearchControls();
+            searchControls.setReturningAttributes( new String[] { m_cfg.userLoginNameAttribute, "cn", "givenName", "sn", "mail" } );
+            searchControls.setSearchScope( SearchControls.SUBTREE_SCOPE );
+            NamingEnumeration<SearchResult> results = ctx.search( uid, filter, searchControls );
+            if( results.hasMore() )
+            {
+                SearchResult user = results.next();
+                Attributes attributes = user.getAttributes();
+                Attribute loginName = attributes.get( m_cfg.userLoginNameAttribute );
+                Attribute cn = attributes.get( "cn" );
+                Attribute mail = attributes.get( "mail" );
+                if ( loginName == null || cn == null || mail == null )
+                {
+                    throw new NamingException( "Malformed directory entry; missing cn, mail or uid." );
+                }
+                UserProfile p = newProfile();
+                p.setUid( user.getNameInNamespace() );
+                p.setLoginName( loginName.get( 0 ).toString() );
+                p.setFullname( LdapConfig.getFullName( user.getAttributes() ) );
+                p.setEmail( mail.get( 0 ).toString() );
+                return p;
+            }
+        }
+        catch( NamingException e )
+        {
+            throw new NoSuchPrincipalException( "Could not find object: " + e.getMessage() );
+        }
+        throw new NoSuchPrincipalException( "Could not find object." );
+    }
+
+    public UserProfile findByWikiName( String index ) throws NoSuchPrincipalException
+    {
+        index = sanitize( index );
+        index = TextUtil.beautifyString( index );
+        return findLdapUser( "(&(objectClass=" + m_cfg.userObjectClass + ")(cn=" + index + "))" );
+    }
+
+    private String sanitize( String index )
+    {
+        index = index.replace( " ", " " );
+        return index.replace( "=", "\\3D" );
+    }
+    
+    public Principal[] getWikiNames() throws WikiSecurityException
+    {
+        String filter = "(objectClass=" + m_cfg.userObjectClass + ")";
+        Hashtable<String, String> env = m_cfg.newJndiEnvironment( null, null );
+        Set<Principal> principals = new HashSet<Principal>();
+        try
+        {
+            DirContext ctx = new InitialLdapContext( env, null );
+            SearchControls searchControls = new SearchControls();
+            searchControls.setReturningAttributes( new String[]{ "cn", "givenName", "sn" } );
+            NamingEnumeration<SearchResult> results = ctx.search( m_cfg.userBase, filter, searchControls );
+            while( results.hasMore() )
+            {
+                SearchResult user = results.next();
+                String fullName = LdapConfig.getFullName( user.getAttributes() );
+                String wikiName = fullName.indexOf( ' ' ) == -1 ? fullName : fullName.replace( " ", "" );
+                principals.add( new WikiPrincipal( wikiName, WikiPrincipal.WIKI_NAME ) );
+            }
+        }
+        catch( NamingException e )
+        {
+            throw new WikiSecurityException( "Could not find object: " + e.getMessage(), e );
+        }
+        return principals.toArray(new Principal[principals.size()]);
+    }
+
+    private LdapConfig m_cfg = null;
+
+    private static final String[] REQUIRED_PROPERTIES = new String[] { LdapConfig.PROPERTY_CONNECTION_URL,
+                                                                       LdapConfig.PROPERTY_USER_BASE };
+
+    public void initialize( WikiEngine engine, Properties props ) throws NoRequiredPropertyException
+    {
+        m_cfg = LdapConfig.getInstance( null, props, REQUIRED_PROPERTIES );
+    }
+
+    /**
+     * LdapUserDatabase does not support this operation.
+     */
+    public void rename( String loginName, String newName )
+                                                          throws NoSuchPrincipalException,
+                                                              DuplicateUserException,
+                                                              WikiSecurityException
+    {
+        throw new WikiSecurityException( "Operation not supported" );
+    }
+
+    /**
+     * LdapUserDatabase does not support this operation.
+     */
+    public void save( UserProfile profile ) throws WikiSecurityException
+    {
+        throw new WikiSecurityException( "Operation not supported" );
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * This implementation validates the password by binding to the LDAP server
+     * as the user. The value of <code>loginName</code> is substituted into the
+     * <code>{0}</code> pattern specified by property
+     * {@link LdapConfig#PROPERTY_LOGIN_ID_PATTERN}.
+     * </p>
+     */
+    public boolean validatePassword( String loginName, String password )
+    {
+        String userPattern = m_cfg.loginIdPattern;
+        String username = userPattern.replace( "{0}", loginName );
+
+        Hashtable<String, String> env = m_cfg.newJndiEnvironment( username, password );
+        try
+        {
+            new InitialLdapContext( env, null );
+            return true;
+        }
+        catch( NamingException e )
+        {
+        }
+        return false;
+    }
+
+}

Modified: incubator/jspwiki/trunk/src/java/org/apache/wiki/auth/user/UserDatabase.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/trunk/src/java/org/apache/wiki/auth/user/UserDatabase.java?rev=804457&r1=804456&r2=804457&view=diff
==============================================================================
--- incubator/jspwiki/trunk/src/java/org/apache/wiki/auth/user/UserDatabase.java (original)
+++ incubator/jspwiki/trunk/src/java/org/apache/wiki/auth/user/UserDatabase.java Sat Aug 15 11:53:37 2009
@@ -75,7 +75,7 @@
      * this Principal using
      * {@link org.apache.wiki.auth.WikiPrincipal#WikiPrincipal(String, String)}
      * with the <code>type</code> parameter set to
-     * {@link org.apache.wiki.auth.WikiPrincipal#WIKI_NAME}. The method
+     * {@link org.apache.wiki.auth.WikiPrincipal#FULL_NAME}. The method
      * {@link org.apache.wiki.WikiSession#getUserPrincipal()} will return this
      * principal as the "primary" principal. Note that this method can also be
      * used to mark a WikiPrincipal as a login name or a wiki name.

Modified: incubator/jspwiki/trunk/src/java/org/apache/wiki/tags/UserProfileTag.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/trunk/src/java/org/apache/wiki/tags/UserProfileTag.java?rev=804457&r1=804456&r2=804457&view=diff
==============================================================================
--- incubator/jspwiki/trunk/src/java/org/apache/wiki/tags/UserProfileTag.java (original)
+++ incubator/jspwiki/trunk/src/java/org/apache/wiki/tags/UserProfileTag.java Sat Aug 15 11:53:37 2009
@@ -64,10 +64,17 @@
  * in the user database
  * <li><code>new</code> - evaluates the body of the tag if user's profile does not
  * exist in the user database
- * <li><code>canChangeLoginName</code> - always true if custom auth used; also true for container auth
- * and current UserDatabase.isSharedWithContainer() is true.</li>
- * <li><code>canChangePassword</code> - always true if custom auth used; also true for container auth
- * and current UserDatabase.isSharedWithContainer() is true.</li>
+ * <li><code>canChangeLoginName</code> - true if custom auth used and
+ * {@link UserManager#PROP_READ_ONLY_PROFILES} is not set; always
+ * false for container auth
+ * is set to <code>true</code></li>
+ * <li><code>canChangePassword</code> - true if custom auth used and
+ * {@link UserManager#PROP_READ_ONLY_PROFILES} is not set; always
+ * false for container auth</li>
+ * <li><code>canChangeEmail</code> - always true unless {@link UserManager#PROP_READ_ONLY_PROFILES}
+ * is set</li>
+ * <li><code>canChangeFullname</code> - always true unless {@link UserManager#PROP_READ_ONLY_PROFILES}
+ * is set</li>
  * </ul>
  * <p>In addition, the values <code>exists</code>, <code>new</code>, <code>canChangeLoginName</code>
  * and <code>canChangeLoginName</code> can also be prefixed with <code>!</code> to indicate the
@@ -113,6 +120,14 @@
 
     private static final String NOT_CHANGE_PASSWORD   = "!canchangepassword";
 
+    private static final String CHANGE_FULL_NAME      = "canchangefullname";
+    
+    private static final String NOT_CHANGE_FULL_NAME  = "!canchangefullname";
+    
+    private static final String CHANGE_EMAIL          = "canchangeemail";
+
+    private static final String NOT_CHANGE_EMAIL      = "!canchangeemail";
+
     private String             m_prop;
 
     public void initTag()
@@ -199,6 +214,20 @@
                 return EVAL_BODY_INCLUDE;
             }
         }
+        else if ( CHANGE_EMAIL.equals( m_prop ) || CHANGE_FULL_NAME.equals( m_prop ) )
+        {
+            if ( !m_wikiContext.getEngine().getUserManager().isReadOnly() )
+            {
+                return EVAL_BODY_INCLUDE;
+            }
+        }
+        else if ( NOT_CHANGE_EMAIL.equals( m_prop ) || NOT_CHANGE_FULL_NAME.equals( m_prop ) );
+        {
+            if ( m_wikiContext.getEngine().getUserManager().isReadOnly() )
+            {
+                return EVAL_BODY_INCLUDE;
+            }
+        }
 
         if ( result != null )
         {

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=804457&r1=804456&r2=804457&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 15 11:53:37 2009
@@ -24,17 +24,14 @@
 import java.io.FileOutputStream;
 import java.io.OutputStream;
 import java.security.Principal;
-import java.util.HashMap;
-import java.util.Map;
 import java.util.Properties;
 
 import junit.framework.TestCase;
 
 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.apache.wiki.auth.*;
+import org.apache.wiki.auth.login.LdapLoginModule;
 import org.freshcookies.security.Keychain;
 
 /**
@@ -42,47 +39,57 @@
  */
 public class LdapAuthorizerTest extends TestCase
 {
-    private Map<String, String> m_options;
+    private TestEngine m_engine;
 
     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 );
+        keychain.setEntry( LdapConfig.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() );
-    }
 
-    /**
-     * @see junit.framework.TestCase#setUp()
-     */
-    protected TestEngine createEngine( Map<String, String> config ) throws Exception
-    {
+        // Create the TestEngine properties
         Properties props = new Properties();
         props.load( TestEngine.findTestProperties() );
-        props.putAll( config );
+
+        // Set the LoginModule options
+        props.put( UserManager.PROP_READ_ONLY_PROFILES, "true" );
+        props.put( AuthenticationManager.PROP_LOGIN_MODULE, LdapLoginModule.class.getName() );
+        props.put( AuthenticationManager.PREFIX_LOGIN_MODULE_OPTIONS + LdapLoginModule.OPTION_CONNECTION_URL, "ldap://127.0.0.1:4890" );
+        props.put( AuthenticationManager.PREFIX_LOGIN_MODULE_OPTIONS + LdapLoginModule.OPTION_LOGIN_ID_PATTERN, "uid={0},ou=people,dc=jspwiki,dc=org" );
+        props.put( AuthenticationManager.PREFIX_LOGIN_MODULE_OPTIONS + LdapLoginModule.OPTION_USER_BASE, "dc=jspwiki,dc=org" );
+        props.put( AuthenticationManager.PREFIX_LOGIN_MODULE_OPTIONS + LdapLoginModule.OPTION_USER_PATTERN, "(&(objectClass=inetOrgPerson)(uid={0}))" );
+        props.put( AuthenticationManager.PREFIX_LOGIN_MODULE_OPTIONS + LdapLoginModule.OPTION_AUTHENTICATION, "simple" );
+        props.put( AuthenticationManager.PREFIX_LOGIN_MODULE_OPTIONS + LdapConfig.PROPERTY_SSL, "false" );
+
+        // Set the Authorizer properties
         props.put( AuthorizationManager.PROP_AUTHORIZER, LdapAuthorizer.class.getCanonicalName() );
+        props.put( LdapConfig.PROPERTY_ROLE_BASE, "ou=roles,dc=jspwiki,dc=org" );
+        props.put( LdapConfig.PROPERTY_ROLE_PATTERN, "(&(objectClass=groupOfUniqueNames)(cn={0}))" );
+        props.put( LdapConfig.PROPERTY_IS_IN_ROLE_PATTERN, "(&(&(objectClass=groupOfUniqueNames)(cn={0}))(uniqueMember={1}))" );
+        props.put( LdapConfig.PROPERTY_BIND_DN, "uid=Fred,ou=people,dc=jspwiki,dc=org" );
         props.put( AuthenticationManager.PROP_KEYCHAIN_PATH, "test-keychain" );
         props.put( AuthenticationManager.PROP_KEYCHAIN_PASSWORD, "keychain-password" );
-        TestEngine engine = new TestEngine( props );
-        return engine;
+
+        m_engine = new TestEngine( props );
+    }
+
+    protected void tearDown() throws Exception
+    {
+        File file = new File("tests/etc/WEB-INF/test-keychain" );
+        if ( file.exists() )
+        {
+            file.delete();
+        }
     }
 
     public void testGetRoles() throws Exception
     {
-        Authorizer authorizer = createEngine( m_options ).getAuthorizationManager().getAuthorizer();
+        Authorizer authorizer = m_engine.getAuthorizationManager().getAuthorizer();
 
         // LDAP should return just 2 roles, Admin and Role1
         Principal[] roles = authorizer.getRoles();
@@ -95,7 +102,7 @@
 
     public void testFindRole() throws Exception
     {
-        Authorizer authorizer = createEngine( m_options ).getAuthorizationManager().getAuthorizer();
+        Authorizer authorizer = m_engine.getAuthorizationManager().getAuthorizer();
         
         // We should be able to find roles Admin and Role1
         assertEquals( new Role("Admin"), authorizer.findRole( "Admin" ) );
@@ -107,18 +114,18 @@
 
     public void testIsUserInRole() throws Exception
     {
-        TestEngine engine = createEngine( m_options );
-        Authorizer authorizer = engine.getAuthorizationManager().getAuthorizer();
+        assertTrue( m_engine.getUserManager().isReadOnly() );
+        Authorizer authorizer = m_engine.getAuthorizationManager().getAuthorizer();
         Role admin = new Role( "Admin" );
         Role role1 = new Role( "Role1" );
         
         // Janne does not belong to any roles
-        WikiSession session = engine.janneSession();
+        WikiSession session = m_engine.janneSession();
         assertFalse( authorizer.isUserInRole( session, admin ) );
         assertFalse( authorizer.isUserInRole( session, role1 ) );
         
         // The Admin belongs to just the Admin role
-        session = engine.adminSession();
+        session = m_engine.adminSession();
         assertTrue( authorizer.isUserInRole( session, admin ) );
         assertFalse( authorizer.isUserInRole( session, role1 ) );
     }