Skip to content

Commit ba97a28

Browse files
authored
JCL-361: Make it easier to modify Access Control resources (#473)
* JCL-361: Make it easier to modify Access Control resources * Adjustments to RDF structure * Adjustment to testing setup
1 parent 6c63b74 commit ba97a28

File tree

6 files changed

+182
-97
lines changed

6 files changed

+182
-97
lines changed

access-grant/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@
2424
<artifactId>inrupt-client-api</artifactId>
2525
<version>${project.version}</version>
2626
</dependency>
27+
<dependency>
28+
<groupId>com.inrupt.client</groupId>
29+
<artifactId>inrupt-client-vocabulary</artifactId>
30+
<version>${project.version}</version>
31+
</dependency>
2732
<dependency>
2833
<groupId>commons-io</groupId>
2934
<artifactId>commons-io</artifactId>

access-grant/src/main/java/com/inrupt/client/accessgrant/AccessGrantClient.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -391,13 +391,13 @@ public <T extends AccessCredential> CompletionStage<List<T>> query(final URI age
391391
final URI type;
392392
final Set<String> supportedTypes;
393393
if (AccessGrant.class.isAssignableFrom(clazz)) {
394-
type = ACCESS_GRANT;
394+
type = URI.create("SolidAccessGrant");
395395
supportedTypes = ACCESS_GRANT_TYPES;
396396
} else if (AccessRequest.class.isAssignableFrom(clazz)) {
397-
type = ACCESS_REQUEST;
397+
type = URI.create("SolidAccessRequest");
398398
supportedTypes = ACCESS_REQUEST_TYPES;
399399
} else if (AccessDenial.class.isAssignableFrom(clazz)) {
400-
type = ACCESS_DENIAL;
400+
type = URI.create("SolidAccessDenial");
401401
supportedTypes = ACCESS_DENIAL_TYPES;
402402
} else {
403403
throw new AccessGrantException("Unsupported type " + clazz + " in query request");
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright 2023 Inrupt Inc.
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy
5+
* of this software and associated documentation files (the "Software"), to deal in
6+
* the Software without restriction, including without limitation the rights to use,
7+
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
8+
* Software, and to permit persons to whom the Software is furnished to do so,
9+
* subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in
12+
* all copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
15+
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
16+
* PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
17+
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
18+
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
19+
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20+
*/
21+
package com.inrupt.client.accessgrant;
22+
23+
import static com.inrupt.client.vocabulary.RDF.type;
24+
25+
import com.inrupt.client.spi.RDFFactory;
26+
import com.inrupt.client.util.URIBuilder;
27+
import com.inrupt.client.vocabulary.ACP;
28+
29+
import java.net.URI;
30+
import java.util.HashSet;
31+
import java.util.Set;
32+
import java.util.UUID;
33+
34+
import org.apache.commons.rdf.api.IRI;
35+
import org.apache.commons.rdf.api.RDF;
36+
import org.apache.commons.rdf.api.Triple;
37+
38+
/**
39+
* Utility methods for use with the Access Grant module.
40+
**/
41+
public final class AccessGrantUtils {
42+
43+
private static final RDF rdf = RDFFactory.getInstance();
44+
private static final IRI SOLID_ACCESS_GRANT = rdf.createIRI("http://www.w3.org/ns/solid/vc#SolidAccessGrant");
45+
46+
private static IRI asIRI(final URI uri) {
47+
return rdf.createIRI(uri.toString());
48+
}
49+
50+
public static Set<Triple> accessControlPolicyTriples(final URI acl, final URI... modes) {
51+
final Set<Triple> triples = new HashSet<>();
52+
final IRI a = asIRI(type);
53+
54+
// Matcher
55+
final IRI matcher = asIRI(URIBuilder.newBuilder(acl).fragment(UUID.randomUUID().toString()).build());
56+
triples.add(rdf.createTriple(matcher, a, asIRI(ACP.Matcher)));
57+
triples.add(rdf.createTriple(matcher, asIRI(ACP.vc), SOLID_ACCESS_GRANT));
58+
59+
// Policy
60+
final IRI policy = asIRI(URIBuilder.newBuilder(acl).fragment(UUID.randomUUID().toString()).build());
61+
triples.add(rdf.createTriple(policy, a, asIRI(ACP.Policy)));
62+
triples.add(rdf.createTriple(policy, asIRI(ACP.allOf), matcher));
63+
for (final URI mode : modes ) {
64+
triples.add(rdf.createTriple(policy, asIRI(ACP.allow), asIRI(mode)));
65+
}
66+
67+
// Access Control
68+
final IRI accessControl = asIRI(URIBuilder.newBuilder(acl).fragment(UUID.randomUUID().toString()).build());
69+
triples.add(rdf.createTriple(accessControl, a, asIRI(ACP.AccessControl)));
70+
triples.add(rdf.createTriple(accessControl, asIRI(ACP.apply), policy));
71+
72+
// Access Control Resource
73+
final IRI subject = asIRI(acl);
74+
triples.add(rdf.createTriple(subject, a, asIRI(ACP.AccessControlResource)));
75+
triples.add(rdf.createTriple(subject, asIRI(ACP.accessControl), accessControl));
76+
triples.add(rdf.createTriple(subject, asIRI(ACP.memberAccessControl), accessControl));
77+
return triples;
78+
}
79+
80+
private AccessGrantUtils() {
81+
// Prevent instantiation
82+
}
83+
}

integration/base/src/main/java/com/inrupt/client/integration/base/AccessGrantScenarios.java

Lines changed: 40 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@
2525
import static org.junit.jupiter.api.Assertions.assertThrows;
2626
import static org.junit.jupiter.api.Assertions.assertTrue;
2727

28-
import com.inrupt.client.Headers;
2928
import com.inrupt.client.Request;
3029
import com.inrupt.client.Response;
3130
import com.inrupt.client.accessgrant.AccessGrant;
3231
import com.inrupt.client.accessgrant.AccessGrantClient;
3332
import com.inrupt.client.accessgrant.AccessGrantSession;
33+
import com.inrupt.client.accessgrant.AccessGrantUtils;
3434
import com.inrupt.client.accessgrant.AccessRequest;
3535
import com.inrupt.client.auth.Credential;
3636
import com.inrupt.client.auth.Session;
@@ -40,7 +40,6 @@
4040
import com.inrupt.client.spi.RDFFactory;
4141
import com.inrupt.client.util.URIBuilder;
4242
import com.inrupt.client.vocabulary.ACL;
43-
import com.inrupt.client.vocabulary.ACP;
4443
import com.inrupt.client.webid.WebIdProfile;
4544

4645
import java.io.ByteArrayInputStream;
@@ -113,7 +112,9 @@ public class AccessGrantScenarios {
113112
private static String testRDFresourceName = "resource.ttl";
114113
private static URI testRDFresourceURI;
115114
private static String sharedTextFileName = "sharedFile.txt";
115+
private static String sharedResourceName = "sharedResource";
116116
private static URI sharedTextFileURI;
117+
private static URI sharedResource;
117118
private static Session session;
118119

119120
@BeforeAll
@@ -159,32 +160,45 @@ static void setup() throws IOException {
159160
sharedTextFileURI = URIBuilder.newBuilder(URI.create(testContainerURI.toString()))
160161
.path(sharedTextFileName)
161162
.build();
163+
sharedResource = URIBuilder.newBuilder(URI.create(testContainerURI.toString()))
164+
.path(sharedResourceName).build();
162165

163166
testRDFresourceURI = URIBuilder.newBuilder(testContainerURI)
164167
.path(testRDFresourceName)
165168
.build();
166169

170+
session = OpenIdSession.ofClientCredentials(
171+
URI.create(issuer), //Client credentials
172+
CLIENT_ID,
173+
CLIENT_SECRET,
174+
AUTH_METHOD);
175+
167176
//create test file in test container
168177
try (final InputStream is = new ByteArrayInputStream(StandardCharsets.UTF_8.encode("Test text").array())) {
169178
final SolidNonRDFSource testResource = new SolidNonRDFSource(sharedTextFileURI, Utils.PLAIN_TEXT, is, null);
170-
session = OpenIdSession.ofClientCredentials(
171-
URI.create(issuer), //Client credentials
172-
CLIENT_ID,
173-
CLIENT_SECRET,
174-
AUTH_METHOD);
175179
final SolidSyncClient authClient = client.session(session);
176180
assertDoesNotThrow(() -> authClient.create(testResource));
177181

178182
prepareACPofResource(authClient, sharedTextFileURI);
179183
}
180184

181-
accessGrantServer = new MockAccessGrantServer(State.WEBID.toString(), sharedTextFileURI.toString());
185+
accessGrantServer = new MockAccessGrantServer(State.WEBID, sharedTextFileURI, sharedResource);
182186
accessGrantServer.start();
183187

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

192+
final AccessGrantClient accessGrantClient = new AccessGrantClient(URI.create(VC_PROVIDER)).session(session);
193+
final Set<String> modes = new HashSet<>(Arrays.asList(GRANT_MODE_READ, GRANT_MODE_APPEND));
194+
final Instant expiration = Instant.parse(GRANT_EXPIRATION);
195+
196+
final AccessRequest request = accessGrantClient.requestAccess(URI.create(webidUrl),
197+
new HashSet<>(Arrays.asList(sharedResource)), modes, PURPOSES, expiration)
198+
.toCompletableFuture().join();
199+
final AccessGrant grant = accessGrantClient.grantAccess(request)
200+
.toCompletableFuture().join();
201+
188202
LOGGER.info("Integration Test Issuer: [{}]", issuer);
189203
LOGGER.info("Integration Test Pod Host: [{}]", podUrl);
190204
LOGGER.info("Integration Test Access Grant server: [{}]", VC_PROVIDER);
@@ -230,8 +244,7 @@ void accessGrantIssuanceLifecycleTest(final Session session) {
230244

231245
//2. call verify endpoint to verify grant
232246

233-
final URI uri = URIBuilder.newBuilder(URI.create(VC_PROVIDER)).path(grant.getIdentifier().toString()).build();
234-
final AccessGrant grantFromVcProvider = accessGrantClient.fetch(uri, AccessGrant.class)
247+
final AccessGrant grantFromVcProvider = accessGrantClient.fetch(grant.getIdentifier(), AccessGrant.class)
235248
.toCompletableFuture().join();
236249
assertEquals(grant.getPurposes(), grantFromVcProvider.getPurposes());
237250

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

262275
//6. call verify endpoint to check the grant is not valid
263-
264276
}
265277

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

282294
//2. call verify endpoint to verify grant
283-
284-
assertDoesNotThrow(accessGrantClient.delete(grant).toCompletableFuture()::join);
285295
}
286296

287297
@ParameterizedTest
@@ -301,8 +311,6 @@ void accessGrantNonRecursiveTest(final Session session) {
301311
.toCompletableFuture().join();
302312
//Steps
303313
//1. call verify endpoint to verify grant
304-
305-
assertDoesNotThrow(accessGrantClient.delete(grant).toCompletableFuture()::join);
306314
}
307315

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

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

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

341349
//query for all grants of a dedicated resource
342350
final List<AccessRequest> requests = accessGrantClient.query(URI.create(webidUrl),
343-
sharedTextFileURI, PURPOSE1, GRANT_MODE_READ, AccessRequest.class)
351+
sharedResource, PURPOSE1, GRANT_MODE_READ, AccessRequest.class)
344352
.toCompletableFuture().join();
345353
assertEquals(1, requests.size());
346354

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

362370
//query for all grants with a dedicated purpose
363371
final List<AccessRequest> requests = accessGrantClient.query(URI.create(webidUrl),
364-
sharedTextFileURI, PURPOSE1, GRANT_MODE_READ, AccessRequest.class)
372+
sharedResource, PURPOSE1, GRANT_MODE_READ, AccessRequest.class)
365373
.toCompletableFuture().join();
366374
assertEquals(1, requests.size());
367375

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

451458
assertDoesNotThrow(accessGrantClient.revoke(grant).toCompletableFuture()::join);
452-
assertDoesNotThrow(accessGrantClient.delete(grant).toCompletableFuture()::join);
453459
}
454460
}
455461

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

