From 2d6ba014f317600ac7d500e5e5a67c29b675d5b8 Mon Sep 17 00:00:00 2001 From: bsilverstein Date: Fri, 8 Jul 2016 13:37:36 -0400 Subject: [PATCH] Preliminary api test for email confirmation #2170 --- .../actionlogging/ActionLogRecord.java | 2 +- .../iq/dataverse/api/AbstractApiBean.java | 2 +- .../edu/harvard/iq/dataverse/api/Admin.java | 9 ++ .../users/AuthenticatedUser.java | 22 ++- .../confirmemail/ConfirmEmailData.java | 17 +-- .../confirmemail/ConfirmEmailPage.java | 24 +-- .../iq/dataverse/util/SystemConfig.java | 10 +- .../iq/dataverse/util/json/JsonPrinter.java | 1 + src/main/webapp/subset/confirmemail.xhtml | 144 ++++++++++++++++++ .../iq/dataverse/api/BuiltinUsersIT.java | 4 +- .../iq/dataverse/api/ConfirmEmailIT.java | 135 ++++++++++++++++ 11 files changed, 344 insertions(+), 26 deletions(-) create mode 100644 src/main/webapp/subset/confirmemail.xhtml create mode 100644 src/test/java/edu/harvard/iq/dataverse/api/ConfirmEmailIT.java diff --git a/src/main/java/edu/harvard/iq/dataverse/actionlogging/ActionLogRecord.java b/src/main/java/edu/harvard/iq/dataverse/actionlogging/ActionLogRecord.java index fd11dbdd0af..0b2449c87fa 100644 --- a/src/main/java/edu/harvard/iq/dataverse/actionlogging/ActionLogRecord.java +++ b/src/main/java/edu/harvard/iq/dataverse/actionlogging/ActionLogRecord.java @@ -42,7 +42,7 @@ public enum ActionType { Admin, - GlobalGroups + GlobalGroups, AuthenticatedUser } @Id diff --git a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java index 169921a5a39..15ee9ba59a5 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java @@ -58,7 +58,7 @@ */ public abstract class AbstractApiBean { - private static final Logger logger = Logger.getLogger(AbstractApiBean.class.getName()); + static final Logger logger = Logger.getLogger(AbstractApiBean.class.getName()); private static final String DATAVERSE_KEY_HEADER_NAME = "X-Dataverse-key"; /** diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Admin.java b/src/main/java/edu/harvard/iq/dataverse/api/Admin.java index 0c131871282..53e1aba7537 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Admin.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Admin.java @@ -520,5 +520,14 @@ public Response validate() { } return okResponse(msg); } + + @Path("confirmEmail/{token}") + @GET + public Response getConfirmEmailToken(@PathParam("token") String token) { + + + return null; + } + } diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/users/AuthenticatedUser.java b/src/main/java/edu/harvard/iq/dataverse/authorization/users/AuthenticatedUser.java index f609ed959e2..2e0029e47f1 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/users/AuthenticatedUser.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/users/AuthenticatedUser.java @@ -70,8 +70,26 @@ public class AuthenticatedUser implements User, Serializable { private String position; private String lastName; private String firstName; + private String confirmToken; + @Column(nullable = true) + private Timestamp emailConfirmed; + + public Timestamp getEmailConfirmed() { + return emailConfirmed; + } + + public void setEmailConfirmed(Timestamp emailConfirmed) { + this.emailConfirmed = emailConfirmed; + } private boolean superuser; + public String getConfirmToken() { + return confirmToken; + } + + public void setConfirmToken(String confirmToken){ + this.confirmToken = confirmToken; + } /** * @todo Remove? Check for accuracy? For Solr JOINs we used to care about * the modification times of users but now we don't index users at all. @@ -118,7 +136,7 @@ public void applyDisplayInfo( AuthenticatedUserDisplayInfo inf ) { setEmail(inf.getEmailAddress()); setAffiliation( inf.getAffiliation() ); setPosition( inf.getPosition()); - + } @Override @@ -242,4 +260,6 @@ public void setShibIdentityProvider(String shibIdentityProvider) { this.shibIdentityProvider = shibIdentityProvider; } + + } diff --git a/src/main/java/edu/harvard/iq/dataverse/confirmemail/ConfirmEmailData.java b/src/main/java/edu/harvard/iq/dataverse/confirmemail/ConfirmEmailData.java index 37c6c2f17b5..0418ca6b7b9 100644 --- a/src/main/java/edu/harvard/iq/dataverse/confirmemail/ConfirmEmailData.java +++ b/src/main/java/edu/harvard/iq/dataverse/confirmemail/ConfirmEmailData.java @@ -20,9 +20,9 @@ import javax.persistence.Table; /** - * @todo Confirm proper transition from builtinuser to authenticateduser - * before removing the import + * * @author bsilverstein + * @todo: Make the feature restrict a user until they are confirmed */ @Table(indexes = {@Index(columnList="token") @@ -55,8 +55,7 @@ public class ConfirmEmailData implements Serializable{ @Column(nullable = false) private Timestamp expires; - public ConfirmEmailData() { - } + public ConfirmEmailData(AuthenticatedUser anAuthenticatedUser) { authenticatedUser = anAuthenticatedUser; @@ -64,13 +63,13 @@ public ConfirmEmailData(AuthenticatedUser anAuthenticatedUser) { long nowInMilliseconds = new Date().getTime(); created = new Timestamp(nowInMilliseconds); long ONE_MINUTE_IN_MILLISECONDS = 60000; - /** @todo: not utilize getMinutesUntilPasswordResetTokenExpires -- - * maybe consolidate the method in SystemConfig to just be about tokens - * rather than duplicate and re-brand the same method - * @todo: make the token's time before expiration way longer + /** + * @todo: make the token's time before expiration way longer + * + * @todo: use database setting instead of jvm option for line 75 configurable expiration value */ - long futureInMilliseconds = nowInMilliseconds + (SystemConfig.getMinutesUntilPasswordResetTokenExpires() * ONE_MINUTE_IN_MILLISECONDS); + long futureInMilliseconds = nowInMilliseconds + (SystemConfig.getMinutesUntilConfirmEmailTokenExpires() * ONE_MINUTE_IN_MILLISECONDS); expires = new Timestamp(new Date(futureInMilliseconds).getTime()); } diff --git a/src/main/java/edu/harvard/iq/dataverse/confirmemail/ConfirmEmailPage.java b/src/main/java/edu/harvard/iq/dataverse/confirmemail/ConfirmEmailPage.java index b8fa4c8b929..d8118048cb7 100644 --- a/src/main/java/edu/harvard/iq/dataverse/confirmemail/ConfirmEmailPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/confirmemail/ConfirmEmailPage.java @@ -80,7 +80,7 @@ public void init() { if (confirmEmailData != null) { user = confirmEmailData.getAuthenticatedUser(); } else { - FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_ERROR, "Password Reset Link", "Your password reset link is not valid.")); + FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_ERROR, "Confirm Email Link", "Your email confirmation link is not valid.")); } } } @@ -100,31 +100,33 @@ public String sendEmailConfirmLink() { } else { logger.log(Level.INFO, "Couldn''t find single account using {0}", emailAddress); } - FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO, "Password Reset Initiated", "")); - } catch (ConfirmEmailException ex) { - /** - * @todo do we really need a special exception for this?? - */ - logger.log(Level.WARNING, "Error While resetting password: " + ex.getMessage(), ex); + FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO, "Email Confirmation Initiated", "")); + } catch (ConfirmEmailException ex) { + logger.log(Level.WARNING, "Error While confirming email: " + ex.getMessage(), ex); } return ""; } //Huge mess below! Need to figure out what's up with this -// public String confirmEmail() { +// public String confirmEmail() throws ConfirmEmailException { // ConfirmEmailInitResponse response; // response = confirmEmailService.sendConfirm(user, false); -// if (response.isConfirmed()) { +// try { if (response.isEmailFound()) { // FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO, response.getMessageSummary(), response.getMessageDetail())); // String authProviderId = AuthenticationProvider.PROVIDER_ID; -// AuthenticatedUser au = authSvc.lookupUser(builtinAuthProviderId, user.getUserName()); +// AuthenticatedUser au = authSvc.lookupUser(builtinAuthProviderId, user.getUserIdentifier()); // session.setUser(au); // return "/dataverse.xhtml?alias=" + dataverseService.findRootDataverse().getAlias() + "faces-redirect=true"; // } else { // FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_ERROR, response.getMessageSummary(), response.getMessageDetail())); // return null; // } -// } // +// } catch(Exception ex) { +// String msg = "Unable to save token for " + user.getEmail(); +// throw new ConfirmEmailException(msg, ex); +// } +// } + public String getToken() { return token; } diff --git a/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java b/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java index ab762a16daf..8e63f5133a5 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java @@ -201,7 +201,15 @@ public String getSolrHostColonPort() { } - + /** + * The number of minutes for which a confirmation email link is valid. + * @todo: ask Phil about making it possible to set this value as sysadmin + * + */ + public static int getMinutesUntilConfirmEmailTokenExpires() { + final int reasonableDefault = 60; + return reasonableDefault; + } /** * The number of minutes for which a password reset token is valid. Can be * overridden by {@link #PASSWORD_RESET_TIMEOUT_IN_MINUTES}. diff --git a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java index dd9957297e7..1ae642cd7fc 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java @@ -88,6 +88,7 @@ public static JsonObjectBuilder jsonForAuthUser(AuthenticatedUser authenticatedU .add("affiliation", authenticatedUser.getAffiliation()) .add("position", authenticatedUser.getPosition()) .add("persistentUserId", authenticatedUser.getAuthenticatedUserLookup().getPersistentUserId()) + .add("confirmToken", authenticatedUser.getConfirmToken()) .add("authenticationProviderId", authenticatedUser.getAuthenticatedUserLookup().getAuthenticationProviderId()); } diff --git a/src/main/webapp/subset/confirmemail.xhtml b/src/main/webapp/subset/confirmemail.xhtml new file mode 100644 index 00000000000..75aa9dc6749 --- /dev/null +++ b/src/main/webapp/subset/confirmemail.xhtml @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+
+
+
+ + +
+
+ + + + + +
+ +
+ +
+ + + + +
+
+
+
+ + +
+
+
+
+ + + + + +
+ +
+ + + + + + + + + + + +
+
+
+
+
+ + + + + + + +
+ + +
+ + + + + +
+
+
+
+
+ \ No newline at end of file diff --git a/src/test/java/edu/harvard/iq/dataverse/api/BuiltinUsersIT.java b/src/test/java/edu/harvard/iq/dataverse/api/BuiltinUsersIT.java index 65bd909c6c8..e5b8c7623b5 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/BuiltinUsersIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/BuiltinUsersIT.java @@ -68,7 +68,7 @@ public void testLeadingWhitespaceInEmailAddress() { // the backend will trim the email address String emailExpected = email.trim(); assertEquals(emailExpected, emailActual); - } + } @Test public void testLeadingWhitespaceInUsername() { @@ -113,7 +113,7 @@ public void testLogin() { } } - + private Response createUser(String username, String firstName, String lastName, String email) { String userAsJson = getUserAsJsonString(username, firstName, lastName, email); String password = getPassword(userAsJson); diff --git a/src/test/java/edu/harvard/iq/dataverse/api/ConfirmEmailIT.java b/src/test/java/edu/harvard/iq/dataverse/api/ConfirmEmailIT.java new file mode 100644 index 00000000000..1e691bbe189 --- /dev/null +++ b/src/test/java/edu/harvard/iq/dataverse/api/ConfirmEmailIT.java @@ -0,0 +1,135 @@ +package edu.harvard.iq.dataverse.api; + +import com.jayway.restassured.RestAssured; +import static com.jayway.restassured.RestAssured.given; +import com.jayway.restassured.http.ContentType; +import com.jayway.restassured.path.json.JsonPath; +import com.jayway.restassured.response.Response; +import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; +import edu.harvard.iq.dataverse.confirmemail.ConfirmEmailData; +import edu.harvard.iq.dataverse.util.json.JsonPrinter; +import java.util.UUID; +import java.util.logging.Logger; +import javax.json.Json; +import javax.json.JsonObjectBuilder; +import static edu.harvard.iq.dataverse.util.json.JsonPrinter.jsonForAuthUser; +import static junit.framework.Assert.assertEquals; +import org.junit.BeforeClass; +import org.junit.Test; + +/** + * + * @author bsilverstein + * @todo: Develop test to reflect access restrictions without confirmation + */ +public class ConfirmEmailIT { + + private static final Logger logger = Logger.getLogger(ConfirmEmailIT.class.getCanonicalName()); + + private static final String builtinUserKey = "burrito"; + private static final String idKey = "id"; + private static final String usernameKey = "userName"; + private static final String emailKey = "email"; + private static final AuthenticatedUser authenticatedUser = new AuthenticatedUser(); + private static final ConfirmEmailData emailData = new ConfirmEmailData(authenticatedUser); + private static final String confirmToken = getConfirmEmailToken(emailData); + + @BeforeClass + public static void setUp() { + RestAssured.baseURI = UtilIT.getRestAssuredBaseUri(); + } + + + + @Test + public void testConfirm() { + // Can't seem to get timestamp to appear in authenticated user Json output + String email = null; + Response createUserToConfirm = createUser(getRandomUsername(), "firstName", "lastName", email); + createUserToConfirm.prettyPrint(); + createUserToConfirm.then().assertThat() + .statusCode(200); + + long userIdToConfirm = JsonPath.from(createUserToConfirm.body().asString()).getLong("data.authenticatedUser.id"); + String userToConfirmApiToken = JsonPath.from(createUserToConfirm.body().asString()).getString("data.apiToken"); + String usernameToConfirm = JsonPath.from(createUserToConfirm.body().asString()).getString("data.user.userName"); + Response getApiToken = getApiTokenUsingUsername(usernameToConfirm, usernameToConfirm); + getApiToken.then().assertThat() + .statusCode(200); + + } + + + private Response createUser(String username, String firstName, String lastName, String email) { + String userAsJson = getUserAsJsonString(username, firstName, lastName, email); + String password = getPassword(userAsJson); + Response response = given() + .body(userAsJson) + .contentType(ContentType.JSON) + .post("/api/builtin-users?key=" + builtinUserKey + "&password=" + password); + return response; + } + + private static String getRandomUsername() { + return UUID.randomUUID().toString().substring(0, 8); + } + + private static String getUserAsJsonString(String username, String firstName, String lastName, String email) { + JsonObjectBuilder builder = Json.createObjectBuilder(); + builder.add(usernameKey, username); + builder.add("firstName", firstName); + builder.add("lastName", lastName); + if (email == null) { + builder.add(emailKey, getEmailFromUserName(username)); + } else { + builder.add(emailKey, email); + } + + String userAsJson = builder.build().toString(); + logger.fine("User to create: " + userAsJson); + return userAsJson; + } + //May be redundant / unusable? + private static String getAuthUserAsJsonString(AuthenticatedUser authenticatedUser){ + JsonObjectBuilder authenticatedUserBuilder = JsonPrinter.jsonForAuthUser(authenticatedUser); + authenticatedUserBuilder.add("id", authenticatedUser.getId()); + authenticatedUserBuilder.add("identifier", authenticatedUser.getIdentifier()); + authenticatedUserBuilder.add("displayName", authenticatedUser.getDisplayInfo().getTitle()); + authenticatedUserBuilder.add("firstName", authenticatedUser.getFirstName()); + authenticatedUserBuilder.add("lastName", authenticatedUser.getLastName()); + authenticatedUserBuilder.add("email", authenticatedUser.getEmail()); + authenticatedUserBuilder.add("superuser", authenticatedUser.isSuperuser()); + authenticatedUserBuilder.add("affiliation", authenticatedUser.getAffiliation()); + authenticatedUserBuilder.add("position", authenticatedUser.getPosition()); + authenticatedUserBuilder.add("persistentUserId", authenticatedUser.getAuthenticatedUserLookup().getPersistentUserId()); + authenticatedUserBuilder.add("confirmToken", authenticatedUser.getConfirmToken()); + authenticatedUserBuilder.add("authenticationProviderId", authenticatedUser.getAuthenticatedUserLookup().getAuthenticationProviderId()); + + String authenticatedUserAsJson = authenticatedUserBuilder.build().toString(); + logger.fine("Authenticated User to create: " + authenticatedUserAsJson); + return authenticatedUserAsJson; + + } + + private static String getPassword(String jsonStr) { + String password = JsonPath.from(jsonStr).get(usernameKey); + return password; + } + + private static String getEmailFromUserName(String username) { + return username + "@mailinator.com"; + } + + private static String getConfirmEmailToken(ConfirmEmailData emailData){ + String confirmToken = emailData.getToken(); + return confirmToken; + } + + private Response getApiTokenUsingUsername(String username, String password) { + Response response = given() + .contentType(ContentType.JSON) + .get("/api/builtin-users/" + username + "/api-token?username=" + username + "&password=" + password); + return response; + } + +}