From 90b3a0280ae39007686c70d065c483517a2c72e5 Mon Sep 17 00:00:00 2001 From: Michael Bar-Sinai Date: Wed, 4 May 2016 00:00:13 +0300 Subject: [PATCH 01/35] Proper detection of IP addresses in JSF context --- .../harvard/iq/dataverse/DataversePage.java | 1 - .../DataverseRequestServiceBean.java | 19 +++++++++- .../groups/GroupServiceBean.java | 3 +- .../engine/command/DataverseRequest.java | 37 ------------------- 4 files changed, 20 insertions(+), 40 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/DataversePage.java b/src/main/java/edu/harvard/iq/dataverse/DataversePage.java index 3d5f4139de2..4c5e479f1fd 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataversePage.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataversePage.java @@ -293,7 +293,6 @@ public String init() { return "/404.xhtml"; } if (!dataverse.isReleased() && !permissionService.on(dataverse).has(Permission.ViewUnpublishedDataverse)) { - System.out.print(" session.getUser().isAuthenticated() " + session.getUser().isAuthenticated()); if (!session.getUser().isAuthenticated()){ return "/loginpage.xhtml" + DataverseHeaderFragment.getRedirectPage(); } else { diff --git a/src/main/java/edu/harvard/iq/dataverse/DataverseRequestServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DataverseRequestServiceBean.java index 50274753a6b..bfeb785b34c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataverseRequestServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataverseRequestServiceBean.java @@ -1,8 +1,11 @@ package edu.harvard.iq.dataverse; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; +import java.util.logging.Level; +import java.util.logging.Logger; import javax.annotation.PostConstruct; import javax.enterprise.context.RequestScoped; +import javax.faces.context.FacesContext; import javax.inject.Inject; import javax.inject.Named; import javax.servlet.http.HttpServletRequest; @@ -26,7 +29,21 @@ public class DataverseRequestServiceBean { @PostConstruct protected void setup() { - dataverseRequest = new DataverseRequest(dataverseSessionSvc.getUser(), httpRequest); + dataverseRequest = new DataverseRequest(dataverseSessionSvc.getUser(), getRequest()); + } + + private HttpServletRequest getRequest() { + if ( httpRequest != null ) { + return httpRequest; + } else { + final FacesContext jsfCtxt = FacesContext.getCurrentInstance(); + if ( jsfCtxt != null ) { + return (HttpServletRequest) jsfCtxt.getExternalContext().getRequest(); + } else { + Logger.getLogger(DataverseRequestServiceBean.class.getName()).log(Level.WARNING, "Cannot get the HTTP request object."); + return null; + } + } } public DataverseRequest getDataverseRequest() { 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 85082d2f02e..81ee7c1cbb5 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 @@ -78,7 +78,8 @@ public Set groupsFor( DataverseRequest req, DvObject dvo ) { Set groups = new HashSet<>(); // first, get all groups the user directly belongs to for ( GroupProvider gp : groupProviders.values() ) { - groups.addAll( gp.groupsFor(req, dvo) ); + final Set newGroups = gp.groupsFor(req, dvo); + groups.addAll(newGroups); } return groupTransitiveClosure(groups, dvo); diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/DataverseRequest.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/DataverseRequest.java index f6624d03cc1..70dbcf58e51 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/DataverseRequest.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/DataverseRequest.java @@ -2,7 +2,6 @@ import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IpAddress; import edu.harvard.iq.dataverse.authorization.users.User; -import java.util.logging.Logger; import javax.servlet.http.HttpServletRequest; /** @@ -16,7 +15,6 @@ public class DataverseRequest { private final User user; private final IpAddress sourceAddress; - private static final Logger logger = Logger.getLogger(DataverseRequest.class.getCanonicalName()); public DataverseRequest(User aUser, HttpServletRequest aHttpServletRequest) { this.user = aUser; @@ -26,41 +24,6 @@ public DataverseRequest(User aUser, HttpServletRequest aHttpServletRequest) { remoteAddressStr = aHttpServletRequest.getRemoteAddr(); } catch (Exception _npe) {} - /* - It was reported earlier, that HttpServletRequest.getRemoteAddr() does - not supply the correct address of the incoming request, when Glassfish - is running behind the Apache proxy. This is NOT true - can be easily - confirmed with the log message below (will be switched to .fine when - released). When this constructor is called by the ApiBlockingFileter, - the remoteAddressStr will correctly show the remote address; whether - we are behind a proxy, or not. - As of now (4.2.3), this is the ONLY situation where we check the remote - IP address for the purposes of Authentication/Authorization. - - HOWEVER, this log message below consistently shows NULL when non-API, - page requests are coming in. This is true BOTH with or without the proxy. - So this must mean that when the DataverseRequest object is created - somewhere outside the ApiBlockingFilter, it likely doesn't get the - HttpServletRequest object properly passed to the constructor. - Which further means that in order to enable IP groups, we'll have - to fix that, and make sure the IP address is properly set for such - requests. - - But, once again, as of now this sourceAddress is only being used for - determining if an API request is coming from localhost. - - (It appears that for all the regular page requests, the DataverseRequest - object gets created in DataverseRequestServiceBean.java, like this: - dataverseRequest = new DataverseRequest(dataverseSessionSvc.getUser(), httpRequest); - with the httpRequest supplied by the @Context... why this isn't working - properly - I don't know) - - -- L.A. 4.2.3 - */ - - - logger.fine("DataverseRequest: Obtained remote address: "+remoteAddressStr); - if ( remoteAddressStr == null ) { remoteAddressStr = "0.0.0.0"; } From a336475ccea3bddc05c6d9b36ce12bd25bfa86d5 Mon Sep 17 00:00:00 2001 From: Michael Bar-Sinai Date: Thu, 4 Aug 2016 08:54:00 -0400 Subject: [PATCH 02/35] IP group json format now supports single addresses (see usage in ipGroup-localhost.json) --- .../data/{ipGroup3.json => ipGroup-all.json} | 0 scripts/api/data/ipGroup-localhost.json | 3 +- scripts/issues/1380/01-add.localhost.sh | 2 + scripts/issues/1380/list-ip-groups.sh | 2 + .../edu/harvard/iq/dataverse/api/Groups.java | 2 +- .../groups/impl/ipaddress/IpGroup.java | 22 +++++- .../impl/ipaddress/ip/IpAddressRange.java | 4 + .../iq/dataverse/util/json/JsonParser.java | 29 ++++--- .../iq/dataverse/util/json/JsonPrinter.java | 77 +++++++++++++++++-- .../impl/ipaddress/ip/IpAddressRangeTest.java | 15 ++-- .../dataverse/util/json/JsonParserTest.java | 31 +++++++- 11 files changed, 158 insertions(+), 29 deletions(-) rename scripts/api/data/{ipGroup3.json => ipGroup-all.json} (100%) create mode 100755 scripts/issues/1380/01-add.localhost.sh create mode 100755 scripts/issues/1380/list-ip-groups.sh diff --git a/scripts/api/data/ipGroup3.json b/scripts/api/data/ipGroup-all.json similarity index 100% rename from scripts/api/data/ipGroup3.json rename to scripts/api/data/ipGroup-all.json diff --git a/scripts/api/data/ipGroup-localhost.json b/scripts/api/data/ipGroup-localhost.json index 4a5f7facfef..c29f8ceeed4 100644 --- a/scripts/api/data/ipGroup-localhost.json +++ b/scripts/api/data/ipGroup-localhost.json @@ -1,6 +1,5 @@ { "alias":"localhost", "name":"Localhost connections", - "ranges" : [["127.0.0.1", "127.0.0.1"], - ["::1", "::1"]] + "addresses": [ "0:0:0:0:0:0:0:1", "127.0.0.1" ] } diff --git a/scripts/issues/1380/01-add.localhost.sh b/scripts/issues/1380/01-add.localhost.sh new file mode 100755 index 00000000000..331011d5fa2 --- /dev/null +++ b/scripts/issues/1380/01-add.localhost.sh @@ -0,0 +1,2 @@ +# Add the localhost group to the system. +curl -X POST -H"Content-Type:application/json" -d@../../api/data/ipGroup-localhost.json localhost:8080/api/admin/groups/ip diff --git a/scripts/issues/1380/list-ip-groups.sh b/scripts/issues/1380/list-ip-groups.sh new file mode 100755 index 00000000000..fba29cced4e --- /dev/null +++ b/scripts/issues/1380/list-ip-groups.sh @@ -0,0 +1,2 @@ +#!/bin/bash +curl -X GET http://localhost:8080/api/admin/groups/ip | jq . 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 bbec80b5bc6..ac05ac060b7 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Groups.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Groups.java @@ -42,7 +42,7 @@ void postConstruct() { @Path("ip") public Response createIpGroups( JsonObject dto ){ try { - IpGroup grp = new JsonParser(null,null,null).parseIpGroup(dto); + IpGroup grp = new JsonParser().parseIpGroup(dto); if ( grp.getPersistedGroupAlias()== null ) { return errorResponse(Response.Status.BAD_REQUEST, "Must provide valid group alias"); diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/IpGroup.java b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/IpGroup.java index 8396cb2168a..a52dd7ebc61 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/IpGroup.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/IpGroup.java @@ -1,6 +1,6 @@ package edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress; -import edu.harvard.iq.dataverse.authorization.users.User; +import com.google.common.base.Objects; import edu.harvard.iq.dataverse.authorization.groups.GroupProvider; import edu.harvard.iq.dataverse.authorization.groups.impl.PersistedGlobalGroup; import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IPv4Address; @@ -130,5 +130,23 @@ public void setIpv4Ranges(Set ipv4Ranges) { this.ipv4Ranges = ipv4Ranges; } - + @Override + public boolean equals( Object o ) { + if ( o == null ) return false; + if ( o == this ) return true; + if ( ! (o instanceof IpGroup) ) return false; + + IpGroup other = (IpGroup) o; + + if ( ! Objects.equal(getId(), other.getId()) ) return false; + if ( ! Objects.equal(getDescription(), other.getDescription()) ) return false; + if ( ! Objects.equal(getDisplayName(), other.getDisplayName()) ) return false; + if ( ! Objects.equal(getPersistedGroupAlias(), other.getPersistedGroupAlias()) ) return false; + return getRanges().equals( other.getRanges() ); + } + + @Override + public int hashCode() { + return getPersistedGroupAlias().hashCode(); + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/ip/IpAddressRange.java b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/ip/IpAddressRange.java index c2d1029b44d..5c6c9d548a4 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/ip/IpAddressRange.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/ip/IpAddressRange.java @@ -63,6 +63,10 @@ public boolean equals(Object obj) { && Objects.equals(this.getTop(), other.getTop()); } + public boolean isSingleAddress() { + return getTop().equals(getBottom()); + } + @Override public String toString() { return "[IpAddressRange " + getTop() + "-" + getBottom() + ']'; 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 ebbe1117c3b..0714b10296e 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 @@ -54,6 +54,10 @@ public JsonParser(DatasetFieldServiceBean datasetFieldSvc, MetadataBlockServiceB this.settingsService = settingsService; } + public JsonParser() { + this( null,null,null ); + } + public boolean isLenient() { return lenient; } @@ -124,20 +128,27 @@ public IpGroup parseIpGroup(JsonObject obj) { IpGroup retVal = new IpGroup(); if (obj.containsKey("id")) { - retVal.setId(Long.valueOf(obj.getString("id"))); + retVal.setId(Long.valueOf(obj.getInt("id"))); } retVal.setDisplayName(obj.getString("name", null)); retVal.setDescription(obj.getString("description", null)); retVal.setPersistedGroupAlias(obj.getString("alias", null)); - JsonArray rangeArray = obj.getJsonArray("ranges"); - for (JsonValue range : rangeArray) { - if (range.getValueType() == JsonValue.ValueType.ARRAY) { - JsonArray rr = (JsonArray) range; - retVal.add(IpAddressRange.make(IpAddress.valueOf(rr.getString(0)), - IpAddress.valueOf(rr.getString(1)))); - - } + if ( obj.containsKey("ranges") ) { + obj.getJsonArray("ranges").stream() + .filter( jv -> jv.getValueType()==JsonValue.ValueType.ARRAY ) + .map( jv -> (JsonArray)jv ) + .forEach( rr -> { + retVal.add( + IpAddressRange.make(IpAddress.valueOf(rr.getString(0)), + IpAddress.valueOf(rr.getString(1)))); + }); + } + if ( obj.containsKey("addresses") ) { + obj.getJsonArray("addresses").stream() + .map( jsVal -> IpAddress.valueOf(((JsonString)jsVal).getString()) ) + .map( addr -> IpAddressRange.make(addr, addr) ) + .forEach( retVal::add ); } return retVal; 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 3e38558f8c5..0873b9f70f5 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 @@ -22,6 +22,7 @@ import edu.harvard.iq.dataverse.authorization.RoleAssigneeDisplayInfo; import edu.harvard.iq.dataverse.authorization.groups.impl.explicit.ExplicitGroup; 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.authorization.groups.impl.shib.ShibGroup; import edu.harvard.iq.dataverse.authorization.providers.AuthenticationProviderRow; @@ -38,11 +39,18 @@ import java.util.TreeSet; import static edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder.jsonObjectBuilder; -import java.math.BigDecimal; +import java.util.Arrays; import java.util.Collection; import java.util.Deque; +import java.util.EnumSet; import java.util.LinkedList; import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collector; +import static java.util.stream.Collectors.toList; import javax.json.JsonArray; import javax.json.JsonObject; @@ -118,17 +126,36 @@ public static JsonObjectBuilder json( RoleAssigneeDisplayInfo d ) { } public static JsonObjectBuilder json( IpGroup grp ) { - JsonArrayBuilder rangeBld = Json.createArrayBuilder(); - for ( IpAddressRange r :grp.getRanges() ) { + // collect single addresses + List singles = grp.getRanges().stream().filter( IpAddressRange::isSingleAddress ) + .map( IpAddressRange::getBottom ) + .map( IpAddress::toString ).collect(toList()); + List> ranges = grp.getRanges().stream().filter( rng -> !rng.isSingleAddress() ) + .map( rng -> Arrays.asList(rng.getBottom().toString(), rng.getTop().toString()) ) + .collect(toList()); + for ( IpAddressRange r : grp.getRanges() ) { + JsonArrayBuilder rangeBld = Json.createArrayBuilder(); rangeBld.add( Json.createArrayBuilder().add(r.getBottom().toString()).add(r.getTop().toString()) ); } - return jsonObjectBuilder() + + JsonObjectBuilder bld = jsonObjectBuilder() .add("alias", grp.getPersistedGroupAlias() ) .add("identifier", grp.getIdentifier()) .add("id", grp.getId() ) .add("name", grp.getDisplayName() ) - .add("description", grp.getDescription() ) - .add("ranges", rangeBld); + .add("description", grp.getDescription() ); + + if ( ! singles.isEmpty() ) { + bld.add("addresses", asJsonArray(singles) ); + } + + if ( ! ranges.isEmpty() ) { + JsonArrayBuilder rangesBld = Json.createArrayBuilder(); + ranges.forEach( r -> rangesBld.add( Json.createArrayBuilder().add(r.get(0)).add(r.get(1))) ); + bld.add("ranges", rangesBld ); + } + + return bld; } public static JsonObjectBuilder json(ShibGroup grp) { @@ -382,8 +409,7 @@ public static JsonObjectBuilder json( FileMetadata fmd ) { .add("label", fmd.getLabel()) .add("version", fmd.getVersion()) .add("datasetVersionId", fmd.getDatasetVersion().getId()) - .add("datafile", json(fmd.getDataFile())) - ; + .add("datafile", json(fmd.getDataFile())); } public static JsonObjectBuilder json( DataFile df ) { @@ -522,4 +548,39 @@ public static JsonArrayBuilder json( Collection jc ) { } return bld; } + + public static Collector toJsonArray() { + return new Collector() { + + @Override + public Supplier supplier() { + return ()->Json.createArrayBuilder(); + } + + @Override + public BiConsumer accumulator() { + return (JsonArrayBuilder b, String s ) -> b.add(s); + } + + @Override + public BinaryOperator combiner() { + return (jab1, jab2) -> { + JsonArrayBuilder retVal = Json.createArrayBuilder(); + jab1.build().forEach( retVal::add ); + jab2.build().forEach( retVal::add ); + return retVal; + }; + } + + @Override + public Function finisher() { + return Function.identity(); + } + + @Override + public Set characteristics() { + return EnumSet.of(Collector.Characteristics.IDENTITY_FINISH); + } + }; + } } diff --git a/src/test/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/ip/IpAddressRangeTest.java b/src/test/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/ip/IpAddressRangeTest.java index fb255692a53..f232b713640 100644 --- a/src/test/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/ip/IpAddressRangeTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/ip/IpAddressRangeTest.java @@ -1,11 +1,5 @@ package edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip; -import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IPv4Range; -import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IPv6Address; -import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IPv6Range; -import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IpAddress; -import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IPv4Address; -import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IpAddressRange; import org.junit.Test; import static org.junit.Assert.*; @@ -78,6 +72,15 @@ public void testIPv4NotApplicable() { ); } + @Test + public void testSingleAddress() { + assertTrue( new IPv4Range( IPv4Address.valueOf("127.5.5.5"), IPv4Address.valueOf("127.5.5.5")).isSingleAddress() ); + assertFalse( new IPv4Range( IPv4Address.valueOf("17.5.5.5"), IPv4Address.valueOf("127.5.5.5")).isSingleAddress() ); + + assertTrue( new IPv6Range( IPv6Address.valueOf("::1:1"), IPv6Address.valueOf("::1:1")).isSingleAddress() ); + assertFalse( new IPv6Range( IPv6Address.valueOf("::1:1"), IPv6Address.valueOf("::1:2")).isSingleAddress() ); + } + public void testRange( Boolean expected, IpAddressRange range, IpAddress... addresses ) { for ( IpAddress ipa : addresses ) { assertEquals( "Testing " + ipa + " in " + range, expected, range.contains(ipa)); 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 3ce93e03a2e..df943afe9dd 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 @@ -14,6 +14,10 @@ import edu.harvard.iq.dataverse.DatasetFieldValue; import edu.harvard.iq.dataverse.DatasetVersion; import edu.harvard.iq.dataverse.Dataverse; +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.ipaddress.ip.IpAddress; +import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IpAddressRange; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import java.io.IOException; import java.io.InputStream; @@ -396,7 +400,32 @@ public void testParseOvercompleteDatasetVersion() throws JsonParseException, IOE DatasetVersion actual = sut.parseDatasetVersion(dsJson); } } - + + @Test + public void testIpGroupRoundTrip() { + + IpGroup original = new IpGroup(); + original.setDescription("Ip group description"); + original.setDisplayName("Test-ip-group"); + original.setId(42l); + original.setPersistedGroupAlias("test-ip-group"); + original.setProvider( new IpGroupProvider(null) ); + + original.add( IpAddressRange.make(IpAddress.valueOf("1.2.1.1"), IpAddress.valueOf("1.2.1.10")) ); + original.add( IpAddressRange.make(IpAddress.valueOf("1.1.1.1"), IpAddress.valueOf("1.1.1.1")) ); + original.add( IpAddressRange.make(IpAddress.valueOf("1:2:3::4:5"), IpAddress.valueOf("1:2:3::4:5")) ); + original.add( IpAddressRange.make(IpAddress.valueOf("1:2:3::3:ff"), IpAddress.valueOf("1:2:3::3:5")) ); + + JsonObject serialized = JsonPrinter.json(original).build(); + + System.out.println( serialized.toString() ); + + IpGroup parsed = new JsonParser().parseIpGroup(serialized); + + assertEquals( original, parsed ); + + } + JsonObject json( String s ) { return Json.createReader( new StringReader(s) ).readObject(); } From 2ee3fad9c33098166426a2e1bb903cb7776e819a Mon Sep 17 00:00:00 2001 From: Michael Bar-Sinai Date: Thu, 4 Aug 2016 09:37:27 -0400 Subject: [PATCH 03/35] Fix error message for attempts to delete IP groups that have assinged roles --- scripts/issues/1380/02-build-dv-structure.sh | 12 ++++++++++++ scripts/issues/1380/delete-ip-group | 9 +++++++++ scripts/issues/1380/keys.txt | 3 +++ scripts/issues/1380/users.out | 6 ++++++ .../java/edu/harvard/iq/dataverse/api/Groups.java | 15 +++++++++++++-- 5 files changed, 43 insertions(+), 2 deletions(-) create mode 100755 scripts/issues/1380/02-build-dv-structure.sh create mode 100755 scripts/issues/1380/delete-ip-group create mode 100644 scripts/issues/1380/keys.txt create mode 100644 scripts/issues/1380/users.out diff --git a/scripts/issues/1380/02-build-dv-structure.sh b/scripts/issues/1380/02-build-dv-structure.sh new file mode 100755 index 00000000000..f0936e3cf69 --- /dev/null +++ b/scripts/issues/1380/02-build-dv-structure.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +echo Run this after running setup-users.sh, and making Pete an +echo admin on the root dataverse. + + +PETE=$(grep :result: users.out | grep Pete | cut -f4 -d: | tr -d \ ) +UMA=$(grep :result: users.out | grep Uma | cut -f4 -d: | tr -d \ ) + +pushd ../../api +./setup-dvs.sh $PETE $UMA +popd diff --git a/scripts/issues/1380/delete-ip-group b/scripts/issues/1380/delete-ip-group new file mode 100755 index 00000000000..b6138d95024 --- /dev/null +++ b/scripts/issues/1380/delete-ip-group @@ -0,0 +1,9 @@ +#/bin/bahx +if [ $# -eq 0 ] + then + echo "Please provide IP group id" + echo "e.g $0 845" + exit 1 +fi + +curl -X DELETE http://localhost:8080/api/admin/groups/ip/$1 diff --git a/scripts/issues/1380/keys.txt b/scripts/issues/1380/keys.txt new file mode 100644 index 00000000000..9989bd2ea9c --- /dev/null +++ b/scripts/issues/1380/keys.txt @@ -0,0 +1,3 @@ +Keys for Pete and Uma. Produced by running setup-all.sh from the /scripts/api folder. +Pete:757a6493-456a-4bf0-943e-9b559d551a3f +Uma:8797f19b-b8aa-4f96-a789-1b99506f2eab diff --git a/scripts/issues/1380/users.out b/scripts/issues/1380/users.out new file mode 100644 index 00000000000..337b9e2ce01 --- /dev/null +++ b/scripts/issues/1380/users.out @@ -0,0 +1,6 @@ +{"status":"OK","data":{"user":{"id":4,"firstName":"Gabbi","lastName":"Guest","userName":"gabbi","affiliation":"low","position":"A Guest","email":"gabbi@malinator.com"},"authenticatedUser":{"id":4,"identifier":"@gabbi","displayName":"Gabbi Guest","firstName":"Gabbi","lastName":"Guest","email":"gabbi@malinator.com","superuser":false,"affiliation":"low","position":"A Guest","persistentUserId":"gabbi","authenticationProviderId":"builtin"},"apiToken":"d1940786-c315-491e-9812-a8ff809289cc"}} +{"status":"OK","data":{"user":{"id":5,"firstName":"Cathy","lastName":"Collaborator","userName":"cathy","affiliation":"mid","position":"Data Scientist","email":"cathy@malinator.com"},"authenticatedUser":{"id":5,"identifier":"@cathy","displayName":"Cathy Collaborator","firstName":"Cathy","lastName":"Collaborator","email":"cathy@malinator.com","superuser":false,"affiliation":"mid","position":"Data Scientist","persistentUserId":"cathy","authenticationProviderId":"builtin"},"apiToken":"0ddfcb1e-fb51-4ce7-88ab-308b23e13e9a"}} +{"status":"OK","data":{"user":{"id":6,"firstName":"Nick","lastName":"NSA","userName":"nick","affiliation":"gov","position":"Signals Intelligence","email":"nick@malinator.com"},"authenticatedUser":{"id":6,"identifier":"@nick","displayName":"Nick NSA","firstName":"Nick","lastName":"NSA","email":"nick@malinator.com","superuser":false,"affiliation":"gov","position":"Signals Intelligence","persistentUserId":"nick","authenticationProviderId":"builtin"},"apiToken":"6d74745d-1733-459a-ae29-422110056ec0"}} +reporting API keys +:result: Pete's key is: 757a6493-456a-4bf0-943e-9b559d551a3f +:result: Uma's key is: 8797f19b-b8aa-4f96-a789-1b99506f2eab \ No newline at end of file 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 ac05ac060b7..9aa6bfd82ce 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Groups.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Groups.java @@ -17,6 +17,7 @@ import javax.json.JsonArrayBuilder; import javax.json.JsonObject; import javax.json.JsonString; +import javax.transaction.TransactionRolledbackException; import javax.ws.rs.DELETE; import javax.ws.rs.POST; import javax.ws.rs.PathParam; @@ -99,8 +100,18 @@ public Response deleteIpGroup( @PathParam("groupIdtf") String groupIdtf ) { try { ipGroupPrv.deleteGroup(grp); return okResponse("Group " + grp.getAlias() + " deleted."); - } catch ( IllegalArgumentException ex ) { - return errorResponse(Response.Status.BAD_REQUEST, ex.getMessage()); + } 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 errorResponse(Response.Status.BAD_REQUEST, e.getMessage()); + } else { + throw topExp; + } } } From affb6b3df9a83e87e459819c999afc532def820d Mon Sep 17 00:00:00 2001 From: Michael Bar-Sinai Date: Thu, 4 Aug 2016 11:03:10 -0400 Subject: [PATCH 04/35] (Part of #1380) IP groups are now honored by the dataset page --- scripts/issues/1380/keys.txt | 2 +- .../edu/harvard/iq/dataverse/DatasetPage.java | 32 ++----------------- .../iq/dataverse/PermissionsWrapper.java | 20 ++++++------ 3 files changed, 15 insertions(+), 39 deletions(-) diff --git a/scripts/issues/1380/keys.txt b/scripts/issues/1380/keys.txt index 9989bd2ea9c..9dc47d356c1 100644 --- a/scripts/issues/1380/keys.txt +++ b/scripts/issues/1380/keys.txt @@ -1,3 +1,3 @@ -Keys for Pete and Uma. Produced by running setup-all.sh from the /scripts/api folder. +Keys for P e t e and U m a. Produced by running setup-all.sh from the /scripts/api folder. Pete:757a6493-456a-4bf0-943e-9b559d551a3f Uma:8797f19b-b8aa-4f96-a789-1b99506f2eab diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java index 91aeac64e14..fb0ca6f47ca 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java @@ -80,7 +80,6 @@ import javax.faces.event.AjaxBehaviorEvent; -import javax.faces.context.ExternalContext; import org.apache.commons.lang.StringEscapeUtils; import org.primefaces.component.tabview.TabView; @@ -555,7 +554,7 @@ public boolean isThumbnailAvailable(FileMetadata fileMetadata) { // Another convenience method - to cache Update Permission on the dataset: public boolean canUpdateDataset() { - return permissionsWrapper.canUpdateDataset(this.session.getUser(), this.dataset); + return permissionsWrapper.canUpdateDataset(dvRequestService.getDataverseRequest(), this.dataset); } public boolean canPublishDataverse() { @@ -576,13 +575,9 @@ public boolean canPublishDataverse() { //} public boolean canViewUnpublishedDataset() { - return permissionsWrapper.canViewUnpublishedDataset(this.session.getUser(), this.dataset); - //return doesSessionUserHaveDataSetPermission(Permission.ViewUnpublishedDataset); + return permissionsWrapper.canViewUnpublishedDataset( dvRequestService.getDataverseRequest(), dataset); } - private Boolean sessionUserAuthenticated = null; - - /* * 4.2.1 optimization. * HOWEVER, this doesn't appear to be saving us anything! @@ -590,28 +585,7 @@ public boolean canViewUnpublishedDataset() { * every time; it doesn't do any new db lookups. */ public boolean isSessionUserAuthenticated() { - logger.fine("entering isSessionUserAuthenticated;"); - if (sessionUserAuthenticated != null) { - logger.fine("using cached isSessionUserAuthenticated;"); - - return sessionUserAuthenticated; - } - - if (session == null) { - return false; - } - - if (session.getUser() == null) { - return false; - } - - if (session.getUser().isAuthenticated()) { - sessionUserAuthenticated = true; - return true; - } - - sessionUserAuthenticated = false; - return false; + return session.getUser().isAuthenticated(); } /** diff --git a/src/main/java/edu/harvard/iq/dataverse/PermissionsWrapper.java b/src/main/java/edu/harvard/iq/dataverse/PermissionsWrapper.java index 9a60f71d0df..d740cb91f93 100644 --- a/src/main/java/edu/harvard/iq/dataverse/PermissionsWrapper.java +++ b/src/main/java/edu/harvard/iq/dataverse/PermissionsWrapper.java @@ -8,6 +8,7 @@ import edu.harvard.iq.dataverse.authorization.Permission; import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.engine.command.Command; +import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.impl.*; import java.util.HashMap; import java.util.Map; @@ -42,7 +43,9 @@ public class PermissionsWrapper implements java.io.Serializable { /** * Check if the current Dataset can Issue Commands * - * @param commandName + * @param dvo Target dataverse object. + * @param command The command to execute + * @return {@code true} if the user can issue the command on the object. */ public boolean canIssueCommand(DvObject dvo, Class command) { if ((dvo==null) || (dvo.getId()==null)){ @@ -131,12 +134,12 @@ public boolean canManageDatasetPermissions(User u, Dataset ds) { return permissionService.userOn(u, ds).has(Permission.ManageDatasetPermissions); } - public boolean canViewUnpublishedDataset(User user, Dataset dataset) { - return doesSessionUserHaveDataSetPermission(user, dataset, Permission.ViewUnpublishedDataset); + public boolean canViewUnpublishedDataset(DataverseRequest dr, Dataset dataset) { + return doesSessionUserHaveDataSetPermission(dr, dataset, Permission.ViewUnpublishedDataset); } - public boolean canUpdateDataset(User user, Dataset dataset) { - return doesSessionUserHaveDataSetPermission(user, dataset, Permission.EditDataset); + public boolean canUpdateDataset(DataverseRequest dr, Dataset dataset) { + return doesSessionUserHaveDataSetPermission(dr, dataset, Permission.EditDataset); } @@ -147,12 +150,12 @@ public boolean canUpdateDataset(User user, Dataset dataset) { * * Check Dataset related permissions * - * @param user + * @param req * @param dataset * @param permissionToCheck * @return */ - public boolean doesSessionUserHaveDataSetPermission(User user, Dataset dataset, Permission permissionToCheck){ + public boolean doesSessionUserHaveDataSetPermission(DataverseRequest req, Dataset dataset, Permission permissionToCheck){ if (permissionToCheck == null){ return false; } @@ -167,8 +170,7 @@ public boolean doesSessionUserHaveDataSetPermission(User user, Dataset dataset, } // Check the permission - // - boolean hasPermission = this.permissionService.userOn(user, dataset).has(permissionToCheck); + boolean hasPermission = this.permissionService.requestOn(req, dataset).has(permissionToCheck); // Save the permission this.datasetPermissionMap.put(permName, hasPermission); From cb2da43750a85d02572b767183cd77a2ab77863a Mon Sep 17 00:00:00 2001 From: Michael Bar-Sinai Date: Fri, 5 Aug 2016 12:13:28 -0400 Subject: [PATCH 05/35] #1380: All DvObject access usecases ( that is: (guest/user/owner)x(ipGroup,no ipGroup)x(API, UI) ) work as required --- scripts/issues/1380/truth-table.numbers | Bin 0 -> 159698 bytes .../DataverseRequestServiceBean.java | 10 +++ .../iq/dataverse/api/AbstractApiBean.java | 2 +- .../edu/harvard/iq/dataverse/api/Access.java | 59 +++++++++++------- .../groups/impl/ipaddress/IpGroup.java | 13 ++-- 5 files changed, 55 insertions(+), 29 deletions(-) create mode 100644 scripts/issues/1380/truth-table.numbers diff --git a/scripts/issues/1380/truth-table.numbers b/scripts/issues/1380/truth-table.numbers new file mode 100644 index 0000000000000000000000000000000000000000..86f67386fbb29a2f473a02ad476c1f7ee7420df0 GIT binary patch literal 159698 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�Z>@-|O3_>R;uL1eCO2Pl0fQ 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 Date: Fri, 5 Aug 2016 15:05:26 -0400 Subject: [PATCH 06/35] IP Groups: Search must operate on DataverseRequest, not User #1380 #1513 This commit affects Saved Search and MyData. --- .../edu/harvard/iq/dataverse/api/Index.java | 2 +- .../edu/harvard/iq/dataverse/api/Search.java | 2 +- .../groups/GroupServiceBean.java | 17 +++++++++ .../iq/dataverse/mydata/DataRetrieverAPI.java | 4 +- .../search/SearchFilesServiceBean.java | 6 ++- .../search/SearchIncludeFragment.java | 9 ++++- .../dataverse/search/SearchServiceBean.java | 37 ++++++++++++++----- .../savedsearch/SavedSearchServiceBean.java | 2 +- 8 files changed, 61 insertions(+), 18 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Index.java b/src/main/java/edu/harvard/iq/dataverse/api/Index.java index 6ccc228cf32..e217b146dd6 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Index.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Index.java @@ -572,7 +572,7 @@ public Response searchDebug( int numResultsPerPage = Integer.MAX_VALUE; SolrQueryResponse solrQueryResponse; try { - solrQueryResponse = searchService.search(user, subtreeScope, query, filterQueries, sortField, sortOrder, paginationStart, dataRelatedToMe, numResultsPerPage); + solrQueryResponse = searchService.search(createDataverseRequest(user), subtreeScope, query, filterQueries, sortField, sortOrder, paginationStart, dataRelatedToMe, numResultsPerPage); } catch (SearchException ex) { return errorResponse(Response.Status.INTERNAL_SERVER_ERROR, ex.getLocalizedMessage() + ": " + ex.getCause().getLocalizedMessage()); } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Search.java b/src/main/java/edu/harvard/iq/dataverse/api/Search.java index c65084cedb7..9b8dfeb2045 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Search.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Search.java @@ -110,7 +110,7 @@ public Response search( SolrQueryResponse solrQueryResponse; try { solrQueryResponse = searchService.search( - user, + createDataverseRequest(user), subtree, query, filterQueries, 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 e6b8d46ed54..338d2e8000a 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 @@ -12,6 +12,7 @@ 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.authorization.users.AuthenticatedUser; +import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import java.util.HashMap; import java.util.HashSet; @@ -139,6 +140,22 @@ public Set groupsFor(AuthenticatedUser au) { return groups; } + /** + * This method wraps two existing methods to honor IP groups (see bug at + * https://github.com/IQSS/dataverse/issues/1513 ) but the plan is to + * introduce a better "get *all* groups given a DataverseRequest" method as + * part of https://github.com/IQSS/dataverse/pull/3103 + */ + public Set groupsFor(DataverseRequest dataverseRequest) { + Set groups = groupsFor(dataverseRequest, null); + User user = dataverseRequest.getUser(); + if (user instanceof AuthenticatedUser) { + AuthenticatedUser authenticatedUser = (AuthenticatedUser) user; + groups.addAll(groupsFor(authenticatedUser)); + } + return groups; + } + /** * Given a set of groups and a DV object, return all the groups that are * reachable from the set. Effectively, if the initial set has an {@link ExplicitGroup}, diff --git a/src/main/java/edu/harvard/iq/dataverse/mydata/DataRetrieverAPI.java b/src/main/java/edu/harvard/iq/dataverse/mydata/DataRetrieverAPI.java index 3744a90241a..9c25f3e30e0 100644 --- a/src/main/java/edu/harvard/iq/dataverse/mydata/DataRetrieverAPI.java +++ b/src/main/java/edu/harvard/iq/dataverse/mydata/DataRetrieverAPI.java @@ -232,7 +232,7 @@ private SolrQueryResponse getTotalCountsFromSolr(AuthenticatedUser searchUser, M SolrQueryResponse solrQueryResponseForCounts; try { solrQueryResponseForCounts = searchService.search( - searchUser, // + createDataverseRequest(searchUser), null, // subtree, default it to Dataverse for now "*", // Get everything--always filterQueries,//filterQueries, @@ -397,7 +397,7 @@ public String retrieveMyDataAsJsonString(@QueryParam("dvobject_types") List filterQueries, String sortField, String sortOrder, int paginationStart, boolean onlyDatatRelatedToMe, int numResultsPerPage) throws SearchException { - return search(user, dataverse, query, filterQueries, sortField, sortOrder, paginationStart, onlyDatatRelatedToMe, numResultsPerPage, true); + public SolrQueryResponse search(DataverseRequest dataverseRequest, Dataverse dataverse, String query, List filterQueries, String sortField, String sortOrder, int paginationStart, boolean onlyDatatRelatedToMe, int numResultsPerPage) throws SearchException { + return search(dataverseRequest, dataverse, query, filterQueries, sortField, sortOrder, paginationStart, onlyDatatRelatedToMe, numResultsPerPage, true); } /** @@ -145,7 +145,7 @@ public SolrQueryResponse search(User user, Dataverse dataverse, String query, Li * @return * @throws SearchException */ - public SolrQueryResponse search(User user, Dataverse dataverse, String query, List filterQueries, String sortField, String sortOrder, int paginationStart, boolean onlyDatatRelatedToMe, int numResultsPerPage, boolean retrieveEntities) throws SearchException { + public SolrQueryResponse search(DataverseRequest dataverseRequest, Dataverse dataverse, String query, List filterQueries, String sortField, String sortOrder, int paginationStart, boolean onlyDatatRelatedToMe, int numResultsPerPage, boolean retrieveEntities) throws SearchException { if (paginationStart < 0) { throw new IllegalArgumentException("paginationStart must be 0 or greater"); @@ -221,7 +221,7 @@ public SolrQueryResponse search(User user, Dataverse dataverse, String query, Li // ----------------------------------- // PERMISSION FILTER QUERY // ----------------------------------- - String permissionFilterQuery = this.getPermissionFilterQuery(user, solrQuery, dataverse, onlyDatatRelatedToMe); + String permissionFilterQuery = this.getPermissionFilterQuery(dataverseRequest, solrQuery, dataverse, onlyDatatRelatedToMe); if (permissionFilterQuery != null) { solrQuery.addFilterQuery(permissionFilterQuery); } @@ -731,8 +731,9 @@ public String getCapitalizedName(String name) { * * @return */ - private String getPermissionFilterQuery(User user, SolrQuery solrQuery, Dataverse dataverse, boolean onlyDatatRelatedToMe) { + private String getPermissionFilterQuery(DataverseRequest dataverseRequest, SolrQuery solrQuery, Dataverse dataverse, boolean onlyDatatRelatedToMe) { + User user = dataverseRequest.getUser(); if (user == null) { throw new NullPointerException("user cannot be null"); } @@ -757,10 +758,26 @@ private String getPermissionFilterQuery(User user, SolrQuery solrQuery, Datavers // ---------------------------------------------------- // (1) Is this a GuestUser? - // Yes, all set, give back "publicOnly" filter string + // Yes, see if GuestUser is part of any groups such as IP Groups. // ---------------------------------------------------- if (user instanceof GuestUser) { - return publicOnly; + String groupsFromProviders = ""; + Set groups = groupService.groupsFor(dataverseRequest); + StringBuilder sb = new StringBuilder(); + for (Group group : groups) { + logger.fine("found group " + group.getIdentifier() + " with alias " + group.getAlias()); + String groupAlias = group.getAlias(); + if (groupAlias != null && !groupAlias.isEmpty()) { + sb.append(" OR "); + // i.e. group_builtIn/all-users, ip/ipGroup3 + sb.append(IndexServiceBean.getGroupPrefix()).append(groupAlias); + } + } + groupsFromProviders = sb.toString(); + logger.fine("groupsFromProviders:" + groupsFromProviders); + String guestWithGroups = "{!join from=" + SearchFields.DEFINITION_POINT + " to=id}" + SearchFields.DISCOVERABLE_BY + ":(" + IndexServiceBean.getPublicGroupString() + groupsFromProviders + ")"; + logger.fine(guestWithGroups); + return guestWithGroups; } // ---------------------------------------------------- @@ -835,7 +852,7 @@ private String getPermissionFilterQuery(User user, SolrQuery solrQuery, Datavers * a given "content document" (dataset version, etc) in Solr. */ String groupsFromProviders = ""; - Set groups = groupService.groupsFor(au); + Set groups = groupService.groupsFor(dataverseRequest); StringBuilder sb = new StringBuilder(); for (Group group : groups) { logger.fine("found group " + group.getIdentifier() + " with alias " + group.getAlias()); diff --git a/src/main/java/edu/harvard/iq/dataverse/search/savedsearch/SavedSearchServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/search/savedsearch/SavedSearchServiceBean.java index 0694b404098..bc328673802 100644 --- a/src/main/java/edu/harvard/iq/dataverse/search/savedsearch/SavedSearchServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/search/savedsearch/SavedSearchServiceBean.java @@ -206,7 +206,7 @@ private SolrQueryResponse findHits(SavedSearch savedSearch) throws SearchExcepti boolean dataRelatedToMe = false; int numResultsPerPage = Integer.MAX_VALUE; SolrQueryResponse solrQueryResponse = searchService.search( - savedSearch.getCreator(), + new DataverseRequest(savedSearch.getCreator(), getHttpServletRequest()), savedSearch.getDefinitionPoint(), savedSearch.getQuery(), savedSearch.getFilterQueriesAsStrings(), From 9d0a91f32885ea96028414439c4e4cb6106cfde3 Mon Sep 17 00:00:00 2001 From: Michael Bar-Sinai Date: Fri, 5 Aug 2016 15:09:23 -0400 Subject: [PATCH 07/35] Initial work towards better explicit group inclusion in queries (part of #1380) --- .../DataverseRequestServiceBean.java | 34 ++----------------- .../iq/dataverse/RoleAssigneeServiceBean.java | 1 - .../edu/harvard/iq/dataverse/api/TestApi.java | 19 +++++++++++ .../authorization/groups/GroupProvider.java | 4 +-- .../groups/GroupServiceBean.java | 15 ++++++++ .../groups/impl/explicit/ExplicitGroup.java | 11 +++--- .../explicit/ExplicitGroupServiceBean.java | 18 +++++++++- 7 files changed, 63 insertions(+), 39 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/DataverseRequestServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DataverseRequestServiceBean.java index 4fc48196c5b..e193b535412 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataverseRequestServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataverseRequestServiceBean.java @@ -1,15 +1,11 @@ package edu.harvard.iq.dataverse; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; -import java.util.logging.Level; -import java.util.logging.Logger; import javax.annotation.PostConstruct; import javax.enterprise.context.RequestScoped; -import javax.faces.context.FacesContext; import javax.inject.Inject; import javax.inject.Named; import javax.servlet.http.HttpServletRequest; -import javax.ws.rs.core.Context; /** * The service bean to go to when one needs the current {@link DataverseRequest}. @@ -22,40 +18,16 @@ public class DataverseRequestServiceBean { @Inject DataverseSession dataverseSessionSvc; - @Context - HttpServletRequest httpRequest; - - private DataverseRequest dataverseRequest; - @Inject private HttpServletRequest request; + private DataverseRequest dataverseRequest; + @PostConstruct protected void setup() { - dataverseRequest = new DataverseRequest(dataverseSessionSvc.getUser(), getRequest()); + dataverseRequest = new DataverseRequest(dataverseSessionSvc.getUser(), request); } - private HttpServletRequest getRequest() { - - if ( request != null ) { - return request; - } else { - Logger.getLogger(DataverseRequestServiceBean.class.getName()).log(Level.WARNING, "request not injected"); - } - - if ( httpRequest != null ) { - return httpRequest; - } else { - final FacesContext jsfCtxt = FacesContext.getCurrentInstance(); - if ( jsfCtxt != null ) { - return (HttpServletRequest) jsfCtxt.getExternalContext().getRequest(); - } else { - Logger.getLogger(DataverseRequestServiceBean.class.getName()).log(Level.WARNING, "Cannot get the HTTP request object."); - return null; - } - } - } - public DataverseRequest getDataverseRequest() { return dataverseRequest; } diff --git a/src/main/java/edu/harvard/iq/dataverse/RoleAssigneeServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/RoleAssigneeServiceBean.java index 76601774000..457bdc56ade 100644 --- a/src/main/java/edu/harvard/iq/dataverse/RoleAssigneeServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/RoleAssigneeServiceBean.java @@ -309,7 +309,6 @@ private List getUserRuntimeGroups(AuthenticatedUser au) { List retVal = new ArrayList(); Set groups = groupSvc.groupsFor(au, null); - StringBuilder sb = new StringBuilder(); for (Group group : groups) { logger.fine("found group " + group.getIdentifier() + " with alias " + group.getAlias()); if (group.getGroupProvider().getGroupProviderAlias().equals("shib") || group.getGroupProvider().getGroupProviderAlias().equals("ip")) { diff --git a/src/main/java/edu/harvard/iq/dataverse/api/TestApi.java b/src/main/java/edu/harvard/iq/dataverse/api/TestApi.java index 3483a0c790c..b93b5edcce9 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/TestApi.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/TestApi.java @@ -2,6 +2,8 @@ import edu.harvard.iq.dataverse.DvObject; import edu.harvard.iq.dataverse.authorization.RoleAssignee; +import edu.harvard.iq.dataverse.authorization.groups.impl.explicit.ExplicitGroup; +import edu.harvard.iq.dataverse.authorization.groups.impl.explicit.ExplicitGroupServiceBean; import edu.harvard.iq.dataverse.authorization.providers.builtin.PasswordEncryption; import edu.harvard.iq.dataverse.authorization.users.User; import javax.ejb.Stateless; @@ -10,8 +12,10 @@ import javax.ws.rs.PathParam; import javax.ws.rs.core.Response; import static edu.harvard.iq.dataverse.util.json.JsonPrinter.*; +import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; +import javax.ejb.EJB; import javax.json.Json; import javax.json.JsonObjectBuilder; import javax.ws.rs.QueryParam; @@ -33,6 +37,9 @@ public class TestApi extends AbstractApiBean { private static final Logger logger = Logger.getLogger(TestApi.class.getName()); + @EJB + ExplicitGroupServiceBean explicitGroups; + @Path("echo/{whatever}") @GET public Response echo( @PathParam("whatever") String body ) { @@ -97,4 +104,16 @@ public Response testUserLookup() { return ex.getResponse(); } } + + @Path("explicitGroups/{identifier: .*}") + @GET + public Response explicitGroupMembership( @PathParam("identifier") String idtf) { + final RoleAssignee roleAssignee = roleAssigneeSvc.getRoleAssignee(idtf); + if (roleAssignee==null ) { + return notFound("Can't find a role assignee with identifier " + idtf); + } + Set groups = explicitGroups.findGroups(roleAssignee); + return okResponse( json(groups) ); + + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/GroupProvider.java b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/GroupProvider.java index 9029d2947b1..91a49c98c4d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/GroupProvider.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/GroupProvider.java @@ -29,8 +29,8 @@ public interface GroupProvider { /** * Looks up the groups this provider has for a dataverse request, in the context of a {@link DvObject}. - * @param req The request whose group memberships we evaluate - * @param dvo the DvObject which is the context for the groups. May be {@code null} + * @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 The set of groups the user is member of. */ public Set groupsFor( DataverseRequest req, DvObject dvo ); 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 e6b8d46ed54..c4c18e6675c 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 @@ -113,7 +113,10 @@ public Set groupsFor( RoleAssignee ra, DvObject dvo ) { * * @param au An AuthenticatedUser. * @return As many groups as we can find for the AuthenticatedUser. + * + * @deprecated use {@link #groupsFor(edu.harvard.iq.dataverse.engine.command.DataverseRequest)} */ + @Deprecated public Set groupsFor(AuthenticatedUser au) { Set groups = new HashSet<>(); groups.addAll(groupsFor(au, null)); @@ -139,6 +142,18 @@ public Set groupsFor(AuthenticatedUser au) { return groups; } + public Set groupsFor( DataverseRequest dr ) { + Set groups = new HashSet<>(); + + // get the global groups + groups.addAll( groupsFor(dr,null) ); + + // add the explicit groups + groups.addAll( explicitGroupService.findGroups(dr.getUser()) ); + + return groups; + } + /** * Given a set of groups and a DV object, return all the groups that are * reachable from the set. Effectively, if the initial set has an {@link ExplicitGroup}, diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/explicit/ExplicitGroup.java b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/explicit/ExplicitGroup.java index d8b55e7e932..b36af8b2fa6 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/explicit/ExplicitGroup.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/explicit/ExplicitGroup.java @@ -56,12 +56,15 @@ +"WHERE eg.owner.id=:ownerId AND ceg.id=:subExGroupId"), @NamedQuery( name="ExplicitGroup.findByOwnerAndRAIdtf", query="SELECT eg FROM ExplicitGroup eg join eg.containedRoleAssignees ra " - +"WHERE eg.owner.id=:ownerId AND ra=:raIdtf") + +"WHERE eg.owner.id=:ownerId AND ra=:raIdtf"), + @NamedQuery( name="ExplicitGroup.findByAuthenticatedUserIdentifier", + query="SELECT eg FROM ExplicitGroup eg JOIN eg.containedAuthenticatedUsers au " + + "WHERE au.userIdentifier=:authenticatedUserIdentifier") + }) @Entity -@Table(indexes = {@Index(columnList="owner_id") - //, @Index(columnList="groupalias") //@unique takes care of this - , @Index(columnList="groupaliasinowner")}) +@Table(indexes = {@Index(columnList="owner_id"), + @Index(columnList="groupaliasinowner")}) public class ExplicitGroup implements Group, java.io.Serializable { @Id diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/explicit/ExplicitGroupServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/explicit/ExplicitGroupServiceBean.java index e6435ef6431..ec35a1ee33d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/explicit/ExplicitGroupServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/explicit/ExplicitGroupServiceBean.java @@ -17,7 +17,6 @@ import javax.persistence.EntityManager; import javax.persistence.NoResultException; import javax.persistence.PersistenceContext; -import org.jboss.logging.Logger; /** * A bean providing the {@link ExplicitGroupProvider}s with container services, @@ -114,6 +113,23 @@ public Set findAvailableFor( DvObject d ) { return provider.updateProvider( egs ); } + /** + * Finds all the explicit groups {@code ra} is a member of. + * @param ra the role assignee whose membership list we seek + * @return set of the explicit groups that contain {@code ra}. + */ + public Set findGroups( RoleAssignee ra ) { + if ( ra instanceof AuthenticatedUser ) { + return provider.updateProvider( + new HashSet<>(em.createNamedQuery("ExplicitGroup.findByAuthenticatedUserIdentifier") + .setParameter("authenticatedUserIdentifier", ra.getIdentifier()) + .getResultList() + )); + } else { + throw new IllegalArgumentException("At this time, only authenticated users are supported"); + } + } + /** * Finds all the groups {@code ra} belongs to in the context of {@code o}. In effect, * collects all the groups {@code ra} belongs to and that are defined at {@code o} From 07563d2fcde480fb654286fa02e5355a3b20a355 Mon Sep 17 00:00:00 2001 From: Michael Bar-Sinai Date: Fri, 5 Aug 2016 16:40:29 -0400 Subject: [PATCH 08/35] #1380: Explicit groups can contain IP groups. Querying group memberships based on JPA --- scripts/issues/1380/add-user | 3 +++ scripts/issues/1380/data/guest.json | 1 + scripts/issues/1380/data/locals.json | 1 + scripts/issues/1380/data/pete.json | 1 + scripts/issues/1380/data/uma.json | 1 + scripts/issues/1380/db-list-dvs | 1 + scripts/issues/1380/dvs.gv | 19 ++++++++++++++ scripts/issues/1380/dvs.pdf | Bin 0 -> 15127 bytes scripts/issues/1380/explicitGroup1.json | 5 ++++ scripts/issues/1380/explicitGroup2.json | 5 ++++ scripts/issues/1380/list-groups-for | 2 ++ .../edu/harvard/iq/dataverse/Dataverse.java | 1 - .../harvard/iq/dataverse/api/Dataverses.java | 1 + .../edu/harvard/iq/dataverse/api/TestApi.java | 2 +- .../groups/GroupServiceBean.java | 21 ++------------- .../groups/impl/explicit/ExplicitGroup.java | 8 +++++- .../explicit/ExplicitGroupServiceBean.java | 24 ++++++++++++++---- .../users/AuthenticatedUser.java | 5 ++++ 18 files changed, 74 insertions(+), 27 deletions(-) create mode 100755 scripts/issues/1380/add-user create mode 100644 scripts/issues/1380/data/guest.json create mode 100644 scripts/issues/1380/data/locals.json create mode 100644 scripts/issues/1380/data/pete.json create mode 100644 scripts/issues/1380/data/uma.json create mode 100755 scripts/issues/1380/db-list-dvs create mode 100644 scripts/issues/1380/dvs.gv create mode 100644 scripts/issues/1380/dvs.pdf create mode 100644 scripts/issues/1380/explicitGroup1.json create mode 100644 scripts/issues/1380/explicitGroup2.json create mode 100755 scripts/issues/1380/list-groups-for diff --git a/scripts/issues/1380/add-user b/scripts/issues/1380/add-user new file mode 100755 index 00000000000..1781181bb79 --- /dev/null +++ b/scripts/issues/1380/add-user @@ -0,0 +1,3 @@ +#!/bin/bash +# add-user dv group user api-token +curl -H "Content-type:application/json" -X POST -d"[$3]" localhost:8080/api/dataverses/$1/groups/$2/roleAssignees?key=$4 diff --git a/scripts/issues/1380/data/guest.json b/scripts/issues/1380/data/guest.json new file mode 100644 index 00000000000..3e4188a7167 --- /dev/null +++ b/scripts/issues/1380/data/guest.json @@ -0,0 +1 @@ +[":guest"] diff --git a/scripts/issues/1380/data/locals.json b/scripts/issues/1380/data/locals.json new file mode 100644 index 00000000000..8bb5e3e4162 --- /dev/null +++ b/scripts/issues/1380/data/locals.json @@ -0,0 +1 @@ +["&ip/localhost"] diff --git a/scripts/issues/1380/data/pete.json b/scripts/issues/1380/data/pete.json new file mode 100644 index 00000000000..298e813d2bc --- /dev/null +++ b/scripts/issues/1380/data/pete.json @@ -0,0 +1 @@ +["@pete"] diff --git a/scripts/issues/1380/data/uma.json b/scripts/issues/1380/data/uma.json new file mode 100644 index 00000000000..3caf8c5c9cc --- /dev/null +++ b/scripts/issues/1380/data/uma.json @@ -0,0 +1 @@ +["@uma"] diff --git a/scripts/issues/1380/db-list-dvs b/scripts/issues/1380/db-list-dvs new file mode 100755 index 00000000000..4161f7fdd03 --- /dev/null +++ b/scripts/issues/1380/db-list-dvs @@ -0,0 +1 @@ +psql dvndb -c "select dvobject.id, name, alias, owner_id from dvobject inner join dataverse on dvobject.id = dataverse.id" diff --git a/scripts/issues/1380/dvs.gv b/scripts/issues/1380/dvs.gv new file mode 100644 index 00000000000..526066000a2 --- /dev/null +++ b/scripts/issues/1380/dvs.gv @@ -0,0 +1,19 @@ +digraph { +d1[label="Root"] +d2[label="Top dataverse of Pete"] +d3[label="Pete's public place"] +d4[label="Pete's restricted data"] +d5[label="Pete's secrets"] +d6[label="Top dataverse of Uma"] +d7[label="Uma's first"] +d8[label="Uma's restricted"] + +d1 -> d2 +d2 -> d3 +d2 -> d4 +d2 -> d5 +d1 -> d6 +d6 -> d7 +d6 -> d8 + +} diff --git a/scripts/issues/1380/dvs.pdf b/scripts/issues/1380/dvs.pdf new file mode 100644 index 0000000000000000000000000000000000000000..5169f449420fdf249f8004f90dc9a8930e03f677 GIT binary patch literal 15127 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 literal 0 HcmV?d00001 diff --git a/scripts/issues/1380/explicitGroup1.json b/scripts/issues/1380/explicitGroup1.json new file mode 100644 index 00000000000..337a0b62dcb --- /dev/null +++ b/scripts/issues/1380/explicitGroup1.json @@ -0,0 +1,5 @@ +{ + "description":"Sample Explicit Group", + "displayName":"Close Collaborators", + "aliasInOwner":"eg1" +} diff --git a/scripts/issues/1380/explicitGroup2.json b/scripts/issues/1380/explicitGroup2.json new file mode 100644 index 00000000000..fbac263665c --- /dev/null +++ b/scripts/issues/1380/explicitGroup2.json @@ -0,0 +1,5 @@ +{ + "description":"Sample Explicit Group", + "displayName":"Not-So-Close Collaborators", + "aliasInOwner":"eg2" +} diff --git a/scripts/issues/1380/list-groups-for b/scripts/issues/1380/list-groups-for new file mode 100755 index 00000000000..063b92c9b6a --- /dev/null +++ b/scripts/issues/1380/list-groups-for @@ -0,0 +1,2 @@ +#!/bin/bash +curl -s -X GET http://localhost:8080/api/test/explicitGroups/$1 | jq . diff --git a/src/main/java/edu/harvard/iq/dataverse/Dataverse.java b/src/main/java/edu/harvard/iq/dataverse/Dataverse.java index a4f8abbb9a7..810af11eefe 100644 --- a/src/main/java/edu/harvard/iq/dataverse/Dataverse.java +++ b/src/main/java/edu/harvard/iq/dataverse/Dataverse.java @@ -31,7 +31,6 @@ import javax.validation.constraints.Size; import org.hibernate.validator.constraints.NotBlank; import org.hibernate.validator.constraints.NotEmpty; -import org.hibernate.validator.constraints.URL; /** * diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java b/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java index 2c0322e719c..01f8e72d430 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java @@ -599,6 +599,7 @@ public Response deleteGroup(@PathParam("identifier") String dvIdtf, @POST @Path("{identifier}/groups/{aliasInOwner}/roleAssignees") + @Consumes("application/json") public Response addRoleAssingees(List roleAssingeeIdentifiers, @PathParam("identifier") String dvIdtf, @PathParam("aliasInOwner") String grpAliasInOwner) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/TestApi.java b/src/main/java/edu/harvard/iq/dataverse/api/TestApi.java index b93b5edcce9..8cb5115bf2d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/TestApi.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/TestApi.java @@ -113,7 +113,7 @@ public Response explicitGroupMembership( @PathParam("identifier") String idtf) { return notFound("Can't find a role assignee with identifier " + idtf); } Set groups = explicitGroups.findGroups(roleAssignee); + logger.log(Level.INFO, "Groups for {0}: {1}", new Object[]{roleAssignee, groups}); return okResponse( json(groups) ); - } } 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 549e5191fe0..77a03797c9f 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 @@ -12,7 +12,6 @@ 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.authorization.users.AuthenticatedUser; -import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import java.util.HashMap; import java.util.HashSet; @@ -127,7 +126,7 @@ public Set groupsFor(AuthenticatedUser au) { try { identifierWithoutAtSign = identifier.substring(1); } catch (IndexOutOfBoundsException ex) { - logger.info("Couldn't trim first character (@ sign) from identifier: " + identifier); + logger.log(Level.INFO, "Couldn''t trim first character (@ sign) from identifier: {0}", identifier); } } if (identifierWithoutAtSign != null) { @@ -136,7 +135,7 @@ public Set groupsFor(AuthenticatedUser au) { if (explicitGroup != null) { groups.add(explicitGroup); } else { - logger.info("Couldn't find group based on alias " + groupAlias); + logger.log(Level.INFO, "Couldn''t find group based on alias {0}", groupAlias); } }); } @@ -155,22 +154,6 @@ public Set groupsFor( DataverseRequest dr ) { return groups; } - /** - * This method wraps two existing methods to honor IP groups (see bug at - * https://github.com/IQSS/dataverse/issues/1513 ) but the plan is to - * introduce a better "get *all* groups given a DataverseRequest" method as - * part of https://github.com/IQSS/dataverse/pull/3103 - */ - public Set groupsFor(DataverseRequest dataverseRequest) { - Set groups = groupsFor(dataverseRequest, null); - User user = dataverseRequest.getUser(); - if (user instanceof AuthenticatedUser) { - AuthenticatedUser authenticatedUser = (AuthenticatedUser) user; - groups.addAll(groupsFor(authenticatedUser)); - } - return groups; - } - /** * Given a set of groups and a DV object, return all the groups that are * reachable from the set. Effectively, if the initial set has an {@link ExplicitGroup}, diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/explicit/ExplicitGroup.java b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/explicit/ExplicitGroup.java index b36af8b2fa6..a3213b2f892 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/explicit/ExplicitGroup.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/explicit/ExplicitGroup.java @@ -59,7 +59,13 @@ +"WHERE eg.owner.id=:ownerId AND ra=:raIdtf"), @NamedQuery( name="ExplicitGroup.findByAuthenticatedUserIdentifier", query="SELECT eg FROM ExplicitGroup eg JOIN eg.containedAuthenticatedUsers au " - + "WHERE au.userIdentifier=:authenticatedUserIdentifier") + + "WHERE au.userIdentifier=:authenticatedUserIdentifier"), + @NamedQuery( name="ExplicitGroup.findByRoleAssgineeIdentifier", + query="SELECT eg FROM ExplicitGroup eg JOIN eg.containedRoleAssignees cra " + + "WHERE cra=:roleAssigneeIdentifier"), + @NamedQuery( name="ExplicitGroup.findByContainedExplicitGroupId", + query="SELECT eg FROM ExplicitGroup eg join eg.containedExplicitGroups ceg " + +"WHERE ceg.id=:containedExplicitGroupId") }) @Entity diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/explicit/ExplicitGroupServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/explicit/ExplicitGroupServiceBean.java index ec35a1ee33d..279ccad3df9 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/explicit/ExplicitGroupServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/explicit/ExplicitGroupServiceBean.java @@ -121,15 +121,29 @@ public Set findAvailableFor( DvObject d ) { public Set findGroups( RoleAssignee ra ) { if ( ra instanceof AuthenticatedUser ) { return provider.updateProvider( - new HashSet<>(em.createNamedQuery("ExplicitGroup.findByAuthenticatedUserIdentifier") - .setParameter("authenticatedUserIdentifier", ra.getIdentifier()) - .getResultList() + new HashSet( + em.createNamedQuery("ExplicitGroup.findByAuthenticatedUserIdentifier", ExplicitGroup.class) + .setParameter("authenticatedUserIdentifier", ra.getIdentifier().substring(1)) + .getResultList() )); + } else if ( ra instanceof ExplicitGroup ) { + return provider.updateProvider( + new HashSet( + em.createNamedQuery("ExplicitGroup.findByContainedExplicitGroupId", ExplicitGroup.class) + .setParameter("containedExplicitGroupId", ((ExplicitGroup) ra).getId()) + .getResultList() + )); + } else { - throw new IllegalArgumentException("At this time, only authenticated users are supported"); + return provider.updateProvider( + new HashSet( + em.createNamedQuery("ExplicitGroup.findByRoleAssgineeIdentifier", ExplicitGroup.class) + .setParameter("roleAssigneeIdentifier", ra.getIdentifier()) + .getResultList() + )); } } - + /** * Finds all the groups {@code ra} belongs to in the context of {@code o}. In effect, * collects all the groups {@code ra} belongs to and that are defined at {@code o} diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/users/AuthenticatedUser.java b/src/main/java/edu/harvard/iq/dataverse/authorization/users/AuthenticatedUser.java index f609ed959e2..128678dc525 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 @@ -242,4 +242,9 @@ public void setShibIdentityProvider(String shibIdentityProvider) { this.shibIdentityProvider = shibIdentityProvider; } + @Override + public String toString() { + return "[AuthenticatedUser identifier:" + getIdentifier() + "]"; + } + } From b901933f8aeb09266b58288ccc1960635ce53a23 Mon Sep 17 00:00:00 2001 From: Michael Bar-Sinai Date: Mon, 8 Aug 2016 14:35:48 -0400 Subject: [PATCH 09/35] (working on #1380) Explicit group queries now return groups containing groups as well (might also address #3056?). Validated the logic of group containment by prohibiting groups from containing groups defined on unrelated dataverses. Added tests, and admin API endpoint for listing role assignments for a given role assignee. --- doc/sphinx-guides/source/api/native-api.rst | 10 ++- scripts/issues/1380/data/3-eg1.json | 1 + .../edu/harvard/iq/dataverse/DataFile.java | 5 ++ .../edu/harvard/iq/dataverse/Dataset.java | 6 +- .../edu/harvard/iq/dataverse/Dataverse.java | 10 ++- .../edu/harvard/iq/dataverse/DvObject.java | 7 ++ .../iq/dataverse/RoleAssigneeServiceBean.java | 29 +++---- .../edu/harvard/iq/dataverse/api/Admin.java | 12 ++- .../groups/GroupServiceBean.java | 14 +-- .../groups/impl/explicit/ExplicitGroup.java | 9 +- .../explicit/ExplicitGroupServiceBean.java | 64 +++++++++++++- .../impl/ipaddress/IpGroupProvider.java | 5 +- .../impl/explicit/ExplicitGroupTest.java | 86 +++++++++++++++++++ .../iq/dataverse/mocks/MocksFactory.java | 1 - 14 files changed, 217 insertions(+), 42 deletions(-) create mode 100644 scripts/issues/1380/data/3-eg1.json create mode 100644 src/test/java/edu/harvard/iq/dataverse/authorization/groups/impl/explicit/ExplicitGroupTest.java diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index 37a40e8c06e..85722d5b296 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -157,9 +157,9 @@ To revert to the default logic, use ``:publicationDate`` as the ``$datasetFieldT Note that the dataset field used has to be a date field:: PUT http://$SERVER/api/datasets/$id/citationdate?key=$apiKey - + Restores the default logic of the field type to be used as the citation date. Same as ``PUT`` with ``:publicationDate`` body:: - + DELETE http://$SERVER/api/datasets/$id/citationdate?key=$apiKey List all the role assignments at the given dataset:: @@ -362,6 +362,12 @@ Toggles superuser mode on the ``AuthenticatedUser`` whose ``identifier`` (withou POST http://$SERVER/api/admin/superuser/$identifier +List all role assignments of a role assignee (i.e. a user or a group):: + + GET http://$SERVER/api/admin/assignments/assignee/$identifier + +Note that ``identifier`` can contain slashes (e.g. ``&ip/localhost-users``). + IpGroups ^^^^^^^^ diff --git a/scripts/issues/1380/data/3-eg1.json b/scripts/issues/1380/data/3-eg1.json new file mode 100644 index 00000000000..a874d69a2e8 --- /dev/null +++ b/scripts/issues/1380/data/3-eg1.json @@ -0,0 +1 @@ +["&explicit/3-eg1"] diff --git a/src/main/java/edu/harvard/iq/dataverse/DataFile.java b/src/main/java/edu/harvard/iq/dataverse/DataFile.java index 8afd52ac181..0ebd440e009 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataFile.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataFile.java @@ -228,6 +228,11 @@ public String getOriginalFileFormat() { return null; } + @Override + public boolean isAncestorOf( DvObject other ) { + return equals(other); + } + /* * A user-friendly version of the "original format": */ diff --git a/src/main/java/edu/harvard/iq/dataverse/Dataset.java b/src/main/java/edu/harvard/iq/dataverse/Dataset.java index c8dc4a7dad7..760d349d1b4 100644 --- a/src/main/java/edu/harvard/iq/dataverse/Dataset.java +++ b/src/main/java/edu/harvard/iq/dataverse/Dataset.java @@ -626,5 +626,9 @@ public String getDisplayName() { protected boolean isPermissionRoot() { return false; } - + + @Override + public boolean isAncestorOf( DvObject other ) { + return equals(other) || equals(other.getOwner()); + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/Dataverse.java b/src/main/java/edu/harvard/iq/dataverse/Dataverse.java index 810af11eefe..49136f1dd53 100644 --- a/src/main/java/edu/harvard/iq/dataverse/Dataverse.java +++ b/src/main/java/edu/harvard/iq/dataverse/Dataverse.java @@ -728,6 +728,14 @@ public boolean isPermissionRoot() { public void setPermissionRoot(boolean permissionRoot) { this.permissionRoot = permissionRoot; } - + + @Override + public boolean isAncestorOf( DvObject other ) { + while ( other != null ) { + if ( equals(other) ) return true; + other = other.getOwner(); + } + return false; + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/DvObject.java b/src/main/java/edu/harvard/iq/dataverse/DvObject.java index 89c27d3d4f4..47d4878f4cc 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DvObject.java +++ b/src/main/java/edu/harvard/iq/dataverse/DvObject.java @@ -283,6 +283,13 @@ public Dataverse getDataverseContext() { return null; } + /** + * + * @param other + * @return {@code true} iff {@code other} is {@code this} or below {@code this} in the containment hierarchy. + */ + public abstract boolean isAncestorOf( DvObject other ); + @OneToMany(mappedBy = "definitionPoint",cascade={ CascadeType.REMOVE, CascadeType.MERGE,CascadeType.PERSIST}, orphanRemoval=true) List roleAssignments; } diff --git a/src/main/java/edu/harvard/iq/dataverse/RoleAssigneeServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/RoleAssigneeServiceBean.java index 457bdc56ade..f16ced6630d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/RoleAssigneeServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/RoleAssigneeServiceBean.java @@ -18,6 +18,7 @@ import java.util.Set; import java.util.TreeMap; import java.util.logging.Logger; +import java.util.stream.Collectors; import javax.annotation.PostConstruct; import javax.ejb.EJB; import javax.ejb.Stateless; @@ -133,7 +134,7 @@ public List getAssigneeDataverseRoleFor(AuthenticatedUser au) { } List retList = new ArrayList(); roleAssigneeIdentifier = roleAssigneeIdentifier.replaceAll("\\s", ""); // remove spaces from string - List userGroups = getUserExplicitGroups(roleAssigneeIdentifier.replace("@", "")); + List userGroups = getUserExplicitGroups(au); List userRunTimeGroups = getUserRuntimeGroups(au); String identifierClause = " WHERE r.assigneeIdentifier= '" + roleAssigneeIdentifier + "'"; if (userGroups != null || userRunTimeGroups != null) { @@ -161,7 +162,7 @@ public List getAssigneeAndRoleIdListFor(AuthenticatedUser au, List userExplicitGroups = getUserExplicitGroups(roleAssigneeIdentifier.replace("@", "")); + List userExplicitGroups = getUserExplicitGroups(au); List userRunTimeGroups = getUserRuntimeGroups(au); String identifierClause = " WHERE r.assigneeIdentifier= '" + roleAssigneeIdentifier + "'"; if (userExplicitGroups != null || userRunTimeGroups != null) { @@ -185,7 +186,7 @@ public List getRoleIdListForGivenAssigneeDvObject(AuthenticatedUser au, Li return null; } roleAssigneeIdentifier = roleAssigneeIdentifier.replaceAll("\\s", ""); // remove spaces from string - List userGroups = getUserExplicitGroups(roleAssigneeIdentifier.replace("@", "")); + List userGroups = getUserExplicitGroups(au); List userRunTimeGroups = getUserRuntimeGroups(au); String identifierClause = " WHERE r.assigneeIdentifier= '" + roleAssigneeIdentifier + "'"; if (userGroups != null || userRunTimeGroups != null) { @@ -252,7 +253,7 @@ public List getRoleIdsFor(AuthenticatedUser au, List dvObjectIdL } roleAssigneeIdentifier = roleAssigneeIdentifier.replaceAll("\\s", ""); // remove spaces from string - List userGroups = getUserExplicitGroups(roleAssigneeIdentifier.replace("@", "")); + List userGroups = getUserExplicitGroups(au); List userRunTimeGroups = getUserRuntimeGroups(au); String identifierClause = " WHERE r.assigneeIdentifier= '" + roleAssigneeIdentifier + "'"; if (userGroups != null || userRunTimeGroups != null) { @@ -289,20 +290,14 @@ private String getDvObjectIdListClause(List dvObjectIdList) { } /** - * @todo Support groups within groups: - * https://github.com/IQSS/dataverse/issues/3056 + * @param ra + * @todo Support groups within groups: https://github.com/IQSS/dataverse/issues/3056 + * @return List of aliases of all explicit groups {@code ra} is in. */ - public List getUserExplicitGroups(String roleAssigneeIdentifier) { - - String qstr = "select groupalias from explicitgroup"; - qstr += " where id in "; - qstr += " (select explicitgroup_id from explicitgroup_authenticateduser where containedauthenticatedusers_id = "; - qstr += " (select id from authenticateduser where useridentifier ='" + roleAssigneeIdentifier + "'"; - qstr += "));"; - //msg("qstr: " + qstr); - - return em.createNativeQuery(qstr) - .getResultList(); + public List getUserExplicitGroups(RoleAssignee ra) { + return explicitGroupSvc.findGroups(ra).stream() + .map( g -> g.getAlias()) + .collect(Collectors.toList()); } private List getUserRuntimeGroups(AuthenticatedUser au) { diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Admin.java b/src/main/java/edu/harvard/iq/dataverse/api/Admin.java index 0c131871282..b066714ff2a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Admin.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Admin.java @@ -520,5 +520,15 @@ public Response validate() { } return okResponse(msg); } - + + + @Path("assignments/assignees/{raIdtf: .*}") + @GET + public Response getAssignmentsFor( @PathParam("raIdtf") String raIdtf ) { + + JsonArrayBuilder arr = Json.createArrayBuilder(); + roleAssigneeSvc.getAssignmentsFor(raIdtf).forEach( a -> arr.add(json(a))); + + return okResponse(arr); + } } 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 77a03797c9f..5e42bd560c2 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 @@ -121,24 +121,14 @@ public Set groupsFor(AuthenticatedUser au) { Set groups = new HashSet<>(); groups.addAll(groupsFor(au, null)); String identifier = au.getIdentifier(); - String identifierWithoutAtSign = null; if (identifier != null) { try { - identifierWithoutAtSign = identifier.substring(1); + groups.addAll( explicitGroupService.findGroups(au) ); } catch (IndexOutOfBoundsException ex) { logger.log(Level.INFO, "Couldn''t trim first character (@ sign) from identifier: {0}", identifier); } } - if (identifierWithoutAtSign != null) { - roleAssigneeSvc.getUserExplicitGroups(identifierWithoutAtSign).stream().forEach((groupAlias) -> { - ExplicitGroup explicitGroup = explicitGroupProvider.get(groupAlias); - if (explicitGroup != null) { - groups.add(explicitGroup); - } else { - logger.log(Level.INFO, "Couldn''t find group based on alias {0}", groupAlias); - } - }); - } + return groups; } diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/explicit/ExplicitGroup.java b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/explicit/ExplicitGroup.java index a3213b2f892..d03eea3c45d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/explicit/ExplicitGroup.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/explicit/ExplicitGroup.java @@ -158,7 +158,8 @@ public void add( User u ) { * Adds the {@link RoleAssignee} to {@code this} group. * * @param ra the role assignee to be added to this group. - * @throws GroupException if {@code ra} is a group, and is an ancestor of {@code this}. + * @throws GroupException if {@code ra} is a group, and is either an ancestor of {@code this}, + * or is defined in a dataverse that is not an ancestor of {@code this.owner}. */ public void add( RoleAssignee ra ) throws GroupException { @@ -176,7 +177,11 @@ public void add( RoleAssignee ra ) throws GroupException { if ( g.contains(this) ) { throw new GroupException(this, "A group cannot be added to one of its childs."); } - containedExplicitGroups.add( g ); + if ( g.owner.isAncestorOf(owner) ) { + containedExplicitGroups.add( g ); + } else { + throw new GroupException(this, "Cannot add " + g + ", as it is not defined in " + owner + " or one of its ancestors."); + } } else { containedRoleAssignees.add( ra.getIdentifier() ); } diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/explicit/ExplicitGroupServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/explicit/ExplicitGroupServiceBean.java index 279ccad3df9..a7d0f0babba 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/explicit/ExplicitGroupServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/explicit/ExplicitGroupServiceBean.java @@ -10,6 +10,7 @@ import java.util.List; import java.util.Set; import java.util.TreeSet; +import java.util.stream.Collectors; import javax.annotation.PostConstruct; import javax.ejb.EJB; import javax.ejb.Stateless; @@ -119,6 +120,18 @@ public Set findAvailableFor( DvObject d ) { * @return set of the explicit groups that contain {@code ra}. */ public Set findGroups( RoleAssignee ra ) { + return findClosure(findDirectGroups(ra)); + } + + /** + * Finds all the explicit groups {@code ra} is directly a member of. + * To find all these groups and the groups the contain them (recursively upwards), + * consider using {@link #findGroups(edu.harvard.iq.dataverse.authorization.RoleAssignee)} + * @param ra the role assignee whose membership list we seek + * @return set of the explicit groups that contain {@code ra} directly. + * @see #findGroups(edu.harvard.iq.dataverse.authorization.RoleAssignee) + */ + public Set findDirectGroups( RoleAssignee ra ) { if ( ra instanceof AuthenticatedUser ) { return provider.updateProvider( new HashSet( @@ -133,7 +146,6 @@ public Set findGroups( RoleAssignee ra ) { .setParameter("containedExplicitGroupId", ((ExplicitGroup) ra).getId()) .getResultList() )); - } else { return provider.updateProvider( new HashSet( @@ -145,15 +157,32 @@ public Set findGroups( RoleAssignee ra ) { } /** - * Finds all the groups {@code ra} belongs to in the context of {@code o}. In effect, + * Finds all the groups {@code ra} is a member of, in the context of {@code o}. + * This includes both direct and indirect memberships. + * @param ra The role assignee whose memberships we seek. + * @param o The {@link DvObject} whose context we search. + * @return All the groups in {@code o}'s context that {@code ra} is a member of. + */ + public Set findGroups( RoleAssignee ra, DvObject o ) { + Set directGroups = findDirectGroups(ra, o); + Set closure = findClosure(directGroups); + return closure.stream() + .filter( g -> g.owner.isAncestorOf(o) ) + .collect( Collectors.toSet() ); + } + + /** + * Finds all the groups {@code ra} directly belongs to in the context of {@code o}. In effect, * collects all the groups {@code ra} belongs to and that are defined at {@code o} - * or one of its ancestors. Does not take group containment into account. + * or one of its ancestors. + * + * Does not take group containment into account. Use * * @param ra The role assignee that belongs to the groups * @param o the DvObject that defines the context of the search. * @return All the groups ra belongs to in the context of o. */ - public Set findGroups( RoleAssignee ra, DvObject o ) { + public Set findDirectGroups( RoleAssignee ra, DvObject o ) { if ( o == null ) return Collections.emptySet(); List groupList = new LinkedList<>(); @@ -185,4 +214,31 @@ public Set findGroups( RoleAssignee ra, DvObject o ) { return provider.updateProvider( new HashSet<>(groupList) ); } + /** + * + * Finds all the groups that contain the groups in {@code seed} (including {@code seed}), and the + * groups that contain these groups, an so on. + * + * @param seed the initial set of groups. + * @return Transitive closure (based on group containment) of the groups in {@code seed}. + */ + protected Set findClosure( Set seed ) { + Set result = new HashSet<>(); + + // The set of groups whose parents were not visited yet. + Set fringe = new HashSet<>(seed); + while ( ! fringe.isEmpty() ) { + ExplicitGroup g = fringe.iterator().next(); + fringe.remove(g); + result.add(g); + + // add all of g's parents to the fringe, unless already visited. + findDirectGroups(g).stream() + .filter( eg -> !(result.contains(eg)||fringe.contains(eg) )) + .forEach( fringe::add ); + } + + return result; + } + } diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/IpGroupProvider.java b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/IpGroupProvider.java index efce422d5d6..f784ef92d9f 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/IpGroupProvider.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/IpGroupProvider.java @@ -8,6 +8,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.Set; +import java.util.logging.Level; import java.util.logging.Logger; /** @@ -42,7 +43,9 @@ public Set groupsFor(RoleAssignee ra, DvObject o) { @Override public Set groupsFor( DataverseRequest req, DvObject dvo ) { if ( req.getSourceAddress() != null ) { - return updateProvider( ipGroupsService.findAllIncludingIp(req.getSourceAddress()) ); + final Set groups = updateProvider( ipGroupsService.findAllIncludingIp(req.getSourceAddress()) ); + logger.log(Level.INFO, "IP groups detected: {0}", groups); + return groups; } else { return Collections.emptySet(); } diff --git a/src/test/java/edu/harvard/iq/dataverse/authorization/groups/impl/explicit/ExplicitGroupTest.java b/src/test/java/edu/harvard/iq/dataverse/authorization/groups/impl/explicit/ExplicitGroupTest.java new file mode 100644 index 00000000000..9d08fd0cfa0 --- /dev/null +++ b/src/test/java/edu/harvard/iq/dataverse/authorization/groups/impl/explicit/ExplicitGroupTest.java @@ -0,0 +1,86 @@ +/* + * (C) Michael Bar-Sinai + */ +package edu.harvard.iq.dataverse.authorization.groups.impl.explicit; + +import edu.harvard.iq.dataverse.Dataverse; +import edu.harvard.iq.dataverse.authorization.groups.GroupException; +import edu.harvard.iq.dataverse.mocks.MocksFactory; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import org.junit.Test; + +/** + * + * @author michael + */ +public class ExplicitGroupTest { + + + ExplicitGroupProvider prv = new ExplicitGroupProvider(null, null); + + public ExplicitGroupTest() { + } + + @Test( expected=GroupException.class ) + public void addGroupToSelf() throws Exception { + ExplicitGroup sut = new ExplicitGroup(); + sut.setDisplayName("a group"); + sut.add( sut ); + fail("A group cannot be added to itself."); + } + + @Test( expected=GroupException.class ) + public void addGroupToDescendant() throws GroupException{ + Dataverse dv = MocksFactory.makeDataverse(); + ExplicitGroup root = new ExplicitGroup(prv); + root.setId( MocksFactory.nextId() ); + root.setGroupAliasInOwner("top"); + ExplicitGroup sub = new ExplicitGroup(prv); + sub.setGroupAliasInOwner("sub"); + sub.setId( MocksFactory.nextId() ); + ExplicitGroup subSub = new ExplicitGroup(prv); + subSub.setGroupAliasInOwner("subSub"); + subSub.setId( MocksFactory.nextId() ); + root.setOwner(dv); + sub.setOwner(dv); + subSub.setOwner(dv); + + sub.add( subSub ); + root.add( sub ); + subSub.add(root); + fail("A group cannot contain its parent"); + } + + @Test( expected=GroupException.class ) + public void addGroupToUnrealtedGroup() throws GroupException { + Dataverse dv1 = MocksFactory.makeDataverse(); + Dataverse dv2 = MocksFactory.makeDataverse(); + ExplicitGroup g1 = new ExplicitGroup(prv); + ExplicitGroup g2 = new ExplicitGroup(prv); + g1.setOwner(dv1); + g2.setOwner(dv2); + + g1.add(g2); + fail("An explicit group cannot contain an explicit group defined in " + + "a dataverse that's not an ancestor of that group's owner dataverse."); + + } + + @Test + public void addGroup() throws GroupException { + Dataverse dvParent = MocksFactory.makeDataverse(); + Dataverse dvSub = MocksFactory.makeDataverse(); + dvSub.setOwner(dvParent); + + ExplicitGroup g1 = new ExplicitGroup(prv); + ExplicitGroup g2 = new ExplicitGroup(prv); + g1.setOwner(dvSub); + g2.setOwner(dvParent); + + g1.add(g2); + assertTrue( g1.contains(g2) ); + } +} + + 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 85995ce37f0..2b08189de67 100644 --- a/src/test/java/edu/harvard/iq/dataverse/mocks/MocksFactory.java +++ b/src/test/java/edu/harvard/iq/dataverse/mocks/MocksFactory.java @@ -22,7 +22,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Date; -import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Random; From 91df48cb78fbfbd4d5407edc830ca2071e503683 Mon Sep 17 00:00:00 2001 From: Michael Bar-Sinai Date: Tue, 9 Aug 2016 12:15:26 -0400 Subject: [PATCH 10/35] #1380: Fixed an issue with ExplicitGroup containment detection. Updated Access logic to support restricted files --- .../iq/dataverse/RoleAssigneeServiceBean.java | 4 +- .../edu/harvard/iq/dataverse/api/Access.java | 7 +- .../harvard/iq/dataverse/api/Dataverses.java | 1 + .../groups/impl/explicit/ExplicitGroup.java | 94 ++++++++--- .../groups/impl/ipaddress/IpGroup.java | 2 + .../groups/impl/ipaddress/ip/IPv4Range.java | 1 + .../groups/impl/ipaddress/ip/IPv6Range.java | 1 + .../authorization/users/GuestUser.java | 7 +- .../impl/explicit/ExplicitGroupTest.java | 152 ++++++++++++++++-- .../groups/impl/ipaddress/IpGroupTest.java | 91 +++++++++++ .../impl/CreateDataverseCommandTest.java | 2 +- .../impl/UpdatePermissionRootCommandTest.java | 14 -- .../mocks/MockRoleAssigneeServiceBean.java | 44 +++++ .../iq/dataverse/mocks/MocksFactory.java | 28 +++- 14 files changed, 392 insertions(+), 56 deletions(-) create mode 100644 src/test/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/IpGroupTest.java create mode 100644 src/test/java/edu/harvard/iq/dataverse/mocks/MockRoleAssigneeServiceBean.java diff --git a/src/main/java/edu/harvard/iq/dataverse/RoleAssigneeServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/RoleAssigneeServiceBean.java index f16ced6630d..424516bdfaf 100644 --- a/src/main/java/edu/harvard/iq/dataverse/RoleAssigneeServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/RoleAssigneeServiceBean.java @@ -52,10 +52,10 @@ public class RoleAssigneeServiceBean { @EJB DataverseRoleServiceBean dataverseRoleService; - Map predefinedRoleAssignees = new TreeMap<>(); + protected Map predefinedRoleAssignees = new TreeMap<>(); @PostConstruct - void setup() { + protected void setup() { GuestUser gu = GuestUser.get(); predefinedRoleAssignees.put(gu.getIdentifier(), gu); predefinedRoleAssignees.put(AuthenticatedUsers.get().getIdentifier(), AuthenticatedUsers.get()); diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Access.java b/src/main/java/edu/harvard/iq/dataverse/api/Access.java index dea69ec3c06..757fa0bebfd 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Access.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Access.java @@ -1088,6 +1088,7 @@ private boolean isAccessAuthorized(DataFile df, String apiToken) { } else { if (apiTokenUser != null && permissionService.requestOn(createDataverseRequest(apiTokenUser), df.getOwner()).has(Permission.ViewUnpublishedDataset)) { logger.log(Level.FINE, "Token-based auth: user {0} is granted access to the restricted, unpublished datafile.", apiTokenUser.getIdentifier()); + return true; } } } @@ -1132,15 +1133,15 @@ private boolean isAccessAuthorized(DataFile df, String apiToken) { return false; } - if (permissionService.userOn(user, df).has(Permission.DownloadFile)) { + if (permissionService.requestOn(createDataverseRequest(user), df).has(Permission.DownloadFile)) { if (published) { logger.log(Level.FINE, "API token-based auth: User {0} has rights to access the datafile.", user.getIdentifier()); return true; } else { // if the file is NOT published, we will let them download the // file ONLY if they also have the permission to view - // unpublished verions: - if (permissionService.userOn(user, df.getOwner()).has(Permission.ViewUnpublishedDataset)) { + // unpublished versions: + if (permissionService.requestOn(createDataverseRequest(user), df.getOwner()).has(Permission.ViewUnpublishedDataset)) { logger.log(Level.FINE, "API token-based auth: User {0} has rights to access the (unpublished) datafile.", user.getIdentifier()); return true; } else { diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java b/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java index 01f8e72d430..fc42091585a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java @@ -79,6 +79,7 @@ import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import static edu.harvard.iq.dataverse.util.json.JsonPrinter.json; +import static edu.harvard.iq.dataverse.util.json.JsonPrinter.json; /** * A REST API for dataverses. diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/explicit/ExplicitGroup.java b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/explicit/ExplicitGroup.java index d03eea3c45d..5a3583a1713 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/explicit/ExplicitGroup.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/explicit/ExplicitGroup.java @@ -7,6 +7,7 @@ import edu.harvard.iq.dataverse.authorization.RoleAssigneeDisplayInfo; import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.authorization.groups.GroupException; +import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IpAddress; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import java.util.HashSet; import java.util.Objects; @@ -174,7 +175,7 @@ public void add( RoleAssignee ra ) throws GroupException { if ( ra instanceof ExplicitGroup ) { // validate no circular deps ExplicitGroup g = (ExplicitGroup) ra; - if ( g.contains(this) ) { + if ( g.structuralContains(this) ) { throw new GroupException(this, "A group cannot be added to one of its childs."); } if ( g.owner.isAncestorOf(owner) ) { @@ -244,42 +245,89 @@ public void setDescription(String description) { @Override public boolean contains(DataverseRequest req) { - return contains( req.getUser() ); + return containsDirectly(req) || containsIndirectly(req); } - public boolean contains(RoleAssignee ra) { - return containsDirectly(ra) || containsIndirectly(ra); - } - - protected boolean containsDirectly( RoleAssignee ra ) { + /** + * Looks at structural containment: whether {@code ra} is part of the + * group's structure. It mostly the same as {@link #contains(edu.harvard.iq.dataverse.engine.command.DataverseRequest)}, + * except for logical containment. So if an ExplicitGroup contains {@link AuthenticatedUsers} but not + * a specific {@link AuthenticatedUser} {@code u}, {@code structuralContains(u)} + * would return {@code false} while {@code contains( request(u, ...) )} would return true; + * + * @param ra + * @return {@code true} iff the role assignee is structurally a part of the group. + */ + public boolean structuralContains(RoleAssignee ra) { + // direct containment if ( ra instanceof AuthenticatedUser ) { - AuthenticatedUser au = (AuthenticatedUser) ra; - return containedAuthenticatedUsers.contains(au); + if ( containedAuthenticatedUsers.contains((AuthenticatedUser)ra) ) { + return true; + } } else if ( ra instanceof ExplicitGroup ) { - ExplicitGroup eg = (ExplicitGroup) ra; - return containedExplicitGroups.contains(eg); + if ( containedExplicitGroups.contains((ExplicitGroup)ra) ) { + return true; + } } else { - return containedRoleAssignees.contains( ra.getIdentifier() ); + if ( containedRoleAssignees.contains(ra.getIdentifier()) ) { + return true; + } } - } - - private boolean containsIndirectly(RoleAssignee ra) { - for ( ExplicitGroup ceg : containedExplicitGroups ) { - if ( ceg.contains(ra) ) { + + // no direct containment. Recurse. + for ( ExplicitGroup eg: containedExplicitGroups ) { + if ( eg.structuralContains(ra) ) { return true; } } - for ( String containedRAIdtf : containedRoleAssignees ) { - RoleAssignee containedRa = provider.findRoleAssignee(containedRAIdtf); - if ( containedRa != null ) { - if ( containedRa instanceof ExplicitGroup ) { - if (((ExplicitGroup)containedRa).contains(ra)) { + return false; + + } + + /** + * @param req + * @return {@code true} iff the request is contained in the group or in an included non-explicit group. + */ + protected boolean containsDirectly( DataverseRequest req ) { + User ra = req.getUser(); + if ( ra instanceof AuthenticatedUser ) { + AuthenticatedUser au = (AuthenticatedUser) ra; + if ( containedAuthenticatedUsers.contains(au) ) { + return true; + } + } + + if ( containedRoleAssignees.contains(ra.getIdentifier()) ) { + return true; + } + + for ( String craIdtf : containedRoleAssignees ) { + // Need to retrieve the actual role assingee, and let it's logic decide. + RoleAssignee cra = provider.findRoleAssignee(craIdtf); + if ( cra != null ) { + if ( cra instanceof Group ) { + Group cgrp = (Group) cra; + if ( cgrp.contains(req) ) { return true; } - } + } // if cra is a user, we would have returned after the .contains() test. + } + } + // If we get here, the request is not in this group. + return false; + } + + /** + * @param req + * @return {@code true} iff the request if contained in an explicit group that's a member of this group. + */ + private boolean containsIndirectly(DataverseRequest req) { + for ( ExplicitGroup ceg : containedExplicitGroups ) { + if ( ceg.contains(req) ) { + return true; } } return false; diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/IpGroup.java b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/IpGroup.java index e04b6ccd3db..82deac0f6a9 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/IpGroup.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/IpGroup.java @@ -45,6 +45,8 @@ public IpGroup(IpGroupProvider provider) { @Override public boolean contains( DataverseRequest rq ) { IpAddress addr = rq.getSourceAddress(); + if ( addr == null ) return false; + for ( IpAddressRange r : ((addr instanceof IPv4Address) ? ipv4Ranges : ipv6Ranges) ) { Boolean containment = r.contains(addr); if ( (containment != null) && containment ) { diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/ip/IPv4Range.java b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/ip/IPv4Range.java index 3b6348737ed..0d4f8575c5a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/ip/IPv4Range.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/ip/IPv4Range.java @@ -79,6 +79,7 @@ public void setBottomAsLong(long bottomAsLong) { @Override public Boolean contains(IpAddress anAddress) { + if ( anAddress == null ) return null; if ( anAddress instanceof IPv4Address ) { IPv4Address adr = (IPv4Address) anAddress; return getBottom().compareTo(adr)<=0 && getTop().compareTo(adr)>=0; diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/ip/IPv6Range.java b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/ip/IPv6Range.java index 5beaead8bd9..d1301d550c7 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/ip/IPv6Range.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/ip/IPv6Range.java @@ -48,6 +48,7 @@ public IPv6Range() {} @Override public Boolean contains(IpAddress anAddress) { + if ( anAddress == null ) return null; if ( anAddress instanceof IPv6Address ) { IPv6Address adr = (IPv6Address) anAddress; return getBottom().compareTo(adr)<=0 && getTop().compareTo(adr)>=0; 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 45177beace5..b2fd3194f70 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 @@ -42,7 +42,12 @@ public boolean isSuperuser() { public boolean equals( Object o ) { return (o instanceof GuestUser); } - + + @Override + public String toString() { + return "[GuestUser :guest]"; + } + @Override public int hashCode() { return 7; diff --git a/src/test/java/edu/harvard/iq/dataverse/authorization/groups/impl/explicit/ExplicitGroupTest.java b/src/test/java/edu/harvard/iq/dataverse/authorization/groups/impl/explicit/ExplicitGroupTest.java index 9d08fd0cfa0..543d3ab1eeb 100644 --- a/src/test/java/edu/harvard/iq/dataverse/authorization/groups/impl/explicit/ExplicitGroupTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/authorization/groups/impl/explicit/ExplicitGroupTest.java @@ -5,7 +5,18 @@ import edu.harvard.iq.dataverse.Dataverse; import edu.harvard.iq.dataverse.authorization.groups.GroupException; -import edu.harvard.iq.dataverse.mocks.MocksFactory; +import edu.harvard.iq.dataverse.authorization.groups.impl.builtin.AllUsers; +import edu.harvard.iq.dataverse.authorization.groups.impl.builtin.AuthenticatedUsers; +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.ipaddress.ip.IpAddress; +import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IpAddressRange; +import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; +import edu.harvard.iq.dataverse.authorization.users.GuestUser; +import edu.harvard.iq.dataverse.engine.command.DataverseRequest; +import edu.harvard.iq.dataverse.mocks.MockRoleAssigneeServiceBean; +import static edu.harvard.iq.dataverse.mocks.MocksFactory.*; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import org.junit.Test; @@ -17,7 +28,8 @@ public class ExplicitGroupTest { - ExplicitGroupProvider prv = new ExplicitGroupProvider(null, null); + MockRoleAssigneeServiceBean roleAssigneeSvc = new MockRoleAssigneeServiceBean(); + ExplicitGroupProvider prv = new ExplicitGroupProvider(null, roleAssigneeSvc); public ExplicitGroupTest() { } @@ -32,16 +44,16 @@ public void addGroupToSelf() throws Exception { @Test( expected=GroupException.class ) public void addGroupToDescendant() throws GroupException{ - Dataverse dv = MocksFactory.makeDataverse(); + Dataverse dv = makeDataverse(); ExplicitGroup root = new ExplicitGroup(prv); - root.setId( MocksFactory.nextId() ); + root.setId( nextId() ); root.setGroupAliasInOwner("top"); ExplicitGroup sub = new ExplicitGroup(prv); sub.setGroupAliasInOwner("sub"); - sub.setId( MocksFactory.nextId() ); + sub.setId( nextId() ); ExplicitGroup subSub = new ExplicitGroup(prv); subSub.setGroupAliasInOwner("subSub"); - subSub.setId( MocksFactory.nextId() ); + subSub.setId( nextId() ); root.setOwner(dv); sub.setOwner(dv); subSub.setOwner(dv); @@ -54,8 +66,8 @@ public void addGroupToDescendant() throws GroupException{ @Test( expected=GroupException.class ) public void addGroupToUnrealtedGroup() throws GroupException { - Dataverse dv1 = MocksFactory.makeDataverse(); - Dataverse dv2 = MocksFactory.makeDataverse(); + Dataverse dv1 = makeDataverse(); + Dataverse dv2 = makeDataverse(); ExplicitGroup g1 = new ExplicitGroup(prv); ExplicitGroup g2 = new ExplicitGroup(prv); g1.setOwner(dv1); @@ -69,8 +81,8 @@ public void addGroupToUnrealtedGroup() throws GroupException { @Test public void addGroup() throws GroupException { - Dataverse dvParent = MocksFactory.makeDataverse(); - Dataverse dvSub = MocksFactory.makeDataverse(); + Dataverse dvParent = makeDataverse(); + Dataverse dvSub = makeDataverse(); dvSub.setOwner(dvParent); ExplicitGroup g1 = new ExplicitGroup(prv); @@ -79,7 +91,125 @@ public void addGroup() throws GroupException { g2.setOwner(dvParent); g1.add(g2); - assertTrue( g1.contains(g2) ); + assertTrue( g1.structuralContains(g2) ); + } + + @Test + public void adds() throws GroupException { + Dataverse dvParent = makeDataverse(); + ExplicitGroup g1 = new ExplicitGroup(prv); + g1.setOwner(dvParent); + + AuthenticatedUser au1 = makeAuthenticatedUser("Lauren", "Ipsum"); + g1.add(au1); + g1.add( GuestUser.get() ); + + assertTrue( g1.structuralContains(GuestUser.get()) ); + assertTrue( g1.structuralContains(au1) ); + assertFalse( g1.structuralContains(makeAuthenticatedUser("Sima", "Kneidle")) ); + assertFalse( g1.structuralContains(AllUsers.get()) ); + } + + + @Test + public void recursiveStructuralContainment() throws GroupException { + Dataverse dvParent = makeDataverse(); + ExplicitGroup parentGroup = roleAssigneeSvc.add(makeExplicitGroup(prv)); + ExplicitGroup childGroup = roleAssigneeSvc.add(makeExplicitGroup(prv)); + ExplicitGroup grandChildGroup = roleAssigneeSvc.add(makeExplicitGroup(prv)); + parentGroup.setOwner(dvParent); + childGroup.setOwner(dvParent); + grandChildGroup.setOwner(dvParent); + + childGroup.add(grandChildGroup); + parentGroup.add(childGroup); + + AuthenticatedUser au = roleAssigneeSvc.add(makeAuthenticatedUser("Jane", "Doe")); + grandChildGroup.add( au ); + childGroup.add( GuestUser.get() ); + + assertTrue( grandChildGroup.structuralContains(au) ); + assertTrue( childGroup.structuralContains(au) ); + assertTrue( parentGroup.structuralContains(au) ); + + assertTrue( childGroup.structuralContains(GuestUser.get()) ); + assertTrue( parentGroup.structuralContains(GuestUser.get()) ); + + grandChildGroup.remove(au); + + assertFalse( grandChildGroup.structuralContains(au) ); + assertFalse( childGroup.structuralContains(au) ); + assertFalse( parentGroup.structuralContains(au) ); + + childGroup.add( AuthenticatedUsers.get() ); + + assertFalse( grandChildGroup.structuralContains(au) ); + assertFalse( childGroup.structuralContains(au) ); + assertFalse( parentGroup.structuralContains(au) ); + assertTrue( childGroup.structuralContains(AuthenticatedUsers.get()) ); + + final IpGroup ipGroup = new IpGroup(new IpGroupProvider(null)); + grandChildGroup.add(ipGroup); + ipGroup.add( IpAddressRange.make(IpAddress.valueOf("0.0.1.1"), IpAddress.valueOf("0.0.255.255")) ); + + assertTrue( grandChildGroup.structuralContains(ipGroup) ); + assertTrue( childGroup.structuralContains(ipGroup) ); + assertTrue( parentGroup.structuralContains(ipGroup) ); + } + + @Test + public void recursiveLogicalContainment() throws GroupException { + Dataverse dvParent = makeDataverse(); + ExplicitGroup parentGroup = roleAssigneeSvc.add(makeExplicitGroup("parent", prv)); + ExplicitGroup childGroup = roleAssigneeSvc.add(makeExplicitGroup("child", prv)); + ExplicitGroup grandChildGroup = roleAssigneeSvc.add(makeExplicitGroup("grandChild", prv)); + parentGroup.setOwner(dvParent); + childGroup.setOwner(dvParent); + grandChildGroup.setOwner(dvParent); + + childGroup.add(grandChildGroup); + parentGroup.add(childGroup); + + AuthenticatedUser au = roleAssigneeSvc.add(makeAuthenticatedUser("Jane", "Doe")); + grandChildGroup.add( au ); + childGroup.add( GuestUser.get() ); + DataverseRequest auReq = makeRequest(au); + DataverseRequest guestReq = makeRequest(); + + assertTrue( grandChildGroup.contains(auReq) ); + assertTrue( childGroup.contains(auReq) ); + assertTrue( parentGroup.contains(auReq) ); + + assertTrue( childGroup.contains(guestReq) ); + assertTrue( parentGroup.contains(guestReq) ); + + grandChildGroup.remove(au); + + assertFalse( grandChildGroup.contains(auReq) ); + assertFalse( childGroup.contains(auReq) ); + assertFalse( parentGroup.contains(auReq) ); + + childGroup.add( AuthenticatedUsers.get() ); + + assertFalse( grandChildGroup.contains(auReq) ); + assertTrue( childGroup.contains(auReq) ); + assertTrue( parentGroup.contains(auReq) ); + + final IpGroup ipGroup = roleAssigneeSvc.add( new IpGroup(new IpGroupProvider(null)) ); + grandChildGroup.add(ipGroup); + ipGroup.add( IpAddressRange.make(IpAddress.valueOf("0.0.1.1"), IpAddress.valueOf("0.0.255.255")) ); + final IpAddress ip = IpAddress.valueOf("0.0.128.128"); + final DataverseRequest request = new DataverseRequest(GuestUser.get(), ip); + + assertTrue( ipGroup.contains(request) ); + assertTrue( grandChildGroup.contains(request) ); + assertTrue( parentGroup.contains(request) ); + + childGroup.add( GuestUser.get() ); + assertTrue( childGroup.contains(guestReq) ); + assertTrue( parentGroup.contains(guestReq) ); + assertFalse( grandChildGroup.contains(guestReq) ); + } } diff --git a/src/test/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/IpGroupTest.java b/src/test/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/IpGroupTest.java new file mode 100644 index 00000000000..b6a3b862435 --- /dev/null +++ b/src/test/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/IpGroupTest.java @@ -0,0 +1,91 @@ +package edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress; + +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.authorization.users.GuestUser; +import edu.harvard.iq.dataverse.engine.command.DataverseRequest; +import edu.harvard.iq.dataverse.mocks.MocksFactory; +import org.junit.Test; +import static org.junit.Assert.*; + +/** + * + * @author michael + */ +public class IpGroupTest { + + public IpGroupTest() { + } + + /** + * Test of contains method, of class IpGroup. + */ + @Test + public void testContains() { + IpGroup sut = new IpGroup(); + sut.setId(MocksFactory.nextId()); + sut.setDescription("A's description"); + sut.setDisplayName("A"); + sut.setPersistedGroupAlias("&ip/a"); + final IpAddressRange allIPv4 = IpAddressRange.make(IpAddress.valueOf("0.0.0.0"), IpAddress.valueOf("255.255.255.255")); + final IpAddressRange allIPv6 = IpAddressRange.make(IpAddress.valueOf("0:0:0:0:0:0:0:0"), + IpAddress.valueOf("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")); + + sut.add(allIPv4); + sut.add(allIPv6); + + assertTrue( sut.contains(new DataverseRequest(GuestUser.get(), IpAddress.valueOf("1.2.3.4"))) ); + assertTrue( sut.contains(new DataverseRequest(GuestUser.get(), IpAddress.valueOf("11::fff"))) ); + + sut.remove( allIPv4 ); + assertFalse( sut.contains(new DataverseRequest(GuestUser.get(), IpAddress.valueOf("1.2.3.4"))) ); + + sut.remove( allIPv6 ); + assertFalse( sut.contains(new DataverseRequest(GuestUser.get(), IpAddress.valueOf("11::fff"))) ); + + sut.add( IpAddressRange.make(IpAddress.valueOf("0.0.0.0"), IpAddress.valueOf("168.0.0.0")) ); + assertFalse( sut.contains(new DataverseRequest(GuestUser.get(), IpAddress.valueOf("169.0.0.0"))) ); + assertTrue( sut.contains(new DataverseRequest(GuestUser.get(), IpAddress.valueOf("167.0.0.0"))) ); + + } + + + /** + * Test of isEditable method, of class IpGroup. + */ + @Test + public void testIsEditable() { + IpGroup instance = new IpGroup(); + assertTrue( instance.isEditable() ); + } + + /** + * Test of equals method, of class IpGroup. + */ + @Test + public void testEquals() { + IpGroup a = new IpGroup(); + a.setId(MocksFactory.nextId()); + a.setDescription("A's description"); + a.setDisplayName("A"); + a.setPersistedGroupAlias("&ip/a"); + a.add( IpAddressRange.make(IpAddress.valueOf("0.0.0.0"), IpAddress.valueOf("1.1.1.1"))); + + assertFalse( a.equals("banana") ); + assertFalse( a.equals( null ) ); + assertTrue( a.equals( a) ); + + IpGroup aa = new IpGroup(); + aa.setId(a.getId()); + aa.setDescription("A's description"); + aa.setDisplayName("A"); + aa.setPersistedGroupAlias("&ip/a"); + aa.add( IpAddressRange.make(IpAddress.valueOf("0.0.0.0"), IpAddress.valueOf("1.1.1.1"))); + + assertTrue( a.equals(aa) ); + aa.add( IpAddressRange.make(IpAddress.valueOf("9.0.0.0"), IpAddress.valueOf("9.1.1.1"))); + assertFalse( a.equals(aa) ); + + } + +} diff --git a/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseCommandTest.java b/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseCommandTest.java index f429a4f3815..9db03cb7a99 100644 --- a/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseCommandTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseCommandTest.java @@ -201,7 +201,7 @@ public void testDefaultOptions() throws CommandException { dv.setCreator(null); dv.setDefaultContributorRole(null); dv.setOwner( makeDataverse() ); - final DataverseRequest request = makeRequest(); + final DataverseRequest request = makeRequest(makeAuthenticatedUser("jk", "rollin'")); CreateDataverseCommand sut = new CreateDataverseCommand(dv, request, null, null); Dataverse result = engine.submit(sut); diff --git a/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/UpdatePermissionRootCommandTest.java b/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/UpdatePermissionRootCommandTest.java index f20a45ac751..c60bac05ee8 100644 --- a/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/UpdatePermissionRootCommandTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/UpdatePermissionRootCommandTest.java @@ -7,24 +7,10 @@ import edu.harvard.iq.dataverse.engine.TestCommandContext; import edu.harvard.iq.dataverse.engine.TestDataverseEngine; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; import org.junit.Before; import org.junit.Test; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; /** * diff --git a/src/test/java/edu/harvard/iq/dataverse/mocks/MockRoleAssigneeServiceBean.java b/src/test/java/edu/harvard/iq/dataverse/mocks/MockRoleAssigneeServiceBean.java new file mode 100644 index 00000000000..07ed882a543 --- /dev/null +++ b/src/test/java/edu/harvard/iq/dataverse/mocks/MockRoleAssigneeServiceBean.java @@ -0,0 +1,44 @@ +package edu.harvard.iq.dataverse.mocks; + +import edu.harvard.iq.dataverse.RoleAssigneeServiceBean; +import edu.harvard.iq.dataverse.authorization.RoleAssignee; +import java.util.HashMap; +import java.util.Map; + +/** + * + * @author michael + */ +public class MockRoleAssigneeServiceBean extends RoleAssigneeServiceBean { + + Map assignees = new HashMap<>(); + + public T add (T ra ) { + assignees.put(ra.getIdentifier(), ra); + return ra; + } + + @Override + public RoleAssignee getRoleAssignee(String identifier) { + if ( predefinedRoleAssignees.isEmpty() ) { + setup(); + } + + if (identifier == null || identifier.isEmpty()) { + throw new IllegalArgumentException("Identifier cannot be null or empty string."); + } + switch (identifier.charAt(0)) { + case ':': + return predefinedRoleAssignees.get(identifier); + case '@': + return assignees.get(identifier); + case '&': + return assignees.get(identifier); + case '#': + throw new IllegalArgumentException("private url users not supported in test - it might be easy to add, though."); + default: + throw new IllegalArgumentException("Unsupported assignee identifier '" + identifier + "'"); + } + } + +} 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 2b08189de67..82730f92bbf 100644 --- a/src/test/java/edu/harvard/iq/dataverse/mocks/MocksFactory.java +++ b/src/test/java/edu/harvard/iq/dataverse/mocks/MocksFactory.java @@ -13,8 +13,12 @@ import edu.harvard.iq.dataverse.MetadataBlock; import edu.harvard.iq.dataverse.authorization.DataverseRole; import edu.harvard.iq.dataverse.authorization.Permission; +import edu.harvard.iq.dataverse.authorization.groups.impl.explicit.ExplicitGroup; +import edu.harvard.iq.dataverse.authorization.groups.impl.explicit.ExplicitGroupProvider; import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IpAddress; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; +import edu.harvard.iq.dataverse.authorization.users.GuestUser; +import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import java.sql.Timestamp; import java.time.LocalDate; @@ -97,8 +101,15 @@ public static AuthenticatedUser makeAuthenticatedUser( String firstName, String return user; } + /** + * @return A request with a guest user. + */ public static DataverseRequest makeRequest() { - return new DataverseRequest( makeAuthenticatedUser("Jane", "Doe"), IpAddress.valueOf("215.0.2.17") ); + return makeRequest( GuestUser.get() ); + } + + public static DataverseRequest makeRequest( User u ) { + return new DataverseRequest( u, IpAddress.valueOf("1.2.3.4") ); } public static Dataverse makeDataverse() { @@ -209,4 +220,19 @@ public static DataverseFieldTypeInputLevel makeDataverseFieldTypeInputLevel( Dat return retVal; } + + public static ExplicitGroup makeExplicitGroup( String name, ExplicitGroupProvider prv ) { + long id = nextId(); + ExplicitGroup eg = new ExplicitGroup(prv); + + eg.setId(id); + eg.setDisplayName( name==null ? "explicitGroup-" + id : name ); + eg.setGroupAliasInOwner("eg" + id); + + return eg; + } + + public static ExplicitGroup makeExplicitGroup( ExplicitGroupProvider prv ) { + return makeExplicitGroup(null, prv); + } } From 86178dbae5dcef566c76ce378a384b87b4ae9a7d Mon Sep 17 00:00:00 2001 From: Michael Bar-Sinai Date: Wed, 10 Aug 2016 16:06:01 -0400 Subject: [PATCH 11/35] #1380: IpGroups store IP ranges properly (solved overflow of long) --- scripts/api/data/ipGroup-all-ipv4.json | 5 ++ scripts/issues/1380/add-ip-group.sh | 4 ++ .../edu/harvard/iq/dataverse/api/Access.java | 5 +- .../edu/harvard/iq/dataverse/api/Admin.java | 1 + .../iq/dataverse/api/BuiltinUsers.java | 1 + .../harvard/iq/dataverse/api/Dataverses.java | 13 +++- .../iq/dataverse/api/MetadataBlocks.java | 2 + .../edu/harvard/iq/dataverse/api/TestApi.java | 30 +++++++++- .../impl/ipaddress/IpGroupProvider.java | 11 +++- .../impl/ipaddress/IpGroupsServiceBean.java | 2 +- .../groups/impl/ipaddress/ip/IPv4Address.java | 16 ++++- .../groups/impl/ipaddress/ip/IPv4Range.java | 21 +++---- .../iq/dataverse/util/json/JsonPrinter.java | 59 ++++++++++++++----- .../impl/ipaddress/ip/IPv4AddressTest.java | 37 ++++++++++-- 14 files changed, 164 insertions(+), 43 deletions(-) create mode 100644 scripts/api/data/ipGroup-all-ipv4.json create mode 100755 scripts/issues/1380/add-ip-group.sh diff --git a/scripts/api/data/ipGroup-all-ipv4.json b/scripts/api/data/ipGroup-all-ipv4.json new file mode 100644 index 00000000000..c5ff32def44 --- /dev/null +++ b/scripts/api/data/ipGroup-all-ipv4.json @@ -0,0 +1,5 @@ +{ + "alias":"all-ipv4", + "name":"IP group to match all IPv4 addresses", + "ranges" : [["0.0.0.0", "255.255.255.255"]] +} diff --git a/scripts/issues/1380/add-ip-group.sh b/scripts/issues/1380/add-ip-group.sh new file mode 100755 index 00000000000..2fba944807c --- /dev/null +++ b/scripts/issues/1380/add-ip-group.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +# Add the passed group to the system. +curl -X POST -H"Content-Type:application/json" -d@../../api/data/$1 localhost:8080/api/admin/groups/ip diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Access.java b/src/main/java/edu/harvard/iq/dataverse/api/Access.java index 757fa0bebfd..11746a19c04 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Access.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Access.java @@ -10,7 +10,6 @@ import edu.harvard.iq.dataverse.DataFile; import edu.harvard.iq.dataverse.FileMetadata; import edu.harvard.iq.dataverse.DataFileServiceBean; -import edu.harvard.iq.dataverse.Dataset; import edu.harvard.iq.dataverse.DatasetVersion; import edu.harvard.iq.dataverse.DatasetVersionServiceBean; import edu.harvard.iq.dataverse.DatasetServiceBean; @@ -37,7 +36,6 @@ import edu.harvard.iq.dataverse.util.SystemConfig; import edu.harvard.iq.dataverse.worldmapauth.WorldMapTokenServiceBean; -import java.util.List; import java.util.logging.Logger; import javax.ejb.EJB; import java.io.InputStream; @@ -1168,7 +1166,6 @@ private boolean isAccessAuthorized(DataFile df, String apiToken) { } return false; - } - + } } \ No newline at end of file diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Admin.java b/src/main/java/edu/harvard/iq/dataverse/api/Admin.java index b066714ff2a..37282f05486 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Admin.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Admin.java @@ -42,6 +42,7 @@ import javax.validation.ConstraintViolationException; import javax.ws.rs.Produces; import javax.ws.rs.core.Response.Status; +import static edu.harvard.iq.dataverse.api.AbstractApiBean.errorResponse; /** * Where the secure, setup API calls live. * @author michael diff --git a/src/main/java/edu/harvard/iq/dataverse/api/BuiltinUsers.java b/src/main/java/edu/harvard/iq/dataverse/api/BuiltinUsers.java index 1460e11f6be..5d718dc11ca 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/BuiltinUsers.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/BuiltinUsers.java @@ -26,6 +26,7 @@ import javax.ws.rs.core.Response.Status; import static edu.harvard.iq.dataverse.util.json.JsonPrinter.json; import static edu.harvard.iq.dataverse.util.json.JsonPrinter.jsonForAuthUser; +import static edu.harvard.iq.dataverse.util.json.JsonPrinter.json; /** * REST API bean for managing {@link BuiltinUser}s. diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java b/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java index fc42091585a..3c676f5d665 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java @@ -79,7 +79,7 @@ import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import static edu.harvard.iq.dataverse.util.json.JsonPrinter.json; -import static edu.harvard.iq.dataverse.util.json.JsonPrinter.json; +import static edu.harvard.iq.dataverse.util.json.JsonPrinter.toJsonArray; /** * A REST API for dataverses. @@ -323,7 +323,9 @@ public Response setMetadataRoot( @PathParam("identifier")String dvIdtf, String b @Path("{identifier}/facets/") public Response listFacets( @PathParam("identifier") String dvIdtf ) { try { - return okResponse( json(execCommand(new ListFacetsCommand(createDataverseRequest(findUserOrDie()), findDataverseOrDie(dvIdtf)) ))); + return okResponse( + execCommand(new ListFacetsCommand(createDataverseRequest(findUserOrDie()), findDataverseOrDie(dvIdtf)) ) + .stream().map(f->json(f).build()).collect(toJsonArray())); } catch (WrappedResponse wr) { return wr.getResponse(); } @@ -539,7 +541,12 @@ public Response createExplicitGroup( ExplicitGroupDTO dto, @PathParam("identifie @Path("{identifier}/groups/") public Response listGroups( @PathParam("identifier") String dvIdtf, @QueryParam("key") String apiKey ) { try { - return okResponse( json(execCommand(new ListExplicitGroupsCommand(createDataverseRequest(findUserOrDie()), findDataverseOrDie(dvIdtf)) ))); + JsonArrayBuilder arr = Json.createArrayBuilder(); + execCommand(new ListExplicitGroupsCommand(createDataverseRequest(findUserOrDie()), findDataverseOrDie(dvIdtf))) + .stream().map( eg->json(eg)) + .forEach( arr::add ); + return okResponse( arr ); + } catch (WrappedResponse wr) { return wr.getResponse(); } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/MetadataBlocks.java b/src/main/java/edu/harvard/iq/dataverse/api/MetadataBlocks.java index 599d8635afa..e0376f71412 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/MetadataBlocks.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/MetadataBlocks.java @@ -11,6 +11,8 @@ import static edu.harvard.iq.dataverse.util.json.JsonPrinter.json; import javax.ws.rs.PathParam; import static edu.harvard.iq.dataverse.util.json.JsonPrinter.json; +import static edu.harvard.iq.dataverse.util.json.JsonPrinter.json; +import static edu.harvard.iq.dataverse.util.json.JsonPrinter.json; /** * Api bean for managing metadata blocks. diff --git a/src/main/java/edu/harvard/iq/dataverse/api/TestApi.java b/src/main/java/edu/harvard/iq/dataverse/api/TestApi.java index 8cb5115bf2d..00f167a2d3e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/TestApi.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/TestApi.java @@ -4,6 +4,10 @@ import edu.harvard.iq.dataverse.authorization.RoleAssignee; import edu.harvard.iq.dataverse.authorization.groups.impl.explicit.ExplicitGroup; import edu.harvard.iq.dataverse.authorization.groups.impl.explicit.ExplicitGroupServiceBean; +import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.IpGroup; +import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.IpGroupsServiceBean; +import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IPv4Address; +import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IpAddress; import edu.harvard.iq.dataverse.authorization.providers.builtin.PasswordEncryption; import edu.harvard.iq.dataverse.authorization.users.User; import javax.ejb.Stateless; @@ -12,6 +16,7 @@ import javax.ws.rs.PathParam; import javax.ws.rs.core.Response; import static edu.harvard.iq.dataverse.util.json.JsonPrinter.*; +import edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; @@ -40,6 +45,9 @@ public class TestApi extends AbstractApiBean { @EJB ExplicitGroupServiceBean explicitGroups; + @EJB + IpGroupsServiceBean ipGroupsSvc; + @Path("echo/{whatever}") @GET public Response echo( @PathParam("whatever") String body ) { @@ -114,6 +122,26 @@ public Response explicitGroupMembership( @PathParam("identifier") String idtf) { } Set groups = explicitGroups.findGroups(roleAssignee); logger.log(Level.INFO, "Groups for {0}: {1}", new Object[]{roleAssignee, groups}); - return okResponse( json(groups) ); + return okResponse( groups.stream().map( g->json(g).build()).collect(toJsonArray()) ); + } + + @Path("ipGroups/containing/{address}") + @GET + public Response getIpGroupsContaining( @PathParam("address") String addrStr ) { + try { + IpAddress addr = IpAddress.valueOf(addrStr); + + JsonObjectBuilder r = NullSafeJsonBuilder.jsonObjectBuilder(); + r.add( "address", addr.toString() ); + r.add( "addressRaw", (addr instanceof IPv4Address) ? ((IPv4Address)addr).toLong() : null); + r.add("groups", ipGroupsSvc.findAllIncludingIp(addr).stream() + .map( IpGroup::toString ) + .collect(stringsToJsonArray())); + return okResponse( r ); + + } catch ( IllegalArgumentException iae ) { + return badRequest(addrStr + " is not a valid address: " + iae.getMessage()); + } } + } diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/IpGroupProvider.java b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/IpGroupProvider.java index f784ef92d9f..d730b952828 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/IpGroupProvider.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/IpGroupProvider.java @@ -3,7 +3,8 @@ 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.User; +import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IPv4Address; +import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IpAddress; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import java.util.Collections; import java.util.HashSet; @@ -44,7 +45,13 @@ public Set groupsFor(RoleAssignee ra, DvObject o) { public Set groupsFor( DataverseRequest req, DvObject dvo ) { if ( req.getSourceAddress() != null ) { final Set groups = updateProvider( ipGroupsService.findAllIncludingIp(req.getSourceAddress()) ); - logger.log(Level.INFO, "IP groups detected: {0}", groups); + + { // FIXME remove this block before merge to DEV + final IpAddress sourceAddress = req.getSourceAddress(); + Long ipAddr = (sourceAddress instanceof IPv4Address) ? ((IPv4Address)sourceAddress).toLong() : null; + logger.log(Level.INFO, "ip: {1} ({2}) groups: {0}", new Object[]{groups, sourceAddress, ipAddr}); + } + return groups; } else { return Collections.emptySet(); diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/IpGroupsServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/IpGroupsServiceBean.java index d135d97796e..77be1358f3d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/IpGroupsServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/IpGroupsServiceBean.java @@ -98,7 +98,7 @@ public Set findAllIncludingIp( IpAddress ipa ) { if ( ipa instanceof IPv4Address ) { IPv4Address ip4 = (IPv4Address) ipa; List groupList = em.createNamedQuery("IPv4Range.findGroupsContainingAddressAsLong", IpGroup.class) - .setParameter("addressAsLong", ip4.toLong()).getResultList(); + .setParameter("addressAsLong", ip4.toBigInteger()).getResultList(); return new HashSet<>(groupList); } else if ( ipa instanceof IPv6Address ) { diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/ip/IPv4Address.java b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/ip/IPv4Address.java index e5255a19d7d..147ade9ddb1 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/ip/IPv4Address.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/ip/IPv4Address.java @@ -1,5 +1,6 @@ package edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip; +import java.math.BigInteger; import java.util.Arrays; /** @@ -33,6 +34,10 @@ public IPv4Address( int a, int b, int c, int d ) { this( new short[]{(short)a,(short)b,(short)c,(short)d} ); } + public IPv4Address( BigInteger bits ) { + this( bits.longValue() ); + } + public IPv4Address( long l ) { bytes[0] = (short)((l >>> 24) & 0xFF); bytes[1] = (short)((l >>> 16) & 0xFF); @@ -59,7 +64,16 @@ public short[] getBytes() { } public long toLong() { - return (get(0)<<24) + (get(1)<<16) + (get(2)<<8) + get(3); + return (get(0)<<24) | (get(1)<<16) | (get(2)<<8) | get(3); + } + + public BigInteger toBigInteger() { + BigInteger res = BigInteger.ZERO; + for ( int i=0; i<3; i++ ) { + res = res.add(BigInteger.valueOf(get(i))) + .shiftLeft(8); + } + return res.add(BigInteger.valueOf(get(3))); } @Override diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/ip/IPv4Range.java b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/ip/IPv4Range.java index 0d4f8575c5a..3ecd7689e1c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/ip/IPv4Range.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/ip/IPv4Range.java @@ -1,5 +1,6 @@ package edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip; +import java.math.BigInteger; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; @@ -31,16 +32,16 @@ public class IPv4Range extends IpAddressRange implements java.io.Serializable { Long id; /** The most significant bits of {@code this} range's top address, i.e the first two numbers of the IP address */ - long topAsLong; + BigInteger topAsLong; /** The least significant bits, i.e the last tow numbers of the IP address */ - long bottomAsLong; + BigInteger bottomAsLong; public IPv4Range(){} public IPv4Range(IPv4Address bottom, IPv4Address top) { - topAsLong = top.toLong(); - bottomAsLong = bottom.toLong(); + topAsLong = top.toBigInteger(); + bottomAsLong = bottom.toBigInteger(); } @Override @@ -49,7 +50,7 @@ public IPv4Address getTop() { } public void setTop( IPv4Address aNewTop ) { - setTopAsLong( aNewTop.toLong() ); + setTopAsLong( aNewTop.toBigInteger() ); } @Override @@ -58,22 +59,22 @@ public IPv4Address getBottom() { } public void setBottom( IPv4Address aNewBottom ) { - setTopAsLong( aNewBottom.toLong() ); + setTopAsLong( aNewBottom.toBigInteger() ); } - public long getTopAsLong() { + public BigInteger getTopAsLong() { return topAsLong; } - public void setTopAsLong(long topAsLong) { + public void setTopAsLong(BigInteger topAsLong) { this.topAsLong = topAsLong; } - public long getBottomAsLong() { + public BigInteger getBottomAsLong() { return bottomAsLong; } - public void setBottomAsLong(long bottomAsLong) { + public void setBottomAsLong(BigInteger bottomAsLong) { this.bottomAsLong = bottomAsLong; } 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 0873b9f70f5..034a7aca404 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 @@ -53,6 +53,7 @@ import static java.util.stream.Collectors.toList; import javax.json.JsonArray; import javax.json.JsonObject; +import javax.json.JsonValue; /** * Convert objects to Json. @@ -518,9 +519,7 @@ public static JsonObjectBuilder json(PrivateUrl privateUrl) { .add("roleAssignment", json(privateUrl.getRoleAssignment())); } - public static JsonObjectBuilder json(T j ) { - if (j instanceof ExplicitGroup) { - ExplicitGroup eg = (ExplicitGroup) j; + public static JsonObjectBuilder json( ExplicitGroup eg ) { JsonArrayBuilder ras = Json.createArrayBuilder(); for ( String u : eg.getContainedRoleAssgineeIdentifiers() ) { ras.add(u); @@ -533,23 +532,16 @@ public static JsonObjectBuilder json(T j ) { .add("displayName", eg.getDisplayName()) .add("containedRoleAssignees", ras); - } else { // implication: (j instanceof DataverseFacet) - DataverseFacet f = (DataverseFacet) j; - return jsonObjectBuilder() - .add("id", String.valueOf(f.getId())) // TODO should just be id I think - .add("name", f.getDatasetFieldType().getDisplayName()); - } } - public static JsonArrayBuilder json( Collection jc ) { - JsonArrayBuilder bld = Json.createArrayBuilder(); - for ( T j : jc ) { - bld.add( json(j) ); - } - return bld; + public static JsonObjectBuilder json(DataverseFacet f) { + return jsonObjectBuilder() + .add("id", String.valueOf(f.getId())) // TODO should just be id I think + .add("name", f.getDatasetFieldType().getDisplayName()); } + - public static Collector toJsonArray() { + public static Collector stringsToJsonArray() { return new Collector() { @Override @@ -583,4 +575,39 @@ public Set characteristics() { } }; } + + public static Collector toJsonArray() { + return new Collector() { + + @Override + public Supplier supplier() { + return ()->Json.createArrayBuilder(); + } + + @Override + public BiConsumer accumulator() { + return (JsonArrayBuilder b, JsonValue s ) -> b.add(s); + } + + @Override + public BinaryOperator combiner() { + return (jab1, jab2) -> { + JsonArrayBuilder retVal = Json.createArrayBuilder(); + jab1.build().forEach( retVal::add ); + jab2.build().forEach( retVal::add ); + return retVal; + }; + } + + @Override + public Function finisher() { + return Function.identity(); + } + + @Override + public Set characteristics() { + return EnumSet.of(Collector.Characteristics.IDENTITY_FINISH); + } + }; + } } diff --git a/src/test/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/ip/IPv4AddressTest.java b/src/test/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/ip/IPv4AddressTest.java index d853dc36f86..d03846a97b4 100644 --- a/src/test/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/ip/IPv4AddressTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/authorization/groups/impl/ipaddress/ip/IPv4AddressTest.java @@ -1,6 +1,7 @@ package edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip; -import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IPv4Address; +import java.math.BigDecimal; +import java.math.BigInteger; import java.util.Arrays; import org.junit.Test; import static org.junit.Assert.*; @@ -57,7 +58,7 @@ public void testComparator() { @Test public void testLongRoundtrip() { - for ( IPv4Address addr : Arrays.asList( + Arrays.asList( new IPv4Address(127,0,36,255), new IPv4Address(0,0,0,0), new IPv4Address(127,0,0,1), @@ -67,10 +68,36 @@ public void testLongRoundtrip() { new IPv4Address(128,128,128,128), new IPv4Address(127,127,127,127), new IPv4Address(255,255,255,255), - new IPv4Address(255,0,34,1)) ) { - assertEquals( addr, new IPv4Address(addr.toLong()) ); - } + new IPv4Address(255,0,34,1) + ).forEach( addr -> assertEquals( addr, new IPv4Address(addr.toLong())) ); } + @Test + public void testBigIntegerRoundtrip() { + Arrays.asList( + new IPv4Address(0,0,0,0), + new IPv4Address(0,0,0,1), + new IPv4Address(0,0,1,0), + new IPv4Address(0,1,0,0), + new IPv4Address(1,0,0,0), + new IPv4Address(1,1,1,1), + new IPv4Address(0,0,127,1), + new IPv4Address(127,0,36,255), + new IPv4Address(127,0,0,1), + new IPv4Address(128,0,0,1), + new IPv4Address(0,0,128,1), + new IPv4Address(128,128,128,128), + new IPv4Address(127,127,127,127), + new IPv4Address(255,255,255,255), + new IPv4Address(255,0,34,1) + ).forEach( addr -> assertEquals( addr, new IPv4Address(addr.toBigInteger())) ); + } + @Test + public void toBigInteger() { + assertEquals( BigInteger.ZERO, new IPv4Address(0,0,0,0).toBigInteger() ); + assertEquals( BigInteger.ONE, new IPv4Address(0,0,0,1).toBigInteger() ); + assertEquals( BigInteger.ONE.shiftLeft(8), + new IPv4Address(0,0,1,0).toBigInteger() ); + } } From 05137a09c25b9d37dac7c56a6080638110b0c510 Mon Sep 17 00:00:00 2001 From: Michael Bar-Sinai Date: Thu, 11 Aug 2016 14:23:04 -0400 Subject: [PATCH 12/35] #1380: Permission resolving for groups-within-groups for a given dvobject implemented. Additional fixes to logic of group containments and accessing of restricted files --- .../edu/harvard/iq/dataverse/DatasetPage.java | 24 +++++----- .../harvard/iq/dataverse/DataversePage.java | 17 +------ .../iq/dataverse/PermissionServiceBean.java | 45 ++++++++++------- .../edu/harvard/iq/dataverse/api/TestApi.java | 2 +- .../authorization/groups/GroupProvider.java | 5 +- .../groups/GroupServiceBean.java | 48 +++++++++---------- .../impl/builtin/BuiltInGroupsProvider.java | 11 +++-- .../explicit/ExplicitGroupServiceBean.java | 6 +-- .../impl/ipaddress/IpGroupProvider.java | 6 ++- 9 files changed, 84 insertions(+), 80 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java index fb0ca6f47ca..4dd6c0e3c6a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java @@ -183,7 +183,6 @@ public enum DisplayMode { private List