NIHVIVO-1207 Factor out the model-related stuff from Authenticate into Authenticator.

This commit is contained in:
jeb228 2010-11-04 19:01:23 +00:00
parent d0c73a4d23
commit db304c4f52
4 changed files with 369 additions and 140 deletions

View file

@ -0,0 +1,84 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
package edu.cornell.mannlib.vitro.webapp.controller.authenticate;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import edu.cornell.mannlib.vitro.webapp.beans.User;
/**
* The tool that a login process will use to interface with the user records in
* the model (or wherever).
*/
public abstract class Authenticator {
// ----------------------------------------------------------------------
// The factory
//
// Unit tests can replace the factory to get a stub class instead.
// Note: this can only work because the factory value is not final.
// ----------------------------------------------------------------------
public static interface AuthenticatorFactory {
Authenticator newInstance(HttpServletRequest request);
}
private static AuthenticatorFactory factory = new AuthenticatorFactory() {
@Override
public Authenticator newInstance(HttpServletRequest request) {
return new BasicAuthenticator(request);
}
};
public static Authenticator getInstance(HttpServletRequest request) {
return factory.newInstance(request);
}
// ----------------------------------------------------------------------
// The interface.
// ----------------------------------------------------------------------
/** Maximum inactive interval for a ordinary logged-in session, in seconds. */
public static final int LOGGED_IN_TIMEOUT_INTERVAL = 300;
/** Maximum inactive interval for a editor (or better) session, in seconds. */
public static final int PRIVILEGED_TIMEOUT_INTERVAL = 32000;
/**
* Does a user by this name exist?
*/
public abstract boolean isExistingUser(String username);
/**
* Does a user by this name have this password?
*/
public abstract boolean isCurrentPassword(String username,
String clearTextPassword);
/**
* Get the user with this name, or null if no such user exists.
*/
public abstract User getUserByUsername(String username);
/**
* Return a list of URIs of the people that this user is allowed to edit.
*/
public abstract List<String> asWhomMayThisUserEdit(User user);
/**
* Record a new password for the user.
*/
public abstract void recordNewPassword(User user,
String newClearTextPassword);
/**
* Record that the user has logged in.
*/
public abstract void recordSuccessfulLogin(User user);
/**
* Set the login status in the session.
*/
public abstract void setLoggedIn(User user);
}

View file

