NIHVIVO-2279 Implement CreatePassword - start to separate into user vs. admin functions.

This commit is contained in:
j2blake 2011-05-25 20:03:02 +00:00
parent a220c5cba6
commit 1bac5c5fbc
18 changed files with 538 additions and 45 deletions

View file

@ -11,11 +11,17 @@ import edu.cornell.mannlib.vitro.webapp.controller.authenticate.Authenticator;
/**
* Information about the account of a user. URI, email, password, etc.
*
* The "password link expires hash" is just a string that is derived from the
* value in the passwordLinkExpires field. It doesn't have to be a hash, and
* there is no need for it to be cryptographic, but it seems embarrassing to
* just send the value as a clear string. There is no real need for security
* here, except that a brute force attack would allow someone to change the
* password on an account that they know has a password change pending.
*/
public class UserAccount {
public final static int MIN_PASSWORD_LENGTH = 6;
public final static int MAX_PASSWORD_LENGTH = 12;
public static final int MIN_PASSWORD_LENGTH = 6;
public static final int MAX_PASSWORD_LENGTH = 12;
public enum Status {
ACTIVE, INACTIVE;
@ -110,8 +116,8 @@ public class UserAccount {
}
public String getPasswordLinkExpiresHash() {
return Authenticator.applyMd5Encoding(String
.valueOf(passwordLinkExpires));
return limitStringLength(8, Authenticator.applyMd5Encoding(String
.valueOf(passwordLinkExpires)));
}
public void setPasswordLinkExpires(long passwordLinkExpires) {
@ -169,6 +175,16 @@ public class UserAccount {
private <T> T nonNull(T value, T defaultValue) {
return (value == null) ? defaultValue : value;
}
private String limitStringLength(int limit, String s) {
if (s == null) {
return "";
} else if (s.length() <= limit) {
return s;
} else {
return s.substring(0, limit);
}
}
@Override
public String toString() {

View file

@ -6,12 +6,14 @@ import static javax.mail.Message.RecipientType.TO;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import edu.cornell.mannlib.vitro.webapp.beans.UserAccount;
import edu.cornell.mannlib.vitro.webapp.beans.UserAccount.Status;
import edu.cornell.mannlib.vitro.webapp.config.ConfigurationProperties;
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.UrlBuilder;
import edu.cornell.mannlib.vitro.webapp.email.FreemarkerEmailFactory;
import edu.cornell.mannlib.vitro.webapp.email.FreemarkerEmailMessage;
@ -52,7 +54,8 @@ public abstract class UserAccountsAddPageStrategy {
// ----------------------------------------------------------------------
private static class EmailStrategy extends UserAccountsAddPageStrategy {
public static final String CREATE_PASSWORD_URL = "/userAccounts/createPassword";
public static final String CREATE_PASSWORD_URL = "/accounts/createPassword";
private static final int DAYS_TO_ACTIVATE_ACCOUNT = 90;
private boolean sentEmail;
@ -73,9 +76,15 @@ public abstract class UserAccountsAddPageStrategy {
@Override
protected void setAdditionalProperties(UserAccount u) {
u.setPasswordLinkExpires(new Date().getTime());
u.setPasswordLinkExpires(figureExpirationDate().getTime());
u.setStatus(Status.INACTIVE);
}
private Date figureExpirationDate() {
Calendar c = Calendar.getInstance();
c.add(Calendar.DATE, DAYS_TO_ACTIVATE_ACCOUNT);
return c.getTime();
}
@Override
protected void addMoreBodyValues(Map<String, Object> body) {
@ -93,8 +102,8 @@ public abstract class UserAccountsAddPageStrategy {
.createNewMessage(page.vreq);
email.addRecipient(TO, page.getAddedAccount().getEmailAddress());
email.setSubject("Your VIVO account has been created.");
email.setHtmlTemplate("userAccounts-createdEmail-html.ftl");
email.setTextTemplate("userAccounts-createdEmail-text.ftl");
email.setHtmlTemplate("userAccounts-acctCreatedEmail-html.ftl");
email.setTextTemplate("userAccounts-acctCreatedEmail-text.ftl");
email.setBodyMap(body);
email.send();
@ -103,11 +112,11 @@ public abstract class UserAccountsAddPageStrategy {
private String buildCreatePasswordLink() {
try {
String uri = page.getAddedAccount().getUri();
String email = page.getAddedAccount().getEmailAddress();
String hash = page.getAddedAccount()
.getPasswordLinkExpiresHash();
String relativeUrl = UrlBuilder.getUrl(CREATE_PASSWORD_URL, "user",
uri, "key", hash);
email, "key", hash);
URL context = new URL(page.vreq.getRequestURL().toString());
URL url = new URL(context, relativeUrl);
@ -116,7 +125,7 @@ public abstract class UserAccountsAddPageStrategy {
return "error_creating_password_link";
}
}
@Override
protected boolean wasPasswordEmailSent() {
return sentEmail;

View file

@ -9,8 +9,10 @@ import org.apache.commons.logging.LogFactory;
import edu.cornell.mannlib.vitro.webapp.auth.requestedAction.Actions;
import edu.cornell.mannlib.vitro.webapp.auth.requestedAction.usepages.ManageUserAccounts;
import edu.cornell.mannlib.vitro.webapp.beans.DisplayMessage;
import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest;
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.FreemarkerHttpServlet;
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.RedirectResponseValues;
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.ResponseValues;
/**
@ -23,6 +25,7 @@ public class UserAccountsController extends FreemarkerHttpServlet {
private static final String ACTION_ADD = "/add";
private static final String ACTION_DELETE = "/delete";
private static final String ACTION_EDIT = "/edit";
private static final String ACTION_CREATE_PASSWORD = "/createPassword";
@Override
protected Actions requiredActions(VitroRequest vreq) {
@ -44,6 +47,8 @@ public class UserAccountsController extends FreemarkerHttpServlet {
return handleEditRequest(vreq);
} else if (ACTION_DELETE.equals(action)) {
return handleDeleteRequest(vreq);
} else if (ACTION_CREATE_PASSWORD.equals(action)) {
return handleCreatePasswordRequest(vreq);
} else {
return handleListRequest(vreq);
}
@ -64,7 +69,6 @@ public class UserAccountsController extends FreemarkerHttpServlet {
private ResponseValues handleEditRequest(VitroRequest vreq) {
UserAccountsEditPage page = new UserAccountsEditPage(vreq);
page.parseParametersAndValidate();
if (page.isSubmit() && page.isValid()) {
page.updateAccount();
UserAccountsListPage listPage = new UserAccountsListPage(vreq);
@ -83,8 +87,30 @@ public class UserAccountsController extends FreemarkerHttpServlet {
.showPageWithDeletions(deletedUris);
}
private ResponseValues handleCreatePasswordRequest(VitroRequest vreq) {
UserAccountsCreatePasswordPage page = new UserAccountsCreatePasswordPage(
vreq);
if (page.isBogus()) {
return showHomePage(vreq,
"Request failed. Please contact your system administrator.");
} else if (page.isSubmit() && page.isValid()) {
page.createPassword();
return showHomePage(vreq,
"Your password has been saved. Please log in.");
} else {
return page.showPage();
}
}
private ResponseValues handleListRequest(VitroRequest vreq) {
UserAccountsListPage page = new UserAccountsListPage(vreq);
return page.showPage();
}
private ResponseValues showHomePage(VitroRequest vreq, String message) {
DisplayMessage.setMessage(vreq, message);
return new RedirectResponseValues("/");
}
}

View file

@ -0,0 +1,165 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
package edu.cornell.mannlib.vitro.webapp.controller.accounts;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import edu.cornell.mannlib.vitro.webapp.beans.UserAccount;
import edu.cornell.mannlib.vitro.webapp.beans.UserAccount.Status;
import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest;
import edu.cornell.mannlib.vitro.webapp.controller.accounts.user.UserAccountsUserController;
import edu.cornell.mannlib.vitro.webapp.controller.authenticate.Authenticator;
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.ResponseValues;
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.TemplateResponseValues;
/**
* TODO
*/
public class UserAccountsCreatePasswordPage extends UserAccountsPage {
private static final Log log = LogFactory
.getLog(UserAccountsCreatePasswordPage.class);
private static final String PARAMETER_SUBMIT = "submitCreatePassword";
private static final String PARAMETER_USER = "user";
private static final String PARAMETER_KEY = "key";
private static final String PARAMETER_PASSWORD = "password";
private static final String PARAMETER_CONFIRM_PASSWORD = "confirmPassword";
private static final String TEMPLATE_NAME = "userAccounts-createPassword.ftl";
private static final String ERROR_NO_PASSWORD = "errorPasswordIsEmpty";
private static final String ERROR_WRONG_PASSWORD_LENGTH = "errorPasswordIsWrongLength";
private static final String ERROR_PASSWORDS_DONT_MATCH = "errorPasswordsDontMatch";
private boolean submit;
private String userEmail = "";
private String key = "";
private String password = "";
private String confirmPassword = "";
private UserAccount userAccount;
/** The result of checking whether this request is even appropriate. */
private String bogusMessage = "";
/** The result of validating a "submit" request. */
private String errorCode = "";
public UserAccountsCreatePasswordPage(VitroRequest vreq) {
super(vreq);
parseRequestParameters();
validateUserAccountInfo();
if (isSubmit() && !isBogus()) {
validateParameters();
}
}
private void parseRequestParameters() {
submit = isFlagOnRequest(PARAMETER_SUBMIT);
userEmail = getStringParameter(PARAMETER_USER, "");
key = getStringParameter(PARAMETER_KEY, "");
password = getStringParameter(PARAMETER_PASSWORD, "");
confirmPassword = getStringParameter(PARAMETER_CONFIRM_PASSWORD, "");
}
private void validateUserAccountInfo() {
userAccount = userAccountsDao.getUserAccountByEmail(userEmail);
if (userAccount == null) {
log.warn("Create password for '" + userEmail
+ "' is bogus: no such user");
bogusMessage = UserAccountsUserController.BOGUS_STANDARD_MESSAGE;
return;
}
if (userAccount.getPasswordLinkExpires() == 0L) {
log.warn("Create password for '" + userEmail
+ "' is bogus: password change is not pending.");
bogusMessage = "The account for " + userEmail
+ " has already been activated.";
return;
}
Date expirationDate = new Date(userAccount.getPasswordLinkExpires());
if (expirationDate.before(new Date())) {
log.warn("Create password for '" + userEmail
+ "' is bogus: expiration date has passed.");
bogusMessage = UserAccountsUserController.BOGUS_STANDARD_MESSAGE;
return;
}
String expectedKey = userAccount.getPasswordLinkExpiresHash();
if (!key.equals(expectedKey)) {
log.warn("Create password for '" + userEmail + "' is bogus: key ("
+ key + ") doesn't match expected key (" + expectedKey
+ ")");
bogusMessage = UserAccountsUserController.BOGUS_STANDARD_MESSAGE;
return;
}
}
private void validateParameters() {
if (password.isEmpty()) {
errorCode = ERROR_NO_PASSWORD;
} else if (!checkPasswordLength(password)) {
errorCode = ERROR_WRONG_PASSWORD_LENGTH;
} else if (!password.equals(confirmPassword)) {
errorCode = ERROR_PASSWORDS_DONT_MATCH;
}
}
private boolean checkPasswordLength(String pw) {
return pw.length() >= UserAccount.MIN_PASSWORD_LENGTH
&& pw.length() <= UserAccount.MAX_PASSWORD_LENGTH;
}
public boolean isBogus() {
return bogusMessage.isEmpty();
}
public String getBogusMessage() {
return bogusMessage;
}
public boolean isSubmit() {
return submit;
}
public boolean isValid() {
return errorCode.isEmpty();
}
public void createPassword() {
userAccount.setMd5Password(Authenticator.applyMd5Encoding(password));
userAccount.setPasswordLinkExpires(0L);
userAccount.setStatus(Status.ACTIVE);
userAccountsDao.updateUserAccount(userAccount);
log.debug("Set password on '" + userAccount.getEmailAddress()
+ "' to '" + password + "'");
}
public final ResponseValues showPage() {
Map<String, Object> body = new HashMap<String, Object>();
body.put("minimumLength", UserAccount.MIN_PASSWORD_LENGTH);
body.put("maximumLength", UserAccount.MAX_PASSWORD_LENGTH);
body.put("userAccount", userAccount);
body.put("key", userAccount.getPasswordLinkExpiresHash());
body.put("password", password);
body.put("confirmPassword", confirmPassword);
body.put("formUrls", buildUrlsMap());
if (!errorCode.isEmpty()) {
body.put(errorCode, Boolean.TRUE);
}
return new TemplateResponseValues(TEMPLATE_NAME, body);
}
}

View file

@ -21,7 +21,7 @@ public class UserAccountsDeleter extends UserAccountsPage {
/** Might be empty, but never null. */
private final String[] uris;
protected UserAccountsDeleter(VitroRequest vreq) {
public UserAccountsDeleter(VitroRequest vreq) {
super(vreq);
String[] values = vreq.getParameterValues(PARAMETER_DELETE_ACCOUNT);

View file

@ -55,14 +55,6 @@ public class UserAccountsEditPage extends UserAccountsPage {
throw new RuntimeException("UserAccountsEditPage.getUpdatedAccount() not implemented.");
}
/**
*
*/
public void parseParametersAndValidate() {
// TODO Auto-generated method stub
throw new RuntimeException("UserAccountsEditPage.parseParametersAndValidate() not implemented.");
}
/**
* @return
*/

View file

@ -20,8 +20,6 @@ import edu.cornell.mannlib.vitro.webapp.beans.UserAccount.Status;
import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest;
import edu.cornell.mannlib.vitro.webapp.controller.accounts.UserAccountsOrdering.Direction;
import edu.cornell.mannlib.vitro.webapp.controller.accounts.UserAccountsOrdering.Field;
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.UrlBuilder;
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.UrlBuilder.ParamMap;
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.ResponseValues;
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.TemplateResponseValues;
@ -100,7 +98,7 @@ public class UserAccountsListPage extends UserAccountsPage {
userAccountsModel, criteria);
Map<String, Object> body = buildTemplateBodyMap(selection);
body.put("newUserAccount", new UserAccountWrapper(vreq, userAccount,
body.put("newUserAccount", new UserAccountWrapper(userAccount,
Collections.<String> emptyList()));
return new TemplateResponseValues(TEMPLATE_NAME, body);
@ -215,7 +213,7 @@ public class UserAccountsListPage extends UserAccountsPage {
UserAccountsSelection selection) {
List<UserAccountWrapper> list = new ArrayList<UserAccountWrapper>();
for (UserAccount account : selection.getUserAccounts()) {
list.add(new UserAccountWrapper(vreq, account,
list.add(new UserAccountWrapper(account,
findPermissionSetLabels(account)));
}
return list;
@ -240,14 +238,11 @@ public class UserAccountsListPage extends UserAccountsPage {
private final List<String> permissionSets;
private final String editUrl;
public UserAccountWrapper(VitroRequest vreq, UserAccount account,
public UserAccountWrapper(UserAccount account,
List<String> permissionSets) {
this.account = account;
this.permissionSets = permissionSets;
UrlBuilder urlBuilder = new UrlBuilder(vreq.getAppBean());
this.editUrl = urlBuilder.getPortalUrl("/userAccounts/edit",
new ParamMap("editAccount", account.getUri()));
this.editUrl = UserAccountsPage.editAccountUrl(account.getUri());
}
public String getUri() {

View file

@ -20,6 +20,7 @@ import com.hp.hpl.jena.ontology.OntModel;
import edu.cornell.mannlib.vitro.webapp.beans.PermissionSet;
import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest;
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.UrlBuilder;
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.UrlBuilder.ParamMap;
import edu.cornell.mannlib.vitro.webapp.dao.UserAccountsDao;
import edu.cornell.mannlib.vitro.webapp.dao.WebappDaoFactory;
import edu.cornell.mannlib.vitro.webapp.dao.jena.OntModelSelector;
@ -102,11 +103,16 @@ public abstract class UserAccountsPage {
protected Map<String, String> buildUrlsMap() {
Map<String, String> map = new HashMap<String, String>();
map.put("list", UrlBuilder.getUrl("/userAccounts/list"));
map.put("add", UrlBuilder.getUrl("/userAccounts/add"));
map.put("delete", UrlBuilder.getUrl("/userAccounts/delete"));
map.put("list", UrlBuilder.getUrl("/accountsAdmin/list"));
map.put("add", UrlBuilder.getUrl("/accountsAdmin/add"));
map.put("delete", UrlBuilder.getUrl("/accountsAdmin/delete"));
map.put("createPassword", UrlBuilder.getUrl("/accounts/createPassword"));
return map;
}
protected static String editAccountUrl(String uri) {
return UrlBuilder.getUrl("/accountsAdmin/edit",
new ParamMap("editAccount", uri));
}
}

View file

@ -0,0 +1,94 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
package edu.cornell.mannlib.vitro.webapp.controller.accounts.admin;
import java.util.Collection;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import edu.cornell.mannlib.vitro.webapp.auth.requestedAction.Actions;
import edu.cornell.mannlib.vitro.webapp.auth.requestedAction.usepages.ManageUserAccounts;
import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest;
import edu.cornell.mannlib.vitro.webapp.controller.accounts.UserAccountsAddPage;
import edu.cornell.mannlib.vitro.webapp.controller.accounts.UserAccountsDeleter;
import edu.cornell.mannlib.vitro.webapp.controller.accounts.UserAccountsEditPage;
import edu.cornell.mannlib.vitro.webapp.controller.accounts.UserAccountsListPage;
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.FreemarkerHttpServlet;
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.ResponseValues;
/**
* Parcel out the different actions required of the Administrators portion of the UserAccounts GUI.
*/
public class UserAccountsAdminController extends FreemarkerHttpServlet {
private static final Log log = LogFactory
.getLog(UserAccountsAdminController.class);
private static final String ACTION_ADD = "/add";
private static final String ACTION_DELETE = "/delete";
private static final String ACTION_EDIT = "/edit";
@Override
protected Actions requiredActions(VitroRequest vreq) {
return new Actions(new ManageUserAccounts());
}
@Override
protected ResponseValues processRequest(VitroRequest vreq) {
if (log.isDebugEnabled()) {
dumpRequestParameters(vreq);
}
String action = vreq.getPathInfo();
log.debug("action = '" + action + "'");
if (ACTION_ADD.equals(action)) {
return handleAddRequest(vreq);
} else if (ACTION_EDIT.equals(action)) {
return handleEditRequest(vreq);
} else if (ACTION_DELETE.equals(action)) {
return handleDeleteRequest(vreq);
} else {
return handleListRequest(vreq);
}
}
private ResponseValues handleAddRequest(VitroRequest vreq) {
UserAccountsAddPage page = new UserAccountsAddPage(vreq);
if (page.isSubmit() && page.isValid()) {
page.createNewAccount();
UserAccountsListPage listPage = new UserAccountsListPage(vreq);
return listPage.showPageWithNewAccount(page.getAddedAccount(),
page.wasPasswordEmailSent());
} else {
return page.showPage();
}
}
private ResponseValues handleEditRequest(VitroRequest vreq) {
UserAccountsEditPage page = new UserAccountsEditPage(vreq);
if (page.isSubmit() && page.isValid()) {
page.updateAccount();
UserAccountsListPage listPage = new UserAccountsListPage(vreq);
return listPage.showPageWithUpdatedAccount(
page.getUpdatedAccount(), page.wasPasswordEmailSent());
} else {
return page.showPage();
}
}
private ResponseValues handleDeleteRequest(VitroRequest vreq) {
UserAccountsDeleter deleter = new UserAccountsDeleter(vreq);
Collection<String> deletedUris = deleter.delete();
return new UserAccountsListPage(vreq)
.showPageWithDeletions(deletedUris);
}
private ResponseValues handleListRequest(VitroRequest vreq) {
UserAccountsListPage page = new UserAccountsListPage(vreq);
return page.showPage();
}
}

View file

@ -0,0 +1,72 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
package edu.cornell.mannlib.vitro.webapp.controller.accounts.user;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import edu.cornell.mannlib.vitro.webapp.auth.requestedAction.Actions;
import edu.cornell.mannlib.vitro.webapp.beans.DisplayMessage;
import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest;
import edu.cornell.mannlib.vitro.webapp.controller.accounts.UserAccountsCreatePasswordPage;
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.FreemarkerHttpServlet;
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.RedirectResponseValues;
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.ResponseValues;
/**
* Parcel out the different actions required of the UserAccounts GUI.
*/
public class UserAccountsUserController extends FreemarkerHttpServlet {
private static final Log log = LogFactory
.getLog(UserAccountsUserController.class);
public static final String BOGUS_STANDARD_MESSAGE = "Request failed. Please contact your system administrator.";
private static final String ACTION_CREATE_PASSWORD = "/createPassword";
@Override
protected Actions requiredActions(VitroRequest vreq) {
return Actions.AUTHORIZED;
}
@Override
protected ResponseValues processRequest(VitroRequest vreq) {
if (log.isDebugEnabled()) {
dumpRequestParameters(vreq);
}
String action = vreq.getPathInfo();
log.debug("action = '" + action + "'");
if (ACTION_CREATE_PASSWORD.equals(action)) {
return handleCreatePasswordRequest(vreq);
} else {
return handleInvalidRequest(vreq);
}
}
private ResponseValues handleCreatePasswordRequest(VitroRequest vreq) {
UserAccountsCreatePasswordPage page = new UserAccountsCreatePasswordPage(
vreq);
if (page.isBogus()) {
return showHomePage(vreq, page.getBogusMessage());
} else if (page.isSubmit() && page.isValid()) {
page.createPassword();
return showHomePage(vreq,
"Your password has been saved. Please log in.");
} else {
return page.showPage();
}
}
private ResponseValues handleInvalidRequest(VitroRequest vreq) {
return showHomePage(vreq, BOGUS_STANDARD_MESSAGE);
}
private ResponseValues showHomePage(VitroRequest vreq, String message) {
DisplayMessage.setMessage(vreq, message);
return new RedirectResponseValues("/");
}
}

View file

@ -124,7 +124,7 @@ public class SiteAdminController extends FreemarkerHttpServlet {
urls.put("users", urlBuilder.getPortalUrl("/listUsers"));
}
if (PolicyHelper.isAuthorizedForActions(vreq, new ManageUserAccounts())) {
urls.put("userList", urlBuilder.getPortalUrl("/userAccounts"));
urls.put("userList", urlBuilder.getPortalUrl("/accountsAdmin"));
}
if (PolicyHelper.isAuthorizedForActions(vreq, new EditSiteInformation())) {