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 org.apache.commons.lang3.math.NumberUtils;
022import org.apache.log4j.Logger;
023import org.apache.wiki.api.core.Engine;
024import org.apache.wiki.api.exceptions.NoRequiredPropertyException;
025import org.apache.wiki.auth.NoSuchPrincipalException;
026import org.apache.wiki.auth.WikiPrincipal;
027import org.apache.wiki.auth.WikiSecurityException;
028import org.apache.wiki.util.ByteUtils;
029import org.apache.wiki.util.CryptoUtil;
030
031import java.nio.charset.StandardCharsets;
032import java.security.MessageDigest;
033import java.security.NoSuchAlgorithmException;
034import java.security.Principal;
035import java.util.ArrayList;
036import java.util.Properties;
037import java.util.UUID;
038
039/**
040 * Abstract UserDatabase class that provides convenience methods for finding profiles, building Principal collections and hashing passwords.
041 *
042 * @since 2.3
043 */
044public abstract class AbstractUserDatabase implements UserDatabase {
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     * Looks up and returns the first {@link UserProfile} in the user database that whose login name, full name, or wiki name matches the
052     * supplied string. This method provides a "forgiving" search algorithm for resolving principal names when the exact profile attribute
053     * that supplied the name is unknown.
054     *
055     * @param index the login name, full name, or wiki name
056     * @see org.apache.wiki.auth.user.UserDatabase#find(java.lang.String)
057     */
058    @Override
059    public UserProfile find( final String index ) throws NoSuchPrincipalException {
060        UserProfile profile = null;
061
062        // Try finding by full name
063        try {
064            profile = findByFullName( index );
065        } catch( final NoSuchPrincipalException e ) {
066        }
067        if( profile != null ) {
068            return profile;
069        }
070
071        // Try finding by wiki name
072        try {
073            profile = findByWikiName( index );
074        } catch( final NoSuchPrincipalException e ) {
075        }
076        if( profile != null ) {
077            return profile;
078        }
079
080        // Try finding by login name
081        try {
082            profile = findByLoginName( index );
083        } catch( final NoSuchPrincipalException e ) {
084        }
085        if( profile != null ) {
086            return profile;
087        }
088
089        throw new NoSuchPrincipalException( "Not in database: " + index );
090    }
091
092    /**
093     * {@inheritDoc}
094     * @see org.apache.wiki.auth.user.UserDatabase#findByEmail(java.lang.String)
095     */
096    @Override
097    public abstract UserProfile findByEmail( String index ) throws NoSuchPrincipalException;
098
099    /**
100     * {@inheritDoc}
101     * @see org.apache.wiki.auth.user.UserDatabase#findByFullName(java.lang.String)
102     */
103    @Override
104    public abstract UserProfile findByFullName( String index ) throws NoSuchPrincipalException;
105
106    /**
107     * {@inheritDoc}
108     * @see org.apache.wiki.auth.user.UserDatabase#findByLoginName(java.lang.String)
109     */
110    @Override
111    public abstract UserProfile findByLoginName( String index ) throws NoSuchPrincipalException;
112
113    /**
114     * {@inheritDoc}
115     * @see org.apache.wiki.auth.user.UserDatabase#findByWikiName(java.lang.String)
116     */
117    @Override
118    public abstract UserProfile findByWikiName( String index ) throws NoSuchPrincipalException;
119
120    /**
121     * <p>Looks up the Principals representing a user from the user database. These
122     * are defined as a set of WikiPrincipals manufactured from the login name,
123     * full name, and wiki name. If the user database does not contain a user
124     * with the supplied identifier, throws a {@link NoSuchPrincipalException}.</p>
125     * <p>When this method creates WikiPrincipals, the Principal containing
126     * the user's full name is marked as containing the common name (see
127     * {@link org.apache.wiki.auth.WikiPrincipal#WikiPrincipal(String, String)}).
128     * @param identifier the name of the principal to retrieve; this corresponds to
129     *            value returned by the user profile's
130     *            {@link UserProfile#getLoginName()}method.
131     * @return the array of Principals representing the user
132     * @see org.apache.wiki.auth.user.UserDatabase#getPrincipals(java.lang.String)
133     * @throws NoSuchPrincipalException If the user database does not contain user with the supplied identifier
134     */
135    @Override
136    public Principal[] getPrincipals( final String identifier ) throws NoSuchPrincipalException {
137        final UserProfile profile = findByLoginName( identifier );
138        final ArrayList< Principal > principals = new ArrayList<>();
139        if( profile.getLoginName() != null && profile.getLoginName().length() > 0 ) {
140            principals.add( new WikiPrincipal( profile.getLoginName(), WikiPrincipal.LOGIN_NAME ) );
141        }
142        if( profile.getFullname() != null && profile.getFullname().length() > 0 ) {
143            principals.add( new WikiPrincipal( profile.getFullname(), WikiPrincipal.FULL_NAME ) );
144        }
145        if( profile.getWikiName() != null && profile.getWikiName().length() > 0 ) {
146            principals.add( new WikiPrincipal( profile.getWikiName(), WikiPrincipal.WIKI_NAME ) );
147        }
148        return principals.toArray( new Principal[ principals.size() ] );
149    }
150
151    /**
152     * {@inheritDoc}
153     *
154     * @see org.apache.wiki.auth.user.UserDatabase#initialize(org.apache.wiki.api.core.Engine, java.util.Properties)
155     */
156    @Override
157    public abstract void initialize( Engine engine, Properties props ) throws NoRequiredPropertyException, WikiSecurityException;
158
159    /**
160     * Factory method that instantiates a new DefaultUserProfile with a new, distinct unique identifier.
161     * 
162     * @return A new, empty profile.
163     */
164    @Override
165    public UserProfile newProfile() {
166        final UserProfile profile = new DefaultUserProfile();
167        profile.setUid( AbstractUserDatabase.generateUid( this ) );
168        return profile;
169    }
170
171    /**
172     * {@inheritDoc}
173     * @see org.apache.wiki.auth.user.UserDatabase#save(org.apache.wiki.auth.user.UserProfile)
174     */
175    @Override
176    public abstract void save( UserProfile profile ) throws WikiSecurityException;
177
178    /**
179     * Validates the password for a given user. If the user does not exist in the user database, this method always returns
180     * <code>false</code>. If the user exists, the supplied password is compared to the stored password. Note that if the stored password's
181     * value starts with <code>{SHA}</code>, the supplied password is hashed prior to the comparison.
182     *
183     * @param loginName the user's login name
184     * @param password the user's password (obtained from user input, e.g., a web form)
185     * @return <code>true</code> if the supplied user password matches the stored password
186     * @see org.apache.wiki.auth.user.UserDatabase#validatePassword(java.lang.String, java.lang.String)
187     */
188    @Override
189    public boolean validatePassword( final String loginName, final String password ) {
190        final String hashedPassword;
191        try {
192            final UserProfile profile = findByLoginName( loginName );
193            String storedPassword = profile.getPassword();
194
195            // Is the password stored as a salted hash (the new 2.8 format?)
196            final boolean newPasswordFormat = storedPassword.startsWith( SSHA_PREFIX );
197
198            // If new format, verify the hash
199            if( newPasswordFormat ) {
200                hashedPassword = getHash( password );
201                return CryptoUtil.verifySaltedPassword( password.getBytes( StandardCharsets.UTF_8 ), storedPassword );
202            }
203
204            // If old format, verify using the old SHA verification algorithm
205            if( storedPassword.startsWith( SHA_PREFIX ) ) {
206                storedPassword = storedPassword.substring( SHA_PREFIX.length() );
207            }
208            hashedPassword = getOldHash( password );
209            final boolean verified = hashedPassword.equals( storedPassword );
210
211            // If in the old format and password verified, upgrade the hash to SSHA
212            if( verified ) {
213                profile.setPassword( password );
214                save( profile );
215            }
216
217            return verified;
218        } catch( final NoSuchPrincipalException e ) {
219        } catch( final NoSuchAlgorithmException e ) {
220            log.error( "Unsupported algorithm: " + e.getMessage() );
221        } catch( final WikiSecurityException e ) {
222            log.error( "Could not upgrade SHA password to SSHA because profile could not be saved. Reason: " + e.getMessage(), e );
223        }
224        return false;
225    }
226
227    /**
228     * Generates a new random user identifier (uid) that is guaranteed to be unique.
229     * 
230     * @param db The database for which the UID should be generated.
231     * @return A random, unique UID.
232     */
233    protected static String generateUid( final UserDatabase db ) {
234        // Keep generating UUIDs until we find one that doesn't collide
235        String uid;
236        boolean collision;
237        
238        do {
239            uid = UUID.randomUUID().toString();
240            collision = true;
241            try {
242                db.findByUid( uid );
243            } catch ( final NoSuchPrincipalException e ) {
244                collision = false;
245            }
246        } 
247        while ( collision || uid == null );
248        return uid;
249    }
250    
251    /**
252     * Private method that calculates the salted SHA-1 hash of a given <code>String</code>. Note that as of JSPWiki 2.8, this method
253     * calculates a <em>salted</em> hash rather than a plain hash.
254     *
255     * @param text the text to hash
256     * @return the result hash
257     */
258    protected String getHash( final String text ) {
259        try {
260            return CryptoUtil.getSaltedPassword( text.getBytes(StandardCharsets.UTF_8 ) );
261        } catch( final NoSuchAlgorithmException e ) {
262            log.error( "Error creating salted SHA password hash:" + e.getMessage() );
263            return text;
264        }
265    }
266
267    /**
268     * Private method that calculates the SHA-1 hash of a given <code>String</code>
269     *
270     * @param text the text to hash
271     * @return the result hash
272     * @deprecated this method is retained for backwards compatibility purposes; use {@link #getHash(String)} instead
273     */
274    String getOldHash( final String text ) {
275        try {
276            final MessageDigest md = MessageDigest.getInstance( "SHA" );
277            md.update( text.getBytes( StandardCharsets.UTF_8 ) );
278            final byte[] digestedBytes = md.digest();
279            return ByteUtils.bytes2hex( digestedBytes );
280        } catch( final NoSuchAlgorithmException e ) {
281            log.error( "Error creating SHA password hash:" + e.getMessage() );
282            return text;
283        }
284    }
285
286    /**
287     * Parses a long integer from a supplied string, or returns 0 if not parsable.
288     *
289     * @param value the string to parse
290     * @return the value parsed
291     */
292    protected long parseLong( final String value ) {
293        if( NumberUtils.isParsable( value ) ) {
294            return Long.parseLong( value );
295        } else {
296            return 0L;
297        }
298    }
299
300}