@ -0,0 +1,217 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
package edu.cornell.mannlib.vitro.webapp.controller.authenticate;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import edu.cornell.mannlib.vedit.beans.LoginFormBean;
import edu.cornell.mannlib.vedit.beans.LoginStatusBean;
import edu.cornell.mannlib.vitro.webapp.beans.User;
import edu.cornell.mannlib.vitro.webapp.controller.edit.Authenticate;
import edu.cornell.mannlib.vitro.webapp.dao.UserDao;
import edu.cornell.mannlib.vitro.webapp.dao.WebappDaoFactory;
import edu.cornell.mannlib.vitro.webapp.dao.jena.LoginEvent;
/**
* The "standard" implementation of Authenticator.
*/
public class BasicAuthenticator extends Authenticator {
/** User roles are recorded in the model like "role:/50", etc. */
private static final String ROLE_NAMESPACE = "role:/";
private static final Log log = LogFactory.getLog(BasicAuthenticator.class);
private final HttpServletRequest request;
public BasicAuthenticator(HttpServletRequest request) {
this.request = request;
}
@Override
public boolean isExistingUser(String username) {
return getUserByUsername(username) != null;
}
@Override
public User getUserByUsername(String username) {
UserDao userDao = getUserDao(request);
if (userDao == null) {
return null;
}
return userDao.getUserByUsername(username);
}
@Override
public boolean isCurrentPassword(String username, String clearTextPassword) {
User user = getUserDao(request).getUserByUsername(username);
if (user == null) {
log.trace("Checking password '" + clearTextPassword
+ "' for user '" + username + "', but user doesn't exist.");
return false;
}
String md5NewPassword = Authenticate
.applyMd5Encoding(clearTextPassword);
return md5NewPassword.equals(user.getMd5password());
}
@Override
public void recordNewPassword(User user, String newClearTextPassword) {
user.setOldPassword(user.getMd5password());
user.setMd5password(Authenticate.applyMd5Encoding(newClearTextPassword));
getUserDao(request).updateUser(user);
}
@Override
public void recordSuccessfulLogin(User user) {
user.setLoginCount(user.getLoginCount() + 1);
if (user.getFirstTime() == null) { // first login
user.setFirstTime(new Date());
}
getUserDao(request).updateUser(user);
}
@Override
public void setLoggedIn(User user) {
HttpSession session = request.getSession();
createLoginFormBean(user, session);
createLoginStatusBean(user, session);
setSessionTimeoutLimit(session);
recordInUserSessionMap(user, session);
notifyOtherUsers(user, session);
}
/**
* Put the login bean into the session.
*
* TODO The LoginFormBean is being phased out.
*/
private void createLoginFormBean(User user, HttpSession session) {
LoginFormBean lfb = new LoginFormBean();
lfb.setUserURI(user.getURI());
lfb.setLoginStatus("authenticated");
lfb.setSessionId(session.getId());
lfb.setLoginRole(user.getRoleURI());
lfb.setLoginRemoteAddr(request.getRemoteAddr());
lfb.setLoginName(user.getUsername());
session.setAttribute("loginHandler", lfb);
}
/**
* Put the login bean into the session.
*
* TODO this should eventually replace the LoginFormBean.
*/
private void createLoginStatusBean(User user, HttpSession session) {
LoginStatusBean lsb = new LoginStatusBean(user.getURI(),
user.getUsername(), parseUserSecurityLevel(user));
LoginStatusBean.setBean(session, lsb);
log.info("Adding status bean: " + lsb);
}
/**
* Editors and other privileged users get a longer timeout interval.
*/
private void setSessionTimeoutLimit(HttpSession session) {
if (LoginStatusBean.getBean(session).isLoggedInAtLeast(
LoginStatusBean.EDITOR)) {
session.setMaxInactiveInterval(PRIVILEGED_TIMEOUT_INTERVAL);
} else {
session.setMaxInactiveInterval(LOGGED_IN_TIMEOUT_INTERVAL);
}
}
/**
* Record the login in the user/session map.
*
* TODO What is this map used for?
*/
private void recordInUserSessionMap(User user, HttpSession session) {
Map<String, HttpSession> userURISessionMap = Authenticate
.getUserURISessionMapFromContext(session.getServletContext());
userURISessionMap.put(user.getURI(), session);
}
/**
* Anyone listening to themodel might need to know that another user is
* logged in.
*/
private void notifyOtherUsers(User user, HttpSession session) {
Authenticate.sendLoginNotifyEvent(new LoginEvent(user.getURI()),
session.getServletContext(), session);
}
@Override
public List<String> asWhomMayThisUserEdit(User user) {
if (user == null) {
return Collections.emptyList();
}
UserDao userDao = getUserDao(request);
if (userDao == null) {
return Collections.emptyList();
}
String userUri = user.getURI();
if (userUri == null) {
return Collections.emptyList();
}
return userDao.getIndividualsUserMayEditAs(userUri);
}
/**
* Get a reference to the {@link UserDao}, or <code>null</code>.
*/
private UserDao getUserDao(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session == null) {
return null;
}
ServletContext servletContext = session.getServletContext();
WebappDaoFactory wadf = (WebappDaoFactory) servletContext
.getAttribute("webappDaoFactory");
if (wadf == null) {
log.error("getUserDao: no WebappDaoFactory");
return null;
}
UserDao userDao = wadf.getUserDao();
if (userDao == null) {
log.error("getUserDao: no UserDao");
}
return userDao;
}
/**
* Parse the role URI from User. Don't crash if it is not valid.
*/
private int parseUserSecurityLevel(User user) {
String roleURI = user.getRoleURI();
try {
if (roleURI.startsWith(ROLE_NAMESPACE)) {
String roleLevel = roleURI.substring(ROLE_NAMESPACE.length());
return Integer.parseInt(roleLevel);
} else {
return Integer.parseInt(roleURI);
}
} catch (NumberFormatException e) {
log.warn("Invalid RoleURI '" + roleURI + "' for user '"
+ user.getURI() + "'");
return 1;
}
}
}

