diff --git a/doc/Architecture/update-user-account-info.puml b/doc/Architecture/update-user-account-info.puml new file mode 100644 index 00000000000..30da04c6e9f --- /dev/null +++ b/doc/Architecture/update-user-account-info.puml @@ -0,0 +1,28 @@ +@startuml +actor user +participant DataverseUserPage as page +participant "provider: AuthenticationProvider" as prv +participant AuthenticationServiceBean as authSvc +user --> page: HTTP GET +== page init == +... +page -> authSvc : lookupProvider(session.user) +activate authSvc +page <-- authSvc: provider +deactivate authSvc +... + +== Can Update User? == +page -> prv: isUserInfoUpdateAllowed() +activate prv +page <-- prv: true/false +deactivate prv +... + +== Save User == +user --> page: save() +page -> page: udi = createUserDisplayInfo() +page -> prv: updateUserInfo( udi ) +page -> authSvc: updateAuthentictedUser( session.user, udi ) + +@enduml diff --git a/src/main/java/Bundle.properties b/src/main/java/Bundle.properties index 85f00219fe4..ea1764dcbc1 100755 --- a/src/main/java/Bundle.properties +++ b/src/main/java/Bundle.properties @@ -206,6 +206,8 @@ login.builtin.credential.password=Password login.builtin.invalidUsernameEmailOrPassword=The username, email address, or password you entered is invalid. Need assistance accessing your account? # how do we exercise login.error? Via a password upgrade failure? See https://github.com/IQSS/dataverse/pull/2922 login.error=Error validating the username, email address, or password. Please try again. If the problem persists, contact an administrator. +user.error.cannotChangePassword=Sorry, your password cannot be changed. Please contact your system administrator. +user.error.wrongPassword=Sorry, wrong password. #confirmemail.xhtml confirmEmail.pageTitle=Email Verification diff --git a/src/main/java/edu/harvard/iq/dataverse/GuestbookResponseServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/GuestbookResponseServiceBean.java index 23a33b0cbf6..e9a26d808bb 100644 --- a/src/main/java/edu/harvard/iq/dataverse/GuestbookResponseServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/GuestbookResponseServiceBean.java @@ -363,15 +363,6 @@ public String getUserName(User user) { AuthenticatedUser authUser = (AuthenticatedUser) user; return authUser.getName(); } - - try { - if (user.isBuiltInUser()) { - BuiltinUser builtinUser = (BuiltinUser) user; - return builtinUser.getDisplayName(); - } - } catch (Exception e) { - return ""; - } return "Guest"; } @@ -380,14 +371,6 @@ public String getUserEMail(User user) { AuthenticatedUser authUser = (AuthenticatedUser) user; return authUser.getEmail(); } - try { - if (user.isBuiltInUser()) { - BuiltinUser builtinUser = (BuiltinUser) user; - return builtinUser.getEmail(); - } - } catch (Exception e) { - return ""; - } return ""; } @@ -396,15 +379,6 @@ public String getUserInstitution(User user) { AuthenticatedUser authUser = (AuthenticatedUser) user; return authUser.getAffiliation(); } - - try { - if (user.isBuiltInUser()) { - BuiltinUser builtinUser = (BuiltinUser) user; - return builtinUser.getAffiliation(); - } - } catch (Exception e) { - return ""; - } return ""; } @@ -413,15 +387,6 @@ public String getUserPosition(User user) { AuthenticatedUser authUser = (AuthenticatedUser) user; return authUser.getPosition(); } - try { - if (user.isBuiltInUser()) { - BuiltinUser builtinUser = (BuiltinUser) user; - return builtinUser.getPosition(); - } - } catch (Exception e) { - return ""; - } - return ""; } diff --git a/src/main/java/edu/harvard/iq/dataverse/ManageGuestbooksPage.java b/src/main/java/edu/harvard/iq/dataverse/ManageGuestbooksPage.java index 320a16e013c..1dd5712cbcd 100644 --- a/src/main/java/edu/harvard/iq/dataverse/ManageGuestbooksPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/ManageGuestbooksPage.java @@ -1,8 +1,3 @@ -/* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. - */ package edu.harvard.iq.dataverse; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticatedUserDisplayInfo.java b/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticatedUserDisplayInfo.java index a18f7af0b03..08c11702ffa 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticatedUserDisplayInfo.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticatedUserDisplayInfo.java @@ -22,6 +22,14 @@ public AuthenticatedUserDisplayInfo(String firstName, String lastName, String em this.lastName = lastName; this.position = position; } + + public AuthenticatedUserDisplayInfo() { + super("","",""); + firstName=""; + lastName=""; + position=""; + } + /** * Copy constructor (old school!) diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationProvider.java b/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationProvider.java index 41a6477cd09..1b7cc87b702 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationProvider.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationProvider.java @@ -3,8 +3,12 @@ import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; /** - * Objects that can authenticate users - for credentials, they provide persistent user id that can be used to - * lookup an {@link AuthenticatedUser} using {@link AuthenticatedUserLookup} objects. + * Objects that can authenticate users. The authentication process yields a unique + * identifier for the user in the user directory this provider represents. + * + * Providers can have optional functionalities, such as updating user data and verifying email addresses. This abilities + * can be queried using the {@code isXXXAllowed()} methods. If an implementation returns {@code true} + * from one of these methods, it has to implement the matching methods. * * {@code AuthenticationPrvider}s are normally registered at startup in {@link AuthenticationServiceBean#startup()}. * @@ -15,5 +19,54 @@ public interface AuthenticationProvider { String getId(); AuthenticationProviderDisplayInfo getInfo(); - + + default boolean isPasswordUpdateAllowed() { return false; }; + default boolean isUserInfoUpdateAllowed() { return false; }; + default boolean isUserDeletionAllowed() { return false; }; + + /** + * Some providers (e.g organizational ones) provide verified email addresses. + * @return {@code true} if we can treat email addresses coming from this provider as verified, {@code false} otherwise. + */ + default boolean isEmailVerified() { return false; }; + + /** + * Updates the password of the user whose id is passed. + * @param userIdInProvider User id in the provider. NOT the {@link AuthenticatedUser#id}, which is internal to the installation. + * @param newPassword password, in clear text + * @throws UnsupportedOperationException if the provider does not support updating passwords. + * @see #isPasswordUpdateAllowed() + */ + default void updatePassword( String userIdInProvider, String newPassword ) { + throw new UnsupportedOperationException(this.toString() + " does not implement password updates"); + }; + + /** + * Verifies that the passed password matches the user's. Note that this method is has tri-state return + * value ({@link Boolean} rather than a {@code boolean}). A {@code null} returned means that the user + * was not found. + * @param userIdInProvider User id in the provider. NOT the {@link AuthenticatedUser#id}, which is internal to the installation. + * @param password The password string we test + * @return {@code True} if the passwords match; {@code False} if they don't; {@code null} if the user was not found. + * @throws UnsupportedOperationException if the provider does not support updating passwords. + * @see #isPasswordUpdateAllowed() + */ + default Boolean verifyPassword( String userIdInProvider, String password ) { + throw new UnsupportedOperationException(this.toString() + " does not implement password updates"); + }; + + /** + * Updates the password of the user whose id is passed. + * @param userIdInProvider User id in the provider. NOT the {@link AuthenticatedUser#id}, which is internal to the installation. + * @param updatedUserData + * @throws UnsupportedOperationException + * @see #isUserInfoUpdateAllowed() + */ + default void updateUserInfo( String userIdInProvider, AuthenticatedUserDisplayInfo updatedUserData ) { + throw new UnsupportedOperationException(this.toString() + " does not implement account detail updates"); + }; + + default void deleteUser( String userIdInProvider ) { + throw new UnsupportedOperationException(this.toString() + " does not implement account deletions"); + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java index cc5051502fb..ed5b55907ac 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java @@ -100,9 +100,8 @@ public void startup() { registerProviderFactory( new BuiltinAuthenticationProviderFactory(builtinUserServiceBean) ); registerProviderFactory( new EchoAuthenticationProviderFactory() ); registerProviderFactory( new OAuth2AuthenticationProviderFactory() ); - /** - * Register shib provider factory here. Test enable/disable via Admin API, etc. - */ + + // Register shib provider factory here. Test enable/disable via Admin API, etc. new ShibAuthenticationProvider(); } catch (AuthorizationSetupException ex) { logger.log(Level.SEVERE, "Exception setting up the authentication provider factories: " + ex.getMessage(), ex); @@ -254,13 +253,13 @@ public void deleteAuthenticatedUser(Object pk) { */ em.remove(confirmEmailData); } - for (UserNotification notification : userNotificationService.findByUser(user.getId())) { - userNotificationService.delete(notification); - } - if (user.isBuiltInUser()) { - BuiltinUser builtin = builtinUserServiceBean.findByUserName(user.getUserIdentifier()); - em.remove(builtin); + userNotificationService.findByUser(user.getId()).forEach(userNotificationService::delete); + + AuthenticationProvider prv = lookupProvider(user); + if ( prv != null && prv.isUserDeletionAllowed() ) { + prv.deleteUser(user.getAuthenticatedUserLookup().getPersistentUserId()); } + actionLogSvc.log( new ActionLogRecord(ActionLogRecord.ActionType.Auth, "deleteUser") .setInfo(user.getUserIdentifier())); em.remove(user.getAuthenticatedUserLookup()); @@ -330,7 +329,11 @@ public AuthenticatedUser authenticate( String authenticationProviderId, Authenti } } - public boolean isEmailAvailable(String email) { + /** + * @param email + * @return {@code true} iff the none of the authenticated users has the passed email address. + */ + public boolean isEmailAddressAvailable(String email) { return em.createNamedQuery("AuthenticatedUser.findByEmail", AuthenticatedUser.class) .setParameter("email", email) .getResultList().isEmpty(); @@ -352,6 +355,10 @@ public AuthenticatedUser lookupUser(String authPrvId, String userPersistentId) { } } + public AuthenticationProvider lookupProvider( AuthenticatedUser user ) { + return authenticationProviders.get(user.getAuthenticatedUserLookup().getAuthenticationProviderId()); + } + public ApiToken findApiToken(String token) { try { return em.createNamedQuery("ApiToken.findByTokenString", ApiToken.class) @@ -362,8 +369,6 @@ public ApiToken findApiToken(String token) { } } - - public ApiToken findApiTokenByUser(AuthenticatedUser au) { if (au == null) { return null; @@ -525,10 +530,14 @@ public AuthenticatedUser createAuthenticatedUser(UserRecordIdentifier userRecord actionLogSvc.log( new ActionLogRecord(ActionLogRecord.ActionType.Auth, "createUser") .setInfo(authenticatedUser.getIdentifier())); - return authenticatedUser; } + /** + * Checks whether the {@code idtf} is already taken by another {@link AuthenticatedUser}. + * @param idtf + * @return {@code true} iff there's already a user by that username. + */ public boolean identifierExists( String idtf ) { return em.createNamedQuery("AuthenticatedUser.countOfIdentifier", Number.class) .setParameter("identifier", idtf) diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/RoleAssigneeDisplayInfo.java b/src/main/java/edu/harvard/iq/dataverse/authorization/RoleAssigneeDisplayInfo.java index 97dc4f0b83e..821d707ce92 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/RoleAssigneeDisplayInfo.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/RoleAssigneeDisplayInfo.java @@ -35,19 +35,16 @@ public String getAffiliation() { return affiliation; } - public RoleAssigneeDisplayInfo setTitle(String title) { + public void setTitle(String title) { this.title = title; - return this; } - public RoleAssigneeDisplayInfo setEmailAddress(String emailAddress) { + public void setEmailAddress(String emailAddress) { this.emailAddress = emailAddress; - return this; } - public RoleAssigneeDisplayInfo setAffiliation(String affiliation) { + public void setAffiliation(String affiliation) { this.affiliation = affiliation; - return this; } @Override diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/UserLister.java b/src/main/java/edu/harvard/iq/dataverse/authorization/UserLister.java deleted file mode 100644 index ccf6323ce08..00000000000 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/UserLister.java +++ /dev/null @@ -1,10 +0,0 @@ -package edu.harvard.iq.dataverse.authorization; - -import edu.harvard.iq.dataverse.authorization.users.User; -import java.util.List; - -public interface UserLister { - - public List listUsers(); - -} diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/BuiltinAuthenticationProvider.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/BuiltinAuthenticationProvider.java index 83fec85023f..b9fad9aebae 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/BuiltinAuthenticationProvider.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/BuiltinAuthenticationProvider.java @@ -1,22 +1,15 @@ package edu.harvard.iq.dataverse.authorization.providers.builtin; -import edu.harvard.iq.dataverse.DvObject; +import edu.harvard.iq.dataverse.authorization.AuthenticatedUserDisplayInfo; import edu.harvard.iq.dataverse.authorization.AuthenticationProviderDisplayInfo; import edu.harvard.iq.dataverse.authorization.AuthenticationRequest; import edu.harvard.iq.dataverse.authorization.AuthenticationResponse; import edu.harvard.iq.dataverse.authorization.CredentialsAuthenticationProvider; -import edu.harvard.iq.dataverse.authorization.UserLister; -import edu.harvard.iq.dataverse.authorization.groups.GroupProvider; -import edu.harvard.iq.dataverse.authorization.users.User; import java.util.Arrays; import java.util.List; import static edu.harvard.iq.dataverse.authorization.CredentialsAuthenticationProvider.Credential; -import edu.harvard.iq.dataverse.authorization.RoleAssignee; -import edu.harvard.iq.dataverse.authorization.groups.Group; -import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.passwordreset.PasswordResetException; import edu.harvard.iq.dataverse.util.BundleUtil; -import java.util.Set; /** * An authentication provider built into the application. Uses JPA and the @@ -24,7 +17,7 @@ * * @author michael */ -public class BuiltinAuthenticationProvider implements CredentialsAuthenticationProvider, UserLister, GroupProvider { +public class BuiltinAuthenticationProvider implements CredentialsAuthenticationProvider { public static final String PROVIDER_ID = "builtin"; private static String KEY_USERNAME_OR_EMAIL; @@ -40,11 +33,6 @@ public BuiltinAuthenticationProvider( BuiltinUserServiceBean aBean ) { CREDENTIALS_LIST = Arrays.asList(new Credential(KEY_USERNAME_OR_EMAIL), new Credential(KEY_PASSWORD, true)); } - @Override - public List listUsers() { - throw new UnsupportedOperationException("Not supported yet."); - } - @Override public String getId() { return PROVIDER_ID; @@ -55,6 +43,61 @@ public AuthenticationProviderDisplayInfo getInfo() { return new AuthenticationProviderDisplayInfo(getId(), "Build-in Provider", "Internal user repository"); } + @Override + public boolean isPasswordUpdateAllowed() { + return true; + } + + @Override + public boolean isUserInfoUpdateAllowed() { + return true; + } + + @Override + public boolean isUserDeletionAllowed() { + return true; + } + + @Override + public void deleteUser(String userIdInProvider) { + bean.removeUser(userIdInProvider); + } + + @Override + public void updatePassword(String userIdInProvider, String newPassword) { + BuiltinUser biUser = bean.findByUserName( userIdInProvider ); + biUser.updateEncryptedPassword(PasswordEncryption.get().encrypt(newPassword), + PasswordEncryption.getLatestVersionNumber()); + bean.save(biUser); + } + + @Override + public void updateUserInfo(String userIdInProvider, AuthenticatedUserDisplayInfo updatedUserData) { + BuiltinUser biUser = bean.findByUserName( userIdInProvider ); + biUser.setFirstName(updatedUserData.getFirstName()); + biUser.setLastName(updatedUserData.getLastName()); + biUser.setEmail( updatedUserData.getEmailAddress()); + biUser.setAffiliation( updatedUserData.getAffiliation() ); + biUser.setPosition(updatedUserData.getPosition()); + + bean.save(biUser); + } + + /** + * Validates that the passed password is indeed the password of the user. + * @param userIdInProvider + * @param password + * @return {@code true} if the password matches the user's password; {@code false} otherwise. + */ + @Override + public Boolean verifyPassword( String userIdInProvider, String password ) { + BuiltinUser biUser = bean.findByUserName( userIdInProvider ); + if ( biUser == null ) return null; + return PasswordEncryption.getVersion(biUser.getPasswordEncryptionVersion()) + .check(password, biUser.getEncryptedPassword()); + } + + @Override public AuthenticationResponse authenticate( AuthenticationRequest authReq ) { BuiltinUser u = bean.findByUsernameOrEmail(authReq.getCredential(KEY_USERNAME_OR_EMAIL) ); @@ -85,33 +128,4 @@ public List getRequiredCredentials() { return CREDENTIALS_LIST; } - @Override - public String getGroupProviderAlias() { - throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. - } - - @Override - public String getGroupProviderInfo() { - throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. - } - - @Override - public Set groupsFor(RoleAssignee u, DvObject o) { - throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. - } - - @Override - public Set groupsFor(DataverseRequest u, DvObject o) { - throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. - } - - @Override - public Group get(String groupAlias) { - throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. - } - - @Override - public Set findGlobalGroups() { - throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. - } } diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/BuiltinUser.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/BuiltinUser.java index 15aac6772f1..244f3828753 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/BuiltinUser.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/BuiltinUser.java @@ -2,6 +2,7 @@ import edu.harvard.iq.dataverse.ValidateEmail; import edu.harvard.iq.dataverse.authorization.AuthenticatedUserDisplayInfo; +import static edu.harvard.iq.dataverse.util.StringUtil.nonEmpty; import java.io.Serializable; import javax.persistence.Column; import javax.persistence.Entity; @@ -143,6 +144,20 @@ public String getDisplayName(){ return this.getFirstName() + " " + this.getLastName(); } + public void applyDisplayInfo( AuthenticatedUserDisplayInfo inf ) { + setFirstName(inf.getFirstName()); + setLastName(inf.getLastName()); + if ( nonEmpty(inf.getEmailAddress()) ) { + setEmail(inf.getEmailAddress()); + } + if ( nonEmpty(inf.getAffiliation()) ) { + setAffiliation( inf.getAffiliation() ); + } + if ( nonEmpty(inf.getPosition()) ) { + setPosition( inf.getPosition()); + } + } + public AuthenticatedUserDisplayInfo getDisplayInfo() { return new AuthenticatedUserDisplayInfo(getFirstName(), getLastName(), getEmail(), getAffiliation(), getPosition() ); } diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/BuiltinUserServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/BuiltinUserServiceBean.java index 1bad489f58c..afb5fc62b39 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/BuiltinUserServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/BuiltinUserServiceBean.java @@ -1,7 +1,6 @@ package edu.harvard.iq.dataverse.authorization.providers.builtin; import edu.harvard.iq.dataverse.search.IndexServiceBean; -import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.passwordreset.PasswordResetData; import edu.harvard.iq.dataverse.passwordreset.PasswordResetException; import edu.harvard.iq.dataverse.passwordreset.PasswordResetInitResponse; @@ -45,7 +44,7 @@ public String encryptPassword(String plainText) { return PasswordEncryption.get().encrypt(plainText); } - public BuiltinUser save(BuiltinUser dataverseUser) { + public BuiltinUser save(BuiltinUser aUser) { /** * Trim the email address no matter what the user entered or is entered * on their behalf in the case of Shibboleth assertions. @@ -53,14 +52,14 @@ public BuiltinUser save(BuiltinUser dataverseUser) { * @todo Why doesn't Bean Validation report that leading and trailing * whitespace in an email address is a problem? */ - dataverseUser.setEmail(dataverseUser.getEmail().trim()); + aUser.setEmail(aUser.getEmail().trim()); /** * We throw a proper IllegalArgumentException here because otherwise * from the API you get a 500 response and "Can't save user: null". */ ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); Validator validator = factory.getValidator(); - Set> violations = validator.validate(dataverseUser); + Set> violations = validator.validate(aUser); if (violations.size() > 0) { StringBuilder sb = new StringBuilder(); violations.stream().forEach((violation) -> { @@ -68,16 +67,16 @@ public BuiltinUser save(BuiltinUser dataverseUser) { }); throw new IllegalArgumentException("BuiltinUser could not be saved to due constraint violations: " + sb); } - if ( dataverseUser.getId() == null ) { + if ( aUser.getId() == null ) { // see that the username is unique if ( em.createNamedQuery("BuiltinUser.findByUserName") - .setParameter("userName", dataverseUser.getUserName()).getResultList().size() > 0 ) { - throw new IllegalArgumentException( "BuiltinUser with username '" + dataverseUser.getUserName() + "' already exists."); + .setParameter("userName", aUser.getUserName()).getResultList().size() > 0 ) { + throw new IllegalArgumentException( "BuiltinUser with username '" + aUser.getUserName() + "' already exists."); } - em.persist( dataverseUser ); - return dataverseUser; + em.persist( aUser ); + return aUser; } else { - return em.merge(dataverseUser); + return em.merge(aUser); } } diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/BuiltinUserPage.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/DataverseUserPage.java similarity index 69% rename from src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/BuiltinUserPage.java rename to src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/DataverseUserPage.java index 5dddaf6432f..306a2898195 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/BuiltinUserPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/DataverseUserPage.java @@ -1,8 +1,3 @@ -/* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. - */ package edu.harvard.iq.dataverse.authorization.providers.builtin; import edu.harvard.iq.dataverse.DataFile; @@ -11,7 +6,6 @@ import edu.harvard.iq.dataverse.DatasetServiceBean; import edu.harvard.iq.dataverse.DatasetVersionServiceBean; import edu.harvard.iq.dataverse.Dataverse; -import edu.harvard.iq.dataverse.DataverseHeaderFragment; import edu.harvard.iq.dataverse.DataverseServiceBean; import edu.harvard.iq.dataverse.DataverseSession; import edu.harvard.iq.dataverse.DvObject; @@ -22,6 +16,8 @@ import edu.harvard.iq.dataverse.UserNotification; import static edu.harvard.iq.dataverse.UserNotification.Type.CREATEDV; import edu.harvard.iq.dataverse.UserNotificationServiceBean; +import edu.harvard.iq.dataverse.authorization.AuthenticatedUserDisplayInfo; +import edu.harvard.iq.dataverse.authorization.AuthenticationProvider; import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; import edu.harvard.iq.dataverse.authorization.UserRecordIdentifier; import edu.harvard.iq.dataverse.authorization.groups.Group; @@ -29,7 +25,6 @@ import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.confirmemail.ConfirmEmailData; import edu.harvard.iq.dataverse.confirmemail.ConfirmEmailException; -import edu.harvard.iq.dataverse.confirmemail.ConfirmEmailInitResponse; import edu.harvard.iq.dataverse.confirmemail.ConfirmEmailServiceBean; import edu.harvard.iq.dataverse.confirmemail.ConfirmEmailUtil; import edu.harvard.iq.dataverse.mydata.MyDataPage; @@ -46,6 +41,7 @@ import java.util.Arrays; import java.util.Date; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; @@ -64,16 +60,14 @@ /** * - * @author xyang */ @ViewScoped @Named("DataverseUserPage") -public class BuiltinUserPage implements java.io.Serializable { +public class DataverseUserPage implements java.io.Serializable { - private static final Logger logger = Logger.getLogger(BuiltinUserPage.class.getCanonicalName()); + private static final Logger logger = Logger.getLogger(DataverseUserPage.class.getCanonicalName()); public enum EditMode { - CREATE, EDIT, CHANGE_PASSWORD, FORGOT }; @@ -108,11 +102,9 @@ public enum EditMode { @Inject PermissionsWrapper permissionsWrapper; - @EJB - AuthenticationServiceBean authSvc; - + private AuthenticatedUserDisplayInfo userDisplayInfo; private AuthenticatedUser currentUser; - private BuiltinUser builtinUser; + private transient AuthenticationProvider userAuthProvider; private EditMode editMode; private String redirectPage = "dataverse.xhtml"; @@ -126,132 +118,34 @@ public enum EditMode { private int activeIndex; private String selectTab = "somedata"; UIInput usernameField; + private String username; - public EditMode getChangePasswordMode () { - return EditMode.CHANGE_PASSWORD; - } - - public AuthenticatedUser getCurrentUser() { - return currentUser; - } - - public void setCurrentUser(AuthenticatedUser currentUser) { - this.currentUser = currentUser; - } - - public BuiltinUser getBuiltinUser() { - return builtinUser; - } - - public void setBuiltinUser(BuiltinUser builtinUser) { - this.builtinUser = builtinUser; - } - - public EditMode getEditMode() { - return editMode; - } - - public void setEditMode(EditMode editMode) { - this.editMode = editMode; - } - - public String getRedirectPage() { - return redirectPage; - } - - public void setRedirectPage(String redirectPage) { - this.redirectPage = redirectPage; - } - - public String getInputPassword() { - return inputPassword; - } - - public void setInputPassword(String inputPassword) { - this.inputPassword = inputPassword; - } - - public String getCurrentPassword() { - return currentPassword; - } - - public void setCurrentPassword(String currentPassword) { - this.currentPassword = currentPassword; - } - - public Long getDataverseId() { - - if (dataverseId == null) { - dataverseId = dataverseService.findRootDataverse().getId(); - } - return dataverseId; - } - - public void setDataverseId(Long dataverseId) { - this.dataverseId = dataverseId; - } - - - - public List getNotificationsList() { - return notificationsList; - } - - public void setNotificationsList(List notificationsList) { - this.notificationsList = notificationsList; - } - - public int getActiveIndex() { - return activeIndex; - } - - public void setActiveIndex(int activeIndex) { - this.activeIndex = activeIndex; - } - - public String getSelectTab() { - return selectTab; - } - - public void setSelectTab(String selectTab) { - this.selectTab = selectTab; - } - - public UIInput getUsernameField() { - return usernameField; - } - - public void setUsernameField(UIInput usernameField) { - this.usernameField = usernameField; - } - public String init() { // prevent creating a user if signup not allowed. boolean safeDefaultIfKeyNotFound = true; boolean signupAllowed = settingsWrapper.isTrueForKey(SettingsServiceBean.Key.AllowSignUp.toString(), safeDefaultIfKeyNotFound); - logger.fine("signup is allowed: " + signupAllowed); if (editMode == EditMode.CREATE && !signupAllowed) { return "/403.xhtml"; } if (editMode == EditMode.CREATE) { - if (!session.getUser().isAuthenticated()) { // in create mode for new user + if (session.getUser().isAuthenticated()) { + editMode = null; // we can't be in create mode for an existing user + + } else { + // in create mode for new user JH.addMessage(FacesMessage.SEVERITY_INFO, BundleUtil.getStringFromBundle("user.signup.tip")); - builtinUser = new BuiltinUser(); + userDisplayInfo = new AuthenticatedUserDisplayInfo(); return ""; - } else { - editMode = null; // we can't be in create mode for an existing user } } if ( session.getUser().isAuthenticated() ) { - currentUser = (AuthenticatedUser) session.getUser(); - notificationsList = userNotificationService.findByUser(((AuthenticatedUser)currentUser).getId()); - if (currentUser.isBuiltInUser()) { - builtinUser = builtinUserService.findByUserName(currentUser.getUserIdentifier()); - } + setCurrentUser((AuthenticatedUser) session.getUser()); + notificationsList = userNotificationService.findByUser(currentUser.getId()); + switch (selectTab) { case "notifications": activeIndex = 1; @@ -296,20 +190,11 @@ public void forgotPassword(ActionEvent e) { public void validateUserName(FacesContext context, UIComponent toValidate, Object value) { String userName = (String) value; - boolean userNameFound = false; - BuiltinUser user = builtinUserService.findByUserName(userName); - if (editMode == EditMode.CREATE) { - if (user != null) { - userNameFound = true; - } - } else { - if (user != null && !user.getId().equals(builtinUser.getId())) { - userNameFound = true; - } - } - if (userNameFound) { + boolean userNameFound = authenticationService.identifierExists(userName); + + if (editMode == EditMode.CREATE && userNameFound) { ((UIInput) toValidate).setValid(false); - FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_ERROR, JH.localize("user.username.taken"), null); + FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_ERROR, BundleUtil.getStringFromBundle("user.username.taken"), null); context.addMessage(toValidate.getClientId(context), message); } } @@ -317,72 +202,22 @@ public void validateUserName(FacesContext context, UIComponent toValidate, Objec public void validateUserEmail(FacesContext context, UIComponent toValidate, Object value) { String userEmail = (String) value; boolean userEmailFound = false; - BuiltinUser user = builtinUserService.findByEmail(userEmail); AuthenticatedUser aUser = authenticationService.getAuthenticatedUserByEmail(userEmail); if (editMode == EditMode.CREATE) { - if (user != null || aUser != null) { + if (aUser != null) { userEmailFound = true; } } else { - //In edit mode... - if (user != null || aUser != null){ - userEmailFound = true; - } - //if there's a match on edit make sure that the email belongs to the + // In edit mode... + // if there's a match on edit make sure that the email belongs to the // user doing the editing by checking ids - if ((user != null && user.getId().equals(builtinUser.getId())) || (aUser!=null && aUser.getId().equals(builtinUser.getId()))){ - userEmailFound = false; + if ( aUser!=null && ! aUser.getId().equals(currentUser.getId()) ){ + userEmailFound = true; } } if (userEmailFound) { ((UIInput) toValidate).setValid(false); - FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_ERROR, JH.localize("user.email.taken"), null); - context.addMessage(toValidate.getClientId(context), message); - } - } - - - public void validateUserNameEmail(FacesContext context, UIComponent toValidate, Object value) { - String userName = (String) value; - boolean userNameFound = false; - BuiltinUser user = builtinUserService.findByUserName(userName); - if (user != null) { - userNameFound = true; - } else { - BuiltinUser user2 = builtinUserService.findByEmail(userName); - if (user2 != null) { - userNameFound = true; - } - } - if (!userNameFound) { - ((UIInput) toValidate).setValid(false); - FacesMessage message = new FacesMessage("Username or Email is incorrect."); - context.addMessage(toValidate.getClientId(context), message); - } - } - - public void validateCurrentPassword(FacesContext context, UIComponent toValidate, Object value) { - - String password = (String) value; - - if (StringUtils.isBlank(password)){ - logger.log(Level.WARNING, "current password is blank"); - - ((UIInput) toValidate).setValid(false); - FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_ERROR, - "Password Error", "Password is blank: re-type it again."); - context.addMessage(toValidate.getClientId(context), message); - return; - - } else { - logger.log(Level.INFO, "current paswword is not blank"); - } - - - - if ( ! PasswordEncryption.getVersion(builtinUser.getPasswordEncryptionVersion()).check(password, builtinUser.getEncryptedPassword()) ) { - ((UIInput) toValidate).setValid(false); - FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_ERROR, "Password Error", "Password is incorrect."); + FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_ERROR, BundleUtil.getStringFromBundle("user.email.taken"), null); context.addMessage(toValidate.getClientId(context), message); } } @@ -398,10 +233,7 @@ public void validateNewPassword(FacesContext context, UIComponent toValidate, Ob "Password Error", "The new password is blank: re-type it again"); context.addMessage(toValidate.getClientId(context), message); return; - - } else { - logger.log(Level.INFO, "new paswword is not blank"); - } + } int minPasswordLength = 6; boolean forceNumber = true; @@ -419,49 +251,49 @@ public void validateNewPassword(FacesContext context, UIComponent toValidate, Ob } } - - - public void updatePassword(String userName) { - String plainTextPassword = PasswordEncryption.generateRandomPassword(); - BuiltinUser user = builtinUserService.findByUserName(userName); - if (user == null) { - user = builtinUserService.findByEmail(userName); - } - user.updateEncryptedPassword(PasswordEncryption.get().encrypt(plainTextPassword), PasswordEncryption.getLatestVersionNumber()); - builtinUserService.save(user); - } - public String save() { boolean passwordChanged = false; - boolean emailChanged = false; - if (editMode == EditMode.CREATE || editMode == EditMode.CHANGE_PASSWORD) { - if (inputPassword != null) { - builtinUser.updateEncryptedPassword(PasswordEncryption.get().encrypt(inputPassword), PasswordEncryption.getLatestVersionNumber()); + final AuthenticationProvider prv = getUserAuthProvider(); + if ( editMode == EditMode.CHANGE_PASSWORD ) { + if ( prv.isPasswordUpdateAllowed() ) { + if ( ! prv.verifyPassword(currentUser.getAuthenticatedUserLookup().getPersistentUserId(), currentPassword) ) { + FacesContext.getCurrentInstance().addMessage("currentPassword", + new FacesMessage(FacesMessage.SEVERITY_ERROR, BundleUtil.getStringFromBundle("user.error.wrongPassword"),null)); + return null; + } + prv.updatePassword(currentUser.getAuthenticatedUserLookup().getPersistentUserId(), inputPassword); passwordChanged = true; + } else { - // just defensive coding: for in case when the validator is not - // working - logger.log(Level.WARNING, "inputPassword is still null"); - FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_ERROR, JH.localize("user.noPasswd"), null); - FacesContext context = FacesContext.getCurrentInstance(); - context.addMessage(null, message); + // erroneous state - we can't change the password for this user, so should not have gotten here. Log and bail out. + logger.log(Level.WARNING, "Attempt to change a password on {0}, whose provider ({1}) does not support password change", new Object[]{currentUser.getIdentifier(), prv}); + JH.addMessage(FacesMessage.SEVERITY_ERROR, BundleUtil.getStringFromBundle("user.error.cannotChangePassword")); return null; } } - builtinUser = builtinUserService.save(builtinUser); - + if (editMode == EditMode.CREATE) { - AuthenticatedUser au = authSvc.createAuthenticatedUser( + // Create a new built-in user. + BuiltinUser builtinUser = new BuiltinUser(); + builtinUser.setUserName( getUsername() ); + builtinUser.applyDisplayInfo(userDisplayInfo); + builtinUser.updateEncryptedPassword(PasswordEncryption.get().encrypt(inputPassword), + PasswordEncryption.getLatestVersionNumber()); + + AuthenticatedUser au = authenticationService.createAuthenticatedUser( new UserRecordIdentifier(BuiltinAuthenticationProvider.PROVIDER_ID, builtinUser.getUserName()), builtinUser.getUserName(), builtinUser.getDisplayInfo(), false); if ( au == null ) { // username exists getUsernameField().setValid(false); - FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_ERROR, JH.localize("user.username.taken"), null); + FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_ERROR, BundleUtil.getStringFromBundle("user.username.taken"), null); FacesContext context = FacesContext.getCurrentInstance(); context.addMessage(getUsernameField().getClientId(context), message); return null; } + + // Authenticated user registered. Save the new bulitin, and log in. + builtinUserService.save(builtinUser); session.setUser(au); userNotificationService.sendNotification(au, new Timestamp(new Date().getTime()), @@ -475,7 +307,7 @@ public String save() { try { redirectPage = URLDecoder.decode(redirectPage, "UTF-8"); } catch (UnsupportedEncodingException ex) { - Logger.getLogger(BuiltinUserPage.class.getName()).log(Level.SEVERE, null, ex); + logger.log(Level.SEVERE, "Server does not support 'UTF-8' encoding.", ex); redirectPage = "dataverse.xhtml&alias=" + dataverseService.findRootDataverse().getAlias(); } @@ -483,35 +315,31 @@ public String save() { return redirectPage + (!redirectPage.contains("?") ? "?" : "&") + "faces-redirect=true"; - } else { String emailBeforeUpdate = currentUser.getEmail(); - AuthenticatedUser savedUser = authSvc.updateAuthenticatedUser(currentUser, builtinUser.getDisplayInfo()); + AuthenticatedUser savedUser = authenticationService.updateAuthenticatedUser(currentUser, userDisplayInfo); String emailAfterUpdate = savedUser.getEmail(); - if (!emailBeforeUpdate.equals(emailAfterUpdate)) { - emailChanged = true; - } editMode = null; - String msg = "Your account information has been successfully updated."; - if (passwordChanged) { - msg = "Your account password has been successfully changed."; - } - if (emailChanged) { - ConfirmEmailUtil confirmEmailUtil = new ConfirmEmailUtil(); - String expTime = confirmEmailUtil.friendlyExpirationTime(systemConfig.getMinutesUntilConfirmEmailTokenExpires()); - msg = msg + " Your email address has changed and must be re-verified. Please check your inbox at " + currentUser.getEmail() + " and follow the link we've sent. \n\nAlso, please note that the link will only work for the next " + expTime + " before it has expired."; - boolean sendEmail = true; + StringBuilder msg = new StringBuilder( passwordChanged ? "Your account password has been successfully changed." + : "Your account information has been successfully updated."); + if (!emailBeforeUpdate.equals(emailAfterUpdate)) { + String expTime = ConfirmEmailUtil.friendlyExpirationTime(systemConfig.getMinutesUntilConfirmEmailTokenExpires()); + msg.append(" Your email address has changed and must be re-verified. Please check your inbox at ") + .append(currentUser.getEmail()) + .append(" and follow the link we've sent. \n\nAlso, please note that the link will only work for the next ") + .append(expTime) + .append(" before it has expired."); // delete unexpired token, if it exists (clean slate) confirmEmailService.deleteTokenForUser(currentUser); try { - ConfirmEmailInitResponse confirmEmailInitResponse = confirmEmailService.beginConfirm(currentUser); + confirmEmailService.beginConfirm(currentUser); } catch (ConfirmEmailException ex) { - logger.info("Unable to send email confirmation link to user id " + savedUser.getId()); + logger.log(Level.INFO, "Unable to send email confirmation link to user id {0}", savedUser.getId()); } session.setUser(currentUser); - JsfHelper.addSuccessMessage(msg); + JsfHelper.addSuccessMessage(msg.toString()); } else { - JsfHelper.addFlashMessage(msg); + JsfHelper.addFlashMessage(msg.toString()); } return null; } @@ -526,16 +354,11 @@ public String cancel() { return null; } - public void submit(ActionEvent e) { - updatePassword(builtinUser.getUserName()); - editMode = null; - } - public String remove(Long notificationId) { UserNotification userNotification = userNotificationService.find(notificationId); userNotificationService.delete(userNotification); for (UserNotification uNotification : notificationsList) { - if (uNotification.getId() == userNotification.getId()) { + if (Objects.equals(uNotification.getId(), userNotification.getId())) { notificationsList.remove(uNotification); break; } @@ -630,47 +453,161 @@ public void displayNotification() { public void sendConfirmEmail() { logger.fine("called sendConfirmEmail()"); String userEmail = currentUser.getEmail(); - ConfirmEmailUtil confirmEmailUtil = new ConfirmEmailUtil(); try { confirmEmailService.beginConfirm(currentUser); List args = Arrays.asList( userEmail, - confirmEmailUtil.friendlyExpirationTime(systemConfig.getMinutesUntilConfirmEmailTokenExpires())); + ConfirmEmailUtil.friendlyExpirationTime(systemConfig.getMinutesUntilConfirmEmailTokenExpires())); JsfHelper.addSuccessMessage(BundleUtil.getStringFromBundle("confirmEmail.submitRequest.success", args)); } catch (ConfirmEmailException ex) { - Logger.getLogger(BuiltinUserPage.class.getName()).log(Level.SEVERE, null, ex); + Logger.getLogger(DataverseUserPage.class.getName()).log(Level.SEVERE, null, ex); } } + /** + * Determines whether the button to send a verification email appears on user page + * @return + */ public boolean showVerifyEmailButton() { - /** - * Determines whether the button to send a verification email appears on user page - */ - if (confirmEmailService.findSingleConfirmEmailDataByUser(currentUser) == null - && currentUser.getEmailConfirmed() == null) { - return true; - } - return false; + final Timestamp emailConfirmed = currentUser.getEmailConfirmed(); + final ConfirmEmailData confirmedDate = confirmEmailService.findSingleConfirmEmailDataByUser(currentUser); + return (!getUserAuthProvider().isEmailVerified()) + && confirmedDate == null + && emailConfirmed == null; } public boolean isEmailIsVerified() { - if (currentUser.getEmailConfirmed() != null && confirmEmailService.findSingleConfirmEmailDataByUser(currentUser) == null) { - return true; - } else return false; + return currentUser.getEmailConfirmed() != null && confirmEmailService.findSingleConfirmEmailDataByUser(currentUser) == null; } public boolean isEmailNotVerified() { - if (currentUser.getEmailConfirmed() == null || confirmEmailService.findSingleConfirmEmailDataByUser(currentUser) != null) { - return true; - } else return false; + return currentUser.getEmailConfirmed() == null || confirmEmailService.findSingleConfirmEmailDataByUser(currentUser) != null; } public boolean isEmailGrandfathered() { - ConfirmEmailUtil confirmEmailUtil = new ConfirmEmailUtil(); - if (currentUser.getEmailConfirmed() == confirmEmailUtil.getGrandfatheredTime()) { - return true; - } else return false; + return currentUser.getEmailConfirmed().equals(ConfirmEmailUtil.getGrandfatheredTime()); + } + + AuthenticationProvider getUserAuthProvider() { + if ( userAuthProvider == null ) { + userAuthProvider = authenticationService.lookupProvider(currentUser); + } + return userAuthProvider; + } + + public boolean isPasswordEditable() { + return getUserAuthProvider().isPasswordUpdateAllowed(); + } + + public boolean isAccountDetailsEditable() { + return getUserAuthProvider().isUserInfoUpdateAllowed(); + } + + public AuthenticatedUserDisplayInfo getUserDisplayInfo() { + return userDisplayInfo; + } + + public void setUserDisplayInfo(AuthenticatedUserDisplayInfo userDisplayInfo) { + this.userDisplayInfo = userDisplayInfo; + } + + public EditMode getChangePasswordMode () { + return EditMode.CHANGE_PASSWORD; + } + + public AuthenticatedUser getCurrentUser() { + return currentUser; + } + + public void setCurrentUser(AuthenticatedUser currentUser) { + this.currentUser = currentUser; + userDisplayInfo = currentUser.getDisplayInfo(); + username = currentUser.getUserIdentifier(); + } + + public EditMode getEditMode() { + return editMode; + } + + public void setEditMode(EditMode editMode) { + this.editMode = editMode; + } + + public String getRedirectPage() { + return redirectPage; + } + + public void setRedirectPage(String redirectPage) { + this.redirectPage = redirectPage; + } + + public String getInputPassword() { + return inputPassword; + } + + public void setInputPassword(String inputPassword) { + this.inputPassword = inputPassword; + } + + public String getCurrentPassword() { + return currentPassword; + } + + public void setCurrentPassword(String currentPassword) { + this.currentPassword = currentPassword; + } + + public Long getDataverseId() { + + if (dataverseId == null) { + dataverseId = dataverseService.findRootDataverse().getId(); + } + return dataverseId; + } + + public void setDataverseId(Long dataverseId) { + this.dataverseId = dataverseId; + } + + public List getNotificationsList() { + return notificationsList; + } + + public void setNotificationsList(List notificationsList) { + this.notificationsList = notificationsList; + } + + public int getActiveIndex() { + return activeIndex; + } + + public void setActiveIndex(int activeIndex) { + this.activeIndex = activeIndex; + } + + public String getSelectTab() { + return selectTab; + } + + public void setSelectTab(String selectTab) { + this.selectTab = selectTab; + } + + public UIInput getUsernameField() { + return usernameField; + } + + public void setUsernameField(UIInput usernameField) { + this.usernameField = usernameField; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; } -} +} \ No newline at end of file diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/PasswordEncryption.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/PasswordEncryption.java index 5eeddf97ca2..4e268e7e225 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/PasswordEncryption.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/PasswordEncryption.java @@ -63,7 +63,12 @@ public String encrypt(String plainText) { @Override public boolean check(String plainText, String hashed) { - return BCrypt.checkpw(plainText, hashed); + try { + return BCrypt.checkpw(plainText, hashed); + } catch (java.lang.IllegalArgumentException iae ) { + // the password was probably not hashed using bcrypt. + return false; + } } }; @@ -78,6 +83,9 @@ public boolean check(String plainText, String hashed) { */ private PasswordEncryption() {} + /** + * @return The current version of the password hashing algorithm. + */ public static Algorithm get() { return getVersion( getLatestVersionNumber() ); } diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/AbstractOAuth2AuthenticationProvider.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/AbstractOAuth2AuthenticationProvider.java index 5321d466560..4f9565c8eae 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/AbstractOAuth2AuthenticationProvider.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/AbstractOAuth2AuthenticationProvider.java @@ -10,8 +10,6 @@ import edu.harvard.iq.dataverse.authorization.AuthenticatedUserDisplayInfo; import edu.harvard.iq.dataverse.authorization.AuthenticationProvider; import edu.harvard.iq.dataverse.authorization.AuthenticationProviderDisplayInfo; -import edu.harvard.iq.dataverse.authorization.AuthenticationRequest; -import edu.harvard.iq.dataverse.authorization.AuthenticationResponse; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; @@ -132,6 +130,18 @@ public OAuth2UserRecord getUserRecord(String code, String state, String redirect } } + @Override + public boolean isUserInfoUpdateAllowed() { + return true; + } + + @Override + public void updateUserInfo(String userIdInProvider, AuthenticatedUserDisplayInfo updatedUserData) { + // ignore - no account info is stored locally. + // We override this to prevent the UnsupportedOperationException thrown by + // the default implementation. + } + @Override public AuthenticationProviderDisplayInfo getInfo() { return new AuthenticationProviderDisplayInfo(getId(), getTitle(), getSubTitle()); diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2FirstLoginPage.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2FirstLoginPage.java index bff26fe991f..2e11d97ab0f 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2FirstLoginPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2FirstLoginPage.java @@ -92,7 +92,7 @@ public String testAction() { } public boolean isEmailAvailable() { - return authenticationSvc.isEmailAvailable(getSelectedEmail()); + return authenticationSvc.isEmailAddressAvailable(getSelectedEmail()); } public boolean isEmailValid() { diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java index 2b259df2611..c23b548ede4 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java @@ -92,11 +92,6 @@ public void exchangeCodeForToken() throws IOException { FacesContext.getCurrentInstance().getExternalContext().redirect("/oauth2/firstLogin.xhtml"); } else { - // update profile - override fields user fills on first login - final AuthenticatedUserDisplayInfo updateInfo = new AuthenticatedUserDisplayInfo(oauthUser.getDisplayInfo()); - updateInfo.setEmailAddress(null); - - dvUser = authenticationSvc.updateAuthenticatedUser(dvUser, updateInfo); // login the user and redirect to HOME. session.setUser(dvUser); FacesContext.getCurrentInstance().getExternalContext().redirect("/"); 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 52700721f0d..8e4253430be 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 @@ -4,7 +4,6 @@ import edu.harvard.iq.dataverse.ValidateEmail; import edu.harvard.iq.dataverse.authorization.AuthenticatedUserDisplayInfo; import edu.harvard.iq.dataverse.authorization.AuthenticatedUserLookup; -import edu.harvard.iq.dataverse.authorization.RoleAssigneeDisplayInfo; import edu.harvard.iq.dataverse.authorization.providers.builtin.BuiltinAuthenticationProvider; import static edu.harvard.iq.dataverse.util.StringUtil.nonEmpty; import java.io.Serializable; @@ -97,8 +96,6 @@ public String getIdentifier() { return IDENTIFIER_PREFIX + userIdentifier; } - - @OneToMany(mappedBy = "user", cascade={CascadeType.REMOVE, CascadeType.MERGE, CascadeType.PERSIST}) private List datasetLocks; @@ -111,7 +108,7 @@ public void setDatasetLocks(List datasetLocks) { } @Override - public RoleAssigneeDisplayInfo getDisplayInfo() { + public AuthenticatedUserDisplayInfo getDisplayInfo() { return new AuthenticatedUserDisplayInfo(firstName, lastName, email, affiliation, position); } @@ -217,15 +214,6 @@ public void setModificationTime(Timestamp modificationTime) { this.modificationTime = modificationTime; } - public boolean isBuiltInUser() { - String authProviderString = authenticatedUserLookup.getAuthenticationProviderId(); - if (authProviderString != null && authProviderString.equals(BuiltinAuthenticationProvider.PROVIDER_ID)) { - return true; - } - - return false; - } - @OneToOne(mappedBy = "authenticatedUser") private AuthenticatedUserLookup authenticatedUserLookup; diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/users/GuestUser.java b/src/main/java/edu/harvard/iq/dataverse/authorization/users/GuestUser.java index b2fd3194f70..f16fa5afe36 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/users/GuestUser.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/users/GuestUser.java @@ -28,11 +28,6 @@ public RoleAssigneeDisplayInfo getDisplayInfo() { @Override public boolean isAuthenticated() { return false; } - @Override - public boolean isBuiltInUser(){ - return false; - } - @Override public boolean isSuperuser() { return false; diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/users/PrivateUrlUser.java b/src/main/java/edu/harvard/iq/dataverse/authorization/users/PrivateUrlUser.java index 88e1f8ffc83..59c3240fdfa 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/users/PrivateUrlUser.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/users/PrivateUrlUser.java @@ -30,21 +30,18 @@ public long getDatasetId() { } /** - * @return By always returning false for isAuthenticated(), we prevent a + * By always returning false for isAuthenticated(), we prevent a * name from appearing in the corner as well as preventing an account page * and MyData from being accessible. The user can still navigate to the home * page but can only see published datasets. + * + * @return {@code false}. */ @Override public boolean isAuthenticated() { return false; } - @Override - public boolean isBuiltInUser() { - return false; - } - @Override public boolean isSuperuser() { return false; diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/users/User.java b/src/main/java/edu/harvard/iq/dataverse/authorization/users/User.java index 29863361a30..ea35f87d178 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/users/User.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/users/User.java @@ -12,11 +12,6 @@ public interface User extends RoleAssignee, Serializable { public boolean isAuthenticated(); - // TODO remove this, should be handled in a more generic fashion, - // e.g. getUserProvider and get the provider's URL from there. This - // would allow Shib-based editing as well. - public boolean isBuiltInUser(); - public boolean isSuperuser(); } diff --git a/src/main/java/edu/harvard/iq/dataverse/confirmemail/ConfirmEmailServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/confirmemail/ConfirmEmailServiceBean.java index e21ba1f98b3..bbe520682e3 100644 --- a/src/main/java/edu/harvard/iq/dataverse/confirmemail/ConfirmEmailServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/confirmemail/ConfirmEmailServiceBean.java @@ -93,13 +93,12 @@ private ConfirmEmailInitResponse sendConfirm(AuthenticatedUser aUser, boolean se * change. */ private void sendLinkOnEmailChange(AuthenticatedUser aUser, String confirmationUrl) throws ConfirmEmailException { - ConfirmEmailUtil confirmEmailUtil = new ConfirmEmailUtil(); String messageBody = BundleUtil.getStringFromBundle("notification.email.changeEmail", Arrays.asList( aUser.getFirstName(), confirmationUrl, - confirmEmailUtil.friendlyExpirationTime(systemConfig.getMinutesUntilConfirmEmailTokenExpires()) + ConfirmEmailUtil.friendlyExpirationTime(systemConfig.getMinutesUntilConfirmEmailTokenExpires()) )); - logger.fine("messageBody:" + messageBody); + logger.log(Level.FINE, "messageBody:{0}", messageBody); try { String toAddress = aUser.getEmail(); @@ -220,7 +219,6 @@ public ConfirmEmailData createToken(AuthenticatedUser au) { } public String optionalConfirmEmailAddonMsg(AuthenticatedUser user) { - ConfirmEmailUtil confirmEmailUtil = new ConfirmEmailUtil(); final String emptyString = ""; if (user == null) { logger.info("Can't return confirm email message. AuthenticatedUser was null!"); @@ -235,7 +233,7 @@ public String optionalConfirmEmailAddonMsg(AuthenticatedUser user) { logger.info("Can't return confirm email message. No ConfirmEmailData for user id " + user.getId()); return emptyString; } - String expTime = confirmEmailUtil.friendlyExpirationTime(systemConfig.getMinutesUntilConfirmEmailTokenExpires()); + String expTime = ConfirmEmailUtil.friendlyExpirationTime(systemConfig.getMinutesUntilConfirmEmailTokenExpires()); String confirmEmailUrl = systemConfig.getDataverseSiteUrl() + "/confirmemail.xhtml?token=" + confirmEmailData.getToken(); List args = Arrays.asList(confirmEmailUrl, expTime); String optionalConfirmEmailMsg = BundleUtil.getStringFromBundle("notification.email.welcomeConfirmEmailAddOn", args); diff --git a/src/main/java/edu/harvard/iq/dataverse/confirmemail/ConfirmEmailUtil.java b/src/main/java/edu/harvard/iq/dataverse/confirmemail/ConfirmEmailUtil.java index d2f84b956df..5f0fcd0433b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/confirmemail/ConfirmEmailUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/confirmemail/ConfirmEmailUtil.java @@ -2,17 +2,22 @@ import edu.harvard.iq.dataverse.util.BundleUtil; import java.sql.Timestamp; -import java.util.Date; public class ConfirmEmailUtil { - + + private ConfirmEmailUtil(){ + // prevent instance creation, this class has only static methods anyway. + } + + private static final Timestamp GRANDFATHERED_TIME = Timestamp.valueOf("2000-01-01 00:00:00.0"); + + /** + * Currently set to Y2K as an easter egg to easily set apart + * grandfathered accounts from post-launch accounts. + * @return + */ public static Timestamp getGrandfatheredTime() { - /** - * Currently set to Y2K as an easter egg to easily set apart - * grandfathered accounts from post-launch accounts. - */ - Timestamp grandfatheredTime = Timestamp.valueOf("2000-01-01 00:00:00.0"); - return grandfatheredTime; + return GRANDFATHERED_TIME; } public static String friendlyExpirationTime(int expirationInt) { diff --git a/src/main/webapp/dataverseuser.xhtml b/src/main/webapp/dataverseuser.xhtml index 30c726e2e45..f8f3b213cda 100644 --- a/src/main/webapp/dataverseuser.xhtml +++ b/src/main/webapp/dataverseuser.xhtml @@ -28,25 +28,26 @@
-
+
- +
- +
@@ -515,7 +516,7 @@
- +
@@ -526,8 +527,11 @@
- - + +
diff --git a/src/test/java/edu/harvard/iq/dataverse/authorization/providers/builtin/BuiltinAuthenticationProviderTest.java b/src/test/java/edu/harvard/iq/dataverse/authorization/providers/builtin/BuiltinAuthenticationProviderTest.java new file mode 100644 index 00000000000..6a1283d844d --- /dev/null +++ b/src/test/java/edu/harvard/iq/dataverse/authorization/providers/builtin/BuiltinAuthenticationProviderTest.java @@ -0,0 +1,115 @@ +package edu.harvard.iq.dataverse.authorization.providers.builtin; + +import edu.harvard.iq.dataverse.authorization.AuthenticatedUserDisplayInfo; +import edu.harvard.iq.dataverse.mocks.MockBuiltinUserServiceBean; +import org.junit.Test; +import static org.junit.Assert.*; +import org.junit.Before; + +/** + * + * @author michael + */ +public class BuiltinAuthenticationProviderTest { + + BuiltinAuthenticationProvider sut = null; + MockBuiltinUserServiceBean bean = null; + + @Before + public void setup() { + bean = new MockBuiltinUserServiceBean(); + sut = new BuiltinAuthenticationProvider(bean); + } + + /** + * Test of getId method, of class BuiltinAuthenticationProvider. + */ + @Test + public void testGetId() { + assertEquals("builtin", sut.getId()); + } + + /** + * Test of getInfo method, of class BuiltinAuthenticationProvider. + */ + @Test + public void testGetInfo() { + String expResult = "builtin"; + String result = sut.getInfo().getId(); + assertEquals(expResult, result); + } + + /** + * Test of isPasswordUpdateAllowed method, of class BuiltinAuthenticationProvider. + */ + @Test + public void testIsPasswordUpdateAllowed() { + assertTrue( sut.isPasswordUpdateAllowed() ); + } + + /** + * Test of isUserInfoUpdateAllowed method, of class BuiltinAuthenticationProvider. + */ + @Test + public void testIsUserInfoUpdateAllowed() { + assertTrue( sut.isUserInfoUpdateAllowed() ); + } + + /** + * Test of isUserDeletionAllowed method, of class BuiltinAuthenticationProvider. + */ + @Test + public void testIsUserDeletionAllowed() { + assertTrue( sut.isUserDeletionAllowed() ); + } + + /** + * Test of deleteUser method, of class BuiltinAuthenticationProvider. + */ + @Test + public void testDeleteUser() { + BuiltinUser u = makeBuiltInUser(); + assertTrue( bean.users.isEmpty() ); + bean.save(u); + assertFalse( bean.users.isEmpty() ); + + sut.deleteUser( u.getUserName() ); + + assertTrue( bean.users.isEmpty() ); + } + + /** + * Test of updatePassword method, of class BuiltinAuthenticationProvider. + */ + @Test + public void testUpdatePassword() { + BuiltinUser user = bean.save(makeBuiltInUser()); + final String newPassword = "newPassword"; + assertFalse( sut.verifyPassword(user.getUserName(), newPassword) ); + sut.updatePassword(user.getUserName(), newPassword); + assertTrue( sut.verifyPassword(user.getUserName(), newPassword)); + } + + /** + * Test of updateUserInfo method, of class BuiltinAuthenticationProvider. + */ + @Test + public void testUpdateUserInfo() { + BuiltinUser user = bean.save(makeBuiltInUser()); + AuthenticatedUserDisplayInfo newInfo = new AuthenticatedUserDisplayInfo("nf", "nl", "ema@il.com", "newAffi", "newPos"); + sut.updateUserInfo(user.getUserName(), newInfo); + assertEquals( newInfo, user.getDisplayInfo() ); + } + + private BuiltinUser makeBuiltInUser() { + BuiltinUser user = new BuiltinUser(); + user.setFirstName("Firsty"); + user.setLastName("Last"); + user.setEmail("email@host.com"); + user.setAffiliation("an institute"); + user.setPosition("a position"); + user.updateEncryptedPassword("password", PasswordEncryption.getLatestVersionNumber()); + return user; + } + +} diff --git a/src/test/java/edu/harvard/iq/dataverse/confirmemail/ConfirmEmailUtilTest.java b/src/test/java/edu/harvard/iq/dataverse/confirmemail/ConfirmEmailUtilTest.java index 5a2a13bbbd9..78d64130e86 100644 --- a/src/test/java/edu/harvard/iq/dataverse/confirmemail/ConfirmEmailUtilTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/confirmemail/ConfirmEmailUtilTest.java @@ -8,34 +8,32 @@ public class ConfirmEmailUtilTest { @Test public void testFriendlyExpirationTime() { - ConfirmEmailUtil confirmEmailUtil = new ConfirmEmailUtil(); System.out.println("Friendly expiration timestamp / measurement test"); - System.out.println("1440 Minutes: " + confirmEmailUtil.friendlyExpirationTime(1440)); + System.out.println("1440 Minutes: " + ConfirmEmailUtil.friendlyExpirationTime(1440)); assertEquals("24 hours", ConfirmEmailUtil.friendlyExpirationTime(1440)); - System.out.println("60 Minutes: " + confirmEmailUtil.friendlyExpirationTime(60)); + System.out.println("60 Minutes: " + ConfirmEmailUtil.friendlyExpirationTime(60)); assertEquals("1 hour", ConfirmEmailUtil.friendlyExpirationTime(60)); - System.out.println("30 Minutes: " + confirmEmailUtil.friendlyExpirationTime(30)); + System.out.println("30 Minutes: " + ConfirmEmailUtil.friendlyExpirationTime(30)); assertEquals("30 minutes", ConfirmEmailUtil.friendlyExpirationTime(30)); - System.out.println("90 Minutes: " + confirmEmailUtil.friendlyExpirationTime(90)); - assertEquals("1.5 hours", confirmEmailUtil.friendlyExpirationTime(90)); - System.out.println("2880 minutes: " + confirmEmailUtil.friendlyExpirationTime(2880)); - assertEquals("48 hours", confirmEmailUtil.friendlyExpirationTime(2880)); - System.out.println("150 minutes: " + confirmEmailUtil.friendlyExpirationTime(150)); - assertEquals("2.5 hours", confirmEmailUtil.friendlyExpirationTime(150)); - System.out.println("165 minutes: " + confirmEmailUtil.friendlyExpirationTime(165)); - assertEquals("2.75 hours", confirmEmailUtil.friendlyExpirationTime(165)); - System.out.println("1 Minute: " + confirmEmailUtil.friendlyExpirationTime(1)); - assertEquals("1 minute", confirmEmailUtil.friendlyExpirationTime(1)); + System.out.println("90 Minutes: " + ConfirmEmailUtil.friendlyExpirationTime(90)); + assertEquals("1.5 hours", ConfirmEmailUtil.friendlyExpirationTime(90)); + System.out.println("2880 minutes: " + ConfirmEmailUtil.friendlyExpirationTime(2880)); + assertEquals("48 hours", ConfirmEmailUtil.friendlyExpirationTime(2880)); + System.out.println("150 minutes: " + ConfirmEmailUtil.friendlyExpirationTime(150)); + assertEquals("2.5 hours", ConfirmEmailUtil.friendlyExpirationTime(150)); + System.out.println("165 minutes: " + ConfirmEmailUtil.friendlyExpirationTime(165)); + assertEquals("2.75 hours", ConfirmEmailUtil.friendlyExpirationTime(165)); + System.out.println("1 Minute: " + ConfirmEmailUtil.friendlyExpirationTime(1)); + assertEquals("1 minute", ConfirmEmailUtil.friendlyExpirationTime(1)); System.out.println(); } @Test public void testGrandfatheredTime() { - ConfirmEmailUtil confirmEmailUtil = new ConfirmEmailUtil(); System.out.println(); System.out.println("Grandfathered account timestamp test"); - System.out.println("Grandfathered Time (y2k): " + confirmEmailUtil.getGrandfatheredTime()); - assertEquals(Timestamp.valueOf("2000-01-01 00:00:00.0"), confirmEmailUtil.getGrandfatheredTime()); + System.out.println("Grandfathered Time (y2k): " + ConfirmEmailUtil.getGrandfatheredTime()); + assertEquals(Timestamp.valueOf("2000-01-01 00:00:00.0"), ConfirmEmailUtil.getGrandfatheredTime()); System.out.println(); } diff --git a/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/CreatePrivateUrlCommandTest.java b/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/CreatePrivateUrlCommandTest.java index a71ad732d8d..348f3e7b2dd 100644 --- a/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/CreatePrivateUrlCommandTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/CreatePrivateUrlCommandTest.java @@ -152,7 +152,6 @@ public void testCreatePrivateUrlSuccessfully() throws CommandException { PrivateUrlUser expectedUser = new PrivateUrlUser(dataset.getId()); assertEquals(expectedUser.getIdentifier(), privateUrl.getRoleAssignment().getAssigneeIdentifier()); assertEquals(expectedUser.isSuperuser(), false); - assertEquals(expectedUser.isBuiltInUser(), false); assertEquals(expectedUser.isAuthenticated(), false); assertEquals(expectedUser.getDisplayInfo().getTitle(), "Private URL Enabled"); assertNotNull(privateUrl.getToken()); diff --git a/src/test/java/edu/harvard/iq/dataverse/mocks/MockBuiltinUserServiceBean.java b/src/test/java/edu/harvard/iq/dataverse/mocks/MockBuiltinUserServiceBean.java new file mode 100644 index 00000000000..36e223da9e2 --- /dev/null +++ b/src/test/java/edu/harvard/iq/dataverse/mocks/MockBuiltinUserServiceBean.java @@ -0,0 +1,37 @@ +package edu.harvard.iq.dataverse.mocks; + +import edu.harvard.iq.dataverse.authorization.providers.builtin.BuiltinUser; +import edu.harvard.iq.dataverse.authorization.providers.builtin.BuiltinUserServiceBean; +import java.util.HashMap; +import java.util.Map; + +/** + * + * @author michael + */ +public class MockBuiltinUserServiceBean extends BuiltinUserServiceBean { + + public final Map users = new HashMap<>(); + + @Override + public BuiltinUser findByUserName(String userName) { + return users.get(userName); + } + + @Override + public BuiltinUser save(BuiltinUser aUser) { + if ( aUser.getId() == null ) { + aUser.setId( MocksFactory.nextId() ); + } + users.put( aUser.getUserName(), aUser ); + return aUser; + } + + @Override + public void removeUser(String userName) { + users.remove(userName); + } + + + +} diff --git a/src/test/java/edu/harvard/iq/dataverse/mocks/MocksFactory.java b/src/test/java/edu/harvard/iq/dataverse/mocks/MocksFactory.java index 82730f92bbf..142e1f5a1bf 100644 --- a/src/test/java/edu/harvard/iq/dataverse/mocks/MocksFactory.java +++ b/src/test/java/edu/harvard/iq/dataverse/mocks/MocksFactory.java @@ -146,10 +146,10 @@ public static Dataset makeDataset() { final List metadatas = new ArrayList<>(10); final List categories = ds.getCategories(); Random rand = new Random(); - for ( DataFile df : files ) { + files.forEach( df ->{ df.getFileMetadata().addCategory(categories.get(rand.nextInt(categories.size()))); metadatas.add( df.getFileMetadata() ); - } + }); ds.setFiles(files); final DatasetVersion initialVersion = ds.getVersions().get(0); initialVersion.setFileMetadatas(metadatas); @@ -171,10 +171,10 @@ public static DatasetVersion makeDatasetVersion(List categorie final List files = makeFiles(10); final List metadatas = new ArrayList<>(10); Random rand = new Random(); - for ( DataFile df : files ) { + files.forEach(df -> { df.getFileMetadata().addCategory(categories.get(rand.nextInt(categories.size()))); metadatas.add( df.getFileMetadata() ); - } + }); retVal.setFileMetadatas(metadatas); List fields = new ArrayList<>();