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
5 changes: 5 additions & 0 deletions access-grant/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@
<artifactId>inrupt-client-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.inrupt.client</groupId>
<artifactId>inrupt-client-vocabulary</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -391,13 +391,13 @@ public <T extends AccessCredential> CompletionStage<List<T>> query(final URI age
final URI type;
final Set<String> supportedTypes;
if (AccessGrant.class.isAssignableFrom(clazz)) {
type = ACCESS_GRANT;
type = URI.create("SolidAccessGrant");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For my own understanding, in this case where the given string isn't a URI, what does the constructor do?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a relative URI. Perfectly valid

supportedTypes = ACCESS_GRANT_TYPES;
} else if (AccessRequest.class.isAssignableFrom(clazz)) {
type = ACCESS_REQUEST;
type = URI.create("SolidAccessRequest");
supportedTypes = ACCESS_REQUEST_TYPES;
} else if (AccessDenial.class.isAssignableFrom(clazz)) {
type = ACCESS_DENIAL;
type = URI.create("SolidAccessDenial");
supportedTypes = ACCESS_DENIAL_TYPES;
} else {
throw new AccessGrantException("Unsupported type " + clazz + " in query request");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Copyright 2023 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.accessgrant;

import static com.inrupt.client.vocabulary.RDF.type;

import com.inrupt.client.spi.RDFFactory;
import com.inrupt.client.util.URIBuilder;
import com.inrupt.client.vocabulary.ACP;

import java.net.URI;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;

import org.apache.commons.rdf.api.IRI;
import org.apache.commons.rdf.api.RDF;
import org.apache.commons.rdf.api.Triple;

/**
* Utility methods for use with the Access Grant module.
**/
public final class AccessGrantUtils {

private static final RDF rdf = RDFFactory.getInstance();
private static final IRI SOLID_ACCESS_GRANT = rdf.createIRI("http://www.w3.org/ns/solid/vc#SolidAccessGrant");

private static IRI asIRI(final URI uri) {
return rdf.createIRI(uri.toString());
}

public static Set<Triple> accessControlPolicyTriples(final URI acl, final URI... modes) {
final Set<Triple> triples = new HashSet<>();
final IRI a = asIRI(type);

// Matcher
final IRI matcher = asIRI(URIBuilder.newBuilder(acl).fragment(UUID.randomUUID().toString()).build());
triples.add(rdf.createTriple(matcher, a, asIRI(ACP.Matcher)));
triples.add(rdf.createTriple(matcher, asIRI(ACP.vc), SOLID_ACCESS_GRANT));

// Policy
final IRI policy = asIRI(URIBuilder.newBuilder(acl).fragment(UUID.randomUUID().toString()).build());
triples.add(rdf.createTriple(policy, a, asIRI(ACP.Policy)));
triples.add(rdf.createTriple(policy, asIRI(ACP.allOf), matcher));
for (final URI mode : modes ) {
triples.add(rdf.createTriple(policy, asIRI(ACP.allow), asIRI(mode)));
}

// Access Control
final IRI accessControl = asIRI(URIBuilder.newBuilder(acl).fragment(UUID.randomUUID().toString()).build());
triples.add(rdf.createTriple(accessControl, a, asIRI(ACP.AccessControl)));
triples.add(rdf.createTriple(accessControl, asIRI(ACP.apply), policy));

// Access Control Resource
final IRI subject = asIRI(acl);
triples.add(rdf.createTriple(subject, a, asIRI(ACP.AccessControlResource)));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this triple necessary? I assumed this type would be present by default in the ACR.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the Inrupt implementation, none of the type triples are strictly necessary. That said, a different impl may require them. Furthermore, the ACR for a resource may be empty, in which case the triple will not already be present.

In short: it never hurts to be explicit, and there may be cases where it is necessary

triples.add(rdf.createTriple(subject, asIRI(ACP.accessControl), accessControl));
triples.add(rdf.createTriple(subject, asIRI(ACP.memberAccessControl), accessControl));
return triples;
}

private AccessGrantUtils() {
// Prevent instantiation
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import com.inrupt.client.Headers;
import com.inrupt.client.Request;
import com.inrupt.client.Response;
import com.inrupt.client.accessgrant.AccessGrant;
import com.inrupt.client.accessgrant.AccessGrantClient;
import com.inrupt.client.accessgrant.AccessGrantSession;
import com.inrupt.client.accessgrant.AccessGrantUtils;
import com.inrupt.client.accessgrant.AccessRequest;
import com.inrupt.client.auth.Credential;
import com.inrupt.client.auth.Session;
Expand All @@ -40,7 +40,6 @@
import com.inrupt.client.spi.RDFFactory;
import com.inrupt.client.util.URIBuilder;
import com.inrupt.client.vocabulary.ACL;
import com.inrupt.client.vocabulary.ACP;
import com.inrupt.client.webid.WebIdProfile;

import java.io.ByteArrayInputStream;
Expand Down Expand Up @@ -113,7 +112,9 @@ public class AccessGrantScenarios {
private static String testRDFresourceName = "resource.ttl";
private static URI testRDFresourceURI;
private static String sharedTextFileName = "sharedFile.txt";
private static String sharedResourceName = "sharedResource";
private static URI sharedTextFileURI;
private static URI sharedResource;
private static Session session;

@BeforeAll
Expand Down Expand Up @@ -159,32 +160,45 @@ static void setup() throws IOException {
sharedTextFileURI = URIBuilder.newBuilder(URI.create(testContainerURI.toString()))
.path(sharedTextFileName)
.build();
sharedResource = URIBuilder.newBuilder(URI.create(testContainerURI.toString()))
.path(sharedResourceName).build();

testRDFresourceURI = URIBuilder.newBuilder(testContainerURI)
.path(testRDFresourceName)
.build();

session = OpenIdSession.ofClientCredentials(
URI.create(issuer), //Client credentials
CLIENT_ID,
CLIENT_SECRET,
AUTH_METHOD);

//create test file in test container
try (final InputStream is = new ByteArrayInputStream(StandardCharsets.UTF_8.encode("Test text").array())) {
final SolidNonRDFSource testResource = new SolidNonRDFSource(sharedTextFileURI, Utils.PLAIN_TEXT, is, null);
session = OpenIdSession.ofClientCredentials(
URI.create(issuer), //Client credentials
CLIENT_ID,
CLIENT_SECRET,
AUTH_METHOD);
final SolidSyncClient authClient = client.session(session);
assertDoesNotThrow(() -> authClient.create(testResource));

prepareACPofResource(authClient, sharedTextFileURI);
}

accessGrantServer = new MockAccessGrantServer(State.WEBID.toString(), sharedTextFileURI.toString());
accessGrantServer = new MockAccessGrantServer(State.WEBID, sharedTextFileURI, sharedResource);
accessGrantServer.start();

VC_PROVIDER = config
.getOptionalValue("inrupt.test.vc.provider", String.class)
.orElse(accessGrantServer.getMockServerUrl());

final AccessGrantClient accessGrantClient = new AccessGrantClient(URI.create(VC_PROVIDER)).session(session);
final Set<String> modes = new HashSet<>(Arrays.asList(GRANT_MODE_READ, GRANT_MODE_APPEND));
final Instant expiration = Instant.parse(GRANT_EXPIRATION);

final AccessRequest request = accessGrantClient.requestAccess(URI.create(webidUrl),
new HashSet<>(Arrays.asList(sharedResource)), modes, PURPOSES, expiration)
.toCompletableFuture().join();
final AccessGrant grant = accessGrantClient.grantAccess(request)
.toCompletableFuture().join();

LOGGER.info("Integration Test Issuer: [{}]", issuer);
LOGGER.info("Integration Test Pod Host: [{}]", podUrl);
LOGGER.info("Integration Test Access Grant server: [{}]", VC_PROVIDER);
Expand Down Expand Up @@ -230,8 +244,7 @@ void accessGrantIssuanceLifecycleTest(final Session session) {

//2. call verify endpoint to verify grant

final URI uri = URIBuilder.newBuilder(URI.create(VC_PROVIDER)).path(grant.getIdentifier().toString()).build();
final AccessGrant grantFromVcProvider = accessGrantClient.fetch(uri, AccessGrant.class)
final AccessGrant grantFromVcProvider = accessGrantClient.fetch(grant.getIdentifier(), AccessGrant.class)
.toCompletableFuture().join();
assertEquals(grant.getPurposes(), grantFromVcProvider.getPurposes());

Expand Down Expand Up @@ -260,7 +273,6 @@ void accessGrantIssuanceLifecycleTest(final Session session) {
assertDoesNotThrow(accessGrantClient.revoke(grant).toCompletableFuture()::join);

//6. call verify endpoint to check the grant is not valid

}

@ParameterizedTest
Expand All @@ -280,8 +292,6 @@ void accessGrantWithRequestOverridesTest(final Session session) {
.toCompletableFuture().join();

//2. call verify endpoint to verify grant

assertDoesNotThrow(accessGrantClient.delete(grant).toCompletableFuture()::join);
}

@ParameterizedTest
Expand All @@ -301,8 +311,6 @@ void accessGrantNonRecursiveTest(final Session session) {
.toCompletableFuture().join();
//Steps
//1. call verify endpoint to verify grant

assertDoesNotThrow(accessGrantClient.delete(grant).toCompletableFuture()::join);
}

// Query access grant related tests
Expand All @@ -316,7 +324,7 @@ void accessGrantQueryByRequestorTest(final Session session) {

//query for all grants issued by the user
final List<AccessRequest> grants = accessGrantClient.query(URI.create(webidUrl),
sharedTextFileURI, PURPOSE1, GRANT_MODE_READ, AccessRequest.class)
sharedResource, PURPOSE1, GRANT_MODE_READ, AccessRequest.class)
.toCompletableFuture().join();
// result is 4 because we retrieve the grants for each path
// sharedTextFileURI =
Expand All @@ -325,7 +333,7 @@ void accessGrantQueryByRequestorTest(final Session session) {

//query for all grants issued by a random user
final List<AccessRequest> randomGrants = accessGrantClient.query(URI.create("https://someuser.test"),
sharedTextFileURI, PURPOSE1, GRANT_MODE_READ, AccessRequest.class)
sharedResource, PURPOSE1, GRANT_MODE_READ, AccessRequest.class)
.toCompletableFuture().join();
assertEquals(0, randomGrants.size());
}
Expand All @@ -340,7 +348,7 @@ void accessGrantQueryByResourceTest(final Session session) {

//query for all grants of a dedicated resource
final List<AccessRequest> requests = accessGrantClient.query(URI.create(webidUrl),
sharedTextFileURI, PURPOSE1, GRANT_MODE_READ, AccessRequest.class)
sharedResource, PURPOSE1, GRANT_MODE_READ, AccessRequest.class)
.toCompletableFuture().join();
assertEquals(1, requests.size());

Expand All @@ -361,14 +369,13 @@ void accessGrantQueryByPurposeTest(final Session session) {

//query for all grants with a dedicated purpose
final List<AccessRequest> requests = accessGrantClient.query(URI.create(webidUrl),
sharedTextFileURI, PURPOSE1, GRANT_MODE_READ, AccessRequest.class)
sharedResource, PURPOSE1, GRANT_MODE_READ, AccessRequest.class)
.toCompletableFuture().join();
assertEquals(1, requests.size());

//query for all grants of an unsupported purpose
final URI purpose = URI.create("https://example.com/12");
final List<AccessRequest> randomGrants = accessGrantClient.query(URI.create(webidUrl),
sharedTextFileURI, purpose, GRANT_MODE_READ, AccessRequest.class)
//query for all grants of dedicated purpose combinations
final List<AccessGrant> randomGrants = accessGrantClient.query(URI.create(webidUrl),
sharedResource, PURPOSE1, GRANT_MODE_WRITE, AccessGrant.class)
.toCompletableFuture().join();
assertEquals(0, randomGrants.size()); //our grant is actually a Read
}
Expand Down Expand Up @@ -449,7 +456,6 @@ void accessGrantSetRdfTest(final Session session) {
authClient.delete(testRDFresourceURI);

assertDoesNotThrow(accessGrantClient.revoke(grant).toCompletableFuture()::join);
assertDoesNotThrow(accessGrantClient.delete(grant).toCompletableFuture()::join);
}
}

Expand Down Expand Up @@ -528,7 +534,6 @@ void accessGrantGetNonRdfTest(final Session session) throws IOException {

authClient.delete(newTestFileURI);
assertDoesNotThrow(accessGrantClient.revoke(grant).toCompletableFuture()::join);
assertDoesNotThrow(accessGrantClient.delete(grant).toCompletableFuture()::join);
}

@ParameterizedTest
Expand Down Expand Up @@ -582,7 +587,6 @@ void accessGrantSetNonRdfTest(final Session session) throws IOException {

authClient.delete(newTestFileURI);
assertDoesNotThrow(accessGrantClient.revoke(grant).toCompletableFuture()::join);
assertDoesNotThrow(accessGrantClient.delete(grant).toCompletableFuture()::join);
}

@ParameterizedTest
Expand Down Expand Up @@ -620,60 +624,15 @@ void accessGrantCreateNonRdfTest(final Session session) throws IOException {

private static void prepareACPofResource(final SolidSyncClient authClient, final URI resourceURI) {

final IRI acpAllOf = rdf.createIRI(ACP.allOf.toString());
final IRI acpVc = rdf.createIRI(ACP.vc.toString());
final IRI acpAllow = rdf.createIRI(ACP.allow.toString());
final IRI acpApply = rdf.createIRI(ACP.apply.toString());
final IRI acpAccessControl = rdf.createIRI(ACP.accessControl.toString());
final IRI aclRead = rdf.createIRI(ACL.Read.toString());
final IRI aclWrite = rdf.createIRI(ACL.Write.toString());

// find the acl Link in the header of the resource
final Request req = Request.newBuilder(resourceURI)
.HEAD()
.build();
final Response<Void> res = authClient.send(req, Response.BodyHandlers.discarding());
final Headers.Link acrLink = res.headers().allValues("Link").stream()
.flatMap(l -> Headers.Link.parse(l).stream())
.filter(link -> link.getParameter("rel").contains("acl"))
.findAny()
.orElse(null);

// add the triples needed for access grant
if (acrLink != null) {
final URI resourceACRurl = acrLink.getUri();
final IRI resourceACRiri = rdf.createIRI(resourceACRurl.toString());

//read the existing triples and add them to the dataset
try (final SolidRDFSource resource = authClient.read(resourceACRurl, SolidRDFSource.class)) {

//creating a new matcher
final URI newMatcherURI = URIBuilder.newBuilder(resourceACRurl).fragment("newMatcher").build();
final IRI newMatcher = rdf.createIRI(newMatcherURI.toString());
final IRI solidAccessGrant = rdf.createIRI("http://www.w3.org/ns/solid/vc#SolidAccessGrant");

resource.add(rdf.createQuad(resourceACRiri, newMatcher, acpVc, solidAccessGrant));

//create a new policy
final URI newPolicyURI = URIBuilder.newBuilder(resourceACRurl).fragment("newPolicy").build();
final IRI newPolicy = rdf.createIRI(newPolicyURI.toString());

resource.add(rdf.createQuad(resourceACRiri, newPolicy, acpAllOf, newMatcher));
resource.add(rdf.createQuad(resourceACRiri, newPolicy, acpAllow, aclRead));
resource.add(rdf.createQuad(resourceACRiri, newPolicy, acpAllow, aclWrite));

//creating a new access control
final URI newAccessControlURI =
URIBuilder.newBuilder(resourceACRurl).fragment("newAccessControl").build();
final IRI newAccessControl = rdf.createIRI(newAccessControlURI.toString());

resource.add(rdf.createQuad(resourceACRiri, newAccessControl, acpApply, newPolicy));

//adding the new access control to the ACP
resource.add(rdf.createQuad(resourceACRiri, resourceACRiri, acpAccessControl, newAccessControl));

authClient.update(resource);
}
try (final SolidNonRDFSource resource = authClient.read(resourceURI, SolidNonRDFSource.class)) {
resource.getMetadata().getAcl().ifPresent(acl -> {
try (final SolidRDFSource acr = authClient.read(acl, SolidRDFSource.class)) {
AccessGrantUtils.accessControlPolicyTriples(acl, ACL.Read, ACL.Write)
.forEach(acr.getGraph()::add);
authClient.update(acr);
}
});
}
}

Expand All @@ -687,8 +646,8 @@ private static Stream<Arguments> provideSessions() throws SolidClientException {
final var token = credential.map(Credential::getToken)
.orElseThrow(() -> new OpenIdException("We could not get a token"));
return Stream.of(
Arguments.of(OpenIdSession.ofIdToken(token), //OpenId token
Arguments.of(OpenIdSession.ofIdToken(token)), //OpenId token
Arguments.of(session)
));
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ void fetchPrivateResourceUnauthenticatedTest(final Session session) {
@MethodSource("provideSessions")
@DisplayName(":authenticatedPublicNode Authenticated fetch of public resource succeeds")
void fetchPublicResourceAuthenticatedTest(final Session session) {
LOGGER.info("Integration Test - AuAuthenticatedth fetch of public resource");
LOGGER.info("Integration Test - Authenticated fetch of public resource");
//create public resource
final SolidSyncClient client = SolidSyncClient.getClient();
try (final SolidRDFSource testResource = new SolidRDFSource(publicResourceURL, null, null)) {
Expand Down
Loading