From 9673e820ba300f3b5ee88760f0a13934dfb90627 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Thu, 28 May 2020 16:41:51 +0200 Subject: [PATCH 01/18] Refactor ConfirmEmailService to offer verification checking and start to use in some places. #6936 --- .../authorization/AuthenticationServiceBean.java | 1 + .../providers/builtin/DataverseUserPage.java | 6 +++--- .../confirmemail/ConfirmEmailServiceBean.java | 15 ++++++++++++++- .../command/impl/MergeInAccountCommand.java | 1 + 4 files changed, 19 insertions(+), 4 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 59e79d41841..5e12eab54e8 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java @@ -293,6 +293,7 @@ public void deleteAuthenticatedUser(Object pk) { if (apiToken != null) { em.remove(apiToken); } + // @todo: this should be handed down to the service instead of doing it here. ConfirmEmailData confirmEmailData = confirmEmailService.findSingleConfirmEmailDataByUser(user); if (confirmEmailData != null) { /** diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/DataverseUserPage.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/DataverseUserPage.java index 006056de348..c66085f9976 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/DataverseUserPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/DataverseUserPage.java @@ -546,6 +546,7 @@ public void sendConfirmEmail() { /** * Determines whether the button to send a verification email appears on user page + * TODO: cant this be refactored to use confirmEmailService.hasVerifiedEmail(currentUser) ? * @return */ public boolean showVerifyEmailButton() { @@ -557,12 +558,11 @@ public boolean showVerifyEmailButton() { } public boolean isEmailIsVerified() { - - return currentUser.getEmailConfirmed() != null && confirmEmailService.findSingleConfirmEmailDataByUser(currentUser) == null; + return confirmEmailService.hasVerifiedEmail(currentUser); } public boolean isEmailNotVerified() { - return currentUser.getEmailConfirmed() == null || confirmEmailService.findSingleConfirmEmailDataByUser(currentUser) != null; + return !confirmEmailService.hasVerifiedEmail(currentUser); } public boolean isEmailGrandfathered() { 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 b45bc6190ca..f96a53b5836 100644 --- a/src/main/java/edu/harvard/iq/dataverse/confirmemail/ConfirmEmailServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/confirmemail/ConfirmEmailServiceBean.java @@ -34,7 +34,7 @@ public class ConfirmEmailServiceBean { private static final Logger logger = Logger.getLogger(ConfirmEmailServiceBean.class.getCanonicalName()); @EJB - AuthenticationServiceBean dataverseUserService; + AuthenticationServiceBean authenticationService; @EJB MailServiceBean mailService; @@ -46,6 +46,19 @@ public class ConfirmEmailServiceBean { @PersistenceContext(unitName = "VDCNet-ejbPU") private EntityManager em; + + /** + * A simple interface to check if a user email has been verified or not. + * @param user + * @return true if verified, false otherwise + */ + public boolean hasVerifiedEmail(AuthenticatedUser user) { + boolean hasTimestamp = user.getEmailConfirmed() != null; + boolean hasNoStaleVerificationTokens = this.findSingleConfirmEmailDataByUser(user) == null; + boolean isVerifiedByAuthProvider = authenticationService.lookupProvider(user).isEmailVerified(); + + return (hasTimestamp && hasNoStaleVerificationTokens) || isVerifiedByAuthProvider; + } /** * Initiate the email confirmation process. diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/MergeInAccountCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/MergeInAccountCommand.java index bf542138044..28db9b890e9 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/MergeInAccountCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/MergeInAccountCommand.java @@ -155,6 +155,7 @@ protected void executeImpl(CommandContext ctxt) throws CommandException { //ConfirmEmailData + // todo: the deletion should be handed down to the service! ConfirmEmailData confirmEmailData = ctxt.confirmEmail().findSingleConfirmEmailDataByUser(consumedAU); if (confirmEmailData != null){ ctxt.em().remove(confirmEmailData); From 45cb9a7cacd12fd47a15e937fb4731eb992e0962 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Thu, 28 May 2020 16:42:48 +0200 Subject: [PATCH 02/18] Add a very basic first implementation of the mail domain group infrastructure. Still lacking tests, API endpoints etc. #6936 --- .../impl/maildomain/MailDomainGroup.java | 118 +++++++++++++++++ .../maildomain/MailDomainGroupProvider.java | 119 ++++++++++++++++++ .../MailDomainGroupServiceBean.java | 114 +++++++++++++++++ 3 files changed, 351 insertions(+) create mode 100644 src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroup.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupProvider.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupServiceBean.java diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroup.java b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroup.java new file mode 100644 index 00000000000..62855b777a8 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroup.java @@ -0,0 +1,118 @@ +package edu.harvard.iq.dataverse.authorization.groups.impl.maildomain; + +import edu.harvard.iq.dataverse.authorization.groups.impl.PersistedGlobalGroup; +import edu.harvard.iq.dataverse.engine.command.DataverseRequest; +import org.hibernate.validator.constraints.NotEmpty; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import javax.persistence.Entity; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Transient; +; + +/** + * A group that explicitly lists email address domains that a user might have to belong to this group. + */ +@NamedQueries({ + @NamedQuery(name="MailDomainGroup.findAll", + query="SELECT g FROM MailDomainGroup g"), + @NamedQuery(name="MailDomainGroup.findByPersistedGroupAlias", + query="SELECT g FROM MailDomainGroup g WHERE g.persistedGroupAlias=:persistedGroupAlias"), +}) +@Entity +public class MailDomainGroup extends PersistedGlobalGroup { + + /** + * All the email address domains that make users belong + * to this group, concatenated with ";" (thus avoiding another relation) + */ + @NotEmpty + private String emailDomains; + + @Transient + private MailDomainGroupProvider provider; + + /** + * Empty Constructor for JPA. + */ + protected MailDomainGroup() {} + + public MailDomainGroup(MailDomainGroupProvider prv) { + provider = prv; + } + + public void setEmailDomains(String domains) { + this.emailDomains = domains; + } + public void setEmailDomains(List domains) { + this.emailDomains = String.join(";", domains); + } + + public String getEmailDomains() { + return this.emailDomains; + } + public List getEmailDomainsAsList() { + return Arrays.asList(this.emailDomains.split(";")); + } + + @Override + public MailDomainGroupProvider getGroupProvider() { + return provider; + } + public void setGroupProvider(MailDomainGroupProvider pvd) { + this.provider = pvd; + } + + /** + * This will throw an UnsupportOperationException if called. + * Due to the necessity of checking if the mail adress is confirmed, + * this cannot be decided in the entity class, but has to happen in the + * provider. + * @param aRequest The request whose inclusion we test + */ + @Override + public boolean contains(DataverseRequest aRequest) { + throw new UnsupportedOperationException("This cannot be supported."); + } + + /** + * This will throw an UnsupportOperationException if called. + * We will not allow edits of this via UI due to it's static nature. Can be changed via API. + */ + @Override + public boolean isEditable() { + throw new UnsupportedOperationException("This is not supported."); + } + + @Override + public int hashCode() { + int hash = 7; + hash = 53 * hash + Objects.hashCode(this.getId()); + hash = 53 * hash + Objects.hashCode(this.emailDomains); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if ( ! (obj instanceof MailDomainGroup)) { + return false; + } + final MailDomainGroup other = (MailDomainGroup) obj; + if ( this.getId() != null && other.getId() != null) { + return Objects.equals(this.getId(), other.getId()); + } else { + return Objects.equals(this.emailDomains, other.getEmailDomains()); + } + } + + @Override + public String toString() { + return "[MailDomainGroup " + this.getPersistedGroupAlias() + "]"; + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupProvider.java b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupProvider.java new file mode 100644 index 00000000000..4563d6dd623 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupProvider.java @@ -0,0 +1,119 @@ +package edu.harvard.iq.dataverse.authorization.groups.impl.maildomain; + +import edu.harvard.iq.dataverse.DvObject; +import edu.harvard.iq.dataverse.authorization.RoleAssignee; +import edu.harvard.iq.dataverse.authorization.groups.GroupProvider; +import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; +import edu.harvard.iq.dataverse.engine.command.DataverseRequest; + +import java.util.*; +import java.util.logging.Logger; + +/** + * Creates and manages mail domain based groups + */ +public class MailDomainGroupProvider implements GroupProvider { + + private static final Logger logger = Logger.getLogger(MailDomainGroupProvider.class.getName()); + + private final MailDomainGroupServiceBean emailGroupSvc; + + public MailDomainGroupProvider(MailDomainGroupServiceBean emailGroupSvc) { + this.emailGroupSvc = emailGroupSvc; + } + + @Override + public String getGroupProviderAlias() { + return "maildomain"; + } + + @Override + public String getGroupProviderInfo() { + return "Groups users by their email address domain."; + } + + /** + * This method is meant for group management. This is a global, unmanageable thing, so this will just result in an empty set. + * @return empty set + */ + @Override + public Set groupsFor(RoleAssignee ra, DvObject o) { + return Collections.emptySet(); + } + + /** + * This method is meant for group management. This is a global, unmanageable thing, so this will just result in an empty set. + * @return empty set + */ + @Override + public Set groupsFor(RoleAssignee ra) { + return Collections.emptySet(); + } + + /** + * Lookup for a request. We don't need the context, so just move to groupsFor(DataverseRequest) + * @param req The request whose group memberships we evaluate. + * @param dvo the DvObject which is the context for the groups. May be {@code null}. + * @return A set of groups, if any. + */ + @Override + public Set groupsFor(DataverseRequest req, DvObject dvo ) { + return groupsFor(req); + } + + /** + * Lookup the user group on each request by using the email address domain. + * @param req + * @return + */ + @Override + public Set groupsFor(DataverseRequest req) { + AuthenticatedUser user = req.getAuthenticatedUser(); + if ( user != null ) { + return updateProvider(emailGroupSvc.findAllWithDomain(user) ); + } else { + return Collections.emptySet(); + } + } + + @Override + public MailDomainGroup get(String groupAlias) { + return updateProvider(emailGroupSvc.findByAlias(groupAlias).orElse(null)); + } + + /** + * Find all email groups available + * @return empty set. + */ + @Override + public Set findGlobalGroups() { + return updateProvider( new HashSet<>(emailGroupSvc.findAll()) ); + } + + /** + * Sets the provider of the passed explicit group to {@code this}. + * @param eg the collection + * @return the passed group, updated. + */ + MailDomainGroup updateProvider(MailDomainGroup eg ) { + if (eg == null) { + return null; + } + eg.setGroupProvider(this); + return eg; + } + + /** + * Sets the provider of the explicit groups to {@code this}. + * @param Collection's type + * @param mdgs the collection + * @return the collection, with all the groups updated. + */ + > T updateProvider(T mdgs ) { + for ( MailDomainGroup mdg : mdgs ) { + updateProvider(mdg); + } + return mdgs; + } +} + diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupServiceBean.java new file mode 100644 index 00000000000..c0e3ffb267b --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupServiceBean.java @@ -0,0 +1,114 @@ +package edu.harvard.iq.dataverse.authorization.groups.impl.maildomain; + +import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; +import edu.harvard.iq.dataverse.confirmemail.ConfirmEmailServiceBean; + +import java.util.*; +import java.util.logging.Logger; +import javax.annotation.PostConstruct; +import javax.ejb.Stateless; +import javax.inject.Inject; +import javax.inject.Named; +import javax.persistence.EntityManager; +import javax.persistence.NoResultException; +import javax.persistence.PersistenceContext; + +/** + * A bean providing the {@link MailDomainGroupProvider}s with container services, such as database connectivity. + * Also containing the business logic to decide about matching groups. + */ +@Named +@Stateless +public class MailDomainGroupServiceBean { + + private static final Logger logger = Logger.getLogger(edu.harvard.iq.dataverse.authorization.groups.impl.explicit.ExplicitGroupServiceBean.class.getName()); + + @PersistenceContext(unitName = "VDCNet-ejbPU") + protected EntityManager em; + + @Inject + ConfirmEmailServiceBean confirmEmailSvc; + + MailDomainGroupProvider provider; + + @PostConstruct + void setup() { + provider = new MailDomainGroupProvider(this); + } + + public MailDomainGroupProvider getProvider() { + return provider; + } + + /** + * Find groups for users mail when email has a domain and is verified + * @param user + * @return + */ + public Set findAllWithDomain(AuthenticatedUser user) { + + // if the mail address is not verified, escape... + if (!confirmEmailSvc.hasVerifiedEmail(user)) { return Collections.emptySet(); } + + // otherwise start to bisect the mail and lookup groups. + Optional oDomain = getDomainFromMail(user.getEmail()); + if ( oDomain.isPresent() ) { + List rs = em.createNamedQuery("MailDomainGroup.findAll", MailDomainGroup.class).getResultList(); + for(MailDomainGroup g : rs) { + if ( g.getEmailDomainsAsList().contains(oDomain.get()) == false ) rs.remove(g); + } + return new HashSet<>(rs); + } + return Collections.emptySet(); + } + + public List findAll() { + return em.createNamedQuery("MailDomainGroup.findAll", MailDomainGroup.class).getResultList(); + } + + Optional findByAlias(String groupAlias) { + try { + return Optional.of( + em.createNamedQuery("MailDomainGroup.findByPersistedGroupAlias", MailDomainGroup.class) + .setParameter("persistedGroupAlias", groupAlias) + .getSingleResult()); + } catch ( NoResultException nre ) { + return Optional.empty(); + } + } + + /* + public MailDomainGroup persist( MailDomainGroup g ) { + if ( g.getId() == null ) { + em.persist( g ); + return g; + } else { + // clean stale data once in a while + if ( Math.random() >= 0.5 ) { + Set stale = new TreeSet<>(); + for ( String idtf : g.getContainedRoleAssignees()) { + if ( roleAssigneeSvc.getRoleAssignee(idtf) == null ) { + stale.add(idtf); + } + } + if ( ! stale.isEmpty() ) { + g.getContainedRoleAssignees().removeAll(stale); + } + } + + return em.merge( g ); + } + } + */ + + public void removeGroup(MailDomainGroup mailDomainGroup) { + em.remove( mailDomainGroup ); + } + + Optional getDomainFromMail(String email) { + String[] parts = email.split("@"); + if (parts.length < 2) { return Optional.empty(); } + return Optional.of(parts[parts.length-1]); + } + +} From 1a6b6807da458f15e3cb20dde91f919d93789faa Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Fri, 29 May 2020 09:20:06 +0200 Subject: [PATCH 03/18] Make domain comparison use lowercase. Add some more JavaDocs. #6936 --- .../MailDomainGroupServiceBean.java | 32 ++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupServiceBean.java index c0e3ffb267b..0ead6d90157 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupServiceBean.java @@ -3,7 +3,11 @@ import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.confirmemail.ConfirmEmailServiceBean; -import java.util.*; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; import java.util.logging.Logger; import javax.annotation.PostConstruct; import javax.ejb.Stateless; @@ -41,9 +45,9 @@ public MailDomainGroupProvider getProvider() { } /** - * Find groups for users mail when email has a domain and is verified + * Find groups for users mail address. Only done when email has been verified. * @param user - * @return + * @return A collection of groups with matching email domains */ public Set findAllWithDomain(AuthenticatedUser user) { @@ -51,21 +55,36 @@ public Set findAllWithDomain(AuthenticatedUser user) { if (!confirmEmailSvc.hasVerifiedEmail(user)) { return Collections.emptySet(); } // otherwise start to bisect the mail and lookup groups. + // NOTE: the email from the user has been validated via {@link EMailValidator} when persisted. Optional oDomain = getDomainFromMail(user.getEmail()); if ( oDomain.isPresent() ) { + // transform to lowercase, in case someone uses uppercase letters. (we store the comparison values in lowercase) + String domain = oDomain.get().toLowerCase(); + + // get all groups and filter List rs = em.createNamedQuery("MailDomainGroup.findAll", MailDomainGroup.class).getResultList(); for(MailDomainGroup g : rs) { - if ( g.getEmailDomainsAsList().contains(oDomain.get()) == false ) rs.remove(g); + if ( g.getEmailDomainsAsList().contains(domain) == false ) rs.remove(g); } + return new HashSet<>(rs); } return Collections.emptySet(); } + /** + * Get all mail domain groups from the database. + * @return A result list from the database. May be null if no results found. + */ public List findAll() { return em.createNamedQuery("MailDomainGroup.findAll", MailDomainGroup.class).getResultList(); } + /** + * Find a specific mail domain group by it's alias. + * @param groupAlias + * @return + */ Optional findByAlias(String groupAlias) { try { return Optional.of( @@ -105,6 +124,11 @@ public void removeGroup(MailDomainGroup mailDomainGroup) { em.remove( mailDomainGroup ); } + /** + * Retrieve the domain part only from a given email. + * @param email + * @return Domain part or empty Optional + */ Optional getDomainFromMail(String email) { String[] parts = email.split("@"); if (parts.length < 2) { return Optional.empty(); } From 0d7379c05db3a1f8f7d1240de5bc0ccc8ad73a95 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Fri, 29 May 2020 12:59:26 +0200 Subject: [PATCH 04/18] Add tests for MailDomainGroup and MailDomainGroupProvider. #6936 --- .../impl/maildomain/MailDomainGroup.java | 6 +- .../MailDomainGroupProviderTest.java | 100 ++++++++++++++++++ .../impl/maildomain/MailDomainGroupTest.java | 76 +++++++++++++ 3 files changed, 177 insertions(+), 5 deletions(-) create mode 100644 src/test/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupProviderTest.java create mode 100644 src/test/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupTest.java diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroup.java b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroup.java index 62855b777a8..320641c9833 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroup.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroup.java @@ -40,10 +40,6 @@ public class MailDomainGroup extends PersistedGlobalGroup { */ protected MailDomainGroup() {} - public MailDomainGroup(MailDomainGroupProvider prv) { - provider = prv; - } - public void setEmailDomains(String domains) { this.emailDomains = domains; } @@ -113,6 +109,6 @@ public boolean equals(Object obj) { @Override public String toString() { - return "[MailDomainGroup " + this.getPersistedGroupAlias() + "]"; + return "[MailDomainGroup " + this.getPersistedGroupAlias() + ": " + this.emailDomains + "]"; } } diff --git a/src/test/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupProviderTest.java b/src/test/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupProviderTest.java new file mode 100644 index 00000000000..60d6ae13b01 --- /dev/null +++ b/src/test/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupProviderTest.java @@ -0,0 +1,100 @@ +package edu.harvard.iq.dataverse.authorization.groups.impl.maildomain; + +import edu.harvard.iq.dataverse.Dataset; +import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IPv4Address; +import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; +import edu.harvard.iq.dataverse.engine.command.DataverseRequest; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class MailDomainGroupProviderTest { + + MailDomainGroupServiceBean svc = Mockito.mock(MailDomainGroupServiceBean.class); + + @Test + void testUserMatched() { + // given + MailDomainGroupProvider pvd = new MailDomainGroupProvider(svc); + + MailDomainGroup t = new MailDomainGroup(); + t.setEmailDomains("foobar.com"); + Set set = new HashSet<>(); + set.add(t); + + AuthenticatedUser u = new AuthenticatedUser(); + when(svc.findAllWithDomain(u)).thenReturn(set); + + DataverseRequest req = new DataverseRequest(u, new IPv4Address(192,168,0,1)); + + // when + Set result = pvd.groupsFor(req); + + // then + assertTrue(result.contains(t)); + assertEquals(pvd, t.getGroupProvider()); + } + + @Test + void testUserNotPresent() { + // given + MailDomainGroupProvider pvd = new MailDomainGroupProvider(svc); + AuthenticatedUser u = null; + DataverseRequest req = new DataverseRequest(u, new IPv4Address(192,168,0,1)); + + // when & then + assertEquals(Collections.emptySet(), pvd.groupsFor(req)); + } + + @Test + void testContextIgnored() { + // given + MailDomainGroupProvider pvd = Mockito.spy(new MailDomainGroupProvider(svc)); + AuthenticatedUser u = null; + DataverseRequest req = new DataverseRequest(u, new IPv4Address(192,168,0,1)); + Dataset a = new Dataset(); + + // when + pvd.groupsFor(req, a); + // then + verify(pvd, times(1)).groupsFor(req); + } + + @Test + void testUnsupportedAsEmpty() { + // given + MailDomainGroupProvider pvd = new MailDomainGroupProvider(svc); + + // when & then + assertEquals(Collections.emptySet(), pvd.groupsFor(new AuthenticatedUser(), new Dataset())); + assertEquals(Collections.emptySet(), pvd.groupsFor(new AuthenticatedUser())); + } + + @Test + void testUpdateProvider() { + // given + MailDomainGroupProvider pvd = new MailDomainGroupProvider(svc); + // include a null value to ensure null safety of the functions + List test = Arrays.asList(new MailDomainGroup(), new MailDomainGroup(), null); + for (MailDomainGroup mdg : test) { + if ( mdg != null ) { + assertNull(mdg.getGroupProvider()); + } + } + + // when + pvd.updateProvider(test); + + // then + for (MailDomainGroup mdg : test) { + if ( mdg != null ) { + assertEquals(pvd, mdg.getGroupProvider()); + } + } + + } +} \ No newline at end of file diff --git a/src/test/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupTest.java b/src/test/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupTest.java new file mode 100644 index 00000000000..6b42820e4ec --- /dev/null +++ b/src/test/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupTest.java @@ -0,0 +1,76 @@ +package edu.harvard.iq.dataverse.authorization.groups.impl.maildomain; + +import edu.harvard.iq.dataverse.engine.command.DataverseRequest; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class MailDomainGroupTest { + + DataverseRequest dvr = Mockito.mock(DataverseRequest.class); + + @Test + void testEmailDomains() { + // given + MailDomainGroup mdg = new MailDomainGroup(); + List domains = Arrays.asList("test.de", "foo.com", "bar.com"); + String domainsStr = "test.de;foo.com;bar.com"; + + // when + mdg.setEmailDomains(domainsStr); + // then + assertEquals(domains, mdg.getEmailDomainsAsList()); + assertEquals(domainsStr, mdg.getEmailDomains()); + + // when 2 + mdg.setEmailDomains(domains); + // then 2 + assertEquals(domains, mdg.getEmailDomainsAsList()); + assertEquals(domainsStr, mdg.getEmailDomains()); + } + + @Test + void testUnsupported() { + // given + MailDomainGroup a = new MailDomainGroup(); + // when, then + assertThrows(UnsupportedOperationException.class, () -> { a.isEditable(); } ); + assertThrows(UnsupportedOperationException.class, () -> { a.contains(dvr); } ); + } + + @Test + void testHashCode() { + // given + MailDomainGroup a = new MailDomainGroup(); + a.setEmailDomains("test.de;test2.de"); + MailDomainGroup b = new MailDomainGroup(); + a.setEmailDomains("test3.de;test4.de"); + MailDomainGroup c = new MailDomainGroup(); + c.setEmailDomains("test.de;test2.de"); + + // when & then + assertNotEquals(a, b); + assertNotEquals(a, c); + assertNotEquals(a.hashCode(), b.hashCode()); + assertNotEquals(a.hashCode(), c.hashCode()); + } + + @Test + void testEquals() { + // given + MailDomainGroup a = new MailDomainGroup(); + a.setEmailDomains("test.de;test2.de"); + MailDomainGroup b = new MailDomainGroup(); + b.setEmailDomains("foo.de;bar.de"); + MailDomainGroup c = new MailDomainGroup(); + c.setEmailDomains("test.de;test2.de"); + + // when & then + assertEquals(a, c); + assertNotEquals(a, b); + } +} \ No newline at end of file From 39075d93b943d7dadf6ddc62f85cb620530c4550 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Fri, 29 May 2020 14:26:25 +0200 Subject: [PATCH 05/18] Add generator for MailDomainGroups during testing. #6936 --- .../groups/impl/maildomain/MailDomainGroupTest.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/test/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupTest.java b/src/test/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupTest.java index 6b42820e4ec..d5340066f4a 100644 --- a/src/test/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupTest.java @@ -1,11 +1,13 @@ package edu.harvard.iq.dataverse.authorization.groups.impl.maildomain; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; +import org.apache.commons.lang.RandomStringUtils; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import java.util.Arrays; import java.util.List; +import java.util.Random; import static org.junit.jupiter.api.Assertions.*; @@ -73,4 +75,14 @@ void testEquals() { assertEquals(a, c); assertNotEquals(a, b); } + + static Random rnd = new Random(); + static MailDomainGroup genGroup() { + MailDomainGroup t = new MailDomainGroup(); + t.setId(rnd.nextLong()); + t.setPersistedGroupAlias(RandomStringUtils.randomAlphanumeric(12)); + t.setDisplayName(RandomStringUtils.randomAlphanumeric(12)); + t.setEmailDomains(RandomStringUtils.randomAlphanumeric(5)+"."+RandomStringUtils.randomAlphanumeric(2)+";"+RandomStringUtils.randomAlphanumeric(5)+"."+RandomStringUtils.randomAlphanumeric(3)); + return t; + } } \ No newline at end of file From fa2a5446767ecf19be2bfae08e37f6470c3700c1 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Fri, 29 May 2020 14:27:16 +0200 Subject: [PATCH 06/18] Make MailDomainGroupServiceBean more testable by making the mail bisection function static. #6936 --- .../groups/impl/maildomain/MailDomainGroupServiceBean.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupServiceBean.java index 0ead6d90157..dc97dc080f6 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupServiceBean.java @@ -129,7 +129,7 @@ public void removeGroup(MailDomainGroup mailDomainGroup) { * @param email * @return Domain part or empty Optional */ - Optional getDomainFromMail(String email) { + static Optional getDomainFromMail(String email) { String[] parts = email.split("@"); if (parts.length < 2) { return Optional.empty(); } return Optional.of(parts[parts.length-1]); From 1b5e0e1b05fd3b395d01a48e8e0119435bce61ea Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Fri, 29 May 2020 14:55:42 +0200 Subject: [PATCH 07/18] Add test for MailDomainGroupServiceBean and fix error when filtering matching entries. #6936 --- .../MailDomainGroupServiceBean.java | 11 +- .../MailDomainGroupServiceBeanTest.java | 129 ++++++++++++++++++ 2 files changed, 135 insertions(+), 5 deletions(-) create mode 100644 src/test/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupServiceBeanTest.java diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupServiceBean.java index dc97dc080f6..c7f7bfd5abd 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupServiceBean.java @@ -9,6 +9,8 @@ import java.util.Optional; import java.util.Set; import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.annotation.PostConstruct; import javax.ejb.Stateless; import javax.inject.Inject; @@ -63,11 +65,10 @@ public Set findAllWithDomain(AuthenticatedUser user) { // get all groups and filter List rs = em.createNamedQuery("MailDomainGroup.findAll", MailDomainGroup.class).getResultList(); - for(MailDomainGroup g : rs) { - if ( g.getEmailDomainsAsList().contains(domain) == false ) rs.remove(g); - } - - return new HashSet<>(rs); + + return rs.stream() + .filter(mg -> mg.getEmailDomainsAsList().contains(domain)) + .collect(Collectors.toSet()); } return Collections.emptySet(); } diff --git a/src/test/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupServiceBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupServiceBeanTest.java new file mode 100644 index 00000000000..099008bbe1f --- /dev/null +++ b/src/test/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupServiceBeanTest.java @@ -0,0 +1,129 @@ +package edu.harvard.iq.dataverse.authorization.groups.impl.maildomain; + +import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; +import edu.harvard.iq.dataverse.confirmemail.ConfirmEmailServiceBean; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import javax.persistence.EntityManager; +import javax.persistence.TypedQuery; +import java.util.*; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class MailDomainGroupServiceBeanTest { + + @Mock + ConfirmEmailServiceBean confirmEmailSvc; + @Mock + EntityManager em; + + MailDomainGroupServiceBean svc; + + @BeforeEach + void setup() { + svc = new MailDomainGroupServiceBean(); + svc.em = em; + svc.confirmEmailSvc = confirmEmailSvc; + } + + @Test + void testFindWithVerifiedEmail() { + // given + List db = new ArrayList<>(Arrays.asList(MailDomainGroupTest.genGroup(),MailDomainGroupTest.genGroup(),MailDomainGroupTest.genGroup())); + MailDomainGroup test = MailDomainGroupTest.genGroup(); + test.setEmailDomains("foobar.com"); + db.add(test); + mockQuery("MailDomainGroup.findAll", db); + + AuthenticatedUser u = new AuthenticatedUser(); + u.setEmail("test@foobar.com"); + when(confirmEmailSvc.hasVerifiedEmail(u)).thenReturn(true); + + // when + Set result = svc.findAllWithDomain(u); + + // then + Set expected = new HashSet<>(Arrays.asList(test)); + assertEquals(expected, result); + } + + @Test + void testFindWithUnverifiedEmail() { + // given + AuthenticatedUser u = new AuthenticatedUser(); + u.setEmail("test@foobar.com"); + when(confirmEmailSvc.hasVerifiedEmail(u)).thenReturn(false); + + // when & then + assertEquals(Collections.emptySet(), svc.findAllWithDomain(u)); + } + + // however this case might ever happen... its a branch in the function, we should test it. + @Test + void testFindWithInvalidEmail() { + // given + AuthenticatedUser u = new AuthenticatedUser(); + u.setEmail("testfoobar.com"); + when(confirmEmailSvc.hasVerifiedEmail(u)).thenReturn(true); + + // when & then + assertEquals(Collections.emptySet(), svc.findAllWithDomain(u)); + } + + @Test + void findAll() { + // given + List db = Arrays.asList(MailDomainGroupTest.genGroup(),MailDomainGroupTest.genGroup(),MailDomainGroupTest.genGroup()); + mockQuery("MailDomainGroup.findAll", db); + + // when & then + assertEquals(db, svc.findAll()); + } + + @Test + void findByAlias() { + // given + MailDomainGroup mg = MailDomainGroupTest.genGroup(); + mockQuery("MailDomainGroup.findByPersistedGroupAlias", mg, "persistedGroupAlias", mg.getPersistedGroupAlias()); + + // when & then + assertEquals(Optional.of(mg), svc.findByAlias(mg.getPersistedGroupAlias())); + } + + private static Stream mailExamples() { + return Stream.of( + Arguments.of("foo@bar.com", Optional.of("bar.com")), + Arguments.of("foo@foo@bar.com", Optional.of("bar.com")), + Arguments.of("foobar.com", Optional.empty())); + } + + @ParameterizedTest + @MethodSource("mailExamples") + void getDomainFromMail(String mail, Optional expected) { + assertEquals(expected, MailDomainGroupServiceBean.getDomainFromMail(mail)); + } + + void mockQuery(String name, List results) { + TypedQuery mockedQuery = mock(TypedQuery.class); + when(mockedQuery.getResultList()).thenReturn(results); + when(this.svc.em.createNamedQuery(name, MailDomainGroup.class)).thenReturn(mockedQuery); + } + + void mockQuery(String name, MailDomainGroup result, String parameter, String value) { + TypedQuery mockedQuery = mock(TypedQuery.class); + when(mockedQuery.getSingleResult()).thenReturn(result); + when(mockedQuery.setParameter(parameter, value)).thenReturn(mockedQuery); + when(this.svc.em.createNamedQuery(name, MailDomainGroup.class)).thenReturn(mockedQuery); + } +} \ No newline at end of file From 69a485fb105ecc6d6fefe8fe9e66e11c0008c05c Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Fri, 29 May 2020 15:42:09 +0200 Subject: [PATCH 08/18] Wire the new mail domain group in the GroupServiceBean, so it will be included in group searches. #6936 --- .../authorization/groups/GroupServiceBean.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/GroupServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/GroupServiceBean.java index eb841af508a..98fe3ad18c3 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/GroupServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/GroupServiceBean.java @@ -9,6 +9,8 @@ import edu.harvard.iq.dataverse.authorization.groups.impl.explicit.ExplicitGroupServiceBean; import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.IpGroupProvider; import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.IpGroupsServiceBean; +import edu.harvard.iq.dataverse.authorization.groups.impl.maildomain.MailDomainGroupProvider; +import edu.harvard.iq.dataverse.authorization.groups.impl.maildomain.MailDomainGroupServiceBean; import edu.harvard.iq.dataverse.authorization.groups.impl.shib.ShibGroupProvider; import edu.harvard.iq.dataverse.authorization.groups.impl.shib.ShibGroupServiceBean; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; @@ -24,6 +26,7 @@ import javax.annotation.PostConstruct; import javax.ejb.EJB; import javax.ejb.Stateless; +import javax.inject.Inject; import javax.inject.Named; /** @@ -41,12 +44,15 @@ public class GroupServiceBean { ShibGroupServiceBean shibGroupService; @EJB ExplicitGroupServiceBean explicitGroupService; + @EJB + MailDomainGroupServiceBean mailDomainGroupService; private final Map groupProviders = new HashMap<>(); private IpGroupProvider ipGroupProvider; private ShibGroupProvider shibGroupProvider; private ExplicitGroupProvider explicitGroupProvider; + private MailDomainGroupProvider mailDomainGroupProvider; @EJB RoleAssigneeServiceBean roleAssigneeSvc; @@ -57,6 +63,7 @@ public void setup() { addGroupProvider( ipGroupProvider = new IpGroupProvider(ipGroupsService) ); addGroupProvider( shibGroupProvider = new ShibGroupProvider(shibGroupService) ); addGroupProvider( explicitGroupProvider = explicitGroupService.getProvider() ); + addGroupProvider( mailDomainGroupProvider = mailDomainGroupService.getProvider() ); Logger.getLogger(GroupServiceBean.class.getName()).log(Level.INFO, null, "PostConstruct group service call"); } @@ -78,6 +85,10 @@ public ShibGroupProvider getShibGroupProvider() { return shibGroupProvider; } + public MailDomainGroupProvider getMailDomainGroupProvider() { + return mailDomainGroupProvider; + } + /** * Finds all the groups {@code req} is part of in {@code dvo}'s context. * Recurses upwards in {@link ExplicitGroup}s, as needed. @@ -109,7 +120,7 @@ public Set groupsFor( RoleAssignee ra, DvObject dvo ) { * groups a Role assignee belongs to as advertised but this method comes * closer. * - * @param au An AuthenticatedUser. + * @param ra An AuthenticatedUser. * @return As many groups as we can find for the AuthenticatedUser. * * @deprecated Does not look into IP Groups. Use {@link #groupsFor(edu.harvard.iq.dataverse.engine.command.DataverseRequest)} From 8d2b9cb202159aba66e3bf4ae6e16d718e7184fc Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Fri, 29 May 2020 16:46:14 +0200 Subject: [PATCH 09/18] Make MailDomainGroup and its test usable from other packages (for JSON parsing/printing). #6936 --- .../groups/impl/maildomain/MailDomainGroup.java | 4 ++-- .../groups/impl/maildomain/MailDomainGroupTest.java | 11 ++++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroup.java b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroup.java index 320641c9833..7fd8c7ffa30 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroup.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroup.java @@ -38,7 +38,7 @@ public class MailDomainGroup extends PersistedGlobalGroup { /** * Empty Constructor for JPA. */ - protected MailDomainGroup() {} + public MailDomainGroup() {} public void setEmailDomains(String domains) { this.emailDomains = domains; @@ -109,6 +109,6 @@ public boolean equals(Object obj) { @Override public String toString() { - return "[MailDomainGroup " + this.getPersistedGroupAlias() + ": " + this.emailDomains + "]"; + return "[MailDomainGroup " + this.getPersistedGroupAlias() + ": id=" + this.getId() + " domains="+this.emailDomains+" ]"; } } diff --git a/src/test/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupTest.java b/src/test/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupTest.java index d5340066f4a..99f86d383bd 100644 --- a/src/test/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupTest.java @@ -11,7 +11,7 @@ import static org.junit.jupiter.api.Assertions.*; -class MailDomainGroupTest { +public class MailDomainGroupTest { DataverseRequest dvr = Mockito.mock(DataverseRequest.class); @@ -77,12 +77,13 @@ void testEquals() { } static Random rnd = new Random(); - static MailDomainGroup genGroup() { + public static MailDomainGroup genGroup() { MailDomainGroup t = new MailDomainGroup(); t.setId(rnd.nextLong()); - t.setPersistedGroupAlias(RandomStringUtils.randomAlphanumeric(12)); - t.setDisplayName(RandomStringUtils.randomAlphanumeric(12)); - t.setEmailDomains(RandomStringUtils.randomAlphanumeric(5)+"."+RandomStringUtils.randomAlphanumeric(2)+";"+RandomStringUtils.randomAlphanumeric(5)+"."+RandomStringUtils.randomAlphanumeric(3)); + t.setPersistedGroupAlias(RandomStringUtils.randomAlphanumeric(4)); + t.setDisplayName(RandomStringUtils.randomAlphanumeric(8)); + t.setDescription(RandomStringUtils.randomAlphanumeric(8)); + t.setEmailDomains(RandomStringUtils.randomAlphanumeric(5)+".com;"+RandomStringUtils.randomAlphanumeric(5)+".co.uk"); return t; } } \ No newline at end of file From 8aaab6110125bac89abff420e1bc0bedeb2e35d0 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Fri, 29 May 2020 16:46:56 +0200 Subject: [PATCH 10/18] Add JSON parser and printer for MailDomainGroup, to be used for REST API endpoints. #6936 --- .../iq/dataverse/util/json/JsonParser.java | 49 ++++++++++++------- .../iq/dataverse/util/json/JsonPrinter.java | 11 +++++ .../dataverse/util/json/JsonParserTest.java | 34 +++++++++++++ 3 files changed, 77 insertions(+), 17 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java index d3a2017e33e..c3e67498150 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java @@ -24,32 +24,21 @@ import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.IpGroup; import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IpAddress; import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IpAddressRange; -import edu.harvard.iq.dataverse.dataaccess.DataAccess; +import edu.harvard.iq.dataverse.authorization.groups.impl.maildomain.MailDomainGroup; import edu.harvard.iq.dataverse.datasetutility.OptionalFileParams; import edu.harvard.iq.dataverse.harvest.client.HarvestingClient; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.workflow.Workflow; import edu.harvard.iq.dataverse.workflow.step.WorkflowStepData; +import org.apache.commons.validator.routines.DomainValidator; + import java.io.StringReader; import java.sql.Timestamp; import java.text.ParseException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.Map.Entry; +import java.util.*; import java.util.logging.Logger; -import javax.json.Json; -import javax.json.JsonArray; -import javax.json.JsonObject; -import javax.json.JsonReader; -import javax.json.JsonString; -import javax.json.JsonValue; +import java.util.stream.Collectors; +import javax.json.*; import javax.json.JsonValue.ValueType; /** @@ -248,6 +237,32 @@ public IpGroup parseIpGroup(JsonObject obj) { return retVal; } + + public MailDomainGroup parseMailDomainGroup(JsonObject obj) throws JsonParseException { + MailDomainGroup grp = new MailDomainGroup(); + + if (obj.containsKey("id")) { + grp.setId(obj.getJsonNumber("id").longValue()); + } + grp.setDisplayName(getMandatoryString(obj, "name")); + grp.setDescription(obj.getString("description", null)); + grp.setPersistedGroupAlias(getMandatoryString(obj, "alias")); + if ( obj.containsKey("domains") ) { + List domains = + Optional.ofNullable(obj.getJsonArray("domains")) + .orElse(Json.createArrayBuilder().build()) + .getValuesAs(JsonString.class) + .stream() + .map(JsonString::getString) + .filter(d -> DomainValidator.getInstance().isValid(d)) + .collect(Collectors.toList()); + grp.setEmailDomains(domains); + } else { + throw new JsonParseException("Field domains is mandatory."); + } + + return grp; + } public DatasetVersion parseDatasetVersion(JsonObject obj) throws JsonParseException { return parseDatasetVersion(obj, new DatasetVersion()); 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 e727def7d44..57fcdd75ac1 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 @@ -16,6 +16,7 @@ import edu.harvard.iq.dataverse.DataverseFacet; import edu.harvard.iq.dataverse.DataverseTheme; import edu.harvard.iq.dataverse.authorization.DataverseRole; +import edu.harvard.iq.dataverse.authorization.groups.impl.maildomain.MailDomainGroup; import edu.harvard.iq.dataverse.authorization.providers.builtin.BuiltinUser; import edu.harvard.iq.dataverse.FileMetadata; import edu.harvard.iq.dataverse.GlobalId; @@ -185,6 +186,16 @@ public static JsonObjectBuilder json(ShibGroup grp) { .add("pattern", grp.getPattern()) .add("id", grp.getId()); } + + public static JsonObjectBuilder json(MailDomainGroup grp) { + JsonObjectBuilder bld = jsonObjectBuilder() + .add("alias", grp.getPersistedGroupAlias() ) + .add("id", grp.getId() ) + .add("name", grp.getDisplayName() ) + .add("description", grp.getDescription() ) + .add("domains", asJsonArray(grp.getEmailDomainsAsList()) ); + return bld; + } public static JsonArrayBuilder rolesToJson(List role) { JsonArrayBuilder bld = Json.createArrayBuilder(); diff --git a/src/test/java/edu/harvard/iq/dataverse/util/json/JsonParserTest.java b/src/test/java/edu/harvard/iq/dataverse/util/json/JsonParserTest.java index 412daf58058..9b452c464b3 100644 --- a/src/test/java/edu/harvard/iq/dataverse/util/json/JsonParserTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/util/json/JsonParserTest.java @@ -20,6 +20,8 @@ import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IpAddressRange; import edu.harvard.iq.dataverse.DataverseTheme.Alignment; import edu.harvard.iq.dataverse.FileMetadata; +import edu.harvard.iq.dataverse.authorization.groups.impl.maildomain.MailDomainGroup; +import edu.harvard.iq.dataverse.authorization.groups.impl.maildomain.MailDomainGroupTest; import edu.harvard.iq.dataverse.authorization.users.GuestUser; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.mocks.MockDatasetFieldSvc; @@ -531,6 +533,38 @@ public void testIpGroupRoundTrip_singleIpv6Address() { assertFalse( parsed.contains( new DataverseRequest(GuestUser.get(), IpAddress.valueOf("2.1.1.1")) )); } + + @Test + public void testValidMailDomainGroup() throws JsonParseException { + // given + MailDomainGroup test = MailDomainGroupTest.genGroup(); + + // when + JsonObject serialized = JsonPrinter.json(test).build(); + MailDomainGroup parsed = new JsonParser().parseMailDomainGroup(serialized); + + // then + assertEquals(test, parsed); + assertEquals(test.hashCode(), parsed.hashCode()); + } + + @Test(expected = JsonParseException.class) + public void testMailDomainGroupMissingName() throws JsonParseException { + // given + String noname = "{ \"id\": 1, \"alias\": \"test\", \"domains\": [] }"; + JsonObject obj = Json.createReader(new StringReader(noname)).readObject(); + // when && then + MailDomainGroup parsed = new JsonParser().parseMailDomainGroup(obj); + } + + @Test(expected = JsonParseException.class) + public void testMailDomainGroupMissingDomains() throws JsonParseException { + // given + String noname = "{ \"name\": \"test\", \"alias\": \"test\" }"; + JsonObject obj = Json.createReader(new StringReader(noname)).readObject(); + // when && then + MailDomainGroup parsed = new JsonParser().parseMailDomainGroup(obj); + } @Test public void testparseFiles() throws JsonParseException { From dc0da65275a8b0acb908f175e07d0f42eaf6fb8c Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Mon, 8 Jun 2020 19:02:55 +0200 Subject: [PATCH 11/18] Add persistence handling to create and update MailDomainGroups to the service and entity layer. #6936 --- .../impl/maildomain/MailDomainGroup.java | 9 +++ .../maildomain/MailDomainGroupProvider.java | 11 ++++ .../MailDomainGroupServiceBean.java | 62 +++++++++++++------ 3 files changed, 62 insertions(+), 20 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroup.java b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroup.java index 7fd8c7ffa30..36c97f0cff1 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroup.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroup.java @@ -111,4 +111,13 @@ public boolean equals(Object obj) { public String toString() { return "[MailDomainGroup " + this.getPersistedGroupAlias() + ": id=" + this.getId() + " domains="+this.emailDomains+" ]"; } + + public MailDomainGroup update(MailDomainGroup src) { + setPersistedGroupAlias(src.getPersistedGroupAlias()); + setDisplayName(src.getDisplayName()); + setDescription(src.getDescription()); + setEmailDomains(src.getEmailDomains()); + setGroupProvider(src.getGroupProvider()); + return this; + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupProvider.java b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupProvider.java index 4563d6dd623..5930e5756f6 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupProvider.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupProvider.java @@ -90,6 +90,17 @@ public Set findGlobalGroups() { return updateProvider( new HashSet<>(emailGroupSvc.findAll()) ); } + /** + * Update an existing instance (if found) or create a new (if groupName = null). + * @param groupName String with the group alias of the group to update or empty if new entity + * @param grp The group to update or add + * @return The saved entity, including updated group provider attribute + */ + public MailDomainGroup saveOrUpdate(Optional groupName, MailDomainGroup grp) { + grp.setGroupProvider(this); + return emailGroupSvc.saveOrUpdate(groupName, grp); + } + /** * Sets the provider of the passed explicit group to {@code this}. * @param eg the collection diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupServiceBean.java index c7f7bfd5abd..43c50e4204e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupServiceBean.java @@ -1,5 +1,7 @@ package edu.harvard.iq.dataverse.authorization.groups.impl.maildomain; +import edu.harvard.iq.dataverse.actionlogging.ActionLogRecord; +import edu.harvard.iq.dataverse.actionlogging.ActionLogServiceBean; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.confirmemail.ConfirmEmailServiceBean; @@ -18,6 +20,7 @@ import javax.persistence.EntityManager; import javax.persistence.NoResultException; import javax.persistence.PersistenceContext; +import javax.ws.rs.NotFoundException; /** * A bean providing the {@link MailDomainGroupProvider}s with container services, such as database connectivity. @@ -34,6 +37,8 @@ public class MailDomainGroupServiceBean { @Inject ConfirmEmailServiceBean confirmEmailSvc; + @Inject + ActionLogServiceBean actionLogSvc; MailDomainGroupProvider provider; @@ -97,29 +102,46 @@ Optional findByAlias(String groupAlias) { } } - /* - public MailDomainGroup persist( MailDomainGroup g ) { - if ( g.getId() == null ) { - em.persist( g ); - return g; - } else { - // clean stale data once in a while - if ( Math.random() >= 0.5 ) { - Set stale = new TreeSet<>(); - for ( String idtf : g.getContainedRoleAssignees()) { - if ( roleAssigneeSvc.getRoleAssignee(idtf) == null ) { - stale.add(idtf); - } - } - if ( ! stale.isEmpty() ) { - g.getContainedRoleAssignees().removeAll(stale); - } + /** + * Update an existing instance (if found) or create a new (if groupName = null or groupName matches alias of grp). + * This method is idempotent. + * This being an EJB bean makes this method transactional, rolling back on unchecked exceptions. + * @param groupName String with the group alias of the group to update or empty if new entity + * @param grp The group to update or add + * @return The saved entity, including updated group provider attribute + * @throws NotFoundException if groupName does not match both a group in database and the alias of the provided group + */ + public MailDomainGroup saveOrUpdate(Optional groupName, MailDomainGroup grp ) { + ActionLogRecord alr = new ActionLogRecord(ActionLogRecord.ActionType.GlobalGroups, "mailDomainCreate"); + alr.setInfo(grp.getIdentifier()); + + // groupName present means PUT means idempotence. + if (groupName.isPresent()) { + Optional old = findByAlias(groupName.get()); + + // if an old instance is found, update: + // (triggering persistence once we leave the function) + if (old.isPresent()) { + old.get().update(grp); + + alr.setActionSubType("mailDomainUpdate"); + actionLogSvc.log( alr ); + + return grp; + } + + // otherwise check if path param and supplied group match. (so people use it according to RFC-2616) + // if not -> throw exception! + if (!groupName.equals(grp.getPersistedGroupAlias())) { + throw new NotFoundException(); } - - return em.merge( g ); } + // or add new ... + em.persist(grp); + actionLogSvc.log( alr ); + + return grp; } - */ public void removeGroup(MailDomainGroup mailDomainGroup) { em.remove( mailDomainGroup ); From 7c72ed52e71dd62da378d7318e5c484ff73ff909 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Mon, 8 Jun 2020 19:03:41 +0200 Subject: [PATCH 12/18] Add first REST endpoints to get, create and update MailDomainGroups. Lacking deletion support. #6936 --- .../edu/harvard/iq/dataverse/api/Groups.java | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Groups.java b/src/main/java/edu/harvard/iq/dataverse/api/Groups.java index 9e1b52be10b..428330c2897 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Groups.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Groups.java @@ -2,14 +2,19 @@ import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.IpGroup; import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.IpGroupProvider; +import edu.harvard.iq.dataverse.authorization.groups.impl.maildomain.MailDomainGroup; +import edu.harvard.iq.dataverse.authorization.groups.impl.maildomain.MailDomainGroupProvider; import edu.harvard.iq.dataverse.authorization.groups.impl.shib.ShibGroup; import edu.harvard.iq.dataverse.authorization.groups.impl.shib.ShibGroupProvider; import edu.harvard.iq.dataverse.util.json.JsonParser; + import javax.ejb.Stateless; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.core.Response; import static edu.harvard.iq.dataverse.util.json.JsonPrinter.*; + +import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Pattern; @@ -35,6 +40,7 @@ public class Groups extends AbstractApiBean { private IpGroupProvider ipGroupPrv; private ShibGroupProvider shibGroupPrv; + private MailDomainGroupProvider mailDomainGroupPrv; Pattern legalGroupName = Pattern.compile("^[-_a-zA-Z0-9]+$"); @@ -42,6 +48,7 @@ public class Groups extends AbstractApiBean { void postConstruct() { ipGroupPrv = groupSvc.getIpGroupProvider(); shibGroupPrv = groupSvc.getShibGroupProvider(); + mailDomainGroupPrv = groupSvc.getMailDomainGroupProvider(); } /** @@ -208,5 +215,98 @@ public Response deleteShibGroup( @PathParam("primaryKey") String id ) { return error(Response.Status.BAD_REQUEST, "Could not find Shibboleth group with an id of " + id); } } + + + /** + * Creates a new {@link MailDomainGroup}. The name of the group is based on the + * {@code alias:} field, but might be changed to ensure uniqueness. + * @param dto + * @return Response describing the created group or the error that prevented + * that group from being created. + */ + @POST + @Path("domain") + public Response createMailDomainGroup( JsonObject dto ){ + try { + MailDomainGroup grp = new JsonParser().parseMailDomainGroup(dto); + mailDomainGroupPrv.saveOrUpdate(Optional.empty(), grp); + + return created("/groups/domain/" + grp.getPersistedGroupAlias(), json(grp) ); + + } catch ( Exception e ) { + logger.log( Level.WARNING, "Error while updating a mail domain group: " + e.getMessage(), e); + return error(Response.Status.INTERNAL_SERVER_ERROR, "Error: " + e.getMessage() ); + } + } + + /** + * Creates or updates the {@link MailDomainGroup} named {@code groupName}. + * @param groupName Name of the group. + * @param dto data of the group. + * @return Response describing the created group or the error that prevented + * that group from being created. + */ + @PUT + @Path("domain/{groupName}") + public Response updateMailDomainGroups(@PathParam("groupName") String groupName, JsonObject dto ){ + try { + if ( groupName == null || groupName.trim().isEmpty() ) { + return badRequest("Group name cannot be empty"); + } + if ( ! legalGroupName.matcher(groupName).matches() ) { + return badRequest("Group name can contain only letters, digits, and the chars '-' and '_'"); + } + + MailDomainGroup grp = new JsonParser().parseMailDomainGroup(dto); + mailDomainGroupPrv.saveOrUpdate(Optional.of(groupName), grp); + + return created("/groups/domain/" + grp.getPersistedGroupAlias(), json(grp) ); + + } catch ( Exception e ) { + logger.log( Level.WARNING, "Error while updating a mail domain group: " + e.getMessage(), e); + return error(Response.Status.INTERNAL_SERVER_ERROR, "Error: " + e.getMessage() ); + } + } + + @GET + @Path("domain") + public Response listMailDomainGroups() { + return ok( mailDomainGroupPrv.findGlobalGroups() + .stream().map(g->json(g)).collect(toJsonArray()) ); + } + + @GET + @Path("domain/{groupIdtf}") + public Response getMailDomainGroup( @PathParam("groupIdtf") String groupIdtf ) { + MailDomainGroup grp = mailDomainGroupPrv.get(groupIdtf); + return (grp == null) ? notFound( "Group " + groupIdtf + " not found") : ok(json(grp)); + } + + /* + @DELETE + @Path("domain/{groupIdtf}") + public Response deleteMailDomainGroup( @PathParam("groupIdtf") String groupIdtf ) { + MailDomainGroup grp = mailDomainGroupPrv.get(groupIdtf); + if (grp == null) return notFound( "Group " + groupIdtf + " not found"); + + try { + mailDomainGroupPrv.deleteGroup(grp); + return ok("Group " + grp.getAlias() + " deleted."); + } catch ( Exception topExp ) { + // get to the cause (unwraps EJB exception wrappers). + Throwable e = topExp; + while ( e.getCause() != null ) { + e = e.getCause(); + } + + if ( e instanceof IllegalArgumentException ) { + return error(Response.Status.BAD_REQUEST, e.getMessage()); + } else { + throw topExp; + } + } + } + + */ } From 694ee50602010376de0a7a0fa7ed953ffd50be3c Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Tue, 9 Jun 2020 16:10:16 +0200 Subject: [PATCH 13/18] Extend exception handling at REST endpoints. Does now provide a catch-all for exceptions, support JsonParseExceptions and JPA ConstraintViolationExceptions. Relates to #3423 and #6936. --- .../ConstraintViolationExceptionHandler.java | 64 +++++++++++++++++ .../JsonParseExceptionHandler.java | 37 ++++++++++ .../api/errorhandlers/ThrowableHandler.java | 70 +++++++++++++++++++ 3 files changed, 171 insertions(+) create mode 100644 src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/ConstraintViolationExceptionHandler.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/JsonParseExceptionHandler.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/ThrowableHandler.java diff --git a/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/ConstraintViolationExceptionHandler.java b/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/ConstraintViolationExceptionHandler.java new file mode 100644 index 00000000000..4cbf31d1d2c --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/ConstraintViolationExceptionHandler.java @@ -0,0 +1,64 @@ +package edu.harvard.iq.dataverse.api.errorhandlers; + +import edu.harvard.iq.dataverse.util.json.JsonPrinter; + +import javax.json.Json; +import javax.json.JsonArray; +import javax.json.JsonArrayBuilder; +import javax.json.JsonObject; +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; +import java.util.List; +import java.util.stream.Collectors; + +@Provider +public class ConstraintViolationExceptionHandler implements ExceptionMapper { + + public class ValidationError { + private String path; + private String message; + + public String getPath() { return path; } + public void setPath(String path) { this.path = path; } + public String getMessage() { return message; } + public void setMessage(String message) { this.message = message; } + } + + @Override + public Response toResponse(ConstraintViolationException exception) { + + List errors = exception.getConstraintViolations().stream() + .map(this::toValidationError) + .collect(Collectors.toList()); + + return Response.status(Response.Status.BAD_REQUEST) + .entity( Json.createObjectBuilder() + .add("status", "ERROR") + .add("code", Response.Status.BAD_REQUEST.getStatusCode()) + .add("message", "JPA validation constraints failed persistence. See list of violations for details.") + .add("violations", toJsonArray(errors)) + .build()) + .type(MediaType.APPLICATION_JSON_TYPE).build(); + } + + private ValidationError toValidationError(ConstraintViolation constraintViolation) { + ValidationError error = new ValidationError(); + error.setPath(constraintViolation.getPropertyPath().toString()); + error.setMessage(constraintViolation.getMessage()); + return error; + } + + private JsonArray toJsonArray(List list) { + JsonArrayBuilder builder = Json.createArrayBuilder(); + list.stream() + .forEach(error -> builder.add( + Json.createObjectBuilder() + .add("path", error.getPath()) + .add("message", error.getMessage()))); + return builder.build(); + } +} \ No newline at end of file diff --git a/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/JsonParseExceptionHandler.java b/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/JsonParseExceptionHandler.java new file mode 100644 index 00000000000..286272d9de3 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/JsonParseExceptionHandler.java @@ -0,0 +1,37 @@ +package edu.harvard.iq.dataverse.api.errorhandlers; + +import edu.harvard.iq.dataverse.util.json.JsonParseException; + +import javax.json.Json; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.BadRequestException; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; +import java.util.UUID; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Make a failing JSON parsing request appear to be a BadRequest (error code 400) + * and send a message what just failed... + */ +@Provider +public class JsonParseExceptionHandler implements ExceptionMapper{ + + @Context + HttpServletRequest request; + + @Override + public Response toResponse(JsonParseException ex){ + return Response.status(Response.Status.BAD_REQUEST) + .entity( Json.createObjectBuilder() + .add("status", "ERROR") + .add("code", Response.Status.BAD_REQUEST.getStatusCode()) + .add("message", ex.getMessage()) + .build()) + .type(MediaType.APPLICATION_JSON_TYPE).build(); + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/ThrowableHandler.java b/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/ThrowableHandler.java new file mode 100644 index 00000000000..8514d099787 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/ThrowableHandler.java @@ -0,0 +1,70 @@ +package edu.harvard.iq.dataverse.api.errorhandlers; + +import javax.annotation.Priority; +import javax.json.Json; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; +import java.util.UUID; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Produces a generic 500 message for the API, being a fallback handler for not specially treated exceptions. + */ +@Provider +public class ThrowableHandler implements ExceptionMapper{ + + private static final Logger logger = Logger.getLogger(ThrowableHandler.class.getName()); + + @Context + HttpServletRequest request; + + @Override + public Response toResponse(Throwable ex){ + String incidentId = UUID.randomUUID().toString(); + logger.log(Level.SEVERE, "Uncaught REST API exception:\n"+ + " Incident: " + incidentId +"\n"+ + " URL: "+getOriginalURL(request)+"\n"+ + " Method: "+request.getMethod(), ex); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity( Json.createObjectBuilder() + .add("status", "ERROR") + .add("code", Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()) + .add("type", ex.getClass().getSimpleName()) + .add("message", "Internal server error. More details available at the server logs.") + .add("incidentId", incidentId) + .build()) + .type(MediaType.APPLICATION_JSON_TYPE).build(); + } + + private String getOriginalURL(HttpServletRequest req) { + // Rebuild the original request URL: http://stackoverflow.com/a/5212336/356408 + String scheme = req.getScheme(); // http + String serverName = req.getServerName(); // hostname.com + int serverPort = req.getServerPort(); // 80 + String contextPath = req.getContextPath(); // /mywebapp + String servletPath = req.getServletPath(); // /servlet/MyServlet + String pathInfo = req.getPathInfo(); // /a/b;c=123 + String queryString = req.getQueryString(); // d=789 + + // Reconstruct original requesting URL + StringBuilder url = new StringBuilder(); + url.append(scheme).append("://").append(serverName); + if (serverPort != 80 && serverPort != 443) { + url.append(":").append(serverPort); + } + url.append(contextPath).append(servletPath); + if (pathInfo != null) { + url.append(pathInfo); + } + if (queryString != null) { + url.append("?").append(queryString); + } + + return url.toString(); + } +} From 389324bad3d78bce16cf9135eff2b9fa11677dc7 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Tue, 9 Jun 2020 16:11:18 +0200 Subject: [PATCH 14/18] Remove try-catch-boilerplate for mail domain group creation by using the extended exception handling. #6936 --- .../edu/harvard/iq/dataverse/api/Groups.java | 46 ++++++++----------- 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Groups.java b/src/main/java/edu/harvard/iq/dataverse/api/Groups.java index 428330c2897..9aa23f7fba8 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Groups.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Groups.java @@ -6,9 +6,11 @@ import edu.harvard.iq.dataverse.authorization.groups.impl.maildomain.MailDomainGroupProvider; import edu.harvard.iq.dataverse.authorization.groups.impl.shib.ShibGroup; import edu.harvard.iq.dataverse.authorization.groups.impl.shib.ShibGroupProvider; +import edu.harvard.iq.dataverse.util.json.JsonParseException; import edu.harvard.iq.dataverse.util.json.JsonParser; import javax.ejb.Stateless; +import javax.interceptor.Interceptors; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.core.Response; @@ -226,17 +228,11 @@ public Response deleteShibGroup( @PathParam("primaryKey") String id ) { */ @POST @Path("domain") - public Response createMailDomainGroup( JsonObject dto ){ - try { - MailDomainGroup grp = new JsonParser().parseMailDomainGroup(dto); - mailDomainGroupPrv.saveOrUpdate(Optional.empty(), grp); - - return created("/groups/domain/" + grp.getPersistedGroupAlias(), json(grp) ); - - } catch ( Exception e ) { - logger.log( Level.WARNING, "Error while updating a mail domain group: " + e.getMessage(), e); - return error(Response.Status.INTERNAL_SERVER_ERROR, "Error: " + e.getMessage() ); - } + public Response createMailDomainGroup( JsonObject dto ) throws JsonParseException { + MailDomainGroup grp = new JsonParser().parseMailDomainGroup(dto); + mailDomainGroupPrv.saveOrUpdate(Optional.empty(), grp); + + return created("/groups/domain/" + grp.getPersistedGroupAlias(), json(grp) ); } /** @@ -248,24 +244,18 @@ public Response createMailDomainGroup( JsonObject dto ){ */ @PUT @Path("domain/{groupName}") - public Response updateMailDomainGroups(@PathParam("groupName") String groupName, JsonObject dto ){ - try { - if ( groupName == null || groupName.trim().isEmpty() ) { - return badRequest("Group name cannot be empty"); - } - if ( ! legalGroupName.matcher(groupName).matches() ) { - return badRequest("Group name can contain only letters, digits, and the chars '-' and '_'"); - } - - MailDomainGroup grp = new JsonParser().parseMailDomainGroup(dto); - mailDomainGroupPrv.saveOrUpdate(Optional.of(groupName), grp); - - return created("/groups/domain/" + grp.getPersistedGroupAlias(), json(grp) ); - - } catch ( Exception e ) { - logger.log( Level.WARNING, "Error while updating a mail domain group: " + e.getMessage(), e); - return error(Response.Status.INTERNAL_SERVER_ERROR, "Error: " + e.getMessage() ); + public Response updateMailDomainGroups(@PathParam("groupName") String groupName, JsonObject dto ) throws JsonParseException { + if ( groupName == null || groupName.trim().isEmpty() ) { + return badRequest("Group name cannot be empty"); + } + if ( ! legalGroupName.matcher(groupName).matches() ) { + return badRequest("Group name can contain only letters, digits, and the chars '-' and '_'"); } + + MailDomainGroup grp = new JsonParser().parseMailDomainGroup(dto); + mailDomainGroupPrv.saveOrUpdate(Optional.of(groupName), grp); + + return created("/groups/domain/" + grp.getPersistedGroupAlias(), json(grp) ); } @GET From 09c7f64b0eab4f9d592868748ec9747d58aa962c Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Tue, 9 Jun 2020 16:11:53 +0200 Subject: [PATCH 15/18] Make JsonParser fail on empty (maybe filtered invalid) domains array of MailDomainGroup. #6936 --- .../java/edu/harvard/iq/dataverse/util/json/JsonParser.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java index c3e67498150..144725c2f81 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java @@ -256,6 +256,8 @@ public MailDomainGroup parseMailDomainGroup(JsonObject obj) throws JsonParseExce .map(JsonString::getString) .filter(d -> DomainValidator.getInstance().isValid(d)) .collect(Collectors.toList()); + if (domains.isEmpty()) + throw new JsonParseException("Field domains may not be an empty array or contain invalid domains."); grp.setEmailDomains(domains); } else { throw new JsonParseException("Field domains is mandatory."); From bd0f68586c649966837e3199ad12c3624b431d5a Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Tue, 9 Jun 2020 17:17:26 +0200 Subject: [PATCH 16/18] Add deletion endpoint for MailDomainGroup and make consistent use of group alias as the identifier everywhere. #6936 --- .../edu/harvard/iq/dataverse/api/Groups.java | 50 ++++++------------- .../maildomain/MailDomainGroupProvider.java | 10 ++-- .../MailDomainGroupServiceBean.java | 27 +++++++--- 3 files changed, 41 insertions(+), 46 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Groups.java b/src/main/java/edu/harvard/iq/dataverse/api/Groups.java index 9aa23f7fba8..5a2a32e794a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Groups.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Groups.java @@ -228,7 +228,7 @@ public Response deleteShibGroup( @PathParam("primaryKey") String id ) { */ @POST @Path("domain") - public Response createMailDomainGroup( JsonObject dto ) throws JsonParseException { + public Response createMailDomainGroup(JsonObject dto) throws JsonParseException { MailDomainGroup grp = new JsonParser().parseMailDomainGroup(dto); mailDomainGroupPrv.saveOrUpdate(Optional.empty(), grp); @@ -237,23 +237,23 @@ public Response createMailDomainGroup( JsonObject dto ) throws JsonParseExceptio /** * Creates or updates the {@link MailDomainGroup} named {@code groupName}. - * @param groupName Name of the group. + * @param groupAlias Name of the group. * @param dto data of the group. * @return Response describing the created group or the error that prevented * that group from being created. */ @PUT - @Path("domain/{groupName}") - public Response updateMailDomainGroups(@PathParam("groupName") String groupName, JsonObject dto ) throws JsonParseException { - if ( groupName == null || groupName.trim().isEmpty() ) { + @Path("domain/{groupAlias}") + public Response updateMailDomainGroups(@PathParam("groupAlias") String groupAlias, JsonObject dto) throws JsonParseException { + if ( groupAlias == null || groupAlias.trim().isEmpty() ) { return badRequest("Group name cannot be empty"); } - if ( ! legalGroupName.matcher(groupName).matches() ) { + if ( ! legalGroupName.matcher(groupAlias).matches() ) { return badRequest("Group name can contain only letters, digits, and the chars '-' and '_'"); } MailDomainGroup grp = new JsonParser().parseMailDomainGroup(dto); - mailDomainGroupPrv.saveOrUpdate(Optional.of(groupName), grp); + mailDomainGroupPrv.saveOrUpdate(Optional.of(groupAlias), grp); return created("/groups/domain/" + grp.getPersistedGroupAlias(), json(grp) ); } @@ -266,37 +266,17 @@ public Response listMailDomainGroups() { } @GET - @Path("domain/{groupIdtf}") - public Response getMailDomainGroup( @PathParam("groupIdtf") String groupIdtf ) { - MailDomainGroup grp = mailDomainGroupPrv.get(groupIdtf); - return (grp == null) ? notFound( "Group " + groupIdtf + " not found") : ok(json(grp)); + @Path("domain/{groupAlias}") + public Response getMailDomainGroup(@PathParam("groupAlias") String groupAlias) { + MailDomainGroup grp = mailDomainGroupPrv.get(groupAlias); + return (grp == null) ? notFound( "Group " + groupAlias + " not found") : ok(json(grp)); } - /* @DELETE - @Path("domain/{groupIdtf}") - public Response deleteMailDomainGroup( @PathParam("groupIdtf") String groupIdtf ) { - MailDomainGroup grp = mailDomainGroupPrv.get(groupIdtf); - if (grp == null) return notFound( "Group " + groupIdtf + " not found"); - - try { - mailDomainGroupPrv.deleteGroup(grp); - return ok("Group " + grp.getAlias() + " deleted."); - } catch ( Exception topExp ) { - // get to the cause (unwraps EJB exception wrappers). - Throwable e = topExp; - while ( e.getCause() != null ) { - e = e.getCause(); - } - - if ( e instanceof IllegalArgumentException ) { - return error(Response.Status.BAD_REQUEST, e.getMessage()); - } else { - throw topExp; - } - } + @Path("domain/{groupAlias}") + public Response deleteMailDomainGroup(@PathParam("groupAlias") String groupAlias) { + mailDomainGroupPrv.delete(groupAlias); + return ok("Group " + groupAlias + " deleted."); } - - */ } diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupProvider.java b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupProvider.java index 5930e5756f6..bf3c5c49fe8 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupProvider.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupProvider.java @@ -92,13 +92,17 @@ public Set findGlobalGroups() { /** * Update an existing instance (if found) or create a new (if groupName = null). - * @param groupName String with the group alias of the group to update or empty if new entity + * @param groupAlias String with the group alias of the group to update or empty if new entity * @param grp The group to update or add * @return The saved entity, including updated group provider attribute */ - public MailDomainGroup saveOrUpdate(Optional groupName, MailDomainGroup grp) { + public MailDomainGroup saveOrUpdate(Optional groupAlias, MailDomainGroup grp) { grp.setGroupProvider(this); - return emailGroupSvc.saveOrUpdate(groupName, grp); + return emailGroupSvc.saveOrUpdate(groupAlias, grp); + } + + public void delete(String groupAlias) { + emailGroupSvc.delete(groupAlias); } /** diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupServiceBean.java index 43c50e4204e..dbe3043fa18 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/maildomain/MailDomainGroupServiceBean.java @@ -4,6 +4,7 @@ import edu.harvard.iq.dataverse.actionlogging.ActionLogServiceBean; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.confirmemail.ConfirmEmailServiceBean; +import org.ocpsoft.rewrite.config.Not; import java.util.Collections; import java.util.HashSet; @@ -106,18 +107,18 @@ Optional findByAlias(String groupAlias) { * Update an existing instance (if found) or create a new (if groupName = null or groupName matches alias of grp). * This method is idempotent. * This being an EJB bean makes this method transactional, rolling back on unchecked exceptions. - * @param groupName String with the group alias of the group to update or empty if new entity + * @param groupAlias String with the group alias of the group to update or empty if new entity * @param grp The group to update or add * @return The saved entity, including updated group provider attribute * @throws NotFoundException if groupName does not match both a group in database and the alias of the provided group */ - public MailDomainGroup saveOrUpdate(Optional groupName, MailDomainGroup grp ) { + public MailDomainGroup saveOrUpdate(Optional groupAlias, MailDomainGroup grp ) { ActionLogRecord alr = new ActionLogRecord(ActionLogRecord.ActionType.GlobalGroups, "mailDomainCreate"); alr.setInfo(grp.getIdentifier()); - // groupName present means PUT means idempotence. - if (groupName.isPresent()) { - Optional old = findByAlias(groupName.get()); + // groupAlias present means PUT means idempotence. + if (groupAlias.isPresent()) { + Optional old = findByAlias(groupAlias.get()); // if an old instance is found, update: // (triggering persistence once we leave the function) @@ -132,7 +133,7 @@ public MailDomainGroup saveOrUpdate(Optional groupName, MailDomainGroup // otherwise check if path param and supplied group match. (so people use it according to RFC-2616) // if not -> throw exception! - if (!groupName.equals(grp.getPersistedGroupAlias())) { + if (!groupAlias.get().equals(grp.getPersistedGroupAlias())) { throw new NotFoundException(); } } @@ -143,8 +144,18 @@ public MailDomainGroup saveOrUpdate(Optional groupName, MailDomainGroup return grp; } - public void removeGroup(MailDomainGroup mailDomainGroup) { - em.remove( mailDomainGroup ); + /** + * Delete a mail domain group if exists. + * @param groupAlias + * @throws NotFoundException if no group with given groupAlias exists. + */ + public void delete(String groupAlias) { + ActionLogRecord alr = new ActionLogRecord(ActionLogRecord.ActionType.GlobalGroups, "mailDomainDelete"); + alr.setInfo(groupAlias); + + Optional tbd = findByAlias(groupAlias); + em.remove(tbd.orElseThrow(() -> new NotFoundException("Cannot find a group with alias "+groupAlias))); + actionLogSvc.log( alr ); } /** From 6b6578888e9157563260b3fb674489806187e346 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Mon, 15 Jun 2020 13:44:13 +0200 Subject: [PATCH 17/18] Add db migration for MailDomainGroups as PersistedGlobalGroup uses SINGLE_TABLE inheritance strategy. #6936 --- .../resources/db/migration/V4.20.0.4__6936-maildomain-groups.sql | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/main/resources/db/migration/V4.20.0.4__6936-maildomain-groups.sql diff --git a/src/main/resources/db/migration/V4.20.0.4__6936-maildomain-groups.sql b/src/main/resources/db/migration/V4.20.0.4__6936-maildomain-groups.sql new file mode 100644 index 00000000000..8c89b66fdec --- /dev/null +++ b/src/main/resources/db/migration/V4.20.0.4__6936-maildomain-groups.sql @@ -0,0 +1 @@ +ALTER TABLE persistedglobalgroup ADD COLUMN IF NOT EXISTS emaildomains text; \ No newline at end of file From 76ae9278b3e9e3b860c2a1af97f449a764252dc5 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Mon, 15 Jun 2020 18:16:34 +0200 Subject: [PATCH 18/18] Add docs for mail domain groups only, without refactoring to a groups page. #6936 --- doc/sphinx-guides/source/admin/index.rst | 1 + .../source/admin/mail-groups.rst | 82 +++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 doc/sphinx-guides/source/admin/mail-groups.rst diff --git a/doc/sphinx-guides/source/admin/index.rst b/doc/sphinx-guides/source/admin/index.rst index 6ff611cb55f..55733ffd99a 100755 --- a/doc/sphinx-guides/source/admin/index.rst +++ b/doc/sphinx-guides/source/admin/index.rst @@ -26,6 +26,7 @@ This guide documents the functionality only available to superusers (such as "da dataverses-datasets solr-search-index ip-groups + mail-groups monitoring reporting-tools-and-queries maintenance diff --git a/doc/sphinx-guides/source/admin/mail-groups.rst b/doc/sphinx-guides/source/admin/mail-groups.rst new file mode 100644 index 00000000000..a7d15af52b4 --- /dev/null +++ b/doc/sphinx-guides/source/admin/mail-groups.rst @@ -0,0 +1,82 @@ +Mail Domain Groups +================== + +Groups can be defined based on the domain part of users (verified) email addresses. Email addresses that match +one or more groups configuration will add the user to them. + +Within the scientific community, in many cases users will use a institutional email address for their account in a +Dataverse installation. This might offer a simple solution for building groups of people, as the domain part can be +seen as a selector for group membership. + +Some use cases: installations that like to avoid Shibboleth, enable self sign up, offer multi-tenancy or can't use +:doc:`ip-groups` plus many more. + +.. hint:: Please be aware that non-verified mail addresses will exclude the user even if matching. This is to avoid + privilege escalation. + +Listing Mail Domain Groups +-------------------------- + +Mail Domain Groups can be listed with the following curl command: + +``curl http://localhost:8080/api/admin/groups/domain`` + +Listing a specific Mail Domain Group +------------------------------------ + +Let's say you used "domainGroup1" as the alias of the Mail Domain Group you created below. +To list just that Mail Domain Group, you can include the alias in the curl command like this: + +``curl http://localhost:8080/api/admin/groups/domain/domainGroup1`` + + +Creating a Mail Domain Group +---------------------------- + +Mail Domain Groups can be created with a simple JSON file: + +.. code-block:: json + :caption: domainGroup1.json + :name: domainGroup1.json + + { + "name": "Users from @example.org", + "alias": "exampleorg", + "description": "Any verified user from Example Org will be included in this group.", + "domains": ["example.org"] + } + +Giving a ``description`` is optional. The ``name`` will be visible in the permission UI, so be sure to pick a sensible +value. + +The ``domains`` field is mandatory to be an array. This enables creation of multi-domain groups, too. + +Obviously you can create as many of these groups you might like, as long as the ``alias`` is unique. + +To load it into your Dataverse installation, either use a ``POST`` or ``PUT`` request (see below): + +``curl -X POST -H 'Content-type: application/json' http://localhost:8080/api/admin/groups/domain --upload-file domainGroup1.json`` + +Updating a Mail Domain Group +---------------------------- + +Editing a group is done by replacing it. Grab your group definition like the :ref:`above example `, +change it as you like and ``PUT`` it into your installation: + +``curl -X PUT -H 'Content-type: application/json' http://localhost:8080/api/admin/groups/domain/domainGroup1 --upload-file domainGroup1.json`` + +Please make sure that the alias of the group you want to change is included in the path. You also need to ensure +that this alias matches with the one given in your JSON file. + +.. hint:: This is an idempotent call, so it will create the group given if not present. + +Deleting a Mail Domain Group +---------------------------- + +To delete a Mail Domain Group with an alias of "domainGroup1", use the curl command below: + +``curl -X DELETE http://localhost:8080/api/admin/groups/domain/domainGroup1`` + +Please note: it is not recommended to delete a Mail Domain Group that has been assigned roles. If you want to delete +a Mail Domain Group, you should first remove its permissions. +