View file

@ -7,7 +7,6 @@ import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -24,31 +23,19 @@ import org.apache.commons.logging.LogFactory;
import com.hp.hpl.jena.ontology.OntModel;
import edu.cornell.mannlib.vedit.beans.LoginFormBean;
import edu.cornell.mannlib.vedit.beans.LoginStatusBean;
import edu.cornell.mannlib.vitro.webapp.auth.policy.RoleBasedPolicy.AuthRole;
import edu.cornell.mannlib.vitro.webapp.beans.User;
import edu.cornell.mannlib.vitro.webapp.controller.Controllers;
import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest;
import edu.cornell.mannlib.vitro.webapp.controller.authenticate.Authenticator;
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.FreemarkerHttpServlet;
import edu.cornell.mannlib.vitro.webapp.controller.login.LoginProcessBean;
import edu.cornell.mannlib.vitro.webapp.controller.login.LoginProcessBean.Message;
import edu.cornell.mannlib.vitro.webapp.controller.login.LoginProcessBean.State;
import edu.cornell.mannlib.vitro.webapp.dao.UserDao;
import edu.cornell.mannlib.vitro.webapp.dao.WebappDaoFactory;
import edu.cornell.mannlib.vitro.webapp.dao.jena.LoginEvent;
import edu.cornell.mannlib.vitro.webapp.dao.jena.LoginLogoutEvent;
public class Authenticate extends FreemarkerHttpServlet {
/**
* Maximum inactive interval for a ordinary logged in user session, in
* seconds.
*/
public static final int LOGGED_IN_TIMEOUT_INTERVAL = 300;
/** Maximum inactive interval for a editor (or better) session, in seconds. */
public static final int PRIVILEGED_TIMEOUT_INTERVAL = 32000;
private static final Log log = LogFactory.getLog(Authenticate.class
.getName());
@ -155,7 +142,7 @@ public class Authenticate extends FreemarkerHttpServlet {
bean.setUsername(username);
}
User user = getUserDao(request).getUserByUsername(username);
User user = getAuthenticator(request).getUserByUsername(username);
log.trace("User is " + (user == null ? "null" : user.getURI()));
if (user == null) {
@ -168,11 +155,7 @@ public class Authenticate extends FreemarkerHttpServlet {
return null;
}
String md5Password = applyMd5Encoding(password);
if (!md5Password.equals(user.getMd5password())) {
log.trace("Encoded passwords don't match: right="
+ user.getMd5password() + ", wrong=" + md5Password);
if (!getAuthenticator(request).isCurrentPassword(username, password)) {
bean.setMessage(Message.INCORRECT_PASSWORD);
return null;
}
@ -239,24 +222,15 @@ public class Authenticate extends FreemarkerHttpServlet {
return null;
}
User user = getUserDao(request).getUserByUsername(bean.getUsername());
log.trace("User is " + (user == null ? "null" : user.getURI()));
String username = bean.getUsername();
if (user == null) {
throw new IllegalStateException(
"Changing password but bean has no user: '"
+ bean.getUsername() + "'");
}
String md5NewPassword = applyMd5Encoding(newPassword);
log.trace("Old password: " + user.getMd5password() + ", new password: "
+ md5NewPassword);
if (md5NewPassword.equals(user.getMd5password())) {
if (getAuthenticator(request).isCurrentPassword(username, newPassword)) {
bean.setMessage(Message.USING_OLD_PASSWORD);
return null;
}
User user = getAuthenticator(request).getUserByUsername(username);
log.trace("User is " + (user == null ? "null" : user.getURI()));
return user;
}
@ -266,10 +240,7 @@ public class Authenticate extends FreemarkerHttpServlet {
private void recordSuccessfulPasswordChange(HttpServletRequest request,
User user) {
String newPassword = request.getParameter(PARAMETER_NEW_PASSWORD);
String md5NewPassword = applyMd5Encoding(newPassword);
user.setOldPassword(user.getMd5password());
user.setMd5password(md5NewPassword);
getUserDao(request).updateUser(user);
getAuthenticator(request).recordNewPassword(user, newPassword);
log.debug("Completed first-time password change.");
recordLoginInfo(request, user.getUsername());
@ -282,52 +253,16 @@ public class Authenticate extends FreemarkerHttpServlet {
private void recordLoginInfo(HttpServletRequest request, String username) {
log.debug("Completed login.");
// Get a fresh user object, so we know it's not stale.
User user = getUserDao(request).getUserByUsername(username);
// Record the login on the user record (start with a fresh copy).
User user = getAuthenticator(request).getUserByUsername(username);
getAuthenticator(request).recordSuccessfulLogin(user);
HttpSession session = request.getSession();
// Put the login info into the session.
// TODO the LoginFormBean is being phased out.
LoginFormBean lfb = new LoginFormBean();
lfb.setUserURI(user.getURI());
lfb.setLoginStatus("authenticated");
lfb.setSessionId(session.getId());
lfb.setLoginRole(user.getRoleURI());
lfb.setLoginRemoteAddr(request.getRemoteAddr());
lfb.setLoginName(user.getUsername());
session.setAttribute("loginHandler", lfb);
// TODO this should eventually replace the LoginFormBean.
LoginStatusBean lsb = new LoginStatusBean(user.getURI(),
user.getUsername(), parseUserSecurityLevel(user));
LoginStatusBean.setBean(session, lsb);
log.info("Adding status bean: " + lsb);
// Record that a new user has logged in to this session.
getAuthenticator(request).setLoggedIn(user);
// Remove the login process info from the session.
HttpSession session = request.getSession();
session.removeAttribute(LoginProcessBean.SESSION_ATTRIBUTE);
// Record the login on the user.
user.setLoginCount(user.getLoginCount() + 1);
if (user.getFirstTime() == null) { // first login
user.setFirstTime(new Date());
}
getUserDao(request).updateUser(user);
// Set the timeout limit on the session - editors, etc, get more.
if (lsb.isLoggedInAtLeast(LoginStatusBean.EDITOR)) {
session.setMaxInactiveInterval(PRIVILEGED_TIMEOUT_INTERVAL);
} else {
session.setMaxInactiveInterval(LOGGED_IN_TIMEOUT_INTERVAL);
}
// Record the user in the user/Session map.
Map<String, HttpSession> userURISessionMap = getUserURISessionMapFromContext(getServletContext());
userURISessionMap.put(user.getURI(), request.getSession());
// Notify the other users of this model.
sendLoginNotifyEvent(new LoginEvent(user.getURI()),
getServletContext(), session);
}
/**
@ -395,23 +330,17 @@ public class Authenticate extends FreemarkerHttpServlet {
// If the user is a self-editor, send them to their home page.
User user = getLoggedInUser(request);
if (user != null
&& user.getRoleURI() != null
&& user.getRoleURI().equals(
Integer.toString(AuthRole.USER.level()))) {
UserDao userDao = getUserDao(request);
if (userDao != null) {
List<String> uris = userDao.getIndividualsUserMayEditAs(user
.getURI());
if (uris != null && uris.size() > 0) {
String userHomePage = request.getContextPath()
+ "/individual?uri="
+ URLEncoder.encode(uris.get(0), "UTF-8");
log.debug("User is logged in. Redirect as self-editor to "
+ userHomePage);
response.sendRedirect(userHomePage);
return;
}
if (userIsANonEditor(user)) {
List<String> uris = getAuthenticator(request).asWhomMayThisUserEdit(
user);
if (uris != null && uris.size() > 0) {
String userHomePage = request.getContextPath()
+ "/individual?uri="
+ URLEncoder.encode(uris.get(0), "UTF-8");
log.debug("User is logged in. Redirect as self-editor to "
+ userHomePage);
response.sendRedirect(userHomePage);
return;
}
}
@ -420,6 +349,15 @@ public class Authenticate extends FreemarkerHttpServlet {
response.sendRedirect(getSiteAdminUrl(request));
}
/** Is the logged in user an AuthRole.USER? */
private boolean userIsANonEditor(User user) {
if (user == null) {
return false;
}
String nonEditorRoleUri = Integer.toString(AuthRole.USER.level());
return nonEditorRoleUri.equals(user.getRoleURI());
}
/**
* There has been an unexpected exception. Complain mightily.
*/
@ -452,43 +390,18 @@ public class Authenticate extends FreemarkerHttpServlet {
* What user are we logged in as?
*/
private User getLoggedInUser(HttpServletRequest request) {
UserDao userDao = getUserDao(request);
if (userDao == null) {
return null;
}
LoginStatusBean lsb = LoginStatusBean.getBean(request);
if (!lsb.isLoggedIn()) {
log.debug("getLoggedInUser: not logged in");
return null;
}
return userDao.getUserByUsername(lsb.getUsername());
return getAuthenticator(request).getUserByUsername(lsb.getUsername());
}
/**
* Get a reference to the {@link UserDao}, or <code>null</code>.
*/
private UserDao getUserDao(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session == null) {
return null;
}
ServletContext servletContext = session.getServletContext();
WebappDaoFactory wadf = (WebappDaoFactory) servletContext
.getAttribute("webappDaoFactory");
if (wadf == null) {
log.error("getUserDao: no WebappDaoFactory");
return null;
}
UserDao userDao = wadf.getUserDao();
if (userDao == null) {
log.error("getUserDao: no UserDao");
}
return userDao;
/** Get a reference to the Authenticator. */
private Authenticator getAuthenticator(HttpServletRequest request) {
return Authenticator.getInstance(request);
}
/** What's the URL for the login screen? */
@ -515,19 +428,6 @@ public class Authenticate extends FreemarkerHttpServlet {
return LoginProcessBean.getBeanFromSession(request);
}
/**
* Parse the role URI from User. Don't crash if it is not valid.
*/
private int parseUserSecurityLevel(User user) {
try {
return Integer.parseInt(user.getRoleURI());
} catch (NumberFormatException e) {
log.warn("Invalid RoleURI '" + user.getRoleURI() + "' for user '"
+ user.getURI() + "'");
return 1;
}
}
// ----------------------------------------------------------------------
// Public utility methods.
// ----------------------------------------------------------------------

View file

@ -11,6 +11,7 @@ import static org.junit.Assert.assertNull;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collections;
import java.util.Date;
import javax.servlet.ServletException;
@ -40,6 +41,7 @@ public class AuthenticateTest extends AbstractTestClass {
private static final String USER_OLDHAND_NAME = "oldHandName";
private static final String USER_OLDHAND_URI = "oldHandURI";
private static final String USER_OLDHAND_PASSWORD = "oldHandPassword";
private static final int USER_OLDHAND_LOGIN_COUNT = 100;
private static final String URL_LOGIN_PAGE = Controllers.LOGIN
+ "?login=block";
@ -73,6 +75,8 @@ public class AuthenticateTest extends AbstractTestClass {
dbaUser.setURI(USER_DBA_URI);
dbaUser.setRoleURI("50");
dbaUser.setMd5password(Authenticate.applyMd5Encoding(USER_DBA_PASSWORD));
dbaUser.setFirstTime(null);
dbaUser.setLoginCount(0);
User ohUser = new User();
ohUser.setUsername(USER_OLDHAND_NAME);
@ -80,7 +84,8 @@ public class AuthenticateTest extends AbstractTestClass {
ohUser.setRoleURI("1");
ohUser.setMd5password(Authenticate
.applyMd5Encoding(USER_OLDHAND_PASSWORD));
ohUser.setLoginCount(100);
ohUser.setLoginCount(USER_OLDHAND_LOGIN_COUNT);
ohUser.setFirstTime(new Date(0));
userDao = new UserDaoStub();
userDao.addUser(dbaUser);
@ -118,6 +123,7 @@ public class AuthenticateTest extends AbstractTestClass {
assertExpectedRedirect(URL_LOGIN_PAGE);
assertNoProcessBean();
assertExpectedStatusBean(LOGIN_STATUS_DBA);
assertExpectedUserValues(USER_DBA_NAME, USER_DBA_PASSWORD, 0, false);
}
@Test
@ -198,6 +204,8 @@ public class AuthenticateTest extends AbstractTestClass {
assertExpectedRedirect(URL_LOGIN_PAGE);
assertExpectedStatusBean(LOGIN_STATUS_OLDHAND);
assertNoProcessBean();
assertExpectedUserValues(USER_OLDHAND_NAME, USER_OLDHAND_PASSWORD,
USER_OLDHAND_LOGIN_COUNT + 1, true);
}
// ----------------------------------------------------------------------
@ -214,6 +222,7 @@ public class AuthenticateTest extends AbstractTestClass {
assertExpectedRedirect(URL_LOGIN_PAGE);
assertNoStatusBean();
assertExpectedProcessBean(FORCED_PASSWORD_CHANGE, USER_DBA_NAME, "", "");
assertExpectedUserValues(USER_DBA_NAME, USER_DBA_PASSWORD, 0, false);
}
@Test
@ -226,6 +235,7 @@ public class AuthenticateTest extends AbstractTestClass {
assertExpectedRedirect(URL_HOME_PAGE);
assertNoStatusBean();
assertNoProcessBean();
assertExpectedUserValues(USER_DBA_NAME, USER_DBA_PASSWORD, 0, false);
}
@Test
@ -239,6 +249,7 @@ public class AuthenticateTest extends AbstractTestClass {
assertNoStatusBean();
assertExpectedProcessBean(FORCED_PASSWORD_CHANGE, USER_DBA_NAME, "",
"Please enter a password between 6 and 12 characters in length.");
assertExpectedUserValues(USER_DBA_NAME, USER_DBA_PASSWORD, 0, false);
}
@Test
@ -252,6 +263,7 @@ public class AuthenticateTest extends AbstractTestClass {
assertNoStatusBean();
assertExpectedProcessBean(FORCED_PASSWORD_CHANGE, USER_DBA_NAME, "",
"The passwords entered do not match.");
assertExpectedUserValues(USER_DBA_NAME, USER_DBA_PASSWORD, 0, false);
}
@Test
@ -266,6 +278,7 @@ public class AuthenticateTest extends AbstractTestClass {
assertExpectedProcessBean(FORCED_PASSWORD_CHANGE, USER_DBA_NAME, "",
"Please choose a different password from the "
+ "temporary one provided initially.");
assertExpectedUserValues(USER_DBA_NAME, USER_DBA_PASSWORD, 0, false);
}
@Test
@ -278,6 +291,7 @@ public class AuthenticateTest extends AbstractTestClass {
assertExpectedRedirect(URL_LOGIN_PAGE);
assertExpectedStatusBean(LOGIN_STATUS_DBA);
assertNoProcessBean();
assertExpectedUserValues(USER_DBA_NAME, "NewPassword", 1, true);
}
// ----------------------------------------------------------------------
@ -388,6 +402,18 @@ public class AuthenticateTest extends AbstractTestClass {
bean.getSecurityLevel());
}
/** Check that this user looks like we expected. */
private void assertExpectedUserValues(String username, String password,
int loginCount, boolean firstTimeIsSet) {
User user = userDao.getUserByUsername(username);
assertEquals("user " + username + " password",
Authenticate.applyMd5Encoding(password), user.getMd5password());
assertEquals("user " + username + " login count", loginCount,
user.getLoginCount());
assertEquals("user " + username + " firstTimeIsSet", firstTimeIsSet,
user.getFirstTime() != null);
}
/** Boilerplate login process for the rediret tests. */
private void loginNotFirstTime() {
setProcessBean(LOGGING_IN);
@ -397,6 +423,8 @@ public class AuthenticateTest extends AbstractTestClass {
assertExpectedStatusBean(LOGIN_STATUS_OLDHAND);
assertNoProcessBean();
assertExpectedUserValues(USER_OLDHAND_NAME, USER_OLDHAND_PASSWORD,
USER_OLDHAND_LOGIN_COUNT + 1, true);
}
@SuppressWarnings("unused")