Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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