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 }