Skip to content

Commit

Permalink
Effective ACL should point to correct location (#903)
Browse files Browse the repository at this point in the history
Resolves #893
  • Loading branch information
acoburn authored Jun 13, 2020
1 parent 4a995db commit 26ea63a
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -40,8 +41,8 @@ public AuthorizedModes(final IRI effectiveAcl, final Set<IRI> modes) {
* Get the location of the effective ACL.
* @return the location of the effective ACL
*/
public IRI getEffectiveAcl() {
return effectiveAcl;
public Optional<IRI> getEffectiveAcl() {
return Optional.ofNullable(effectiveAcl);
}

/**
Expand Down
19 changes: 16 additions & 3 deletions auth/webac/src/main/java/org/trellisldp/webac/WebAcFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<IRI> 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);
Expand Down
48 changes: 36 additions & 12 deletions auth/webac/src/main/java/org/trellisldp/webac/WebAcService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 ->
Expand All @@ -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);
}

Expand Down Expand Up @@ -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<Resource> getNearestResource(final IRI identifier) {
Expand All @@ -291,7 +292,7 @@ private Predicate<IRI> isAgentInGroup(final IRI agent) {
}).toCompletableFuture().join();
}

private Stream<Authorization> 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)
Expand All @@ -300,20 +301,21 @@ private Stream<Authorization> getAllAuthorizationsFor(final Resource resource, f
final List<Authorization> 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<Authorization> getAuthorizationFromGraph(final IRI identifier, final Graph graph) {
Expand All @@ -326,6 +328,28 @@ static List<Authorization> getAuthorizationFromGraph(final IRI identifier, final
}).filter(auth -> auth.getAccessTo().contains(identifier)).collect(toList());
}

static class Authorizations {
private final IRI resource;
private final Stream<Authorization> stream;

public Authorizations(final IRI resource) {
this(resource, Stream.empty());
}

public Authorizations(final IRI resource, final Stream<Authorization> stream) {
this.resource = resource;
this.stream = stream;
}

public IRI getIdentifier() {
return resource;
}

public Stream<Authorization> stream() {
return stream;
}
}

static boolean hasWritableMode(final Set<IRI> modes) {
return modes.contains(ACL.Write) || modes.contains(ACL.Append);
}
Expand Down
95 changes: 95 additions & 0 deletions auth/webac/src/test/java/org/trellisldp/webac/WebAcFilterTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -490,8 +490,11 @@ void testFilterResponseNoControl() {
@Test
void testFilterResponseWithControl() {
final MultivaluedMap<String, Object> headers = new MultivaluedHashMap<>();
final MultivaluedMap<String, String> stringHeaders = new MultivaluedHashMap<>();
stringHeaders.putSingle("Link", "<http://www.w3.org/ns/ldp#BasicContainer>; 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));

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

@Test
void testFilterResponseWithControl2() {
final MultivaluedMap<String, Object> headers = new MultivaluedHashMap<>();
final MultivaluedMap<String, String> stringHeaders = new MultivaluedHashMap<>();
stringHeaders.putSingle("Link", "<http://www.w3.org/ns/ldp#BasicContainer>; 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<Object> 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<String, Object> headers = new MultivaluedHashMap<>();
final MultivaluedMap<String, String> stringHeaders = new MultivaluedHashMap<>();
stringHeaders.putSingle("Link", "<http://www.w3.org/ns/ldp#RDFSource>; 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<Object> 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<String, Object> headers = new MultivaluedHashMap<>();
final MultivaluedMap<String, String> stringHeaders = new MultivaluedHashMap<>();
stringHeaders.putSingle("Link", "<http://www.w3.org/ns/ldp#BasicContainer>; 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<Object> 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<String, Object> headers = new MultivaluedHashMap<>();
Expand All @@ -530,8 +621,10 @@ void testFilterResponseDelete() {
@Test
void testFilterResponseBaseUrl() {
final MultivaluedMap<String, Object> headers = new MultivaluedHashMap<>();
final MultivaluedMap<String, String> 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));
Expand All @@ -555,10 +648,12 @@ void testFilterResponseBaseUrl() {
void testFilterResponseWebac2() {
final MultivaluedMap<String, Object> headers = new MultivaluedHashMap<>();
final MultivaluedMap<String, String> params = new MultivaluedHashMap<>();
final MultivaluedMap<String, String> 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)))
Expand Down

0 comments on commit 26ea63a

Please sign in to comment.