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 @@ -129,7 +129,8 @@ public void run(final T config, final Environment environment) throws Exception
config.getCache().getMustRevalidate(), config.getCache().getNoCache()));

// Authorization
ofNullable(getWebacService(config, getServiceBundler().getResourceService())).ifPresent(webac -> {
ofNullable(getWebacService(config, getServiceBundler().getResourceService(),
getServiceBundler().getIOService())).ifPresent(webac -> {
final List<String> challenges = new ArrayList<>();
of(config.getAuth().getJwt()).filter(JwtAuthConfiguration::getEnabled).map(x -> "Bearer")
.ifPresent(challenges::add);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import javax.ws.rs.container.ContainerRequestFilter;

import org.apache.commons.rdf.api.IRI;
import org.trellisldp.api.IOService;
import org.trellisldp.api.ResourceService;
import org.trellisldp.api.RuntimeTrellisException;
import org.trellisldp.auth.basic.BasicAuthFilter;
Expand Down Expand Up @@ -68,12 +69,12 @@ public static Authenticator getJwtAuthenticator(final JwtAuthConfiguration confi
}

public static WebAcService getWebacService(final TrellisConfiguration config,
final ResourceService resourceService) {
final ResourceService resourceService, final IOService ioService) {
if (config.getAuth().getWebac().getEnabled()) {
final Cache<String, Set<IRI>> authCache = newBuilder().maximumSize(config.getAuth().getWebac()
.getCacheSize()).expireAfterWrite(config.getAuth().getWebac()
.getCacheExpireSeconds(), SECONDS).build();
final WebAcService webac = new WebAcService(resourceService, new TrellisCache<>(authCache));
final WebAcService webac = new WebAcService(resourceService, ioService, new TrellisCache<>(authCache));
try {
webac.initialize();
} catch (final Exception ex) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.trellisldp.api.IOService;
import org.trellisldp.api.Resource;
import org.trellisldp.api.ResourceService;
import org.trellisldp.api.RuntimeTrellisException;
Expand All @@ -44,12 +45,15 @@
import org.trellisldp.auth.oauth.JwtAuthenticator;
import org.trellisldp.auth.oauth.NullAuthenticator;
import org.trellisldp.dropwizard.config.TrellisConfiguration;
import org.trellisldp.io.JenaIOService;

/**
* @author acoburn
*/
class TrellisUtilsTest {

private IOService ioService = new JenaIOService();

@Mock
private Environment mockEnv;

Expand Down Expand Up @@ -91,21 +95,22 @@ void testGetWebacService() throws Exception {
when(mockResourceService.get(any())).thenAnswer(inv -> completedFuture(mockResource));
when(mockResource.hasAcl()).thenReturn(true);

assertNotNull(TrellisUtils.getWebacService(config, mockResourceService), "WebAC configuration not present!");
assertNotNull(TrellisUtils.getWebacService(config, mockResourceService, ioService),
"WebAC configuration not present!");

config.getAuth().getWebac().setEnabled(false);

assertNull(TrellisUtils.getWebacService(config, mockResourceService),
assertNull(TrellisUtils.getWebacService(config, mockResourceService, ioService),
"WebAC config persists after disabling it!");

config.getAuth().getWebac().setEnabled(true);

final ResourceService mockRS = mock(ResourceService.class, inv -> {
throw new RuntimeTrellisException("expected");
});
assertThrows(RuntimeTrellisException.class, () -> TrellisUtils.getWebacService(config, mockRS));
assertThrows(RuntimeTrellisException.class, () -> TrellisUtils.getWebacService(config, mockRS, ioService));
config.getAuth().getWebac().setEnabled(false);
assertNull(TrellisUtils.getWebacService(config, mockRS),
assertNull(TrellisUtils.getWebacService(config, mockRS, ioService),
"WebAC config persists after disabling it!");
}

Expand Down
1 change: 1 addition & 0 deletions components/webac/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,6 @@ dependencies {
testImplementation "org.glassfish.jersey.core:jersey-server:$jerseyVersion"
testImplementation "org.glassfish.jersey.test-framework.providers:jersey-test-framework-provider-grizzly2:$jerseyVersion"
testImplementation "org.mockito:mockito-core:$mockitoVersion"
testImplementation project(':trellis-io-jena')
testImplementation project(':trellis-triplestore')
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
import static org.apache.commons.rdf.api.RDFSyntax.TURTLE;
import static org.eclipse.microprofile.config.ConfigProvider.getConfig;
import static org.slf4j.LoggerFactory.getLogger;
import static org.trellisldp.api.Resource.SpecialResources.DELETED_RESOURCE;
Expand All @@ -34,6 +35,8 @@
import static org.trellisldp.api.TrellisUtils.getInstance;
import static org.trellisldp.api.TrellisUtils.toGraph;

import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
Expand All @@ -57,14 +60,7 @@
import org.apache.commons.rdf.api.RDFTerm;
import org.apache.commons.rdf.api.Triple;
import org.slf4j.Logger;
import org.trellisldp.api.CacheService;
import org.trellisldp.api.Metadata;
import org.trellisldp.api.NoopResourceService;
import org.trellisldp.api.Resource;
import org.trellisldp.api.ResourceService;
import org.trellisldp.api.RuntimeTrellisException;
import org.trellisldp.api.Session;
import org.trellisldp.api.TrellisUtils;
import org.trellisldp.api.*;
import org.trellisldp.http.core.ServiceBundler;
import org.trellisldp.vocabulary.ACL;
import org.trellisldp.vocabulary.FOAF;
Expand All @@ -85,36 +81,37 @@ public class WebAcService {
/** The configuration key controlling whether to check member resources at the AuthZ enforcement point. */
public static final String CONFIG_WEBAC_MEMBERSHIP_CHECK = "trellis.webac.membership.check";

/** The configuration key controlling the location of a default root ACL. */
public static final String CONFIG_WEBAC_ROOT_ACL_LOCATION = "trellis.webac.root.acl";

private static final Logger LOGGER = getLogger(WebAcService.class);
private static final CompletionStage<Void> DONE = CompletableFuture.completedFuture(null);
private static final RDF rdf = getInstance();
private static final IRI root = rdf.createIRI(TRELLIS_DATA_PREFIX);
private static final IRI rootAuth = rdf.createIRI(TRELLIS_DATA_PREFIX + "#auth");
private static final Set<IRI> allModes = new HashSet<>();

/** The permissive Authorizations in effect when no ACL is present on the root node. */
private static final List<Authorization> defaultRootAuthorizations =
unmodifiableList(getDefaultRootAuthorizations());

static {
allModes.add(ACL.Read);
allModes.add(ACL.Write);
allModes.add(ACL.Control);
allModes.add(ACL.Append);
}

/** The permissive Authorizations in effect when no ACL is present on the root node. */
private final List<Authorization> defaultRootAuthorizations;
private final ResourceService resourceService;
private final IOService ioService;
private final CacheService<String, Set<IRI>> cache;
private final boolean checkMembershipResources;

/**
* Create a WebAC-based authorization service.
*/
public WebAcService() {
this(new NoopResourceService());
this(new NoopResourceService(), null);
}

private static List<Authorization> getDefaultRootAuthorizations() {
private List<Authorization> getDefaultRootAuthorizations() {
try (final Dataset dataset = generateDefaultRootAuthorizationsDataset()) {
return dataset.getGraph(Trellis.PreferAccessControl).map(graph -> Authorization.from(rootAuth, graph))
.map(Collections::singletonList).orElse(emptyList());
Expand All @@ -123,15 +120,30 @@ private static List<Authorization> getDefaultRootAuthorizations() {
}
}

private static Dataset generateDefaultRootAuthorizationsDataset() {
private Dataset generateDefaultRootAuthorizationsDataset() {
final Dataset dataset = rdf.createDataset();
dataset.add(rdf.createQuad(Trellis.PreferAccessControl, rootAuth, ACL.mode, ACL.Read));
dataset.add(rdf.createQuad(Trellis.PreferAccessControl, rootAuth, ACL.mode, ACL.Write));
dataset.add(rdf.createQuad(Trellis.PreferAccessControl, rootAuth, ACL.mode, ACL.Control));
dataset.add(rdf.createQuad(Trellis.PreferAccessControl, rootAuth, ACL.mode, ACL.Append));
dataset.add(rdf.createQuad(Trellis.PreferAccessControl, rootAuth, ACL.agentClass, FOAF.Agent));
dataset.add(rdf.createQuad(Trellis.PreferAccessControl, rootAuth, ACL.default_, root));
dataset.add(rdf.createQuad(Trellis.PreferAccessControl, rootAuth, ACL.accessTo, root));
final String location = getConfig().getOptionalValue(CONFIG_WEBAC_ROOT_ACL_LOCATION, String.class)
.orElse("/org/trellisldp/webac/defaultAcl.ttl");
try (final InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(location)) {
if (ioService != null && is != null) {
ioService.read(is, TURTLE, TRELLIS_DATA_PREFIX)
.map(triple -> rdf.createQuad(Trellis.PreferAccessControl, triple.getSubject(),
triple.getPredicate(), triple.getObject()))
.forEach(dataset::add);
}
} catch (final IOException ex) {
LOGGER.warn("Error reading ACL from {}: {}", location, ex.getMessage());
}

if (dataset.size() == 0) {
dataset.add(rdf.createQuad(Trellis.PreferAccessControl, rootAuth, ACL.mode, ACL.Read));
dataset.add(rdf.createQuad(Trellis.PreferAccessControl, rootAuth, ACL.mode, ACL.Write));
dataset.add(rdf.createQuad(Trellis.PreferAccessControl, rootAuth, ACL.mode, ACL.Control));
dataset.add(rdf.createQuad(Trellis.PreferAccessControl, rootAuth, ACL.mode, ACL.Append));
dataset.add(rdf.createQuad(Trellis.PreferAccessControl, rootAuth, ACL.agentClass, FOAF.Agent));
dataset.add(rdf.createQuad(Trellis.PreferAccessControl, rootAuth, ACL.default_, root));
dataset.add(rdf.createQuad(Trellis.PreferAccessControl, rootAuth, ACL.accessTo, root));
}
return dataset;
}

Expand Down Expand Up @@ -183,41 +195,47 @@ public WebAcService(final ServiceBundler services) {
@Inject
public WebAcService(final ServiceBundler services,
@TrellisAuthorizationCache final CacheService<String, Set<IRI>> cache) {
this(services.getResourceService(), cache);
this(services.getResourceService(), services.getIOService(), cache);
}

/**
* Create a WebAC-based authorization service.
*
* @param resourceService the resource service
* @param ioService the IO service (may be {@code null})
*/
public WebAcService(final ResourceService resourceService) {
this(resourceService, new NoopAuthorizationCache());
public WebAcService(final ResourceService resourceService, final IOService ioService) {
this(resourceService, ioService, new NoopAuthorizationCache());
}

/**
* Create a WebAC-based authorization service.
*
* @param resourceService the resource service
* @param ioService the IO service (may be {@code null})
* @param cache a cache
*/
public WebAcService(final ResourceService resourceService, final CacheService<String, Set<IRI>> cache) {
this(resourceService, cache, getConfig()
public WebAcService(final ResourceService resourceService, final IOService ioService,
final CacheService<String, Set<IRI>> cache) {
this(resourceService, ioService, cache, getConfig()
.getOptionalValue(CONFIG_WEBAC_MEMBERSHIP_CHECK, Boolean.class).orElse(Boolean.FALSE));
}

/**
* Create a WebAC-based authorization service.
*
* @param resourceService the resource service
* @param ioService the IO service (may be {@code null})
* @param cache a cache
* @param checkMembershipResources whether to check membership resource permissions (default=false)
*/
public WebAcService(final ResourceService resourceService,
public WebAcService(final ResourceService resourceService, final IOService ioService,
final CacheService<String, Set<IRI>> cache, final boolean checkMembershipResources) {
this.resourceService = requireNonNull(resourceService, "A non-null ResourceService must be provided!");
this.ioService = ioService;
this.cache = cache;
this.checkMembershipResources = checkMembershipResources;
this.defaultRootAuthorizations = unmodifiableList(getDefaultRootAuthorizations());
}

/**
Expand Down Expand Up @@ -319,7 +337,7 @@ private Stream<Authorization> getAllAuthorizationsFor(final Resource resource, f
throw new RuntimeTrellisException("Error closing graph", ex);
}
} else if (root.equals(resource.getIdentifier())) {
return WebAcService.defaultRootAuthorizations.stream();
return defaultRootAuthorizations.stream();
}
// Nothing here, check the parent
LOGGER.debug("No ACL for {}; looking up parent resource", resource.getIdentifier());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
@prefix acl: <http://www.w3.org/ns/auth/acl#> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .

<#auth> acl:mode acl:Read, acl:Write, acl:Append, acl:Control ;
acl:agentClass foaf:Agent ;
acl:default <> ;
acl:accessTo <> .
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,9 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.function.Executable;
import org.mockito.Mock;
import org.trellisldp.api.CacheService;
import org.trellisldp.api.Resource;
import org.trellisldp.api.ResourceService;
import org.trellisldp.api.Session;
import org.trellisldp.api.*;
import org.trellisldp.http.core.ServiceBundler;
import org.trellisldp.io.JenaIOService;
import org.trellisldp.vocabulary.ACL;
import org.trellisldp.vocabulary.FOAF;
import org.trellisldp.vocabulary.LDP;
Expand All @@ -57,6 +55,7 @@
class WebAcServiceTest {

private static final RDF rdf = new JenaRDF();
private static final IOService ioService = new JenaIOService();

private static final IRI memberIRI = rdf.createIRI(TRELLIS_DATA_PREFIX + "member");
private static final IRI nonexistentIRI = rdf.createIRI(TRELLIS_DATA_PREFIX + "parent/child/nonexistent");
Expand Down Expand Up @@ -151,6 +150,16 @@ void testDefaultResourceService() {
assertDoesNotThrow(() -> new WebAcService());
}

@Test
void testDefaultRootAcl() {
try {
System.setProperty(WebAcService.CONFIG_WEBAC_ROOT_ACL_LOCATION, "non-existent.ttl");
assertDoesNotThrow(() -> new WebAcService(mockResourceService, ioService));
} finally {
System.clearProperty(WebAcService.CONFIG_WEBAC_ROOT_ACL_LOCATION);
}
}

@Test
void testInitialize() {
when(mockRootResource.hasAcl()).thenReturn(false);
Expand Down Expand Up @@ -286,7 +295,7 @@ void testCanWrite5() {

@Test
void testCanWrite6() {
final WebAcService testService2 = new WebAcService(mockResourceService,
final WebAcService testService2 = new WebAcService(mockResourceService, ioService,
new WebAcService.NoopAuthorizationCache(), false);
when(mockSession.getAgent()).thenReturn(agentIRI);
when(mockParentResource.getInteractionModel()).thenReturn(LDP.DirectContainer);
Expand All @@ -308,7 +317,7 @@ void testCanWrite6() {

@Test
void testCanWrite7() {
final WebAcService testService2 = new WebAcService(mockResourceService,
final WebAcService testService2 = new WebAcService(mockResourceService, ioService,
new WebAcService.NoopAuthorizationCache(), false);
when(mockSession.getAgent()).thenReturn(addisonIRI);
when(mockParentResource.getInteractionModel()).thenReturn(LDP.IndirectContainer);
Expand Down Expand Up @@ -711,7 +720,7 @@ void testUnauthenticatedUser() {

@Test
void testCacheCanWrite1() {
final WebAcService testCacheService = new WebAcService(mockResourceService, mockCache);
final WebAcService testCacheService = new WebAcService(mockResourceService, ioService, mockCache);
when(mockSession.getAgent()).thenReturn(acoburnIRI);
assertAll("Check writability with cache", checkCannotWrite(testCacheService, nonexistentIRI),
checkCannotWrite(testCacheService, resourceIRI),
Expand All @@ -722,7 +731,7 @@ void testCacheCanWrite1() {

@Test
void testCacheCanWrite2() {
final WebAcService testCacheService = new WebAcService(mockResourceService, mockCache);
final WebAcService testCacheService = new WebAcService(mockResourceService, ioService, mockCache);
when(mockSession.getAgent()).thenReturn(addisonIRI);
assertAll("Check writability with cache", checkCanWrite(testCacheService, nonexistentIRI),
checkCanWrite(testCacheService, resourceIRI), checkCanWrite(testCacheService, childIRI),
Expand All @@ -732,7 +741,7 @@ void testCacheCanWrite2() {

@Test
void testCacheCanWrite3() {
final WebAcService testCacheService = new WebAcService(mockResourceService, mockCache);
final WebAcService testCacheService = new WebAcService(mockResourceService, ioService, mockCache);
when(mockSession.getAgent()).thenReturn(agentIRI);
when(mockSession.getDelegatedBy()).thenReturn(of(addisonIRI));
assertAll("Check delegated writability with cache", checkCanWrite(testCacheService, nonexistentIRI),
Expand Down