001/* 
002    Licensed to the Apache Software Foundation (ASF) under one
003    or more contributor license agreements.  See the NOTICE file
004    distributed with this work for additional information
005    regarding copyright ownership.  The ASF licenses this file
006    to you under the Apache License, Version 2.0 (the
007    "License"); you may not use this file except in compliance
008    with the License.  You may obtain a copy of the License at
009
010       http://www.apache.org/licenses/LICENSE-2.0
011
012    Unless required by applicable law or agreed to in writing,
013    software distributed under the License is distributed on an
014    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015    KIND, either express or implied.  See the License for the
016    specific language governing permissions and limitations
017    under the License.  
018 */
019package org.apache.wiki.auth.user;
020
021import java.io.UnsupportedEncodingException;
022import java.security.MessageDigest;
023import java.security.NoSuchAlgorithmException;
024import java.security.Principal;
025import java.util.ArrayList;
026import java.util.Properties;
027import java.util.UUID;
028
029import org.apache.log4j.Logger;
030import org.apache.wiki.WikiEngine;
031import org.apache.wiki.api.exceptions.NoRequiredPropertyException;
032import org.apache.wiki.auth.NoSuchPrincipalException;
033import org.apache.wiki.auth.WikiPrincipal;
034import org.apache.wiki.auth.WikiSecurityException;
035import org.apache.wiki.util.ByteUtils;
036import org.apache.wiki.util.CryptoUtil;
037
038/**
039 * Abstract UserDatabase class that provides convenience methods for finding
040 * profiles, building Principal collections and hashing passwords.
041 * @since 2.3
042 */
043public abstract class AbstractUserDatabase implements UserDatabase
044{
045
046    protected static final Logger log = Logger.getLogger( AbstractUserDatabase.class );
047    protected static final String SHA_PREFIX = "{SHA}";
048    protected static final String SSHA_PREFIX = "{SSHA}";
049
050    /**
051     * No-op method that in previous versions of JSPWiki was intended to
052     * atomically commit changes to the user database. Now, the {@link #rename(String, String)},
053     * {@link #save(UserProfile)} and {@link #deleteByLoginName(String)} methods
054     * are atomic themselves.
055     * @throws WikiSecurityException
056     * @deprecated there is no need to call this method because the save, rename and
057     * delete methods contain their own commit logic
058     */
059    public synchronized void commit() throws WikiSecurityException
060    { }
061
062    /**
063     * Looks up and returns the first {@link UserProfile}in the user database
064     * that whose login name, full name, or wiki name matches the supplied
065     * string. This method provides a "forgiving" search algorithm for resolving
066     * principal names when the exact profile attribute that supplied the name
067     * is unknown.
068     * @param index the login name, full name, or wiki name
069     * @see org.apache.wiki.auth.user.UserDatabase#find(java.lang.String)
070     */
071    public UserProfile find( String index ) throws NoSuchPrincipalException
072    {
073        UserProfile profile = null;
074
075        // Try finding by full name
076        try
077        {
078            profile = findByFullName( index );
079        }
080        catch ( NoSuchPrincipalException e )
081        {
082        }
083        if ( profile != null )
084        {
085            return profile;
086        }
087
088        // Try finding by wiki name
089        try
090        {
091            profile = findByWikiName( index );
092        }
093        catch ( NoSuchPrincipalException e )
094        {
095        }
096        if ( profile != null )
097        {
098            return profile;
099        }
100
101        // Try finding by login name
102        try
103        {
104            profile = findByLoginName( index );
105        }
106        catch ( NoSuchPrincipalException e )
107        {
108        }
109        if ( profile != null )
110        {
111            return profile;
112        }
113
114        throw new NoSuchPrincipalException( "Not in database: " + index );
115    }
116
117    /**
118     * {@inheritDoc}
119     * @see org.apache.wiki.auth.user.UserDatabase#findByEmail(java.lang.String)
120     */
121    public abstract UserProfile findByEmail( String index ) throws NoSuchPrincipalException;
122
123    /**
124     * {@inheritDoc}
125     * @see org.apache.wiki.auth.user.UserDatabase#findByFullName(java.lang.String)
126     */
127    public abstract UserProfile findByFullName( String index ) throws NoSuchPrincipalException;
128
129    /**
130     * {@inheritDoc}
131     * @see org.apache.wiki.auth.user.UserDatabase#findByLoginName(java.lang.String)
132     */
133    public abstract UserProfile findByLoginName( String index ) throws NoSuchPrincipalException;
134
135    /**
136     * {@inheritDoc}
137     * @see org.apache.wiki.auth.user.UserDatabase#findByWikiName(java.lang.String)
138     */
139    public abstract UserProfile findByWikiName( String index ) throws NoSuchPrincipalException;
140
141    /**
142     * <p>Looks up the Principals representing a user from the user database. These
143     * are defined as a set of WikiPrincipals manufactured from the login name,
144     * full name, and wiki name. If the user database does not contain a user
145     * with the supplied identifier, throws a {@link NoSuchPrincipalException}.</p>
146     * <p>When this method creates WikiPrincipals, the Principal containing
147     * the user's full name is marked as containing the common name (see
148     * {@link org.apache.wiki.auth.WikiPrincipal#WikiPrincipal(String, String)}).
149     * @param identifier the name of the principal to retrieve; this corresponds to
150     *            value returned by the user profile's
151     *            {@link UserProfile#getLoginName()}method.
152     * @return the array of Principals representing the user
153     * @see org.apache.wiki.auth.user.UserDatabase#getPrincipals(java.lang.String)
154     * @throws NoSuchPrincipalException {@inheritDoc}
155     */
156    public Principal[] getPrincipals( String identifier ) throws NoSuchPrincipalException
157    {
158        try
159        {
160            UserProfile profile = findByLoginName( identifier );
161            ArrayList<Principal> principals = new ArrayList<Principal>();
162            if ( profile.getLoginName() != null && profile.getLoginName().length() > 0 )
163            {
164                principals.add( new WikiPrincipal( profile.getLoginName(), WikiPrincipal.LOGIN_NAME ) );
165            }
166            if ( profile.getFullname() != null && profile.getFullname().length() > 0 )
167            {
168                principals.add( new WikiPrincipal( profile.getFullname(), WikiPrincipal.FULL_NAME ) );
169            }
170            if ( profile.getWikiName() != null && profile.getWikiName().length() > 0 )
171            {
172                principals.add( new WikiPrincipal( profile.getWikiName(), WikiPrincipal.WIKI_NAME ) );
173            }
174            return principals.toArray( new Principal[principals.size()] );
175        }
176        catch( NoSuchPrincipalException e )
177        {
178            throw e;
179        }
180    }
181
182    /**
183     * {@inheritDoc}
184     * @see org.apache.wiki.auth.user.UserDatabase#initialize(org.apache.wiki.WikiEngine, java.util.Properties)
185     */
186    public abstract void initialize( WikiEngine engine, Properties props ) throws NoRequiredPropertyException,
187            WikiSecurityException;
188
189    /**
190     * Factory method that instantiates a new DefaultUserProfile with a new, distinct
191     * unique identifier.
192     * 
193     * @return A new, empty profile.
194     */
195    public UserProfile newProfile()
196    {
197        return DefaultUserProfile.newProfile( this );
198    }
199
200    /**
201     * {@inheritDoc}
202     * @see org.apache.wiki.auth.user.UserDatabase#save(org.apache.wiki.auth.user.UserProfile)
203     */
204    public abstract void save( UserProfile profile ) throws WikiSecurityException;
205
206    /**
207     * Validates the password for a given user. If the user does not exist in
208     * the user database, this method always returns <code>false</code>. If
209     * the user exists, the supplied password is compared to the stored
210     * password. Note that if the stored password's value starts with
211     * <code>{SHA}</code>, the supplied password is hashed prior to the
212     * comparison.
213     * @param loginName the user's login name
214     * @param password the user's password (obtained from user input, e.g., a web form)
215     * @return <code>true</code> if the supplied user password matches the
216     * stored password
217     * @see org.apache.wiki.auth.user.UserDatabase#validatePassword(java.lang.String,
218     *      java.lang.String)
219     */
220    public boolean validatePassword( String loginName, String password )
221    {
222        String hashedPassword;
223        try
224        {
225            UserProfile profile = findByLoginName( loginName );
226            String storedPassword = profile.getPassword();
227            
228            // Is the password stored as a salted hash (the new 2.8 format?)
229            boolean newPasswordFormat = storedPassword.startsWith( SSHA_PREFIX );
230            
231            // If new format, verify the hash
232            if ( newPasswordFormat )
233            {
234                hashedPassword = getHash( password );
235                return CryptoUtil.verifySaltedPassword( password.getBytes("UTF-8"), storedPassword );
236            }
237
238            // If old format, verify using the old SHA verification algorithm
239            if ( storedPassword.startsWith( SHA_PREFIX ) )
240            {
241                storedPassword = storedPassword.substring( SHA_PREFIX.length() );
242            }
243            hashedPassword = getOldHash( password );
244            boolean verified = hashedPassword.equals( storedPassword ); 
245            
246            // If in the old format and password verified, upgrade the hash to SSHA
247            if ( verified )
248            {
249                profile.setPassword( password );
250                save( profile );
251            }
252            
253            return verified;
254        }
255        catch( NoSuchPrincipalException e )
256        {
257        }
258        catch( NoSuchAlgorithmException e )
259        {
260            log.error( "Unsupported algorithm: " + e.getMessage() );
261        }
262        catch( UnsupportedEncodingException e )
263        {
264            log.fatal( "You do not have UTF-8!?!" );
265        }
266        catch( WikiSecurityException e )
267        {
268            log.error( "Could not upgrade SHA password to SSHA because profile could not be saved. Reason: " + e.getMessage() );
269            e.printStackTrace();
270        }
271        return false;
272    }
273
274    /**
275     * Generates a new random user identifier (uid) that is guaranteed to be unique.
276     * 
277     * @param db The database for which the UID should be generated.
278     * @return A random, unique UID.
279     */
280    protected static String generateUid( UserDatabase db )
281    {
282        // Keep generating UUIDs until we find one that doesn't collide
283        String uid = null;
284        boolean collision;
285        
286        do 
287        {
288            uid = UUID.randomUUID().toString();
289            collision = true;
290            try
291            {
292                db.findByUid( uid );
293            }
294            catch ( NoSuchPrincipalException e )
295            {
296                collision = false;
297            }
298        } 
299        while ( collision || uid == null );
300        return uid;
301    }
302    
303    /**
304     * Private method that calculates the salted SHA-1 hash of a given
305     * <code>String</code>. Note that as of JSPWiki 2.8, this method calculates
306     * a <em>salted</em> hash rather than a plain hash.
307     * @param text the text to hash
308     * @return the result hash
309     */
310    protected String getHash( String text )
311    {
312        String hash = null;
313        try
314        {
315            hash = CryptoUtil.getSaltedPassword( text.getBytes("UTF-8") );
316        }
317        catch( NoSuchAlgorithmException e )
318        {
319            log.error( "Error creating salted SHA password hash:" + e.getMessage() );
320            hash = text;
321        }
322        catch( UnsupportedEncodingException e )
323        {
324            log.fatal("You do not have UTF-8!?!");
325        }
326        return hash;
327    }
328
329    /**
330     * Private method that calculates the SHA-1 hash of a given
331     * <code>String</code>
332     * @param text the text to hash
333     * @return the result hash
334     * @deprecated this method is retained for backwards compatibility purposes; use {@link #getHash(String)} instead
335     */
336    protected String getOldHash( String text )
337    {
338        String hash = null;
339        try
340        {
341            MessageDigest md = MessageDigest.getInstance( "SHA" );
342            md.update( text.getBytes("UTF-8") );
343            byte[] digestedBytes = md.digest();
344            hash = ByteUtils.bytes2hex( digestedBytes );
345        }
346        catch( NoSuchAlgorithmException e )
347        {
348            log.error( "Error creating SHA password hash:" + e.getMessage() );
349            hash = text;
350        }
351        catch (UnsupportedEncodingException e)
352        {
353            log.fatal("UTF-8 not supported!?!");
354        }
355        return hash;
356    }
357
358    /**
359     * Parses a long integer from a supplied string, or returns 0 if not parsable.
360     * @param value the string to parse
361     * @return the value parsed
362     */
363    protected long parseLong( String value )
364    {
365        if ( value == null || value.length() == 0 )
366        {
367            return 0;
368        }
369        try
370        {
371            return Long.parseLong( value );
372        }
373        catch ( NumberFormatException e )
374        {
375            return 0;
376        }
377    }
378
379}