Skip to content

Commit 27220ae

Browse files
jholleranNSeydoux
andauthored
JCL-480: Support verification of linked Access Request (#2360)
Make access request verification optional: This makes request verification opt-in for backwards-compatibility --------- Co-authored-by: Nicolas Ayral Seydoux <[email protected]>
1 parent c8f81e1 commit 27220ae

File tree

6 files changed

+118
-18
lines changed

6 files changed

+118
-18
lines changed

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,9 @@ static AccessGrant parse(final String serialization) throws IOException {
138138
final Optional<URI> other = asUri(consent.get("isProvidedTo"));
139139

140140
final URI recipient = person.orElseGet(() -> controller.orElseGet(() -> other.orElse(null)));
141-
final URI accessRequest = asUri(consent.get("request")).orElse(null);
141+
final URI accessRequest = asUri(consent.get("verifiedRequest")).orElse(
142+
asUri(consent.get("request")).orElse(null)
143+
);
142144
final Set<String> modes = asSet(consent.get("mode")).orElseGet(Collections::emptySet);
143145
final Set<URI> resources = asSet(consent.get("forPersonalData")).orElseGet(Collections::emptySet)
144146
.stream().map(URI::create).collect(Collectors.toSet());

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

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ public class AccessGrantClient {
107107
private static final String FOR_PERSONAL_DATA = "forPersonalData";
108108
private static final String HAS_STATUS = "hasStatus";
109109
private static final String REQUEST = "request";
110+
private static final String VERIFIED_REQUEST = "verifiedRequest";
110111
private static final String MODE = "mode";
111112
private static final String PROVIDED_CONSENT = "providedConsent";
112113
private static final String FOR_PURPOSE = "forPurpose";
@@ -252,17 +253,28 @@ private CompletionStage<AccessRequest> requestAccess(final URI recipient, final
252253
}
253254

254255
/**
255-
* Issue an access grant based on an access request.
256+
* Issue an access grant based on an access request. The access request is not verified.
256257
*
257258
* @param request the access request
258259
* @return the next stage of completion containing the issued access grant
259260
*/
260261
public CompletionStage<AccessGrant> grantAccess(final AccessRequest request) {
262+
return grantAccess(request, false);
263+
}
264+
265+
/**
266+
* Issue an access grant based on an access request.
267+
*
268+
* @param request the access request
269+
* @param verifyRequest whether the request should be verified before issuing the access grant
270+
* @return the next stage of completion containing the issued access grant
271+
*/
272+
public CompletionStage<AccessGrant> grantAccess(final AccessRequest request, final boolean verifyRequest) {
261273
Objects.requireNonNull(request, "Request may not be null!");
262274
return v1Metadata().thenCompose(metadata -> {
263275
final Map<String, Object> data = buildAccessGrantv1(request.getCreator(), request.getResources(),
264276
request.getModes(), request.getPurposes(), request.getExpiration(), request.getIssuedAt(),
265-
request.getIdentifier());
277+
request.getIdentifier(), verifyRequest);
266278
final Request req = Request.newBuilder(metadata.issueEndpoint)
267279
.header(CONTENT_TYPE, APPLICATION_JSON)
268280
.POST(Request.BodyPublishers.ofByteArray(serialize(data))).build();
@@ -285,17 +297,28 @@ public CompletionStage<AccessGrant> grantAccess(final AccessRequest request) {
285297
}
286298

287299
/**
288-
* Issue an access denial receipt based on an access request.
300+
* Issue an access denial receipt based on an access request. The access request is not verified.
289301
*
290302
* @param request the access request
291303
* @return the next stage of completion containing the issued access denial
292304
*/
293305
public CompletionStage<AccessDenial> denyAccess(final AccessRequest request) {
306+
return denyAccess(request, false);
307+
}
308+
309+
/**
310+
* Issue an access denial receipt based on an access request.
311+
*
312+
* @param request the access request
313+
* @param verifyRequest whether the request should be verified before issuing the access denial
314+
* @return the next stage of completion containing the issued access denial
315+
*/
316+
public CompletionStage<AccessDenial> denyAccess(final AccessRequest request, final boolean verifyRequest) {
294317
Objects.requireNonNull(request, "Request may not be null!");
295318
return v1Metadata().thenCompose(metadata -> {
296319
final Map<String, Object> data = buildAccessDenialv1(request.getCreator(), request.getResources(),
297320
request.getModes(), request.getPurposes(), request.getExpiration(), request.getIssuedAt(),
298-
request.getIdentifier());
321+
request.getIdentifier(), verifyRequest);
299322
final Request req = Request.newBuilder(metadata.issueEndpoint)
300323
.header(CONTENT_TYPE, APPLICATION_JSON)
301324
.POST(Request.BodyPublishers.ofByteArray(serialize(data))).build();
@@ -716,15 +739,22 @@ static URI asUri(final Object value) {
716739
return null;
717740
}
718741

719-
static Map<String, Object> buildAccessDenialv1(final URI agent, final Set<URI> resources, final Set<String> modes,
720-
final Set<URI> purposes, final Instant expiration, final Instant issuance, final URI accessRequest) {
742+
static Map<String, Object> buildAccessDenialv1(
743+
final URI agent,
744+
final Set<URI> resources,
745+
final Set<String> modes,
746+
final Set<URI> purposes,
747+
final Instant expiration,
748+
final Instant issuance,
749+
final URI accessRequest,
750+
final boolean verifiedRequest) {
721751
Objects.requireNonNull(agent, "Access denial agent may not be null!");
722752
final Map<String, Object> consent = new HashMap<>();
723753
consent.put(MODE, modes);
724754
consent.put(HAS_STATUS, "https://w3id.org/GConsent#ConsentStatusRefused");
725755
consent.put(FOR_PERSONAL_DATA, resources);
726756
consent.put(IS_PROVIDED_TO, agent);
727-
consent.put(REQUEST, accessRequest);
757+
linkRequest(consent, accessRequest, verifiedRequest);
728758
if (!purposes.isEmpty()) {
729759
consent.put(FOR_PURPOSE, purposes);
730760
}
@@ -747,15 +777,22 @@ static Map<String, Object> buildAccessDenialv1(final URI agent, final Set<URI> r
747777
return data;
748778
}
749779

750-
static Map<String, Object> buildAccessGrantv1(final URI agent, final Set<URI> resources, final Set<String> modes,
751-
final Set<URI> purposes, final Instant expiration, final Instant issuance, final URI accessRequest) {
780+
static Map<String, Object> buildAccessGrantv1(
781+
final URI agent,
782+
final Set<URI> resources,
783+
final Set<String> modes,
784+
final Set<URI> purposes,
785+
final Instant expiration,
786+
final Instant issuance,
787+
final URI accessRequest,
788+
final boolean verifiedRequest) {
752789
Objects.requireNonNull(agent, "Access grant agent may not be null!");
753790
final Map<String, Object> consent = new HashMap<>();
754791
consent.put(MODE, modes);
755792
consent.put(HAS_STATUS, "https://w3id.org/GConsent#ConsentStatusExplicitlyGiven");
756793
consent.put(FOR_PERSONAL_DATA, resources);
757794
consent.put(IS_PROVIDED_TO, agent);
758-
consent.put(REQUEST, accessRequest);
795+
linkRequest(consent, accessRequest, verifiedRequest);
759796
if (!purposes.isEmpty()) {
760797
consent.put(FOR_PURPOSE, purposes);
761798
}
@@ -861,4 +898,12 @@ static boolean isAccessDenial(final URI type) {
861898
return SOLID_ACCESS_DENIAL.equals(type.toString()) || QN_ACCESS_DENIAL.equals(type)
862899
|| FQ_ACCESS_DENIAL.equals(type);
863900
}
901+
902+
private static void linkRequest(final Map<String, Object> consent, final URI request, final boolean verifiedLink) {
903+
if (verifiedLink) {
904+
consent.put(VERIFIED_REQUEST, request);
905+
} else {
906+
consent.put(REQUEST, request);
907+
}
908+
}
864909
}

access-grant/src/test/java/com/inrupt/client/accessgrant/AccessGrantClientTest.java

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@
5353
import org.junit.jupiter.api.AfterAll;
5454
import org.junit.jupiter.api.BeforeAll;
5555
import org.junit.jupiter.api.Test;
56+
import org.junit.jupiter.params.ParameterizedTest;
57+
import org.junit.jupiter.params.provider.ValueSource;
5658

5759
class AccessGrantClientTest {
5860

@@ -299,8 +301,9 @@ void testRequestAccessNoAuth() {
299301
assertInstanceOf(AccessGrantException.class, err.getCause());
300302
}
301303

302-
@Test
303-
void testGrantAccess() {
304+
@ParameterizedTest
305+
@ValueSource(booleans = {true, false})
306+
void testGrantAccess(final boolean verifyRequest) {
304307
final Map<String, Object> claims = new HashMap<>();
305308
claims.put("webid", WEBID);
306309
claims.put("sub", SUB);
@@ -318,7 +321,7 @@ void testGrantAccess() {
318321
final AccessRequest request = client.requestAccess(recipient, resources, modes, purposes, expiration)
319322
.toCompletableFuture().join();
320323

321-
final AccessGrant grant = client.grantAccess(request).toCompletableFuture().join();
324+
final AccessGrant grant = client.grantAccess(request, verifyRequest).toCompletableFuture().join();
322325

323326
assertTrue(grant.getTypes().contains("SolidAccessGrant"));
324327
assertEquals(Optional.of(recipient), grant.getRecipient());
@@ -327,10 +330,14 @@ void testGrantAccess() {
327330
assertEquals(baseUri, grant.getIssuer());
328331
assertEquals(purposes, grant.getPurposes());
329332
assertEquals(resources, grant.getResources());
333+
// The request URL is static in the mock response, but it is dynamic in the request, so they will mismatch
334+
// if compared.
335+
assertNotNull(grant.getAccessRequest());
330336
}
331337

332-
@Test
333-
void testDenyAccess() {
338+
@ParameterizedTest
339+
@ValueSource(booleans = {true, false})
340+
void testDenyAccess(final boolean verifyRequest) {
334341
final Map<String, Object> claims = new HashMap<>();
335342
claims.put("webid", WEBID);
336343
claims.put("sub", SUB);
@@ -348,7 +355,7 @@ void testDenyAccess() {
348355
final AccessRequest request = client.requestAccess(recipient, resources, modes, purposes, expiration)
349356
.toCompletableFuture().join();
350357

351-
final AccessDenial denial = client.denyAccess(request).toCompletableFuture().join();
358+
final AccessDenial denial = client.denyAccess(request, verifyRequest).toCompletableFuture().join();
352359

353360
assertTrue(denial.getTypes().contains("SolidAccessDenial"));
354361
assertEquals(Optional.of(recipient), denial.getRecipient());

access-grant/src/test/java/com/inrupt/client/accessgrant/MockAccessGrantServer.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,17 @@ private void setupMocks() {
277277
.withHeader("Content-Type", "application/json")
278278
.withBody(getResource("/vc-4.json", wireMockServer.baseUrl()))));
279279

280+
wireMockServer.stubFor(post(urlEqualTo("/issue"))
281+
.atPriority(1)
282+
.withHeader("Authorization", containing("Bearer eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9."))
283+
.withRequestBody(containing("\"providedConsent\""))
284+
.withRequestBody(containing("\"2022-08-27T12:00:00Z\""))
285+
.withRequestBody(containing("\"verifiedRequest\""))
286+
.willReturn(aResponse()
287+
.withStatus(200)
288+
.withHeader("Content-Type", "application/json")
289+
.withBody(getResource("/vc-4-verified.json", wireMockServer.baseUrl()))));
290+
280291
// Access Request
281292
wireMockServer.stubFor(post(urlEqualTo("/issue"))
282293
.atPriority(1)
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"@context":[
3+
"https://www.w3.org/2018/credentials/v1",
4+
"https://w3id.org/security/suites/ed25519-2020/v1",
5+
"https://w3id.org/vc-revocation-list-2020/v1",
6+
"https://schema.inrupt.com/credentials/v2.jsonld"],
7+
"id":"{{baseUrl}}/access-grant-4",
8+
"type":["VerifiableCredential","SolidAccessGrant"],
9+
"issuer":"{{baseUrl}}",
10+
"expirationDate":"2022-08-27T12:00:00Z",
11+
"issuanceDate":"2022-08-25T20:34:05.153Z",
12+
"credentialStatus":{
13+
"id":"https://accessgrant.example/status/CVAM#2832",
14+
"revocationListCredential":"https://accessgrant.example/status/CVAM",
15+
"revocationListIndex":"2832",
16+
"type":"RevocationList2020Status"},
17+
"credentialSubject":{
18+
"id":"https://id.test/username",
19+
"providedConsent":{
20+
"mode":["Read","Append"],
21+
"hasStatus":"https://w3id.org/GConsent#ConsentStatusExplicitlyGiven",
22+
"isProvidedTo":"https://id.test/agent",
23+
"forPurpose":["https://purpose.test/Purpose1"],
24+
"forPersonalData":["https://storage.test/data/"],
25+
"verifiedRequest": "http://localhost:33367/access-request-5"}},
26+
27+
"proof":{
28+
"created":"2022-08-25T20:34:05.236Z",
29+
"proofPurpose":"assertionMethod",
30+
"proofValue":"nIeQF44XVik7onnAbdkbp8xxJ2C8JoTw6-VtCkAzxuWYRFsSfYpft5MuAJaivyeKDmaK82Lj_YsME2xgL2WIBQ",
31+
"type":"Ed25519Signature2020",
32+
"verificationMethod":"https://accessgrant.example/key/1e332728-4af5-46e4-a5db-4f7b89e3f378"}
33+
}
34+

access-grant/src/test/resources/vc-4.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
"hasStatus":"https://w3id.org/GConsent#ConsentStatusExplicitlyGiven",
2222
"isProvidedTo":"https://id.test/agent",
2323
"forPurpose":["https://purpose.test/Purpose1"],
24-
"forPersonalData":["https://storage.test/data/"]}},
24+
"forPersonalData":["https://storage.test/data/"],
25+
"request": "http://localhost:33367/access-request-5"}},
2526
"proof":{
2627
"created":"2022-08-25T20:34:05.236Z",
2728
"proofPurpose":"assertionMethod",

0 commit comments

Comments
 (0)