Skip to content

Commit 26ea63a

Browse files
authored
Effective ACL should point to correct location (#903)
Resolves #893
1 parent 4a995db commit 26ea63a

File tree

4 files changed

+150
-17
lines changed

4 files changed

+150
-17
lines changed

auth/webac/src/main/java/org/trellisldp/webac/AuthorizedModes.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import static java.util.Collections.unmodifiableSet;
1919

20+
import java.util.Optional;
2021
import java.util.Set;
2122

2223
import org.apache.commons.rdf.api.IRI;
@@ -40,8 +41,8 @@ public AuthorizedModes(final IRI effectiveAcl, final Set<IRI> modes) {
4041
* Get the location of the effective ACL.
4142
* @return the location of the effective ACL
4243
*/
43-
public IRI getEffectiveAcl() {
44-
return effectiveAcl;
44+
public Optional<IRI> getEffectiveAcl() {
45+
return Optional.ofNullable(effectiveAcl);
4546
}
4647

4748
/**

auth/webac/src/main/java/org/trellisldp/webac/WebAcFilter.java

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
import javax.ws.rs.container.ContainerRequestFilter;
5050
import javax.ws.rs.container.ContainerResponseContext;
5151
import javax.ws.rs.container.ContainerResponseFilter;
52+
import javax.ws.rs.core.Link;
5253
import javax.ws.rs.ext.Provider;
5354

5455
import org.apache.commons.rdf.api.IRI;
@@ -223,14 +224,26 @@ public void filter(final ContainerRequestContext req, final ContainerResponseCon
223224
final String path = req.getUriInfo().getPath();
224225
res.getHeaders().add(LINK, fromUri(fromPath(path.startsWith(SLASH) ? path : SLASH + path)
225226
.queryParam(HttpConstants.EXT, HttpConstants.ACL).build()).rel(rel).build());
226-
res.getHeaders().add(LINK, fromUri(fromPath(modes.getEffectiveAcl().getIRIString()
227-
.replace(TRELLIS_DATA_PREFIX, SLASH))
227+
modes.getEffectiveAcl().map(IRI::getIRIString).map(acl -> effectiveAclToUrlPath(acl, res))
228+
.ifPresent(urlPath -> res.getHeaders().add(LINK, fromUri(fromPath(urlPath)
228229
.queryParam(HttpConstants.EXT, HttpConstants.ACL).build())
229-
.rel(Trellis.effectiveAcl.getIRIString()).build());
230+
.rel(Trellis.effectiveAcl.getIRIString()).build()));
230231
}
231232
}
232233
}
233234

235+
static String effectiveAclToUrlPath(final String internalPath, final ContainerResponseContext response) {
236+
final boolean isContainer = response.getStringHeaders().getOrDefault(LINK, emptyList()).stream()
237+
.map(Link::valueOf).anyMatch(link ->
238+
link.getUri().toString().endsWith("Container") && link.getRels().contains(Link.TYPE));
239+
final String urlPath = internalPath.replace(TRELLIS_DATA_PREFIX, SLASH);
240+
if (SLASH.equals(urlPath) || !isContainer) {
241+
return urlPath;
242+
}
243+
return urlPath + SLASH;
244+
245+
}
246+
234247
protected void verifyCanAppend(final Set<IRI> modes, final Session session, final String path) {
235248
if (!modes.contains(ACL.Append) && !modes.contains(ACL.Write)) {
236249
LOGGER.warn("User: {} cannot Append to {}", session.getAgent(), path);

auth/webac/src/main/java/org/trellisldp/webac/WebAcService.java

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ public AuthorizedModes getAuthorizedModes(final IRI identifier, final Session se
215215
requireNonNull(session, "A non-null session must be provided!");
216216

217217
if (Trellis.AdministratorAgent.equals(session.getAgent())) {
218-
return new AuthorizedModes(identifier, allModes);
218+
return new AuthorizedModes(null, allModes);
219219
}
220220

221221
final AuthorizedModes cachedModes = cache.get(generateCacheKey(identifier, session.getAgent()), k ->
@@ -225,7 +225,7 @@ public AuthorizedModes getAuthorizedModes(final IRI identifier, final Session se
225225
final AuthorizedModes delegatedModes = cache.get(generateCacheKey(identifier, delegate),
226226
k -> getAuthz(identifier, delegate));
227227
modes.retainAll(delegatedModes.getAccessModes());
228-
return new AuthorizedModes(cachedModes.getEffectiveAcl(), modes);
228+
return new AuthorizedModes(cachedModes.getEffectiveAcl().orElse(null), modes);
229229
}).orElse(cachedModes);
230230
}
231231

@@ -255,16 +255,17 @@ private AuthorizedModes getAuthz(final IRI identifier, final IRI agent) {
255255
modes.remove(ACL.Append);
256256
}
257257
});
258-
return new AuthorizedModes(authModes.getEffectiveAcl(), modes);
258+
return new AuthorizedModes(authModes.getEffectiveAcl().orElse(null), modes);
259259
}
260260
return authModes;
261261
}
262262

263263
private AuthorizedModes getModesFor(final IRI identifier, final IRI agent) {
264-
return getNearestResource(identifier).map(resource ->
265-
new AuthorizedModes(resource.getIdentifier(), getAllAuthorizationsFor(resource, false)
266-
.filter(agentFilter(agent)).flatMap(auth -> auth.getMode().stream()).collect(toSet())))
267-
.orElseGet(() -> new AuthorizedModes(root, emptySet()));
264+
return getNearestResource(identifier).map(resource -> {
265+
final Authorizations authorizations = getAllAuthorizationsFor(resource, false);
266+
return new AuthorizedModes(authorizations.getIdentifier(), authorizations.stream()
267+
.filter(agentFilter(agent)).flatMap(auth -> auth.getMode().stream()).collect(toSet()));
268+
}).orElseGet(() -> new AuthorizedModes(root, emptySet()));
268269
}
269270

270271
private Optional<Resource> getNearestResource(final IRI identifier) {
@@ -291,7 +292,7 @@ private Predicate<IRI> isAgentInGroup(final IRI agent) {
291292
}).toCompletableFuture().join();
292293
}
293294

294-
private Stream<Authorization> getAllAuthorizationsFor(final Resource resource, final boolean inherited) {
295+
private Authorizations getAllAuthorizationsFor(final Resource resource, final boolean inherited) {
295296
LOGGER.debug("Checking ACL for: {}", resource.getIdentifier());
296297
if (resource.hasMetadata(Trellis.PreferAccessControl)) {
297298
try (final Graph graph = resource.stream(Trellis.PreferAccessControl).map(Quad::asTriple)
@@ -300,20 +301,21 @@ private Stream<Authorization> getAllAuthorizationsFor(final Resource resource, f
300301
final List<Authorization> authorizations = getAuthorizationFromGraph(resource.getIdentifier(), graph);
301302
// Check for any acl:default statements if checking for inheritance
302303
if (inherited) {
303-
return authorizations.stream().filter(getInheritedAuth(resource.getIdentifier()));
304+
return new Authorizations(resource.getIdentifier(),
305+
authorizations.stream().filter(getInheritedAuth(resource.getIdentifier())));
304306
}
305307
// If not inheriting, just return the relevant Authorizations
306-
return authorizations.stream();
308+
return new Authorizations(resource.getIdentifier(), authorizations.stream());
307309
} catch (final Exception ex) {
308310
throw new RuntimeTrellisException("Error closing graph", ex);
309311
}
310312
} else if (root.equals(resource.getIdentifier())) {
311-
return defaultRootAuthorizations.stream();
313+
return new Authorizations(root, defaultRootAuthorizations.stream());
312314
}
313315
// Nothing here, check the parent
314316
LOGGER.debug("No ACL for {}; looking up parent resource", resource.getIdentifier());
315317
return getContainer(resource.getIdentifier()).flatMap(this::getNearestResource)
316-
.map(res -> getAllAuthorizationsFor(res, true)).orElseGet(Stream::empty);
318+
.map(res -> getAllAuthorizationsFor(res, true)).orElseGet(() -> new Authorizations(root));
317319
}
318320

319321
static List<Authorization> getAuthorizationFromGraph(final IRI identifier, final Graph graph) {
@@ -326,6 +328,28 @@ static List<Authorization> getAuthorizationFromGraph(final IRI identifier, final
326328
}).filter(auth -> auth.getAccessTo().contains(identifier)).collect(toList());
327329
}
328330

331+
static class Authorizations {
332+
private final IRI resource;
333+
private final Stream<Authorization> stream;
334+
335+
public Authorizations(final IRI resource) {
336+
this(resource, Stream.empty());
337+
}
338+
339+
public Authorizations(final IRI resource, final Stream<Authorization> stream) {
340+
this.resource = resource;
341+
this.stream = stream;
342+
}
343+
344+
public IRI getIdentifier() {
345+
return resource;
346+
}
347+
348+
public Stream<Authorization> stream() {
349+
return stream;
350+
}
351+
}
352+
329353
static boolean hasWritableMode(final Set<IRI> modes) {
330354
return modes.contains(ACL.Write) || modes.contains(ACL.Append);
331355
}

auth/webac/src/test/java/org/trellisldp/webac/WebAcFilterTest.java

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,8 +490,11 @@ void testFilterResponseNoControl() {
490490
@Test
491491
void testFilterResponseWithControl() {
492492
final MultivaluedMap<String, Object> headers = new MultivaluedHashMap<>();
493+
final MultivaluedMap<String, String> stringHeaders = new MultivaluedHashMap<>();
494+
stringHeaders.putSingle("Link", "<http://www.w3.org/ns/ldp#BasicContainer>; rel=\"type\"");
493495
when(mockResponseContext.getStatusInfo()).thenReturn(OK);
494496
when(mockResponseContext.getHeaders()).thenReturn(headers);
497+
when(mockResponseContext.getStringHeaders()).thenReturn(stringHeaders);
495498
when(mockContext.getProperty(eq(WebAcFilter.SESSION_WEBAC_MODES)))
496499
.thenReturn(new AuthorizedModes(effectiveAcl, allModes));
497500

@@ -510,6 +513,94 @@ void testFilterResponseWithControl() {
510513
link.getRels().contains(Trellis.effectiveAcl.getIRIString())));
511514
}
512515

516+
@Test
517+
void testFilterResponseWithControl2() {
518+
final MultivaluedMap<String, Object> headers = new MultivaluedHashMap<>();
519+
final MultivaluedMap<String, String> stringHeaders = new MultivaluedHashMap<>();
520+
stringHeaders.putSingle("Link", "<http://www.w3.org/ns/ldp#BasicContainer>; rel=\"blah\"");
521+
when(mockResponseContext.getStatusInfo()).thenReturn(OK);
522+
when(mockResponseContext.getHeaders()).thenReturn(headers);
523+
when(mockResponseContext.getStringHeaders()).thenReturn(stringHeaders);
524+
when(mockContext.getProperty(eq(WebAcFilter.SESSION_WEBAC_MODES)))
525+
.thenReturn(new AuthorizedModes(effectiveAcl, allModes));
526+
527+
final WebAcFilter filter = new WebAcFilter();
528+
filter.setAccessService(mockWebAcService);
529+
530+
assertTrue(headers.isEmpty());
531+
filter.filter(mockContext, mockResponseContext);
532+
assertFalse(headers.isEmpty());
533+
534+
final List<Object> links = headers.get("Link");
535+
assertTrue(links.stream().map(Link.class::cast).anyMatch(link ->
536+
link.getRels().contains("acl") && "/?ext=acl".equals(link.getUri().toString())));
537+
assertTrue(links.stream().map(Link.class::cast).anyMatch(link ->
538+
"/?ext=acl".equals(link.getUri().toString()) &&
539+
link.getRels().contains(Trellis.effectiveAcl.getIRIString())));
540+
}
541+
542+
@Test
543+
void testFilterResourceResponseWithControl() {
544+
final IRI localEffectiveAcl = rdf.createIRI(TRELLIS_DATA_PREFIX + "resource");
545+
final MultivaluedMap<String, Object> headers = new MultivaluedHashMap<>();
546+
final MultivaluedMap<String, String> stringHeaders = new MultivaluedHashMap<>();
547+
stringHeaders.putSingle("Link", "<http://www.w3.org/ns/ldp#RDFSource>; rel=\"type\"");
548+
when(mockResponseContext.getStatusInfo()).thenReturn(OK);
549+
when(mockResponseContext.getHeaders()).thenReturn(headers);
550+
when(mockResponseContext.getStringHeaders()).thenReturn(stringHeaders);
551+
when(mockUriInfo.getPath()).thenReturn("/resource");
552+
when(mockWebAcService.getAuthorizedModes(any(IRI.class), any(Session.class)))
553+
.thenReturn(new AuthorizedModes(localEffectiveAcl, allModes));
554+
555+
when(mockContext.getProperty(eq(WebAcFilter.SESSION_WEBAC_MODES)))
556+
.thenReturn(new AuthorizedModes(localEffectiveAcl, allModes));
557+
558+
final WebAcFilter filter = new WebAcFilter();
559+
filter.setAccessService(mockWebAcService);
560+
561+
assertTrue(headers.isEmpty());
562+
filter.filter(mockContext, mockResponseContext);
563+
assertFalse(headers.isEmpty());
564+
565+
final List<Object> links = headers.get("Link");
566+
assertTrue(links.stream().map(Link.class::cast).anyMatch(link ->
567+
link.getRels().contains("acl") && "/resource?ext=acl".equals(link.getUri().toString())));
568+
assertTrue(links.stream().map(Link.class::cast).anyMatch(link ->
569+
"/resource?ext=acl".equals(link.getUri().toString()) &&
570+
link.getRels().contains(Trellis.effectiveAcl.getIRIString())));
571+
}
572+
573+
@Test
574+
void testFilterContainerResponseWithControl() {
575+
final IRI localEffectiveAcl = rdf.createIRI(TRELLIS_DATA_PREFIX + "container");
576+
final MultivaluedMap<String, Object> headers = new MultivaluedHashMap<>();
577+
final MultivaluedMap<String, String> stringHeaders = new MultivaluedHashMap<>();
578+
stringHeaders.putSingle("Link", "<http://www.w3.org/ns/ldp#BasicContainer>; rel=\"type\"");
579+
when(mockResponseContext.getStatusInfo()).thenReturn(OK);
580+
when(mockResponseContext.getHeaders()).thenReturn(headers);
581+
when(mockResponseContext.getStringHeaders()).thenReturn(stringHeaders);
582+
when(mockUriInfo.getPath()).thenReturn("/container/");
583+
when(mockWebAcService.getAuthorizedModes(any(IRI.class), any(Session.class)))
584+
.thenReturn(new AuthorizedModes(localEffectiveAcl, allModes));
585+
586+
when(mockContext.getProperty(eq(WebAcFilter.SESSION_WEBAC_MODES)))
587+
.thenReturn(new AuthorizedModes(localEffectiveAcl, allModes));
588+
589+
final WebAcFilter filter = new WebAcFilter();
590+
filter.setAccessService(mockWebAcService);
591+
592+
assertTrue(headers.isEmpty());
593+
filter.filter(mockContext, mockResponseContext);
594+
assertFalse(headers.isEmpty());
595+
596+
final List<Object> links = headers.get("Link");
597+
assertTrue(links.stream().map(Link.class::cast).anyMatch(link ->
598+
link.getRels().contains("acl") && "/container/?ext=acl".equals(link.getUri().toString())));
599+
assertTrue(links.stream().map(Link.class::cast).anyMatch(link ->
600+
"/container/?ext=acl".equals(link.getUri().toString()) &&
601+
link.getRels().contains(Trellis.effectiveAcl.getIRIString())));
602+
}
603+
513604
@Test
514605
void testFilterResponseDelete() {
515606
final MultivaluedMap<String, Object> headers = new MultivaluedHashMap<>();
@@ -530,8 +621,10 @@ void testFilterResponseDelete() {
530621
@Test
531622
void testFilterResponseBaseUrl() {
532623
final MultivaluedMap<String, Object> headers = new MultivaluedHashMap<>();
624+
final MultivaluedMap<String, String> stringHeaders = new MultivaluedHashMap<>();
533625
when(mockResponseContext.getStatusInfo()).thenReturn(OK);
534626
when(mockResponseContext.getHeaders()).thenReturn(headers);
627+
when(mockResponseContext.getStringHeaders()).thenReturn(stringHeaders);
535628
when(mockUriInfo.getPath()).thenReturn("/path");
536629
when(mockContext.getProperty(eq(WebAcFilter.SESSION_WEBAC_MODES)))
537630
.thenReturn(new AuthorizedModes(effectiveAcl, allModes));
@@ -555,10 +648,12 @@ void testFilterResponseBaseUrl() {
555648
void testFilterResponseWebac2() {
556649
final MultivaluedMap<String, Object> headers = new MultivaluedHashMap<>();
557650
final MultivaluedMap<String, String> params = new MultivaluedHashMap<>();
651+
final MultivaluedMap<String, String> stringHeaders = new MultivaluedHashMap<>();
558652
params.add("ext", "foo");
559653
params.add("ext", "acl");
560654
when(mockResponseContext.getStatusInfo()).thenReturn(OK);
561655
when(mockResponseContext.getHeaders()).thenReturn(headers);
656+
when(mockResponseContext.getStringHeaders()).thenReturn(stringHeaders);
562657
when(mockUriInfo.getQueryParameters()).thenReturn(params);
563658
when(mockUriInfo.getPath()).thenReturn("path/");
564659
when(mockContext.getProperty(eq(WebAcFilter.SESSION_WEBAC_MODES)))

0 commit comments

Comments
 (0)