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}