529535
authClient.delete(newTestFileURI);
530536
assertDoesNotThrow(accessGrantClient.revoke(grant).toCompletableFuture()::join);
531-
assertDoesNotThrow(accessGrantClient.delete(grant).toCompletableFuture()::join);
532537
}
533538

534539
@ParameterizedTest
@@ -582,7 +587,6 @@ void accessGrantSetNonRdfTest(final Session session) throws IOException {
582587

583588
authClient.delete(newTestFileURI);
584589
assertDoesNotThrow(accessGrantClient.revoke(grant).toCompletableFuture()::join);
585-
assertDoesNotThrow(accessGrantClient.delete(grant).toCompletableFuture()::join);
586590
}
587591

588592
@ParameterizedTest
@@ -620,60 +624,15 @@ void accessGrantCreateNonRdfTest(final Session session) throws IOException {
620624

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

623-
final IRI acpAllOf = rdf.createIRI(ACP.allOf.toString());
624-
final IRI acpVc = rdf.createIRI(ACP.vc.toString());
625-
final IRI acpAllow = rdf.createIRI(ACP.allow.toString());
626-
final IRI acpApply = rdf.createIRI(ACP.apply.toString());
627-
final IRI acpAccessControl = rdf.createIRI(ACP.accessControl.toString());
628-
final IRI aclRead = rdf.createIRI(ACL.Read.toString());
629-
final IRI aclWrite = rdf.createIRI(ACL.Write.toString());
630-
631627
// find the acl Link in the header of the resource
632-
final Request req = Request.newBuilder(resourceURI)
633-
.HEAD()
634-
.build();
635-
final Response<Void> res = authClient.send(req, Response.BodyHandlers.discarding());
636-
final Headers.Link acrLink = res.headers().allValues("Link").stream()
637-
.flatMap(l -> Headers.Link.parse(l).stream())
638-
.filter(link -> link.getParameter("rel").contains("acl"))
639-
.findAny()
640-
.orElse(null);
641-
642-
// add the triples needed for access grant
643-
if (acrLink != null) {
644-
final URI resourceACRurl = acrLink.getUri();
645-
final IRI resourceACRiri = rdf.createIRI(resourceACRurl.toString());
646-
647-
//read the existing triples and add them to the dataset
648-
try (final SolidRDFSource resource = authClient.read(resourceACRurl, SolidRDFSource.class)) {
649-
650-
//creating a new matcher
651-
final URI newMatcherURI = URIBuilder.newBuilder(resourceACRurl).fragment("newMatcher").build();
652-
final IRI newMatcher = rdf.createIRI(newMatcherURI.toString());
653-
final IRI solidAccessGrant = rdf.createIRI("http://www.w3.org/ns/solid/vc#SolidAccessGrant");
654-
655-
resource.add(rdf.createQuad(resourceACRiri, newMatcher, acpVc, solidAccessGrant));
656-
657-
//create a new policy
658-
final URI newPolicyURI = URIBuilder.newBuilder(resourceACRurl).fragment("newPolicy").build();
659-
final IRI newPolicy = rdf.createIRI(newPolicyURI.toString());
660-
661-
resource.add(rdf.createQuad(resourceACRiri, newPolicy, acpAllOf, newMatcher));
662-
resource.add(rdf.createQuad(resourceACRiri, newPolicy, acpAllow, aclRead));
663-
resource.add(rdf.createQuad(resourceACRiri, newPolicy, acpAllow, aclWrite));
664-
665-
//creating a new access control
666-
final URI newAccessControlURI =
667-
URIBuilder.newBuilder(resourceACRurl).fragment("newAccessControl").build();
668-
final IRI newAccessControl = rdf.createIRI(newAccessControlURI.toString());
669-
670-
resource.add(rdf.createQuad(resourceACRiri, newAccessControl, acpApply, newPolicy));
671-
672-
//adding the new access control to the ACP
673-
resource.add(rdf.createQuad(resourceACRiri, resourceACRiri, acpAccessControl, newAccessControl));
674-
675-
authClient.update(resource);
676-
}
628+
try (final SolidNonRDFSource resource = authClient.read(resourceURI, SolidNonRDFSource.class)) {
629+
resource.getMetadata().getAcl().ifPresent(acl -> {
630+
try (final SolidRDFSource acr = authClient.read(acl, SolidRDFSource.class)) {
631+
AccessGrantUtils.accessControlPolicyTriples(acl, ACL.Read, ACL.Write)
632+
.forEach(acr.getGraph()::add);
633+
authClient.update(acr);
634+
}
635+
});
677636
}
678637
}
679638

@@ -687,8 +646,8 @@ private static Stream<Arguments> provideSessions() throws SolidClientException {
687646
final var token = credential.map(Credential::getToken)
688647
.orElseThrow(() -> new OpenIdException("We could not get a token"));
689648
return Stream.of(
690-
Arguments.of(OpenIdSession.ofIdToken(token), //OpenId token
649+
Arguments.of(OpenIdSession.ofIdToken(token)), //OpenId token
691650
Arguments.of(session)
692-
));
651+
);
693652
}
694653
}

integration/base/src/main/java/com/inrupt/client/integration/base/AuthenticationScenarios.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ void fetchPrivateResourceUnauthenticatedTest(final Session session) {
203203
@MethodSource("provideSessions")
204204
@DisplayName(":authenticatedPublicNode Authenticated fetch of public resource succeeds")
205205
void fetchPublicResourceAuthenticatedTest(final Session session) {
206-
LOGGER.info("Integration Test - AuAuthenticatedth fetch of public resource");
206+
LOGGER.info("Integration Test - Authenticated fetch of public resource");
207207
//create public resource
208208
final SolidSyncClient client = SolidSyncClient.getClient();
209209
try (final SolidRDFSource testResource = new SolidRDFSource(publicResourceURL, null, null)) {

0 commit comments

Comments
 (0)