diff --git a/acp/pom.xml b/acp/pom.xml
new file mode 100644
index 00000000000..82fff84ebf7
--- /dev/null
+++ b/acp/pom.xml
@@ -0,0 +1,114 @@
+
+
+ 4.0.0
+
+ com.inrupt.client
+ inrupt-client
+ 2.0.0-SNAPSHOT
+
+
+ inrupt-client-acp
+ Inrupt Java Client Libraries - Access Control Policies
+
+ Access Control Policy support for the Inrupt Client Libraries.
+
+
+
+
+ com.inrupt.client
+ inrupt-client-api
+ ${project.version}
+
+
+ com.inrupt.client
+ inrupt-client-vocabulary
+ ${project.version}
+
+
+ com.inrupt.client
+ inrupt-client-solid
+ ${project.version}
+
+
+ org.slf4j
+ slf4j-api
+ ${slf4j.version}
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ ${junit.version}
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-params
+ ${junit.version}
+ test
+
+
+ org.wiremock
+ wiremock
+ ${wiremock.version}
+ test
+
+
+ com.inrupt.client
+ inrupt-client-jackson
+ ${project.version}
+ test
+
+
+ com.inrupt.client
+ inrupt-client-guava
+ ${project.version}
+ test
+
+
+ com.inrupt.client
+ inrupt-client-core
+ ${project.version}
+ test
+
+
+ com.inrupt.client
+ inrupt-client-httpclient
+ ${project.version}
+ test
+
+
+ com.inrupt.client
+ inrupt-client-jena
+ ${project.version}
+ test
+
+
+ org.slf4j
+ slf4j-simple
+ ${slf4j.version}
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+
+
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+
+
+
+
diff --git a/acp/src/main/java/com/inrupt/client/acp/AccessControl.java b/acp/src/main/java/com/inrupt/client/acp/AccessControl.java
new file mode 100644
index 00000000000..0e351769acd
--- /dev/null
+++ b/acp/src/main/java/com/inrupt/client/acp/AccessControl.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright Inrupt Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
+ * Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+ * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package com.inrupt.client.acp;
+
+import static com.inrupt.client.acp.AccessControlResource.asIRI;
+
+import com.inrupt.client.vocabulary.ACP;
+import com.inrupt.client.vocabulary.RDF;
+import com.inrupt.rdf.wrapping.commons.ValueMappings;
+import com.inrupt.rdf.wrapping.commons.WrapperIRI;
+
+import java.util.Set;
+
+import org.apache.commons.rdf.api.Graph;
+import org.apache.commons.rdf.api.IRI;
+import org.apache.commons.rdf.api.RDFTerm;
+
+/**
+ * An AccessControl type for use with Access Control Policies.
+ *
+ *
An access control applies {@link Policy} objects directly to a resource
+ * via {@code acp:accessControl} or to container members via {@code acp:memberAccessControl}
+ */
+public class AccessControl extends WrapperIRI {
+
+ /**
+ * Create a new AccessControl.
+ *
+ * @param identifier the access control identifier
+ * @param graph the underlying graph
+ */
+ public AccessControl(final RDFTerm identifier, final Graph graph) {
+ super(identifier, graph);
+ graph.add((IRI) identifier, asIRI(RDF.type), asIRI(ACP.AccessControl));
+ }
+
+ public Set apply() {
+ return objects(asIRI(ACP.apply), Policy::asResource, ValueMappings.as(Policy.class));
+ }
+
+ static IRI asResource(final AccessControl accessControl, final Graph graph) {
+ graph.add(accessControl, asIRI(RDF.type), asIRI(ACP.AccessControl));
+ accessControl.apply().forEach(policy -> {
+ graph.add(accessControl, asIRI(ACP.apply), policy);
+ Policy.asResource(policy, graph);
+ });
+
+ return accessControl;
+ }
+}
+
diff --git a/acp/src/main/java/com/inrupt/client/acp/AccessControlResource.java b/acp/src/main/java/com/inrupt/client/acp/AccessControlResource.java
new file mode 100644
index 00000000000..eacf803375e
--- /dev/null
+++ b/acp/src/main/java/com/inrupt/client/acp/AccessControlResource.java
@@ -0,0 +1,436 @@
+/*
+ * Copyright Inrupt Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
+ * Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+ * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package com.inrupt.client.acp;
+
+import com.inrupt.client.RDFSource;
+import com.inrupt.client.solid.SolidClient;
+import com.inrupt.client.solid.SolidSyncClient;
+import com.inrupt.client.vocabulary.ACP;
+import com.inrupt.client.vocabulary.RDF;
+import com.inrupt.rdf.wrapping.commons.ValueMappings;
+import com.inrupt.rdf.wrapping.commons.WrapperIRI;
+
+import java.net.URI;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+import org.apache.commons.rdf.api.BlankNodeOrIRI;
+import org.apache.commons.rdf.api.Dataset;
+import org.apache.commons.rdf.api.Graph;
+import org.apache.commons.rdf.api.IRI;
+import org.apache.commons.rdf.api.Quad;
+import org.apache.commons.rdf.api.RDFTerm;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * An Access Control Resource type.
+ *
+ *
This is the root type for a resource that expresses access control policies.
+ */
+public class AccessControlResource extends RDFSource {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(AccessControlResource.class);
+
+ public static final URI SOLID_ACCESS_GRANT = URI.create("http://www.w3.org/ns/solid/vc#SolidAccessGrant");
+
+ private static final Map EXPANSION_MAPPINGS = Map.of(
+ ACP.accessControl, asIRI(ACP.AccessControl),
+ ACP.memberAccessControl, asIRI(ACP.AccessControl),
+ ACP.apply, asIRI(ACP.Policy),
+ ACP.allOf, asIRI(ACP.Matcher),
+ ACP.anyOf, asIRI(ACP.Matcher),
+ ACP.noneOf, asIRI(ACP.Matcher));
+
+ private static final List EXPANSION_PROPERTIES = List.of(ACP.accessControl, ACP.memberAccessControl,
+ ACP.apply, ACP.allOf, ACP.anyOf, ACP.noneOf);
+
+ /**
+ * Definitions for different matcher types, for use with {@link #find}.
+ */
+ public enum MatcherType {
+ AGENT(ACP.agent), CLIENT(ACP.client), ISSUER(ACP.issuer), VC(ACP.vc);
+
+ private final URI predicate;
+
+ MatcherType(final URI predicate) {
+ this.predicate = predicate;
+ }
+
+ /**
+ * Return the matcher type as an IRI.
+ *
+ * @return the IRI for this predicate
+ */
+ public IRI asIRI() {
+ return AccessControlResource.asIRI(asURI());
+ }
+
+ /**
+ * Return the matcher type as a URI.
+ *
+ * @return the URI for this predicate
+ */
+ public URI asURI() {
+ return predicate;
+ }
+ }
+
+ /**
+ * Create a new Access Control Resource.
+ *
+ * @param identifier the resource identifier
+ * @param dataset the underlying dataset for the resource
+ */
+ public AccessControlResource(final URI identifier, final Dataset dataset) {
+ super(identifier, dataset);
+ dataset.add(null, asIRI(identifier), asIRI(RDF.type), asIRI(ACP.AccessControlResource));
+ }
+
+ /**
+ * Retrieve the acp:accessControl structures.
+ *
+ *
accessControl resources are applied (non-recursively) to a container or resource.
+ *
+ * @return a collection of {@link AccessControl} objects
+ */
+ public Set accessControl() {
+ return new ACPNode(asIRI(getIdentifier()), getGraph()).accessControl();
+ }
+
+ /**
+ * Retrieve the acp:memberAccessControl structures.
+ *
+ *
memberAccessControl resources are applied recursively to a containment hierarchy.
+ *
+ * @return a collection of {@link AccessControl} objects
+ */
+ public Set memberAccessControl() {
+ return new ACPNode(asIRI(getIdentifier()), getGraph()).memberAccessControl();
+ }
+
+ /**
+ * Expand the internal data using a synchronous client.
+ *
+ * @param client the solid client
+ * @return an expanded access control resource
+ */
+ public AccessControlResource expand(final SolidSyncClient client) {
+ // Copy the data from the existing ACR into a new dataset
+ final var dataset = rdf.createDataset();
+ stream().forEach(dataset::add);
+
+ try (final var cache = rdf.createDataset()) {
+ for (final var property : EXPANSION_PROPERTIES) {
+ expandType(dataset, cache, asIRI(property), EXPANSION_MAPPINGS.get(property),
+ uri -> populateCache(client, uri, cache));
+ }
+ } catch (final Exception ex) {
+ LOGGER.atDebug().setMessage("Unable to close dataset: {}").addArgument(ex::getMessage).log();
+ }
+
+ return new AccessControlResource(getIdentifier(), dataset);
+ }
+
+ /**
+ * Expand the internal data using an asynchronous client.
+ *
+ * @param client the solid client
+ * @return an expanded access control resource
+ */
+ public AccessControlResource expand(final SolidClient client) {
+ // Copy the data from the existing ACR into a new dataset
+ final var dataset = rdf.createDataset();
+ stream().forEach(dataset::add);
+
+ try (final var cache = rdf.createDataset()) {
+ for (final var property : EXPANSION_PROPERTIES) {
+ expandType(dataset, cache, asIRI(property), EXPANSION_MAPPINGS.get(property),
+ uri -> populateCacheAsync(client, uri, cache));
+ }
+ } catch (final Exception ex) {
+ LOGGER.atDebug().setMessage("Unable to close dataset: {}").addArgument(ex::getMessage).log();
+ }
+
+ return new AccessControlResource(getIdentifier(), dataset);
+ }
+
+ /**
+ * Compact the internal data.
+ */
+ public void compact() {
+ final var accessControls = stream(null, null, asIRI(RDF.type), asIRI(ACP.AccessControl))
+ .map(Quad::getSubject).toList();
+ for (final var accessControl : accessControls) {
+ removeUnusedStatements(accessControl);
+ }
+
+ final var policies = stream(null, null, asIRI(RDF.type), asIRI(ACP.Policy)).map(Quad::getSubject).toList();
+ for (final var policy : policies) {
+ removeUnusedStatements(policy);
+ }
+
+ final var matchers = stream(null, null, asIRI(RDF.type), asIRI(ACP.Matcher)).map(Quad::getSubject).toList();
+ for (final var matcher : matchers) {
+ removeUnusedStatements(matcher);
+ }
+ }
+
+ /**
+ * Merge two or more policies into a single policies with combined matchers.
+ *
+ * @param allow the modes to allow
+ * @param policies the policies to merge
+ * @return the merged policy
+ */
+ public Policy merge(final Set allow, final Policy... policies) {
+ final var baseUri = getIdentifier().getScheme() + ":" + getIdentifier().getSchemeSpecificPart();
+ final var policy = new Policy(asIRI(baseUri + "#" + UUID.randomUUID()), getGraph());
+ for (final var p : policies) {
+ policy.allOf().addAll(p.allOf());
+ policy.anyOf().addAll(p.anyOf());
+ policy.noneOf().addAll(p.noneOf());
+ }
+ policy.allow().addAll(allow);
+ return policy;
+ }
+
+ /**
+ * Find a policy, given a type, value and set of modes.
+ *
+ * @param type the matcher type
+ * @param value the matcher value, may be {@code null}
+ * @param modes the expected modes of the enclosing policy, may be {@code null}
+ * @return the matched policies
+ */
+ public Set find(final MatcherType type, final URI value, final Set modes) {
+ final IRI matcherValue = value != null ? asIRI(value) : null;
+ final Set matcherModes = modes != null ? modes : Collections.emptySet();
+ return stream(null, null, type.asIRI(), matcherValue)
+ .map(Quad::getSubject)
+ .flatMap(matcher -> stream(null, null, null, matcher))
+ .map(Quad::getSubject)
+ .filter(policy -> contains(null, policy, asIRI(RDF.type), asIRI(ACP.Policy)))
+ .filter(policy -> stream(null, policy, asIRI(ACP.allow), null)
+ .map(Quad::getObject).filter(IRI.class::isInstance).map(IRI.class::cast)
+ .map(IRI::getIRIString).map(URI::create).toList().containsAll(matcherModes))
+ .map(policy -> new Policy(policy, getGraph()))
+ .collect(Collectors.toSet());
+ }
+
+ /**
+ * Add policies to the access control resource.
+ *
+ * @param policies the policies to add
+ * @return the access control structure
+ */
+ public AccessControl accessControl(final Policy... policies) {
+ final var baseUri = getIdentifier().getScheme() + ":" + getIdentifier().getSchemeSpecificPart();
+ final var ac = new AccessControl(asIRI(baseUri + "#" + UUID.randomUUID()), getGraph());
+ for (final var policy : policies) {
+ ac.apply().add(policy);
+ }
+ return ac;
+ }
+
+ /**
+ * Create a policy that matches authenticated agents.
+ *
+ * @param access the access levels, such as Read or Write
+ * @return the new policy
+ */
+ public Policy authenticatedAgentPolicy(final URI... access) {
+ return agentPolicy(ACP.AuthenticatedAgent, access);
+ }
+
+ /**
+ * Create a policy that matches all agents.
+ *
+ * @param access the access levels, such as Read or Write
+ * @return the new policy
+ */
+ public Policy anyAgentPolicy(final URI... access) {
+ return agentPolicy(ACP.PublicAgent, access);
+ }
+
+ /**
+ * Create a policy that matches all clients.
+ *
+ * @param access the access levels, such as Read or Write
+ * @return the new policy
+ */
+ public Policy anyClientPolicy(final URI... access) {
+ return clientPolicy(ACP.PublicClient, access);
+ }
+
+ /**
+ * Create a policy that matches all issuers.
+ *
+ * @param access the access levels, such as Read or Write
+ * @return the new policy
+ */
+ public Policy anyIssuerPolicy(final URI... access) {
+ return issuerPolicy(ACP.PublicIssuer, access);
+ }
+
+ /**
+ * Create a policy that matches access grants.
+ *
+ * @param access the access levels, such as Read or Write
+ * @return the new policy
+ */
+ public Policy accessGrantsPolicy(final URI... access) {
+ return simplePolicy(matcher -> matcher.vc().add(SOLID_ACCESS_GRANT), access);
+ }
+
+ /**
+ * Create a policy that matches a particular agent.
+ *
+ * @param agent the agent identifier
+ * @param access the access levels, such as Read or Write
+ * @return the new policy
+ */
+ public Policy agentPolicy(final URI agent, final URI... access) {
+ return simplePolicy(matcher -> matcher.agent().add(agent), access);
+ }
+
+ /**
+ * Create a policy that matches a particular client.
+ *
+ * @param client the client identifier
+ * @param access the access levels, such as Read or Write
+ * @return the new policy
+ */
+ public Policy clientPolicy(final URI client, final URI... access) {
+ return simplePolicy(matcher -> matcher.client().add(client), access);
+ }
+
+ /**
+ * Create a policy that matches a particular issuer.
+ *
+ * @param issuer the issuer identifier
+ * @param access the access levels, such as Read or Write
+ * @return the new policy
+ */
+ public Policy issuerPolicy(final URI issuer, final URI... access) {
+ return simplePolicy(matcher -> matcher.issuer().add(issuer), access);
+ }
+
+ static class ACPNode extends WrapperIRI {
+ public ACPNode(final RDFTerm original, final Graph graph) {
+ super(original, graph);
+ }
+
+ public Set memberAccessControl() {
+ return objects(asIRI(ACP.memberAccessControl),
+ AccessControl::asResource, ValueMappings.as(AccessControl.class));
+ }
+
+ public Set accessControl() {
+ return objects(asIRI(ACP.accessControl),
+ AccessControl::asResource, ValueMappings.as(AccessControl.class));
+ }
+ }
+
+ Policy simplePolicy(final Consumer handler, final URI... access) {
+ final var baseUri = getIdentifier().getScheme() + ":" + getIdentifier().getSchemeSpecificPart();
+ final var matcher = new Matcher(asIRI(baseUri + "#" + UUID.randomUUID()), getGraph());
+ handler.accept(matcher);
+
+ final var policy = new Policy(asIRI(baseUri + "#" + UUID.randomUUID()), getGraph());
+ for (final var item : access) {
+ policy.allow().add(item);
+ }
+ policy.allOf().add(matcher);
+ return policy;
+ }
+
+ void removeUnusedStatements(final T resource) {
+ if (!contains(null, null, null, resource)) {
+ for (final var quad : stream(null, resource, null, null).toList()) {
+ remove(quad);
+ }
+ }
+ }
+
+ void expandType(final Dataset dataset, final Dataset cache, final IRI predicate, final IRI type,
+ final Consumer handler) {
+ final var subjects = dataset.stream(null, null, predicate, null)
+ .map(Quad::getObject).filter(IRI.class::isInstance).map(IRI.class::cast)
+ .filter(subject -> !dataset.contains(null, subject, asIRI(RDF.type), type)).toList();
+
+ for (final var subject : subjects) {
+ if (!cache.contains(null, subject, asIRI(RDF.type), type)) {
+ handler.accept(URI.create(subject.getIRIString()));
+ }
+ cache.stream(null, subject, null, null).forEach(dataset::add);
+ }
+ }
+
+ void populateCache(final SolidSyncClient client, final URI uri, final Dataset cache) {
+ try (final var acr = client.read(uri, AccessControlResource.class)) {
+ acr.stream()
+ .filter(quad -> !isAccessControlResourceType(quad))
+ .forEach(cache::add);
+ } catch (final Exception ex) {
+ LOGGER.atDebug()
+ .setMessage("Unable to fetch access control resource from {}: {}")
+ .addArgument(uri)
+ .addArgument(ex::getMessage)
+ .log();
+ }
+ }
+
+ void populateCacheAsync(final SolidClient client, final URI uri, final Dataset cache) {
+ client.read(uri, AccessControlResource.class).thenAccept(res -> {
+ try (final var acr = res) {
+ acr.stream()
+ .filter(quad -> !isAccessControlResourceType(quad))
+ .forEach(cache::add);
+ }
+ })
+ .exceptionally(err -> {
+ LOGGER.atDebug()
+ .setMessage("Unable to fetch access control resource from {}: {}")
+ .addArgument(uri)
+ .addArgument(err::getMessage)
+ .log();
+ return null;
+ }).toCompletableFuture().join();
+ }
+
+ static boolean isAccessControlResourceType(final Quad quad) {
+ return asIRI(RDF.type).equals(quad.getPredicate()) && asIRI(ACP.AccessControlResource).equals(quad.getObject());
+ }
+
+ static IRI asIRI(final URI uri) {
+ return asIRI(uri.toString());
+ }
+
+ static IRI asIRI(final String uri) {
+ return rdf.createIRI(uri);
+ }
+}
diff --git a/acp/src/main/java/com/inrupt/client/acp/Matcher.java b/acp/src/main/java/com/inrupt/client/acp/Matcher.java
new file mode 100644
index 00000000000..58de0310f67
--- /dev/null
+++ b/acp/src/main/java/com/inrupt/client/acp/Matcher.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright Inrupt Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
+ * Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+ * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package com.inrupt.client.acp;
+
+import static com.inrupt.client.acp.AccessControlResource.asIRI;
+
+import com.inrupt.client.vocabulary.ACP;
+import com.inrupt.client.vocabulary.RDF;
+import com.inrupt.rdf.wrapping.commons.TermMappings;
+import com.inrupt.rdf.wrapping.commons.ValueMappings;
+import com.inrupt.rdf.wrapping.commons.WrapperIRI;
+
+import java.net.URI;
+import java.util.Set;
+
+import org.apache.commons.rdf.api.Graph;
+import org.apache.commons.rdf.api.IRI;
+import org.apache.commons.rdf.api.RDFTerm;
+
+/**
+ * A Matcher type for use with Access Control Policies.
+ *
+ *
A matcher is associated with {@link Policy} objects, defining
+ * agents, clients, issuers or verifiable credential (vc) types.
+ */
+public class Matcher extends WrapperIRI {
+
+ /**
+ * Create a new Matcher.
+ *
+ * @param identifier the matcher identifier
+ * @param graph the underlying graph
+ */
+ public Matcher(final RDFTerm identifier, final Graph graph) {
+ super(identifier, graph);
+ graph.add((IRI) identifier, asIRI(RDF.type), asIRI(ACP.Matcher));
+ }
+
+
+ /**
+ * Retrieve the acp:vc values.
+ *
+ * @return a collection of verifiable credential types
+ */
+ public Set vc() {
+ return objects(asIRI(ACP.vc), TermMappings::asIri, ValueMappings::iriAsUri);
+ }
+
+ /**
+ * Retrieve the acp:agent values.
+ *
+ * @return a collection of agent identifiers
+ */
+ public Set agent() {
+ return objects(asIRI(ACP.agent), TermMappings::asIri, ValueMappings::iriAsUri);
+ }
+
+ /**
+ * Retrieve the acp:client values.
+ *
+ * @return a collection of client identifiers
+ */
+ public Set client() {
+ return objects(asIRI(ACP.client), TermMappings::asIri, ValueMappings::iriAsUri);
+ }
+
+ /**
+ * Retrieve the acp:issuer values.
+ *
+ * @return a collection of issuer identifiers
+ */
+ public Set issuer() {
+ return objects(asIRI(ACP.issuer), TermMappings::asIri, ValueMappings::iriAsUri);
+ }
+
+ static RDFTerm asResource(final Matcher matcher, final Graph graph) {
+ graph.add(matcher, asIRI(RDF.type), asIRI(ACP.Matcher));
+ matcher.vc().forEach(vc ->
+ graph.add(matcher, asIRI(ACP.vc), asIRI(vc)));
+ matcher.agent().forEach(agent ->
+ graph.add(matcher, asIRI(ACP.agent), asIRI(agent)));
+ matcher.client().forEach(client ->
+ graph.add(matcher, asIRI(ACP.client), asIRI(client)));
+ matcher.issuer().forEach(issuer ->
+ graph.add(matcher, asIRI(ACP.issuer), asIRI(issuer)));
+ return matcher;
+ }
+}
+
diff --git a/acp/src/main/java/com/inrupt/client/acp/Policy.java b/acp/src/main/java/com/inrupt/client/acp/Policy.java
new file mode 100644
index 00000000000..3f57945e29a
--- /dev/null
+++ b/acp/src/main/java/com/inrupt/client/acp/Policy.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright Inrupt Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
+ * Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+ * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package com.inrupt.client.acp;
+
+import static com.inrupt.client.acp.AccessControlResource.asIRI;
+
+import com.inrupt.client.vocabulary.ACP;
+import com.inrupt.client.vocabulary.RDF;
+import com.inrupt.rdf.wrapping.commons.TermMappings;
+import com.inrupt.rdf.wrapping.commons.ValueMappings;
+import com.inrupt.rdf.wrapping.commons.WrapperIRI;
+
+import java.net.URI;
+import java.util.Set;
+
+import org.apache.commons.rdf.api.Graph;
+import org.apache.commons.rdf.api.IRI;
+import org.apache.commons.rdf.api.RDFTerm;
+
+/**
+ * A Policy type for use with Access Control Policies.
+ *
+ *
A policy will reference various {@link Matcher} objects
+ * and apply access rules such as {@code Read} or {@code Write}.
+ */
+public class Policy extends WrapperIRI {
+
+ /**
+ * Create a new Policy.
+ *
+ * @param identifier the policy identifier
+ * @param graph the underlying graph for this resource
+ */
+ public Policy(final RDFTerm identifier, final Graph graph) {
+ super(identifier, graph);
+ graph.add((IRI) identifier, asIRI(RDF.type), asIRI(ACP.Policy));
+ }
+
+ /**
+ * Retrieve the acp:allOf structures.
+ *
+ * @return a collection of {@link Matcher} objects
+ */
+ public Set allOf() {
+ return objects(asIRI(ACP.allOf),
+ Matcher::asResource, ValueMappings.as(Matcher.class));
+ }
+
+ /**
+ * Retrieve the acp:anyOf structures.
+ *
+ * @return a collection of {@link Matcher} objects
+ */
+ public Set anyOf() {
+ return objects(asIRI(ACP.anyOf),
+ Matcher::asResource, ValueMappings.as(Matcher.class));
+ }
+
+ /**
+ * Retrieve the acp:noneOf structures.
+ *
+ * @return a collection of {@link Matcher} objects
+ */
+ public Set noneOf() {
+ return objects(asIRI(ACP.noneOf),
+ Matcher::asResource, ValueMappings.as(Matcher.class));
+ }
+
+ /**
+ * Retrieve the acp:allow values.
+ *
+ * @return a collection of access values, such as {@code ACL.Read}
+ */
+ public Set allow() {
+ return objects(asIRI(ACP.allow),
+ TermMappings::asIri, ValueMappings::iriAsUri);
+ }
+
+ static IRI asResource(final Policy policy, final Graph graph) {
+ graph.add(policy, asIRI(RDF.type), asIRI(ACP.Policy));
+ policy.allOf().forEach(matcher -> {
+ graph.add(policy, asIRI(ACP.allOf), matcher);
+ Matcher.asResource(matcher, graph);
+ });
+ policy.anyOf().forEach(matcher -> {
+ graph.add(policy, asIRI(ACP.anyOf), matcher);
+ Matcher.asResource(matcher, graph);
+ });
+ policy.noneOf().forEach(matcher -> {
+ graph.add(policy, asIRI(ACP.noneOf), matcher);
+ Matcher.asResource(matcher, graph);
+ });
+ policy.allow().forEach(allow ->
+ graph.add(policy, asIRI(ACP.allow), asIRI(allow)));
+ return policy;
+ }
+}
+
diff --git a/acp/src/main/java/com/inrupt/client/acp/package-info.java b/acp/src/main/java/com/inrupt/client/acp/package-info.java
new file mode 100644
index 00000000000..5b96b0cf47d
--- /dev/null
+++ b/acp/src/main/java/com/inrupt/client/acp/package-info.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright Inrupt Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
+ * Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+ * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+/**
+ *
Access Control Policy support for the Inrupt Java Client Libraries.
+ *
+ *
This module contains classes and methods to convert ACP resources into
+ * a @{@link AccessControlResource} Java object.
+ *
+ *
The following example reads a Solid Access Control Resource and presents it as an {@link AccessControlResource}
+ * Java object:
+ *
+ *
{@code
+ * try (AccessControlResource acr = client.read(uri, AccessControlResource.class)) {
+ * // find policies that grant {@code agent} read access
+ * Set policies = acr.expand(client).find(MatcherType.AGENT, agent, Set.of(ACL.Read));
+ *
+ * // remove these policies from the ACR
+ * Set accessControls = new HashSet<>();
+ * accessControls.addAll(acr.accessControl());
+ * accessControls.addAll(acr.memberAccessControl());
+ * for (Policy p : policies) {
+ * for (AccessControl accessControl = accessControls) {
+ * for (Policy policy : accessControl.apply()) {
+ * policy.remove(p);
+ * }
+ * }
+ * }
+ * acr.compact();
+ * client.update(acr);
+ * }}
+ *
+ */
+package com.inrupt.client.acp;
diff --git a/acp/src/test/java/com/inrupt/client/acp/AccessControlResourceTest.java b/acp/src/test/java/com/inrupt/client/acp/AccessControlResourceTest.java
new file mode 100644
index 00000000000..bf7afcd0ec4
--- /dev/null
+++ b/acp/src/test/java/com/inrupt/client/acp/AccessControlResourceTest.java
@@ -0,0 +1,392 @@
+/*
+ * Copyright Inrupt Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
+ * Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+ * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package com.inrupt.client.acp;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.jupiter.api.Assertions.*;
+
+import com.inrupt.client.solid.SolidClient;
+import com.inrupt.client.solid.SolidSyncClient;
+import com.inrupt.client.spi.RDFFactory;
+import com.inrupt.client.vocabulary.ACL;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.Set;
+
+import org.apache.commons.rdf.api.RDF;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+class AccessControlResourceTest {
+
+ static final AcpMockHttpService mockHttpServer = new AcpMockHttpService();
+ static final SolidSyncClient client = SolidSyncClient.getClient();
+ static final RDF rdf = RDFFactory.getInstance();
+
+ @BeforeAll
+ static void setup() {
+ mockHttpServer.start();
+ }
+
+ @AfterAll
+ static void teardown() {
+ mockHttpServer.stop();
+ }
+
+ @Test
+ void testAcr1CheckValues() {
+ final var uri = mockHttpServer.acr1();
+ try (final AccessControlResource acr = client.read(uri, AccessControlResource.class)) {
+ assertEquals(2, acr.accessControl().size());
+ assertEquals(2, acr.memberAccessControl().size());
+
+ final var matchers1 = acr.accessControl().stream()
+ .flatMap(ac -> ac.apply().stream())
+ .filter(policy ->
+ policy.allow().contains(ACL.Write) && policy.allow().size() == 2)
+ .flatMap(policy -> policy.allOf().stream())
+ .toList();
+ assertEquals(1, matchers1.size());
+ assertTrue(matchers1.get(0).agent().contains(URI.create("https://id.example/user")));
+
+ final var matchers2 = acr.accessControl().stream()
+ .flatMap(ac -> ac.apply().stream())
+ .filter(policy ->
+ !policy.allow().contains(ACL.Write) && policy.allow().size() == 1)
+ .flatMap(policy -> policy.allOf().stream())
+ .toList();
+ assertEquals(1, matchers2.size());
+ assertTrue(matchers2.get(0).vc().contains(URI.create("http://www.w3.org/ns/solid/vc#SolidAccessGrant")));
+
+ final var matchers3 = acr.memberAccessControl().stream()
+ .flatMap(ac -> ac.apply().stream())
+ .filter(policy ->
+ policy.allow().contains(ACL.Write) && policy.allow().size() == 2)
+ .flatMap(policy -> policy.allOf().stream())
+ .toList();
+ assertEquals(1, matchers3.size());
+ assertTrue(matchers3.get(0).agent().contains(URI.create("https://id.example/user")));
+
+ final var matchers4 = acr.memberAccessControl().stream()
+ .flatMap(ac -> ac.apply().stream())
+ .filter(policy ->
+ !policy.allow().contains(ACL.Write) && policy.allow().size() == 1)
+ .flatMap(policy -> policy.allOf().stream())
+ .toList();
+ assertEquals(1, matchers4.size());
+ assertTrue(matchers4.get(0).client().contains(URI.create("https://app.example/id")));
+ assertTrue(matchers4.get(0).vc().contains(URI.create("http://www.w3.org/ns/solid/vc#SolidAccessGrant")));
+ }
+ }
+
+ @Test
+ void testAcr1Mutation() throws IOException {
+ final var uri = mockHttpServer.acr1();
+ try (final AccessControlResource acr = client.read(uri, AccessControlResource.class)) {
+ final var quads = acr.stream().toList();
+
+ acr.accessControl().forEach(ac ->
+ ac.apply().forEach(policy -> policy.allow().add(ACL.Append)));
+ assertEquals(quads.size(), acr.size() - 2);
+
+ try (var entity = acr.getEntity()) {
+ assertTrue(new String(entity.readAllBytes(), UTF_8).contains(ACL.Append.toString()));
+ }
+
+ acr.accessControl().forEach(ac ->
+ ac.apply().forEach(policy -> policy.allow().remove(ACL.Write)));
+ assertEquals(quads.size(), acr.size() - 1);
+ }
+ }
+
+ @Test
+ void testAcr2CheckValues() {
+ final var uri = mockHttpServer.acr2();
+ try (final AccessControlResource acr = client.read(uri, AccessControlResource.class)) {
+ assertEquals(2, acr.accessControl().size());
+ assertEquals(3, acr.memberAccessControl().size());
+
+ final var matchers1 = acr.accessControl().stream()
+ .flatMap(ac -> ac.apply().stream())
+ .filter(policy ->
+ policy.allow().contains(ACL.Write) && policy.allow().size() == 2)
+ .flatMap(policy -> policy.allOf().stream())
+ .toList();
+ assertEquals(1, matchers1.size());
+ assertTrue(matchers1.get(0).agent().contains(URI.create("https://id.example/user2")));
+
+ final var matchers2 = acr.accessControl().stream()
+ .flatMap(ac -> ac.apply().stream())
+ .filter(policy ->
+ !policy.allow().contains(ACL.Write) && policy.allow().size() == 1)
+ .flatMap(policy -> policy.allOf().stream())
+ .toList();
+ assertEquals(1, matchers2.size());
+ assertTrue(matchers2.get(0).agent().contains(URI.create("https://bot.example/id")));
+
+ final var matchers3 = acr.memberAccessControl().stream()
+ .flatMap(ac -> ac.apply().stream())
+ .filter(policy ->
+ !policy.allow().contains(ACL.Write) && policy.allow().size() == 1)
+ .flatMap(policy -> policy.allOf().stream())
+ .toList();
+ assertEquals(2, matchers3.size());
+ assertTrue(matchers3.stream().anyMatch(matcher ->
+ matcher.vc().contains(URI.create("http://www.w3.org/ns/solid/vc#SolidAccessGrant"))));
+ assertTrue(matchers3.stream().anyMatch(matcher ->
+ matcher.agent().contains(URI.create("https://bot.example/id"))));
+ }
+ }
+
+ @Test
+ void buildAcr() {
+ final var identifier = "https://data.example/resource";
+ final var dataset = rdf.createDataset();
+ final var uri = URI.create(identifier);
+
+ final var matcher1 = new Matcher(rdf.createIRI(identifier + "#matcher1"), rdf.createGraph());
+ matcher1.agent().add(URI.create("https://id.example/agent"));
+
+ final var matcher2 = new Matcher(rdf.createIRI(identifier + "#matcher2"), rdf.createGraph());
+ matcher2.client().add(URI.create("https://app.example/id"));
+
+ final var matcher3 = new Matcher(rdf.createIRI(identifier + "#matcher3"), rdf.createGraph());
+ matcher3.issuer().add(URI.create("https://openid.example"));
+
+ final var policy = new Policy(rdf.createIRI(identifier + "#policy"), rdf.createGraph());
+ policy.allOf().add(matcher1);
+ policy.anyOf().add(matcher2);
+ policy.noneOf().add(matcher3);
+ policy.allow().add(ACL.Read);
+ policy.allow().add(ACL.Write);
+
+ final var accessControl = new AccessControl(rdf.createIRI(identifier + "#access-control"), rdf.createGraph());
+ accessControl.apply().add(policy);
+
+ final var acr = new AccessControlResource(uri, dataset);
+ acr.accessControl().add(accessControl);
+
+ assertEquals(16, acr.size());
+ }
+
+ @Test
+ void testAcrFindPolicies() {
+ final var uri = mockHttpServer.acr2();
+ try (final AccessControlResource acr = client.read(uri, AccessControlResource.class)) {
+ assertEquals(1, acr.find(AccessControlResource.MatcherType.VC,
+ AccessControlResource.SOLID_ACCESS_GRANT, Set.of(ACL.Read)).size());
+
+ assertEquals(0, acr.find(AccessControlResource.MatcherType.VC,
+ AccessControlResource.SOLID_ACCESS_GRANT, Set.of(ACL.Read, ACL.Write)).size());
+
+ assertEquals(0, acr.find(AccessControlResource.MatcherType.AGENT,
+ URI.create("https://bot.example/id"), Set.of(ACL.Read, ACL.Write)).size());
+
+ assertEquals(1, acr.find(AccessControlResource.MatcherType.AGENT,
+ URI.create("https://bot.example/id"), Set.of(ACL.Read)).size());
+
+ assertEquals(2, acr.find(AccessControlResource.MatcherType.AGENT,
+ null, Set.of(ACL.Read)).size());
+
+ assertEquals(1, acr.find(AccessControlResource.MatcherType.AGENT,
+ URI.create("https://bot.example/id"), null).size());
+ }
+ }
+
+ @Test
+ void testAcr1RemoveAccessControl() {
+ final var uri = mockHttpServer.acr2();
+ try (final AccessControlResource acr = client.read(uri, AccessControlResource.class)) {
+ assertEquals(2, acr.accessControl().size());
+ assertEquals(3, acr.memberAccessControl().size());
+
+ // Check dataset size
+ assertEquals(29, acr.size());
+
+ acr.accessControl().stream().findFirst().ifPresent(acr.accessControl()::remove);
+
+ // Check dataset size
+ assertEquals(28, acr.size());
+ acr.compact();
+ assertEquals(28, acr.size());
+
+ assertEquals(1, acr.accessControl().size());
+ assertEquals(3, acr.memberAccessControl().size());
+
+ }
+ }
+
+ @Test
+ void testAcr1RemoveValues() {
+ final var uri = mockHttpServer.acr2();
+ try (final AccessControlResource acr = client.read(uri, AccessControlResource.class)) {
+ assertEquals(2, acr.accessControl().size());
+ assertEquals(3, acr.memberAccessControl().size());
+
+ // Check dataset size
+ assertEquals(29, acr.size());
+
+ // Remove single matcher and compact
+ for (final var accessControl : acr.accessControl()) {
+ for (final var policy : accessControl.apply()) {
+ if (policy.allow().contains(ACL.Write) && policy.allow().size() == 2) {
+ for (final var matcher : policy.allOf()) {
+ policy.allOf().remove(matcher);
+ }
+ }
+ }
+ }
+ assertEquals(28, acr.size());
+ acr.compact();
+ assertEquals(26, acr.size());
+
+ // Remove single policy and compact
+ for (final var accessControl : acr.accessControl()) {
+ for (final var policy : accessControl.apply()) {
+ if (policy.allow().contains(ACL.Write) && policy.allow().size() == 2) {
+ accessControl.apply().remove(policy);
+ }
+ }
+ }
+ assertEquals(25, acr.size());
+ acr.compact();
+ assertEquals(22, acr.size());
+
+ // Remove single access control and compact - no compaction involved
+ final var ac = acr.accessControl().stream().findFirst().get();
+ acr.accessControl().remove(ac);
+ assertEquals(21, acr.size());
+ acr.compact();
+ assertEquals(21, acr.size());
+ }
+ }
+
+ @Test
+ void buildAcrWithExistingPolicies() {
+ final var identifier = "https://data.example/resource";
+ final var dataset = rdf.createDataset();
+ final var uri = URI.create(identifier);
+
+ final var acr = new AccessControlResource(uri, dataset);
+ acr.accessControl().add(acr.accessControl(
+ acr.authenticatedAgentPolicy(ACL.Read, ACL.Write),
+ acr.anyAgentPolicy(ACL.Read),
+ acr.anyClientPolicy(ACL.Read, ACL.Write)));
+ acr.memberAccessControl().add(acr.accessControl(
+ acr.anyIssuerPolicy(ACL.Read),
+ acr.accessGrantsPolicy(ACL.Read, ACL.Write)));
+
+ assertEquals(38, acr.size());
+ }
+
+ @Test
+ void expandAcr3Sync() {
+ final var uri = mockHttpServer.acr3();
+ try (final AccessControlResource acr = client.read(uri, AccessControlResource.class)) {
+ assertEquals(2, acr.accessControl().size());
+ assertEquals(3, acr.memberAccessControl().size());
+
+ // Check dataset size
+ assertEquals(12, acr.size());
+
+ final var expanded = acr.expand(client);
+ assertEquals(29, expanded.size());
+ assertEquals(12, acr.size());
+ }
+ }
+
+ @Test
+ void expandAcr3Async() {
+ final var uri = mockHttpServer.acr3();
+ final var asyncClient = SolidClient.getClient();
+ asyncClient.read(uri, AccessControlResource.class).thenAccept(res -> {
+ try (final var acr = res) {
+ assertEquals(2, acr.accessControl().size());
+ assertEquals(3, acr.memberAccessControl().size());
+
+ // Check dataset size
+ assertEquals(12, acr.size());
+
+ final var expanded = acr.expand(asyncClient);
+ assertEquals(29, expanded.size());
+ assertEquals(12, acr.size());
+ }
+ }).toCompletableFuture().join();
+ }
+
+ @Test
+ void expandAcr4Sync() {
+ final var uri = mockHttpServer.acr4();
+ try (final AccessControlResource acr = client.read(uri, AccessControlResource.class)) {
+ assertEquals(2, acr.accessControl().size());
+ assertEquals(3, acr.memberAccessControl().size());
+
+ // Check dataset size
+ assertEquals(20, acr.size());
+
+ final var expanded = acr.expand(client);
+ assertEquals(22, expanded.size());
+ assertEquals(20, acr.size());
+ }
+ }
+
+ @Test
+ void expandAcr4Async() {
+ final var uri = mockHttpServer.acr4();
+ final var asyncClient = SolidClient.getClient();
+ asyncClient.read(uri, AccessControlResource.class).thenAccept(res -> {
+ try (final var acr = res) {
+ assertEquals(2, acr.accessControl().size());
+ assertEquals(3, acr.memberAccessControl().size());
+
+ // Check dataset size
+ assertEquals(20, acr.size());
+
+ final var expanded = acr.expand(asyncClient);
+ assertEquals(22, expanded.size());
+ assertEquals(20, acr.size());
+ }
+ }).toCompletableFuture().join();
+ }
+
+ @Test
+ void testMerge() {
+ final var identifier = "https://data.example/resource";
+ final var agent = URI.create("https://id.example/agent");
+ final var app = URI.create("https://app.example/id");
+
+ final var acr = new AccessControlResource(URI.create(identifier), rdf.createDataset());
+ final var policy = acr.merge(Set.of(ACL.Read, ACL.Write), acr.agentPolicy(agent), acr.clientPolicy(app));
+
+ assertEquals(2, policy.allow().size());
+ assertTrue(policy.allow().contains(ACL.Read));
+ assertTrue(policy.allow().contains(ACL.Write));
+
+ assertEquals(2, policy.allOf().size());
+ assertEquals(0, policy.anyOf().size());
+ assertEquals(0, policy.noneOf().size());
+
+ assertTrue(policy.allOf().stream().anyMatch(matcher -> matcher.client().contains(app)));
+ assertTrue(policy.allOf().stream().anyMatch(matcher -> matcher.agent().contains(agent)));
+ }
+}
diff --git a/acp/src/test/java/com/inrupt/client/acp/AcpMockHttpService.java b/acp/src/test/java/com/inrupt/client/acp/AcpMockHttpService.java
new file mode 100644
index 00000000000..e15d587f014
--- /dev/null
+++ b/acp/src/test/java/com/inrupt/client/acp/AcpMockHttpService.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright Inrupt Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
+ * Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+ * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package com.inrupt.client.acp;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.*;
+
+import com.github.tomakehurst.wiremock.WireMockServer;
+import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
+
+import java.net.URI;
+
+public class AcpMockHttpService {
+
+ private final WireMockServer wireMockServer;
+
+ public AcpMockHttpService() {
+ wireMockServer = new WireMockServer(WireMockConfiguration.options().dynamicPort());
+ }
+
+ public URI acr1() {
+ return URI.create(wireMockServer.baseUrl() + "/acr-1");
+ }
+
+ public URI acr2() {
+ return URI.create(wireMockServer.baseUrl() + "/acr-2");
+ }
+
+ public URI acr3() {
+ return URI.create(wireMockServer.baseUrl() + "/acr-3");
+ }
+
+ public URI acr4() {
+ return URI.create(wireMockServer.baseUrl() + "/acr-4");
+ }
+
+ private void setupMocks() {
+ wireMockServer.stubFor(get(urlEqualTo("/acr-1"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "text/turtle")
+ .withBodyFile("acr-1.ttl")
+ )
+ );
+ wireMockServer.stubFor(get(urlEqualTo("/acr-2"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "text/turtle")
+ .withBodyFile("acr-2.ttl")
+ )
+ );
+ wireMockServer.stubFor(get(urlEqualTo("/acr-3"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "text/turtle")
+ .withBodyFile("acr-3.ttl")
+ )
+ );
+ wireMockServer.stubFor(get(urlEqualTo("/acr-4"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "text/turtle")
+ .withBodyFile("acr-4.ttl")
+ )
+ );
+ wireMockServer.stubFor(get(urlEqualTo("/not-an-acr"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "text/turtle")
+ .withBodyFile("not-an-acr.ttl")
+ )
+ );
+ wireMockServer.stubFor(get(urlEqualTo("/also-not-an-acr"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBodyFile("not-an-acr.json")
+ )
+ );
+ }
+
+ public String start() {
+ wireMockServer.start();
+
+ setupMocks();
+
+ return wireMockServer.baseUrl();
+ }
+
+ public void stop() {
+ wireMockServer.stop();
+ }
+}
+
diff --git a/acp/src/test/resources/__files/acr-1.ttl b/acp/src/test/resources/__files/acr-1.ttl
new file mode 100644
index 00000000000..4750ad5fd5f
--- /dev/null
+++ b/acp/src/test/resources/__files/acr-1.ttl
@@ -0,0 +1,31 @@
+@prefix acp: .
+@prefix acl: .
+@prefix vc: .
+
+<>
+ a acp:AccessControlResource ;
+ acp:accessControl <#owner-access-control> , <#access-grants-access-control> ;
+ acp:memberAccessControl <#owner-access-control> , <#access-grants-access-control> .
+
+<#owner-access-control>
+ a acp:AccessControl ;
+ acp:apply <#owner-policy> .
+<#owner-policy>
+ a acp:Policy ;
+ acp:allOf <#owner-matcher> ;
+ acp:allow acl:Read , acl:Write .
+<#owner-matcher>
+ a acp:Matcher ;
+ acp:agent .
+
+<#access-grants-access-control>
+ a acp:AccessControl ;
+ acp:apply <#access-grants-policy> .
+<#access-grants-policy>
+ a acp:Policy ;
+ acp:allOf <#access-grants-matcher> ;
+ acp:allow acl:Read .
+<#access-grants-matcher>
+ a acp:Matcher ;
+ acp:client ;
+ acp:vc vc:SolidAccessGrant .
diff --git a/acp/src/test/resources/__files/acr-2.ttl b/acp/src/test/resources/__files/acr-2.ttl
new file mode 100644
index 00000000000..fb9497c424b
--- /dev/null
+++ b/acp/src/test/resources/__files/acr-2.ttl
@@ -0,0 +1,42 @@
+@prefix acp: .
+@prefix acl: .
+@prefix vc: .
+
+<>
+ a acp:AccessControlResource ;
+ acp:accessControl <#owner-access-control> , <#indexer-access-control> ;
+ acp:memberAccessControl <#owner-access-control> , <#indexer-access-control> , <#vc-access-control> .
+
+<#owner-access-control>
+ a acp:AccessControl ;
+ acp:apply <#owner-policy> .
+<#owner-policy>
+ a acp:Policy ;
+ acp:allOf <#owner-matcher> ;
+ acp:allow acl:Read , acl:Write .
+<#owner-matcher>
+ a acp:Matcher ;
+ acp:agent .
+
+<#indexer-access-control>
+ a acp:AccessControl ;
+ acp:apply <#indexer-policy> .
+<#indexer-policy>
+ a acp:Policy ;
+ acp:allOf <#indexer-matcher> ;
+ acp:allow acl:Read .
+<#indexer-matcher>
+ a acp:Matcher ;
+ acp:issuer ;
+ acp:agent .
+
+<#vc-access-control>
+ a acp:AccessControl ;
+ acp:apply <#vc-policy> .
+<#vc-policy>
+ a acp:Policy ;
+ acp:allOf <#vc-matcher> ;
+ acp:allow acl:Read .
+<#vc-matcher>
+ a acp:Matcher ;
+ acp:vc vc:SolidAccessGrant .
diff --git a/acp/src/test/resources/__files/acr-3.ttl b/acp/src/test/resources/__files/acr-3.ttl
new file mode 100644
index 00000000000..8ffaeca57a8
--- /dev/null
+++ b/acp/src/test/resources/__files/acr-3.ttl
@@ -0,0 +1,18 @@
+@prefix acp: .
+
+<>
+ a acp:AccessControlResource ;
+ acp:accessControl <#owner-access-control> , <#indexer-access-control> ;
+ acp:memberAccessControl <#owner-access-control> , <#indexer-access-control> , <#vc-access-control> .
+
+<#owner-access-control>
+ a acp:AccessControl ;
+ acp:apply .
+
+<#indexer-access-control>
+ a acp:AccessControl ;
+ acp:apply .
+
+<#vc-access-control>
+ a acp:AccessControl ;
+ acp:apply .
diff --git a/acp/src/test/resources/__files/acr-4.ttl b/acp/src/test/resources/__files/acr-4.ttl
new file mode 100644
index 00000000000..eb277f5d394
--- /dev/null
+++ b/acp/src/test/resources/__files/acr-4.ttl
@@ -0,0 +1,31 @@
+@prefix acp: .
+@prefix acl: .
+@prefix vc: .
+
+<>
+ a acp:AccessControlResource ;
+ acp:accessControl <#owner-access-control> , <#indexer-access-control> ;
+ acp:memberAccessControl <#owner-access-control> , <#indexer-access-control> , <#vc-access-control> .
+
+<#owner-access-control>
+ a acp:AccessControl ;
+ acp:apply .
+
+<#indexer-access-control>
+ a acp:AccessControl ;
+ acp:apply <#indexer-policy> .
+<#indexer-policy>
+ a acp:Policy ;
+ acp:noneOf ;
+ acp:allow acl:Read .
+
+<#vc-access-control>
+ a acp:AccessControl ;
+ acp:apply <#vc-policy> .
+<#vc-policy>
+ a acp:Policy ;
+ acp:anyOf <#vc-matcher> ;
+ acp:allow acl:Read .
+<#vc-matcher>
+ a acp:Matcher ;
+ acp:vc vc:SolidAccessGrant .
diff --git a/acp/src/test/resources/__files/not-an-acr.json b/acp/src/test/resources/__files/not-an-acr.json
new file mode 100644
index 00000000000..48272642adc
--- /dev/null
+++ b/acp/src/test/resources/__files/not-an-acr.json
@@ -0,0 +1,4 @@
+{
+ "type": "Type",
+ "title": "Example resource"
+}
diff --git a/acp/src/test/resources/__files/not-an-acr.ttl b/acp/src/test/resources/__files/not-an-acr.ttl
new file mode 100644
index 00000000000..ad179e6536d
--- /dev/null
+++ b/acp/src/test/resources/__files/not-an-acr.ttl
@@ -0,0 +1,7 @@
+@prefix dc: .
+@prefix ex: .
+
+<>
+ a ex:Type ;
+ dc:title "Example resource" .
+
diff --git a/acp/src/test/resources/simplelogger.properties b/acp/src/test/resources/simplelogger.properties
new file mode 100644
index 00000000000..682ae67e438
--- /dev/null
+++ b/acp/src/test/resources/simplelogger.properties
@@ -0,0 +1 @@
+org.slf4j.simpleLogger.log.org.eclipse.jetty=warn
diff --git a/bom/pom.xml b/bom/pom.xml
index d9bd3a59387..108f9aa59b1 100644
--- a/bom/pom.xml
+++ b/bom/pom.xml
@@ -28,6 +28,11 @@
inrupt-client-accessgrant${project.version}
+
+ com.inrupt.client
+ inrupt-client-acp
+ ${project.version}
+ com.inrupt.clientinrupt-client-caffeine
diff --git a/pom.xml b/pom.xml
index 1e815dfee88..3bd191f193b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -97,6 +97,7 @@
access-grant
+ acpapibomcaffeine
@@ -205,7 +206,6 @@
https://jena.apache.org/documentation/javadoc/arq/
https://www.antlr.org/api/Java/
https://rdf4j.org/javadoc/5.1.0/
- http://www.javadoc.io/doc/com.fasterxml.jackson.core/jackson-databind/
inrupt-client-accessgrant
${project.version}
+
+ com.inrupt.client
+ inrupt-client-acp
+ ${project.version}
+ com.inrupt.clientinrupt-client-caffeine
@@ -107,6 +112,11 @@
inrupt-client-webid${project.version}
+
+ com.inrupt.client
+ inrupt-client-vocabulary
+ ${project.version}
+
diff --git a/runtime/pom.xml b/runtime/pom.xml
index 2461d401b2c..734b6972825 100644
--- a/runtime/pom.xml
+++ b/runtime/pom.xml
@@ -26,6 +26,11 @@
inrupt-client-accessgrant${project.version}
+
+ com.inrupt.client
+ inrupt-client-acp
+ ${project.version}
+ com.inrupt.clientinrupt-client-caffeine
diff --git a/vocabulary/pom.xml b/vocabulary/pom.xml
index bf413e47185..11e56732a32 100644
--- a/vocabulary/pom.xml
+++ b/vocabulary/pom.xml
@@ -46,6 +46,10 @@
+
+ org.jacoco
+ jacoco-maven-plugin
+
diff --git a/vocabulary/src/main/java/com/inrupt/client/vocabulary/ACP.java b/vocabulary/src/main/java/com/inrupt/client/vocabulary/ACP.java
index 60b01bac0b1..c5e5dbad9de 100644
--- a/vocabulary/src/main/java/com/inrupt/client/vocabulary/ACP.java
+++ b/vocabulary/src/main/java/com/inrupt/client/vocabulary/ACP.java
@@ -31,6 +31,19 @@ public final class ACP {
private static String namespace = "http://www.w3.org/ns/solid/acp#";
+ // Named Individuals
+ /** The acp:AuthenticatedAgent URI. */
+ public static final URI AuthenticatedAgent = URI.create(namespace + "AuthenticatedAgent");
+
+ /** The acp:PublicAgent URI. */
+ public static final URI PublicAgent = URI.create(namespace + "PublicAgent");
+
+ /** The acp:PublicClient URI. */
+ public static final URI PublicClient = URI.create(namespace + "PublicClient");
+
+ /** The acp:PublicIssuer URI. */
+ public static final URI PublicIssuer = URI.create(namespace + "PublicIssuer");
+
// Properties
/**
* The acp:resource URI.
@@ -76,6 +89,18 @@ public final class ACP {
* The acp:vc URI.
*/
public static final URI vc = URI.create(namespace + "vc");
+ /**
+ * The acp:client URI.
+ */
+ public static final URI client = URI.create(namespace + "client");
+ /**
+ * The acp:agent URI.
+ */
+ public static final URI agent = URI.create(namespace + "agent");
+ /**
+ * The acp:issuer URI.
+ */
+ public static final URI issuer = URI.create(namespace + "issuer");
// Classes
/**