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}