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     */
019    package org.apache.wiki.auth.user;
020    
021    import java.io.*;
022    import java.security.MessageDigest;
023    import java.security.NoSuchAlgorithmException;
024    import java.security.Principal;
025    import java.util.*;
026    
027    import org.apache.catalina.util.HexUtils;
028    import org.apache.log4j.Logger;
029    import org.apache.wiki.WikiEngine;
030    import org.apache.wiki.api.exceptions.NoRequiredPropertyException;
031    import org.apache.wiki.auth.NoSuchPrincipalException;
032    import org.apache.wiki.auth.WikiPrincipal;
033    import org.apache.wiki.auth.WikiSecurityException;
034    import org.apache.wiki.util.CryptoUtil;
035    
036    /**
037     * Abstract UserDatabase class that provides convenience methods for finding
038     * profiles, building Principal collections and hashing passwords.
039     * @since 2.3
040     */
041    public abstract class AbstractUserDatabase implements UserDatabase
042    {
043    
044        protected static final Logger log = Logger.getLogger( AbstractUserDatabase.class );
045        protected static final String SHA_PREFIX = "{SHA}";
046        protected static final String SSHA_PREFIX = "{SSHA}";
047    
048        /**
049         * No-op method that in previous versions of JSPWiki was intended to
050         * atomically commit changes to the user database. Now, the {@link #rename(String, String)},
051         * {@link #save(UserProfile)} and {@link #deleteByLoginName(String)} methods
052         * are atomic themselves.
053         * @throws WikiSecurityException
054         * @deprecated there is no need to call this method because the save, rename and
055         * delete methods contain their own commit logic
056         */
057        @SuppressWarnings("deprecation")
058        public synchronized void commit() throws WikiSecurityException
059        { }
060    
061        /**
062         * Looks up and returns the first {@link UserProfile}in the user database
063         * that whose login name, full name, or wiki name matches the supplied
064         * string. This method provides a "forgiving" search algorithm for resolving
065         * principal names when the exact profile attribute that supplied the name
066         * is unknown.
067         * @param index the login name, full name, or wiki name
068         * @see org.apache.wiki.auth.user.UserDatabase#find(java.lang.String)
069         */
070        public UserProfile find( String index ) throws NoSuchPrincipalException
071        {
072            UserProfile profile = null;
073    
074            // Try finding by full name
075            try
076            {
077                profile = findByFullName( index );
078            }
079            catch ( NoSuchPrincipalException e )
080            {
081            }
082            if ( profile != null )
083            {
084                return profile;
085            }
086    
087            // Try finding by wiki name
088            try
089            {
090                profile = findByWikiName( index );
091            }
092            catch ( NoSuchPrincipalException e )
093            {
094            }
095            if ( profile != null )
096            {
097                return profile;
098            }
099    
100            // Try finding by login name
101            try
102            {
103                profile = findByLoginName( index );
104            }
105            catch ( NoSuchPrincipalException e )
106            {
107            }
108            if ( profile != null )
109            {
110                return profile;
111            }
112    
113            throw new NoSuchPrincipalException( "Not in database: " + index );
114        }
115    
116        /**
117         * {@inheritDoc}
118         * @see org.apache.wiki.auth.user.UserDatabase#findByEmail(java.lang.String)
119         */
120        public abstract UserProfile findByEmail( String index ) throws NoSuchPrincipalException;
121    
122        /**
123         * {@inheritDoc}
124         * @see org.apache.wiki.auth.user.UserDatabase#findByFullName(java.lang.String)
125         */
126        public abstract UserProfile findByFullName( String index ) throws NoSuchPrincipalException;
127    
128        /**
129         * {@inheritDoc}
130         * @see org.apache.wiki.auth.user.UserDatabase#findByLoginName(java.lang.String)
131         */
132        public abstract UserProfile findByLoginName( String index ) throws NoSuchPrincipalException;
133    
134        /**
135         * {@inheritDoc}
136         * @see org.apache.wiki.auth.user.UserDatabase#findByWikiName(java.lang.String)
137         */
138        public abstract UserProfile findByWikiName( String index ) throws NoSuchPrincipalException;
139    
140        /**
141         * <p>Looks up the Principals representing a user from the user database. These
142         * are defined as a set of WikiPrincipals manufactured from the login name,
143         * full name, and wiki name. If the user database does not contain a user
144         * with the supplied identifier, throws a {@link NoSuchPrincipalException}.</p>
145         * <p>When this method creates WikiPrincipals, the Principal containing
146         * the user's full name is marked as containing the common name (see
147         * {@link org.apache.wiki.auth.WikiPrincipal#WikiPrincipal(String, String)}).
148         * @param identifier the name of the principal to retrieve; this corresponds to
149         *            value returned by the user profile's
150         *            {@link UserProfile#getLoginName()}method.
151         * @return the array of Principals representing the user
152         * @see org.apache.wiki.auth.user.UserDatabase#getPrincipals(java.lang.String)
153         * @throws NoSuchPrincipalException {@inheritDoc}
154         */
155        public Principal[] getPrincipals( String identifier ) throws NoSuchPrincipalException
156        {
157            try
158            {
159                UserProfile profile = findByLoginName( identifier );
160                ArrayList<Principal> principals = new ArrayList<Principal>();
161                if ( profile.getLoginName() != null && profile.getLoginName().length() > 0 )
162                {
163                    principals.add( new WikiPrincipal( profile.getLoginName(), WikiPrincipal.LOGIN_NAME ) );
164                }
165                if ( profile.getFullname() != null && profile.getFullname().length() > 0 )
166                {
167                    principals.add( new WikiPrincipal( profile.getFullname(), WikiPrincipal.FULL_NAME ) );
168                }
169                if ( profile.getWikiName() != null && profile.getWikiName().length() > 0 )
170                {
171                    principals.add( new WikiPrincipal( profile.getWikiName(), WikiPrincipal.WIKI_NAME ) );
172                }
173                return principals.toArray( new Principal[principals.size()] );
174            }
175            catch( NoSuchPrincipalException e )
176            {
177                throw e;
178            }
179        }
180    
181        /**
182         * {@inheritDoc}
183         * @see org.apache.wiki.auth.user.UserDatabase#initialize(org.apache.wiki.WikiEngine, java.util.Properties)
184         */
185        public abstract void initialize( WikiEngine engine, Properties props ) throws NoRequiredPropertyException,
186                WikiSecurityException;
187    
188        /**
189         * Factory method that instantiates a new DefaultUserProfile with a new, distinct
190         * unique identifier.
191         * 
192         * @return A new, empty profile.
193         */
194        public UserProfile newProfile()
195        {
196            return DefaultUserProfile.newProfile( this );
197        }
198    
199        /**
200         * {@inheritDoc}
201         * @see org.apache.wiki.auth.user.UserDatabase#save(org.apache.wiki.auth.user.UserProfile)
202         */
203        public abstract void save( UserProfile profile ) throws WikiSecurityException;
204    
205        /**
206         * Validates the password for a given user. If the user does not exist in
207         * the user database, this method always returns <code>false</code>. If
208         * the user exists, the supplied password is compared to the stored
209         * password. Note that if the stored password's value starts with
210         * <code>{SHA}</code>, the supplied password is hashed prior to the
211         * comparison.
212         * @param loginName the user's login name
213         * @param password the user's password (obtained from user input, e.g., a web form)
214         * @return <code>true</code> if the supplied user password matches the
215         * stored password
216         * @throws NoSuchAlgorithmException 
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 = HexUtils.convert( 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    }