blob: 74822151f9b6ff9f3221436159349b2da65ded0c [file] [log] [blame]
/*
JSPWiki - a JSP-based WikiWiki clone.
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
package org.apache.wiki.auth.user;
import java.io.*;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.util.*;
import org.apache.wiki.NoRequiredPropertyException;
import org.apache.wiki.WikiEngine;
import org.apache.wiki.auth.NoSuchPrincipalException;
import org.apache.wiki.auth.WikiPrincipal;
import org.apache.wiki.auth.WikiSecurityException;
import org.apache.wiki.log.Logger;
import org.apache.wiki.log.LoggerFactory;
import org.apache.wiki.util.CryptoUtil;
import org.apache.wiki.util.HexUtils;
/**
* Abstract UserDatabase class that provides convenience methods for finding
* profiles, building Principal collections and hashing passwords.
* @since 2.3
*/
public abstract class AbstractUserDatabase implements UserDatabase
{
protected static final Logger log = LoggerFactory.getLogger( AbstractUserDatabase.class );
protected static final String SHA_PREFIX = "{SHA}";
protected static final String SSHA_PREFIX = "{SSHA}";
/**
* No-op method that in previous versions of JSPWiki was intended to
* atomically commit changes to the user database. Now, the {@link #rename(String, String)},
* {@link #save(UserProfile)} and {@link #deleteByLoginName(String)} methods
* are atomic themselves.
* @throws WikiSecurityException
* @deprecated there is no need to call this method because the save, rename and
* delete methods contain their own commit logic
*/
public synchronized void commit() throws WikiSecurityException
{ }
/**
* Looks up and returns the first {@link UserProfile}in the user database
* that whose login name, full name, or wiki name matches the supplied
* string. This method provides a "forgiving" search algorithm for resolving
* principal names when the exact profile attribute that supplied the name
* is unknown.
* @param index the login name, full name, or wiki name
* @see org.apache.wiki.auth.user.UserDatabase#find(java.lang.String)
*/
public UserProfile find( String index ) throws NoSuchPrincipalException
{
UserProfile profile = null;
// Try finding by full name
try
{
profile = findByFullName( index );
}
catch ( NoSuchPrincipalException e )
{
}
if ( profile != null )
{
return profile;
}
// Try finding by wiki name
try
{
profile = findByWikiName( index );
}
catch ( NoSuchPrincipalException e )
{
}
if ( profile != null )
{
return profile;
}
// Try finding by login name
try
{
profile = findByLoginName( index );
}
catch ( NoSuchPrincipalException e )
{
}
if ( profile != null )
{
return profile;
}
throw new NoSuchPrincipalException( "Not in database: " + index );
}
/**
* {@inheritDoc}
* @see org.apache.wiki.auth.user.UserDatabase#findByEmail(java.lang.String)
*/
public abstract UserProfile findByEmail( String index ) throws NoSuchPrincipalException;
/**
* {@inheritDoc}
* @see org.apache.wiki.auth.user.UserDatabase#findByFullName(java.lang.String)
*/
public abstract UserProfile findByFullName( String index ) throws NoSuchPrincipalException;
/**
* {@inheritDoc}
* @see org.apache.wiki.auth.user.UserDatabase#findByLoginName(java.lang.String)
*/
public abstract UserProfile findByLoginName( String index ) throws NoSuchPrincipalException;
/**
* {@inheritDoc}
* @see org.apache.wiki.auth.user.UserDatabase#findByWikiName(java.lang.String)
*/
public abstract UserProfile findByWikiName( String index ) throws NoSuchPrincipalException;
/**
* <p>Looks up the Principals representing a user from the user database. These
* are defined as a set of WikiPrincipals manufactured from the login name,
* full name, and wiki name. If the user database does not contain a user
* with the supplied identifier, throws a {@link NoSuchPrincipalException}.</p>
* <p>When this method creates WikiPrincipals, the Principal containing
* the user's full name is marked as containing the common name (see
* {@link org.apache.wiki.auth.WikiPrincipal#WikiPrincipal(String, String)}).
* @param identifier the name of the principal to retrieve; this corresponds to
* value returned by the user profile's
* {@link UserProfile#getLoginName()}method.
* @return the array of Principals representing the user
* @see org.apache.wiki.auth.user.UserDatabase#getPrincipals(java.lang.String)
* @throws NoSuchPrincipalException {@inheritDoc}
*/
public Principal[] getPrincipals( String identifier ) throws NoSuchPrincipalException
{
try
{
UserProfile profile = findByLoginName( identifier );
ArrayList<Principal> principals = new ArrayList<Principal>();
if ( profile.getLoginName() != null && profile.getLoginName().length() > 0 )
{
principals.add( new WikiPrincipal( profile.getLoginName(), WikiPrincipal.LOGIN_NAME ) );
}
if ( profile.getFullname() != null && profile.getFullname().length() > 0 )
{
principals.add( new WikiPrincipal( profile.getFullname(), WikiPrincipal.FULL_NAME ) );
}
if ( profile.getWikiName() != null && profile.getWikiName().length() > 0 )
{
principals.add( new WikiPrincipal( profile.getWikiName(), WikiPrincipal.WIKI_NAME ) );
}
return principals.toArray( new Principal[principals.size()] );
}
catch( NoSuchPrincipalException e )
{
throw e;
}
}
/**
* {@inheritDoc}
* @see org.apache.wiki.auth.user.UserDatabase#initialize(org.apache.wiki.WikiEngine, java.util.Properties)
*/
public abstract void initialize( WikiEngine engine, Properties props ) throws NoRequiredPropertyException;
/**
* Factory method that instantiates a new DefaultUserProfile with a new, distinct
* unique identifier.
*
* @return A new, empty profile.
*/
public UserProfile newProfile()
{
return DefaultUserProfile.newProfile( this );
}
/**
* {@inheritDoc}
* @see org.apache.wiki.auth.user.UserDatabase#save(org.apache.wiki.auth.user.UserProfile)
*/
public abstract void save( UserProfile profile ) throws WikiSecurityException;
/**
* Validates the password for a given user. If the user does not exist in
* the user database, this method always returns <code>false</code>. If
* the user exists, the supplied password is compared to the stored
* password. Note that if the stored password's value starts with
* <code>{SHA}</code>, the supplied password is hashed prior to the
* comparison.
* @param loginName the user's login name
* @param password the user's password (obtained from user input, e.g., a web form)
* @return <code>true</code> if the supplied user password matches the
* stored password
* @throws NoSuchAlgorithmException
* @see org.apache.wiki.auth.user.UserDatabase#validatePassword(java.lang.String,
* java.lang.String)
*/
public boolean validatePassword( String loginName, String password )
{
String hashedPassword;
try
{
UserProfile profile = findByLoginName( loginName );
String storedPassword = profile.getPassword();
// Is the password stored as a salted hash (the new 2.8 format?)
boolean newPasswordFormat = storedPassword.startsWith( SSHA_PREFIX );
// If new format, verify the hash
if ( newPasswordFormat )
{
hashedPassword = getHash( password );
return CryptoUtil.verifySaltedPassword( password.getBytes("UTF-8"), storedPassword );
}
// If old format, verify using the old SHA verification algorithm
if ( storedPassword.startsWith( SHA_PREFIX ) )
{
storedPassword = storedPassword.substring( SHA_PREFIX.length() );
}
hashedPassword = getOldHash( password );
boolean verified = hashedPassword.equals( storedPassword );
// If in the old format and password verified, upgrade the hash to SSHA
if ( verified )
{
profile.setPassword( password );
save( profile );
}
return verified;
}
catch( NoSuchPrincipalException e )
{
}
catch( NoSuchAlgorithmException e )
{
log.error( "Unsupported algorithm: " + e.getMessage() );
}
catch( UnsupportedEncodingException e )
{
log.error( "You do not have UTF-8!?!" );
}
catch( WikiSecurityException e )
{
log.error( "Could not upgrade SHA password to SSHA because profile could not be saved. Reason: " + e.getMessage() );
e.printStackTrace();
}
return false;
}
/**
* Generates a new random user identifier (uid) that is guaranteed to be unique.
*
* @param db The database for which the UID should be generated.
* @return A random, unique UID.
*/
protected static String generateUid( UserDatabase db )
{
// Keep generating UUIDs until we find one that doesn't collide
String uid = null;
boolean collision;
do
{
uid = UUID.randomUUID().toString();
collision = true;
try
{
db.findByUid( uid );
}
catch ( NoSuchPrincipalException e )
{
collision = false;
}
}
while ( collision || uid == null );
return uid;
}
/**
* Private method that calculates the salted SHA-1 hash of a given
* <code>String</code>. Note that as of JSPWiki 2.8, this method calculates
* a <em>salted</em> hash rather than a plain hash.
* @param text the text to hash
* @return the result hash
*/
protected String getHash( String text )
{
String hash = null;
try
{
hash = CryptoUtil.getSaltedPassword( text.getBytes("UTF-8") );
}
catch( NoSuchAlgorithmException e )
{
log.error( "Error creating salted SHA password hash:" + e.getMessage() );
hash = text;
}
catch( UnsupportedEncodingException e )
{
log.error("You do not have UTF-8!?!");
}
return hash;
}
/**
* Private method that calculates the SHA-1 hash of a given
* <code>String</code>
* @param text the text to hash
* @return the result hash
* @deprecated this method is retained for backwards compatibility purposes; use {@link #getHash(String)} instead
*/
protected String getOldHash( String text )
{
String hash = null;
try
{
MessageDigest md = MessageDigest.getInstance( "SHA" );
md.update( text.getBytes("UTF-8") );
byte[] digestedBytes = md.digest();
hash = HexUtils.convert( digestedBytes );
}
catch( NoSuchAlgorithmException e )
{
log.error( "Error creating SHA password hash:" + e.getMessage() );
hash = text;
}
catch (UnsupportedEncodingException e)
{
log.error("UTF-8 not supported!?!");
}
return hash;
}
/**
* Parses a long integer from a supplied string, or returns 0 if not parsable.
* @param value the string to parse
* @return the value parsed
*/
protected long parseLong( String value )
{
if ( value == null || value.length() == 0 )
{
return 0;
}
try
{
return Long.parseLong( value );
}
catch ( NumberFormatException e )
{
return 0;
}
}
}