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