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 }