From 26ea63af5e0b82d66beef9c017d1cdf8ee327151 Mon Sep 17 00:00:00 2001 From: Aaron Coburn Date: Sat, 13 Jun 2020 09:27:36 -0400 Subject: [PATCH] Effective ACL should point to correct location (#903) Resolves #893 --- .../org/trellisldp/webac/AuthorizedModes.java | 5 +- .../org/trellisldp/webac/WebAcFilter.java | 19 +++- .../org/trellisldp/webac/WebAcService.java | 48 +++++++--- .../org/trellisldp/webac/WebAcFilterTest.java | 95 +++++++++++++++++++ 4 files changed, 150 insertions(+), 17 deletions(-) diff --git a/auth/webac/src/main/java/org/trellisldp/webac/AuthorizedModes.java b/auth/webac/src/main/java/org/trellisldp/webac/AuthorizedModes.java index 7491a24f8..457f4255f 100644 --- a/auth/webac/src/main/java/org/trellisldp/webac/AuthorizedModes.java +++ b/auth/webac/src/main/java/org/trellisldp/webac/AuthorizedModes.java @@ -17,6 +17,7 @@ import static java.util.Collections.unmodifiableSet; +import java.util.Optional; import java.util.Set; import org.apache.commons.rdf.api.IRI; @@ -40,8 +41,8 @@ public AuthorizedModes(final IRI effectiveAcl, final Set modes) { * Get the location of the effective ACL. * @return the location of the effective ACL */ - public IRI getEffectiveAcl() { - return effectiveAcl; + public Optional getEffectiveAcl() { + return Optional.ofNullable(effectiveAcl); } /** diff --git a/auth/webac/src/main/java/org/trellisldp/webac/WebAcFilter.java b/auth/webac/src/main/java/org/trellisldp/webac/WebAcFilter.java index 73f91faba..2d731780f 100644 --- a/auth/webac/src/main/java/org/trellisldp/webac/WebAcFilter.java +++ b/auth/webac/src/main/java/org/trellisldp/webac/WebAcFilter.java @@ -49,6 +49,7 @@ import javax.ws.rs.container.ContainerRequestFilter; import javax.ws.rs.container.ContainerResponseContext; import javax.ws.rs.container.ContainerResponseFilter; +import javax.ws.rs.core.Link; import javax.ws.rs.ext.Provider; import org.apache.commons.rdf.api.IRI; @@ -223,14 +224,26 @@ public void filter(final ContainerRequestContext req, final ContainerResponseCon final String path = req.getUriInfo().getPath(); res.getHeaders().add(LINK, fromUri(fromPath(path.startsWith(SLASH) ? path : SLASH + path) .queryParam(HttpConstants.EXT, HttpConstants.ACL).build()).rel(rel).build()); - res.getHeaders().add(LINK, fromUri(fromPath(modes.getEffectiveAcl().getIRIString() - .replace(TRELLIS_DATA_PREFIX, SLASH)) + modes.getEffectiveAcl().map(IRI::getIRIString).map(acl -> effectiveAclToUrlPath(acl, res)) + .ifPresent(urlPath -> res.getHeaders().add(LINK, fromUri(fromPath(urlPath) .queryParam(HttpConstants.EXT, HttpConstants.ACL).build()) - .rel(Trellis.effectiveAcl.getIRIString()).build()); + .rel(Trellis.effectiveAcl.getIRIString()).build())); } } } + static String effectiveAclToUrlPath(final String internalPath, final ContainerResponseContext response) { + final boolean isContainer = response.getStringHeaders().getOrDefault(LINK, emptyList()).stream() + .map(Link::valueOf).anyMatch(link -> + link.getUri().toString().endsWith("Container") && link.getRels().contains(Link.TYPE)); + final String urlPath = internalPath.replace(TRELLIS_DATA_PREFIX, SLASH); + if (SLASH.equals(urlPath) || !isContainer) { + return urlPath; + } + return urlPath + SLASH; + + } + protected void verifyCanAppend(final Set modes, final Session session, final String path) { if (!modes.contains(ACL.Append) && !modes.contains(ACL.Write)) { LOGGER.warn("User: {} cannot Append to {}", session.getAgent(), path); diff --git a/auth/webac/src/main/java/org/trellisldp/webac/WebAcService.java b/auth/webac/src/main/java/org/trellisldp/webac/WebAcService.java index 2c1d53044..bef73d0ff 100644 --- a/auth/webac/src/main/java/org/trellisldp/webac/WebAcService.java +++ b/auth/webac/src/main/java/org/trellisldp/webac/WebAcService.java @@ -215,7 +215,7 @@ public AuthorizedModes getAuthorizedModes(final IRI identifier, final Session se requireNonNull(session, "A non-null session must be provided!"); if (Trellis.AdministratorAgent.equals(session.getAgent())) { - return new AuthorizedModes(identifier, allModes); + return new AuthorizedModes(null, allModes); } final AuthorizedModes cachedModes = cache.get(generateCacheKey(identifier, session.getAgent()), k -> @@ -225,7 +225,7 @@ public AuthorizedModes getAuthorizedModes(final IRI identifier, final Session se final AuthorizedModes delegatedModes = cache.get(generateCacheKey(identifier, delegate), k -> getAuthz(identifier, delegate)); modes.retainAll(delegatedModes.getAccessModes()); - return new AuthorizedModes(cachedModes.getEffectiveAcl(), modes); + return new AuthorizedModes(cachedModes.getEffectiveAcl().orElse(null), modes); }).orElse(cachedModes); } @@ -255,16 +255,17 @@ private AuthorizedModes getAuthz(final IRI identifier, final IRI agent) { modes.remove(ACL.Append); } }); - return new AuthorizedModes(authModes.getEffectiveAcl(), modes); + return new AuthorizedModes(authModes.getEffectiveAcl().orElse(null), modes); } return authModes; } private AuthorizedModes getModesFor(final IRI identifier, final IRI agent) { - return getNearestResource(identifier).map(resource -> - new AuthorizedModes(resource.getIdentifier(), getAllAuthorizationsFor(resource, false) - .filter(agentFilter(agent)).flatMap(auth -> auth.getMode().stream()).collect(toSet()))) - .orElseGet(() -> new AuthorizedModes(root, emptySet())); + return getNearestResource(identifier).map(resource -> { + final Authorizations authorizations = getAllAuthorizationsFor(resource, false); + return new AuthorizedModes(authorizations.getIdentifier(), authorizations.stream() + .filter(agentFilter(agent)).flatMap(auth -> auth.getMode().stream()).collect(toSet())); + }).orElseGet(() -> new AuthorizedModes(root, emptySet())); } private Optional getNearestResource(final IRI identifier) { @@ -291,7 +292,7 @@ private Predicate isAgentInGroup(final IRI agent) { }).toCompletableFuture().join(); } - private Stream getAllAuthorizationsFor(final Resource resource, final boolean inherited) { + private Authorizations getAllAuthorizationsFor(final Resource resource, final boolean inherited) { LOGGER.debug("Checking ACL for: {}", resource.getIdentifier()); if (resource.hasMetadata(Trellis.PreferAccessControl)) { try (final Graph graph = resource.stream(Trellis.PreferAccessControl).map(Quad::asTriple) @@ -300,20 +301,21 @@ private Stream getAllAuthorizationsFor(final Resource resource, f final List authorizations = getAuthorizationFromGraph(resource.getIdentifier(), graph); // Check for any acl:default statements if checking for inheritance if (inherited) { - return authorizations.stream().filter(getInheritedAuth(resource.getIdentifier())); + return new Authorizations(resource.getIdentifier(), + authorizations.stream().filter(getInheritedAuth(resource.getIdentifier()))); } // If not inheriting, just return the relevant Authorizations - return authorizations.stream(); + return new Authorizations(resource.getIdentifier(), authorizations.stream()); } catch (final Exception ex) { throw new RuntimeTrellisException("Error closing graph", ex); } } else if (root.equals(resource.getIdentifier())) { - return defaultRootAuthorizations.stream(); + return new Authorizations(root, defaultRootAuthorizations.stream()); } // Nothing here, check the parent LOGGER.debug("No ACL for {}; looking up parent resource", resource.getIdentifier()); return getContainer(resource.getIdentifier()).flatMap(this::getNearestResource) - .map(res -> getAllAuthorizationsFor(res, true)).orElseGet(Stream::empty); + .map(res -> getAllAuthorizationsFor(res, true)).orElseGet(() -> new Authorizations(root)); } static List getAuthorizationFromGraph(final IRI identifier, final Graph graph) { @@ -326,6 +328,28 @@ static List getAuthorizationFromGraph(final IRI identifier, final }).filter(auth -> auth.getAccessTo().contains(identifier)).collect(toList()); } + static class Authorizations { + private final IRI resource; + private final Stream stream; + + public Authorizations(final IRI resource) { + this(resource, Stream.empty()); + } + + public Authorizations(final IRI resource, final Stream stream) { + this.resource = resource; + this.stream = stream; + } + + public IRI getIdentifier() { + return resource; + } + + public Stream stream() { + return stream; + } + } + static boolean hasWritableMode(final Set modes) { return modes.contains(ACL.Write) || modes.contains(ACL.Append); } diff --git a/auth/webac/src/test/java/org/trellisldp/webac/WebAcFilterTest.java b/auth/webac/src/test/java/org/trellisldp/webac/WebAcFilterTest.java index 1b43bc84a..cc5078e29 100644 --- a/auth/webac/src/test/java/org/trellisldp/webac/WebAcFilterTest.java +++ b/auth/webac/src/test/java/org/trellisldp/webac/WebAcFilterTest.java @@ -490,8 +490,11 @@ void testFilterResponseNoControl() { @Test void testFilterResponseWithControl() { final MultivaluedMap headers = new MultivaluedHashMap<>(); + final MultivaluedMap stringHeaders = new MultivaluedHashMap<>(); + stringHeaders.putSingle("Link", "; rel=\"type\""); when(mockResponseContext.getStatusInfo()).thenReturn(OK); when(mockResponseContext.getHeaders()).thenReturn(headers); + when(mockResponseContext.getStringHeaders()).thenReturn(stringHeaders); when(mockContext.getProperty(eq(WebAcFilter.SESSION_WEBAC_MODES))) .thenReturn(new AuthorizedModes(effectiveAcl, allModes)); @@ -510,6 +513,94 @@ void testFilterResponseWithControl() { link.getRels().contains(Trellis.effectiveAcl.getIRIString()))); } + @Test + void testFilterResponseWithControl2() { + final MultivaluedMap headers = new MultivaluedHashMap<>(); + final MultivaluedMap stringHeaders = new MultivaluedHashMap<>(); + stringHeaders.putSingle("Link", "; rel=\"blah\""); + when(mockResponseContext.getStatusInfo()).thenReturn(OK); + when(mockResponseContext.getHeaders()).thenReturn(headers); + when(mockResponseContext.getStringHeaders()).thenReturn(stringHeaders); + when(mockContext.getProperty(eq(WebAcFilter.SESSION_WEBAC_MODES))) + .thenReturn(new AuthorizedModes(effectiveAcl, allModes)); + + final WebAcFilter filter = new WebAcFilter(); + filter.setAccessService(mockWebAcService); + + assertTrue(headers.isEmpty()); + filter.filter(mockContext, mockResponseContext); + assertFalse(headers.isEmpty()); + + final List links = headers.get("Link"); + assertTrue(links.stream().map(Link.class::cast).anyMatch(link -> + link.getRels().contains("acl") && "/?ext=acl".equals(link.getUri().toString()))); + assertTrue(links.stream().map(Link.class::cast).anyMatch(link -> + "/?ext=acl".equals(link.getUri().toString()) && + link.getRels().contains(Trellis.effectiveAcl.getIRIString()))); + } + + @Test + void testFilterResourceResponseWithControl() { + final IRI localEffectiveAcl = rdf.createIRI(TRELLIS_DATA_PREFIX + "resource"); + final MultivaluedMap headers = new MultivaluedHashMap<>(); + final MultivaluedMap stringHeaders = new MultivaluedHashMap<>(); + stringHeaders.putSingle("Link", "; rel=\"type\""); + when(mockResponseContext.getStatusInfo()).thenReturn(OK); + when(mockResponseContext.getHeaders()).thenReturn(headers); + when(mockResponseContext.getStringHeaders()).thenReturn(stringHeaders); + when(mockUriInfo.getPath()).thenReturn("/resource"); + when(mockWebAcService.getAuthorizedModes(any(IRI.class), any(Session.class))) + .thenReturn(new AuthorizedModes(localEffectiveAcl, allModes)); + + when(mockContext.getProperty(eq(WebAcFilter.SESSION_WEBAC_MODES))) + .thenReturn(new AuthorizedModes(localEffectiveAcl, allModes)); + + final WebAcFilter filter = new WebAcFilter(); + filter.setAccessService(mockWebAcService); + + assertTrue(headers.isEmpty()); + filter.filter(mockContext, mockResponseContext); + assertFalse(headers.isEmpty()); + + final List links = headers.get("Link"); + assertTrue(links.stream().map(Link.class::cast).anyMatch(link -> + link.getRels().contains("acl") && "/resource?ext=acl".equals(link.getUri().toString()))); + assertTrue(links.stream().map(Link.class::cast).anyMatch(link -> + "/resource?ext=acl".equals(link.getUri().toString()) && + link.getRels().contains(Trellis.effectiveAcl.getIRIString()))); + } + + @Test + void testFilterContainerResponseWithControl() { + final IRI localEffectiveAcl = rdf.createIRI(TRELLIS_DATA_PREFIX + "container"); + final MultivaluedMap headers = new MultivaluedHashMap<>(); + final MultivaluedMap stringHeaders = new MultivaluedHashMap<>(); + stringHeaders.putSingle("Link", "; rel=\"type\""); + when(mockResponseContext.getStatusInfo()).thenReturn(OK); + when(mockResponseContext.getHeaders()).thenReturn(headers); + when(mockResponseContext.getStringHeaders()).thenReturn(stringHeaders); + when(mockUriInfo.getPath()).thenReturn("/container/"); + when(mockWebAcService.getAuthorizedModes(any(IRI.class), any(Session.class))) + .thenReturn(new AuthorizedModes(localEffectiveAcl, allModes)); + + when(mockContext.getProperty(eq(WebAcFilter.SESSION_WEBAC_MODES))) + .thenReturn(new AuthorizedModes(localEffectiveAcl, allModes)); + + final WebAcFilter filter = new WebAcFilter(); + filter.setAccessService(mockWebAcService); + + assertTrue(headers.isEmpty()); + filter.filter(mockContext, mockResponseContext); + assertFalse(headers.isEmpty()); + + final List links = headers.get("Link"); + assertTrue(links.stream().map(Link.class::cast).anyMatch(link -> + link.getRels().contains("acl") && "/container/?ext=acl".equals(link.getUri().toString()))); + assertTrue(links.stream().map(Link.class::cast).anyMatch(link -> + "/container/?ext=acl".equals(link.getUri().toString()) && + link.getRels().contains(Trellis.effectiveAcl.getIRIString()))); + } + @Test void testFilterResponseDelete() { final MultivaluedMap headers = new MultivaluedHashMap<>(); @@ -530,8 +621,10 @@ void testFilterResponseDelete() { @Test void testFilterResponseBaseUrl() { final MultivaluedMap headers = new MultivaluedHashMap<>(); + final MultivaluedMap stringHeaders = new MultivaluedHashMap<>(); when(mockResponseContext.getStatusInfo()).thenReturn(OK); when(mockResponseContext.getHeaders()).thenReturn(headers); + when(mockResponseContext.getStringHeaders()).thenReturn(stringHeaders); when(mockUriInfo.getPath()).thenReturn("/path"); when(mockContext.getProperty(eq(WebAcFilter.SESSION_WEBAC_MODES))) .thenReturn(new AuthorizedModes(effectiveAcl, allModes)); @@ -555,10 +648,12 @@ void testFilterResponseBaseUrl() { void testFilterResponseWebac2() { final MultivaluedMap headers = new MultivaluedHashMap<>(); final MultivaluedMap params = new MultivaluedHashMap<>(); + final MultivaluedMap stringHeaders = new MultivaluedHashMap<>(); params.add("ext", "foo"); params.add("ext", "acl"); when(mockResponseContext.getStatusInfo()).thenReturn(OK); when(mockResponseContext.getHeaders()).thenReturn(headers); + when(mockResponseContext.getStringHeaders()).thenReturn(stringHeaders); when(mockUriInfo.getQueryParameters()).thenReturn(params); when(mockUriInfo.getPath()).thenReturn("path/"); when(mockContext.getProperty(eq(WebAcFilter.SESSION_WEBAC_MODES)))