/*
* Copyright 2000-2004 The Apache Software Foundation.
*
* Licensed 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.jetspeed.services.security.turbine;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.security.MessageDigest;
import java.security.Principal;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Vector;
import javax.mail.internet.MimeUtility;
import javax.servlet.ServletConfig;
import org.apache.jetspeed.om.profile.Profile;
import org.apache.jetspeed.om.security.BaseJetspeedUser;
import org.apache.jetspeed.om.security.JetspeedUser;
import org.apache.jetspeed.om.security.UserIdPrincipal;
import org.apache.jetspeed.om.security.UserNamePrincipal;
import org.apache.jetspeed.om.security.turbine.TurbineUser;
import org.apache.jetspeed.om.security.turbine.TurbineUserPeer;
import org.apache.jetspeed.services.JetspeedSecurity;
import org.apache.jetspeed.services.Profiler;
import org.apache.jetspeed.services.PsmlManager;
import org.apache.jetspeed.services.logging.JetspeedLogFactoryService;
import org.apache.jetspeed.services.logging.JetspeedLogger;
import org.apache.jetspeed.services.rundata.JetspeedRunData;
import org.apache.jetspeed.services.rundata.JetspeedRunDataService;
import org.apache.jetspeed.services.security.CredentialsManagement;
import org.apache.jetspeed.services.security.InsufficientPrivilegeException;
import org.apache.jetspeed.services.security.JetspeedSecurityException;
import org.apache.jetspeed.services.security.JetspeedSecurityService;
import org.apache.jetspeed.services.security.NotUniqueUserException;
import org.apache.jetspeed.services.security.UnknownUserException;
import org.apache.jetspeed.services.security.UserException;
import org.apache.jetspeed.services.security.UserManagement;
import org.apache.torque.om.NumberKey;
import org.apache.torque.util.Criteria;
import org.apache.turbine.services.InitializationException;
import org.apache.turbine.services.TurbineBaseService;
import org.apache.turbine.services.TurbineServices;
import org.apache.turbine.services.localization.Localization;
import org.apache.turbine.services.resources.ResourceService;
import org.apache.turbine.services.rundata.RunDataService;
import org.apache.turbine.util.RunData;
/**
* Default Jetspeed-Turbine User Management implementation
*
*
* @author <a href="mailto:david@bluesunrise.com">David Sean Taylor</a>
* @author <a href="mailto:morciuch@apache.org">Mark Orciuch</a>
* @version $Id: TurbineUserManagement.java,v 1.13 2004/02/23 03:54:49 jford Exp
* $
*/
public class TurbineUserManagement extends TurbineBaseService implements
UserManagement, CredentialsManagement {
/**
* Static initialization of the logger for this class
*/
private static final JetspeedLogger logger = JetspeedLogFactoryService
.getLogger(TurbineUserManagement.class.getName());
private final static String CONFIG_SECURE_PASSWORDS_KEY = "secure.passwords";
private final static String CONFIG_SECURE_PASSWORDS_ALGORITHM = "secure.passwords.algorithm";
private final static String CONFIG_SYSTEM_USERS = "system.users";
boolean securePasswords = false;
String passwordsAlgorithm = "SHA";
Vector systemUsers = null;
private final static String CONFIG_NEWUSER_ROLES = "newuser.roles";
private final static String[] DEFAULT_CONFIG_NEWUSER_ROLES = { "user" };
String roles[] = null;
/** The JetspeedRunData Service. */
private JetspeedRunDataService runDataService = null;
// /////////////////////////////////////////////////////////////////////////
// User Management Interfaces
// /////////////////////////////////////////////////////////////////////////
/**
* Retrieves a <code>JetspeedUser</code> given the primary principle. The
* principal can be any valid Jetspeed Security Principal:
* <code>org.apache.jetspeed.om.security.UserNamePrincipal</code>
* <code>org.apache.jetspeed.om.security.UserIdPrincipal</code>
*
* The security service may optionally check the current user context to
* determine if the requestor has permission to perform this action.
*
* @param principal
* a principal identity to be retrieved.
* @return a <code>JetspeedUser</code> associated to the principal identity.
* @exception UserException
* when the security provider has a general failure retrieving a
* user.
* @exception UnknownUserException
* when the security provider cannot match the principal identity
* to a user.
* @exception InsufficientPrivilegeException
* when the requestor is denied due to insufficient privilege
*/
public JetspeedUser getUser(Principal principal)
throws JetspeedSecurityException {
// TODO: check requestor for permission
Criteria criteria = new Criteria();
if (principal instanceof UserNamePrincipal) {
criteria.add(TurbineUserPeer.LOGIN_NAME, principal.getName());
} else if (principal instanceof UserIdPrincipal) {
criteria.add(TurbineUserPeer.USER_ID, principal.getName());
} else {
throw new UserException("Invalid Principal Type in getUser: "
+ principal.getClass().getName());
}
List users;
try {
users = TurbineUserPeer.doSelectUsers(criteria);
} catch (Exception e) {
String message = "Failed to retrieve user '" + principal.getName() + "'";
logger.error(message, e);
throw new UserException(message, e);
}
if (users.size() > 1) {
throw new UserException("Multiple Users with same username '"
+ principal.getName() + "'");
}
if (users.size() == 1) {
return (JetspeedUser) users.get(0);
}
throw new UnknownUserException("Unknown user '" + principal.getName() + "'");
}
public JetspeedUser getUser(RunData rundata, Principal principal)
throws JetspeedSecurityException {
return getUser(principal);
}
/**
* Retrieves a collection of all <code>JetspeedUser</code>s. The security
* service may optionally check the current user context to determine if the
* requestor has permission to perform this action.
*
* @return a collection of <code>JetspeedUser</code> entities.
* @exception UserException
* when the security provider has a general failure retrieving
* users.
* @exception InsufficientPrivilegeException
* when the requestor is denied due to insufficient privilege
*/
public Iterator getUsers() throws JetspeedSecurityException {
Criteria criteria = new Criteria();
List users;
try {
users = TurbineUserPeer.doSelectUsers(criteria);
} catch (Exception e) {
logger.error("Failed to retrieve users ", e);
throw new UserException("Failed to retrieve users ", e);
}
return users.iterator();
}
/**
* Retrieves a collection of <code>JetspeedUser</code>s filtered by a security
* provider-specific query string. For example SQL, OQL, JDOQL. The security
* service may optionally check the current user context to determine if the
* requestor has permission to perform this action.
*
* @return a collection of <code>JetspeedUser</code> entities.
* @exception UserException
* when the security provider has a general failure retrieving
* users.
* @exception InsufficientPrivilegeException
* when the requestor is denied due to insufficient privilege
*/
public Iterator getUsers(String filter) throws JetspeedSecurityException {
// TODO: implement this with a SQL string
Criteria criteria = new Criteria();
List users;
try {
users = TurbineUserPeer.doSelectUsers(criteria);
} catch (Exception e) {
logger.error("Failed to retrieve users ", e);
throw new UserException("Failed to retrieve users ", e);
}
return users.iterator();
}
/**
* Saves a <code>JetspeedUser</code>'s attributes into permanent storage. The
* user's account is required to exist in the storage. The security service
* may optionally check the current user context to determine if the requestor
* has permission to perform this action.
*
* @exception UserException
* when the security provider has a general failure retrieving
* users.
* @exception InsufficientPrivilegeException
* when the requestor is denied due to insufficient privilege
*/
public void saveUser(JetspeedUser user) throws JetspeedSecurityException {
if (!accountExists(user, true)) {
throw new UnknownUserException("Cannot save user '" + user.getUserName()
+ "', User doesn't exist");
}
Criteria criteria = TurbineUserPeer.buildCriteria(user);
try {
TurbineUserPeer.doUpdate(criteria);
} catch (Exception e) {
logger.error("Failed to save user object ", e);
throw new UserException("Failed to save user object ", e);
}
}
/**
* Adds a <code>JetspeedUser</code> into permanent storage. The security
* service can throw a <code>NotUniqueUserException</code> when the public
* credentials fail to meet the security provider-specific unique constraints.
* The security service may optionally check the current user context to
* determine if the requestor has permission to perform this action.
*
* @exception UserException
* when the security provider has a general failure retrieving
* users.
* @exception NotUniqueUserException
* when the public credentials fail to meet the security
* provider-specific unique constraints.
* @exception InsufficientPrivilegeException
* when the requestor is denied due to insufficient privilege
*/
public void addUser(JetspeedUser user) throws JetspeedSecurityException {
if (accountExists(user)) {
throw new NotUniqueUserException("The account '" + user.getUserName()
+ "' already exists");
}
String initialPassword = user.getPassword();
String encrypted = JetspeedSecurity.encryptPassword(initialPassword);
user.setPassword(encrypted);
Criteria criteria = TurbineUserPeer.buildCriteria(user);
try {
NumberKey key = (NumberKey) TurbineUserPeer.doInsert(criteria);
((BaseJetspeedUser) user).setUserId(key.toString());
} catch (Exception e) {
String message = "Failed to create account '" + user.getUserName() + "'";
logger.error(message, e);
throw new UserException(message, e);
}
addDefaultPSML(user);
}
/*
* A default PSML page is added for the user, and the Jetspeed default roles
* are assigned to the new user.
*
* @param user The new user.
*
* @throws
*/
protected void addDefaultPSML(JetspeedUser user)
throws JetspeedSecurityException {
for (int ix = 0; ix < roles.length; ix++) {
try {
JetspeedSecurity.grantRole(user.getUserName(), JetspeedSecurity
.getRole(roles[ix]).getName());
} catch (Exception e) {
logger.error(
"Could not grant role: " + roles[ix] + " to user "
+ user.getUserName(), e);
}
}
try {
JetspeedRunData rundata = getRunData();
if (rundata != null && Profiler.useRoleProfileMerging() == false) {
Profile profile = Profiler.createProfile();
profile.setUser(user);
profile.setMediaType("html");
Profiler.createProfile(getRunData(), profile);
}
} catch (Exception e) {
logger.error("Failed to create profile for new user ", e);
removeUser(new UserNamePrincipal(user.getUserName()));
throw new UserException("Failed to create profile for new user ", e);
}
}
/**
* Removes a <code>JetspeedUser</code> from the permanent store. The security
* service may optionally check the current user context to determine if the
* requestor has permission to perform this action.
*
* @param principal
* the principal identity to be retrieved.
* @exception UserException
* when the security provider has a general failure retrieving a
* user.
* @exception UnknownUserException
* when the security provider cannot match the principal identity
* to a user.
* @exception InsufficientPrivilegeException
* when the requestor is denied due to insufficient privilege
*/
public void removeUser(Principal principal) throws JetspeedSecurityException {
if (systemUsers.contains(principal.getName())) {
throw new UserException("[" + principal.getName()
+ "] is a system user and cannot be removed");
}
JetspeedUser user = getUser(principal);
Criteria criteria = new Criteria();
if (principal instanceof UserNamePrincipal) {
criteria.add(TurbineUserPeer.LOGIN_NAME, principal.getName());
} else if (principal instanceof UserIdPrincipal) {
criteria.add(TurbineUserPeer.USER_ID, principal.getName());
} else {
throw new UserException("Invalid Principal Type in removeUser: "
+ principal.getClass().getName());
}
try {
TurbineUserPeer.doDelete(criteria);
PsmlManager.removeUserDocuments(user);
} catch (Exception e) {
String message = "Failed to remove account '" + user.getUserName() + "'";
logger.error(message, e);
throw new UserException(message, e);
}
}
// /////////////////////////////////////////////////////////////////////////
// Credentials Management
// /////////////////////////////////////////////////////////////////////////
/**
* Allows for a user to change their own password.
*
* @param user
* the JetspeedUser to change password
* @param oldPassword
* the current password supplied by the user.
* @param newPassword
* the current password requested by the user.
* @exception UserException
* when the security provider has a general failure retrieving a
* user.
* @exception UnknownUserException
* when the security provider cannot match the principal identity
* to a user.
* @exception InsufficientPrivilegeException
* when the requestor is denied due to insufficient privilege
*/
public void changePassword(JetspeedUser user, String oldPassword,
String newPassword) throws JetspeedSecurityException {
oldPassword = JetspeedSecurity.convertPassword(oldPassword);
newPassword = JetspeedSecurity.convertPassword(newPassword);
String encrypted = JetspeedSecurity.encryptPassword(oldPassword);
if (!accountExists(user)) {
throw new UnknownUserException(
Localization.getString("UPDATEACCOUNT_NOUSER"));
}
if (!user.getPassword().equals(encrypted)) {
throw new UserException(
Localization.getString("UPDATEACCOUNT_BADOLDPASSWORD"));
}
user.setPassword(JetspeedSecurity.encryptPassword(newPassword));
// Set the last password change date
user.setPasswordChanged(new Date());
// save the changes in the database immediately, to prevent the password
// being 'reverted' to the old value if the user data is lost somehow
// before it is saved at session's expiry.
saveUser(user);
}
/**
* Forcibly sets new password for a User.
*
* Provides an administrator the ability to change the forgotten or
* compromised passwords. Certain implementatations of this feature would
* require administrative level access to the authenticating server / program.
*
* @param user
* the user to change the password for.
* @param password
* the new password.
* @exception UserException
* when the security provider has a general failure retrieving a
* user.
* @exception UnknownUserException
* when the security provider cannot match the principal identity
* to a user.
* @exception InsufficientPrivilegeException
* when the requestor is denied due to insufficient privilege
*/
public void forcePassword(JetspeedUser user, String password)
throws JetspeedSecurityException {
if (!accountExists(user)) {
throw new UnknownUserException("The account '" + user.getUserName()
+ "' does not exist");
}
user.setPassword(JetspeedSecurity.encryptPassword(password));
// save the changes in the database immediately, to prevent the
// password being 'reverted' to the old value if the user data
// is lost somehow before it is saved at session's expiry.
saveUser(user);
}
/**
* This method provides client-side encryption of passwords.
*
* If <code>secure.passwords</code> are enabled in JetspeedSecurity
* properties, the password will be encrypted, if not, it will be returned
* unchanged. The <code>secure.passwords.algorithm</code> property can be used
* to chose which digest algorithm should be used for performing the
* encryption. <code>SHA</code> is used by default.
*
* @param password
* the password to process
* @return processed password
*/
public String encryptPassword(String password)
throws JetspeedSecurityException {
if (securePasswords == false) {
return password;
}
if (password == null) {
return null;
}
try {
MessageDigest md = MessageDigest.getInstance(passwordsAlgorithm);
// We need to use unicode here, to be independent of platform's
// default encoding. Thanks to SGawin for spotting this.
byte[] digest = md.digest(password.getBytes("UTF-8"));
ByteArrayOutputStream bas = new ByteArrayOutputStream(digest.length
+ digest.length / 3 + 1);
OutputStream encodedStream = MimeUtility.encode(bas, "base64");
encodedStream.write(digest);
encodedStream.flush();
encodedStream.close();
return bas.toString();
} catch (Exception e) {
logger.error("Unable to encrypt password." + e.getMessage(), e);
return null;
}
}
// /////////////////////////////////////////////////////////////////////////
// Service Init
// /////////////////////////////////////////////////////////////////////////
/**
* This is the early initialization method called by the Turbine
* <code>Service</code> framework
*
* @param conf
* The <code>ServletConfig</code>
* @exception throws a <code>InitializationException</code> if the service
* fails to initialize
*/
public synchronized void init(ServletConfig conf)
throws InitializationException {
if (getInit())
return;
super.init(conf);
// get configuration parameters from Jetspeed Resources
ResourceService serviceConf = ((TurbineServices) TurbineServices
.getInstance()).getResources(JetspeedSecurityService.SERVICE_NAME);
securePasswords = serviceConf.getBoolean(CONFIG_SECURE_PASSWORDS_KEY,
securePasswords);
passwordsAlgorithm = serviceConf.getString(
CONFIG_SECURE_PASSWORDS_ALGORITHM, passwordsAlgorithm);
systemUsers = serviceConf.getVector(CONFIG_SYSTEM_USERS, new Vector());
try {
roles = serviceConf.getStringArray(CONFIG_NEWUSER_ROLES);
} catch (Exception e) {
}
if (null == roles || roles.length == 0) {
roles = DEFAULT_CONFIG_NEWUSER_ROLES;
}
this.runDataService = (JetspeedRunDataService) TurbineServices
.getInstance().getService(RunDataService.SERVICE_NAME);
setInit(true);
}
// /////////////////////////////////////////////////////////////////////////
// Internal
// /////////////////////////////////////////////////////////////////////////
/**
* Check whether a specified user's account exists.
*
* The login name is used for looking up the account.
*
* @param user
* the user to be checked.
* @param checkUniqueId
* make sure that we aren't overwriting another user with different
* id
* @return true if the specified account exists
* @throws UserException
* if there was a general db access error
*
*/
protected boolean accountExists(JetspeedUser user) throws UserException {
return accountExists(user, false);
}
protected boolean accountExists(JetspeedUser user, boolean checkUniqueId)
throws UserException {
String id = user.getUserId();
Criteria criteria = new Criteria();
criteria.add(TurbineUserPeer.LOGIN_NAME, user.getUserName());
List users;
try {
users = TurbineUserPeer.doSelect(criteria);
} catch (Exception e) {
logger.error("Failed to check account's presence", e);
throw new UserException("Failed to check account's presence", e);
}
if (users.size() < 1) {
return false;
}
TurbineUser retrieved = (TurbineUser) users.get(0);
int key = retrieved.getUserId();
String keyId = String.valueOf(key);
if (checkUniqueId && !keyId.equals(id)) {
throw new UserException("User exists but under a different unique ID");
}
return true;
}
protected JetspeedRunData getRunData() {
JetspeedRunData rundata = null;
if (this.runDataService != null) {
rundata = this.runDataService.getCurrentRunData();
}
return rundata;
}
}