diff --git a/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessGrant.java b/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessGrant.java index 101dea1c89e..8dfef4d2b45 100644 --- a/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessGrant.java +++ b/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessGrant.java @@ -138,7 +138,9 @@ static AccessGrant parse(final String serialization) throws IOException { final Optional other = asUri(consent.get("isProvidedTo")); final URI recipient = person.orElseGet(() -> controller.orElseGet(() -> other.orElse(null))); - final URI accessRequest = asUri(consent.get("request")).orElse(null); + final URI accessRequest = asUri(consent.get("verifiedRequest")).orElse( + asUri(consent.get("request")).orElse(null) + ); final Set modes = asSet(consent.get("mode")).orElseGet(Collections::emptySet); final Set resources = asSet(consent.get("forPersonalData")).orElseGet(Collections::emptySet) .stream().map(URI::create).collect(Collectors.toSet()); diff --git a/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessGrantClient.java b/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessGrantClient.java index 41fd98596aa..89716d47a5c 100644 --- a/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessGrantClient.java +++ b/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessGrantClient.java @@ -107,6 +107,7 @@ public class AccessGrantClient { private static final String FOR_PERSONAL_DATA = "forPersonalData"; private static final String HAS_STATUS = "hasStatus"; private static final String REQUEST = "request"; + private static final String VERIFIED_REQUEST = "verifiedRequest"; private static final String MODE = "mode"; private static final String PROVIDED_CONSENT = "providedConsent"; private static final String FOR_PURPOSE = "forPurpose"; @@ -252,17 +253,28 @@ private CompletionStage requestAccess(final URI recipient, final } /** - * Issue an access grant based on an access request. + * Issue an access grant based on an access request. The access request is not verified. * * @param request the access request * @return the next stage of completion containing the issued access grant */ public CompletionStage grantAccess(final AccessRequest request) { + return grantAccess(request, false); + } + + /** + * Issue an access grant based on an access request. + * + * @param request the access request + * @param verifyRequest whether the request should be verified before issuing the access grant + * @return the next stage of completion containing the issued access grant + */ + public CompletionStage grantAccess(final AccessRequest request, final boolean verifyRequest) { Objects.requireNonNull(request, "Request may not be null!"); return v1Metadata().thenCompose(metadata -> { final Map data = buildAccessGrantv1(request.getCreator(), request.getResources(), request.getModes(), request.getPurposes(), request.getExpiration(), request.getIssuedAt(), - request.getIdentifier()); + request.getIdentifier(), verifyRequest); final Request req = Request.newBuilder(metadata.issueEndpoint) .header(CONTENT_TYPE, APPLICATION_JSON) .POST(Request.BodyPublishers.ofByteArray(serialize(data))).build(); @@ -285,17 +297,28 @@ public CompletionStage grantAccess(final AccessRequest request) { } /** - * Issue an access denial receipt based on an access request. + * Issue an access denial receipt based on an access request. The access request is not verified. * * @param request the access request * @return the next stage of completion containing the issued access denial */ public CompletionStage denyAccess(final AccessRequest request) { + return denyAccess(request, false); + } + + /** + * Issue an access denial receipt based on an access request. + * + * @param request the access request + * @param verifyRequest whether the request should be verified before issuing the access denial + * @return the next stage of completion containing the issued access denial + */ + public CompletionStage denyAccess(final AccessRequest request, final boolean verifyRequest) { Objects.requireNonNull(request, "Request may not be null!"); return v1Metadata().thenCompose(metadata -> { final Map data = buildAccessDenialv1(request.getCreator(), request.getResources(), request.getModes(), request.getPurposes(), request.getExpiration(), request.getIssuedAt(), - request.getIdentifier()); + request.getIdentifier(), verifyRequest); final Request req = Request.newBuilder(metadata.issueEndpoint) .header(CONTENT_TYPE, APPLICATION_JSON) .POST(Request.BodyPublishers.ofByteArray(serialize(data))).build(); @@ -716,15 +739,22 @@ static URI asUri(final Object value) { return null; } - static Map buildAccessDenialv1(final URI agent, final Set resources, final Set modes, - final Set purposes, final Instant expiration, final Instant issuance, final URI accessRequest) { + static Map buildAccessDenialv1( + final URI agent, + final Set resources, + final Set modes, + final Set purposes, + final Instant expiration, + final Instant issuance, + final URI accessRequest, + final boolean verifiedRequest) { Objects.requireNonNull(agent, "Access denial agent may not be null!"); final Map consent = new HashMap<>(); consent.put(MODE, modes); consent.put(HAS_STATUS, "https://w3id.org/GConsent#ConsentStatusRefused"); consent.put(FOR_PERSONAL_DATA, resources); consent.put(IS_PROVIDED_TO, agent); - consent.put(REQUEST, accessRequest); + linkRequest(consent, accessRequest, verifiedRequest); if (!purposes.isEmpty()) { consent.put(FOR_PURPOSE, purposes); } @@ -747,15 +777,22 @@ static Map buildAccessDenialv1(final URI agent, final Set r return data; } - static Map buildAccessGrantv1(final URI agent, final Set resources, final Set modes, - final Set purposes, final Instant expiration, final Instant issuance, final URI accessRequest) { + static Map buildAccessGrantv1( + final URI agent, + final Set resources, + final Set modes, + final Set purposes, + final Instant expiration, + final Instant issuance, + final URI accessRequest, + final boolean verifiedRequest) { Objects.requireNonNull(agent, "Access grant agent may not be null!"); final Map consent = new HashMap<>(); consent.put(MODE, modes); consent.put(HAS_STATUS, "https://w3id.org/GConsent#ConsentStatusExplicitlyGiven"); consent.put(FOR_PERSONAL_DATA, resources); consent.put(IS_PROVIDED_TO, agent); - consent.put(REQUEST, accessRequest); + linkRequest(consent, accessRequest, verifiedRequest); if (!purposes.isEmpty()) { consent.put(FOR_PURPOSE, purposes); } @@ -861,4 +898,12 @@ static boolean isAccessDenial(final URI type) { return SOLID_ACCESS_DENIAL.equals(type.toString()) || QN_ACCESS_DENIAL.equals(type) || FQ_ACCESS_DENIAL.equals(type); } + + private static void linkRequest(final Map consent, final URI request, final boolean verifiedLink) { + if (verifiedLink) { + consent.put(VERIFIED_REQUEST, request); + } else { + consent.put(REQUEST, request); + } + } } diff --git a/access-grant/src/test/java/com/inrupt/client/accessgrant/AccessGrantClientTest.java b/access-grant/src/test/java/com/inrupt/client/accessgrant/AccessGrantClientTest.java index e977a10e072..750847aaf18 100644 --- a/access-grant/src/test/java/com/inrupt/client/accessgrant/AccessGrantClientTest.java +++ b/access-grant/src/test/java/com/inrupt/client/accessgrant/AccessGrantClientTest.java @@ -53,6 +53,8 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; class AccessGrantClientTest { @@ -299,8 +301,9 @@ void testRequestAccessNoAuth() { assertInstanceOf(AccessGrantException.class, err.getCause()); } - @Test - void testGrantAccess() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testGrantAccess(final boolean verifyRequest) { final Map claims = new HashMap<>(); claims.put("webid", WEBID); claims.put("sub", SUB); @@ -318,7 +321,7 @@ void testGrantAccess() { final AccessRequest request = client.requestAccess(recipient, resources, modes, purposes, expiration) .toCompletableFuture().join(); - final AccessGrant grant = client.grantAccess(request).toCompletableFuture().join(); + final AccessGrant grant = client.grantAccess(request, verifyRequest).toCompletableFuture().join(); assertTrue(grant.getTypes().contains("SolidAccessGrant")); assertEquals(Optional.of(recipient), grant.getRecipient()); @@ -327,10 +330,14 @@ void testGrantAccess() { assertEquals(baseUri, grant.getIssuer()); assertEquals(purposes, grant.getPurposes()); assertEquals(resources, grant.getResources()); + // The request URL is static in the mock response, but it is dynamic in the request, so they will mismatch + // if compared. + assertNotNull(grant.getAccessRequest()); } - @Test - void testDenyAccess() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testDenyAccess(final boolean verifyRequest) { final Map claims = new HashMap<>(); claims.put("webid", WEBID); claims.put("sub", SUB); @@ -348,7 +355,7 @@ void testDenyAccess() { final AccessRequest request = client.requestAccess(recipient, resources, modes, purposes, expiration) .toCompletableFuture().join(); - final AccessDenial denial = client.denyAccess(request).toCompletableFuture().join(); + final AccessDenial denial = client.denyAccess(request, verifyRequest).toCompletableFuture().join(); assertTrue(denial.getTypes().contains("SolidAccessDenial")); assertEquals(Optional.of(recipient), denial.getRecipient()); diff --git a/access-grant/src/test/java/com/inrupt/client/accessgrant/MockAccessGrantServer.java b/access-grant/src/test/java/com/inrupt/client/accessgrant/MockAccessGrantServer.java index dda3e6423a8..9a3eb57dec8 100644 --- a/access-grant/src/test/java/com/inrupt/client/accessgrant/MockAccessGrantServer.java +++ b/access-grant/src/test/java/com/inrupt/client/accessgrant/MockAccessGrantServer.java @@ -277,6 +277,17 @@ private void setupMocks() { .withHeader("Content-Type", "application/json") .withBody(getResource("/vc-4.json", wireMockServer.baseUrl())))); + wireMockServer.stubFor(post(urlEqualTo("/issue")) + .atPriority(1) + .withHeader("Authorization", containing("Bearer eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.")) + .withRequestBody(containing("\"providedConsent\"")) + .withRequestBody(containing("\"2022-08-27T12:00:00Z\"")) + .withRequestBody(containing("\"verifiedRequest\"")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(getResource("/vc-4-verified.json", wireMockServer.baseUrl())))); + // Access Request wireMockServer.stubFor(post(urlEqualTo("/issue")) .atPriority(1) diff --git a/access-grant/src/test/resources/vc-4-verified.json b/access-grant/src/test/resources/vc-4-verified.json new file mode 100644 index 00000000000..ee74a4f5a39 --- /dev/null +++ b/access-grant/src/test/resources/vc-4-verified.json @@ -0,0 +1,34 @@ +{ + "@context":[ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/security/suites/ed25519-2020/v1", + "https://w3id.org/vc-revocation-list-2020/v1", + "https://schema.inrupt.com/credentials/v2.jsonld"], + "id":"{{baseUrl}}/access-grant-4", + "type":["VerifiableCredential","SolidAccessGrant"], + "issuer":"{{baseUrl}}", + "expirationDate":"2022-08-27T12:00:00Z", + "issuanceDate":"2022-08-25T20:34:05.153Z", + "credentialStatus":{ + "id":"https://accessgrant.example/status/CVAM#2832", + "revocationListCredential":"https://accessgrant.example/status/CVAM", + "revocationListIndex":"2832", + "type":"RevocationList2020Status"}, + "credentialSubject":{ + "id":"https://id.test/username", + "providedConsent":{ + "mode":["Read","Append"], + "hasStatus":"https://w3id.org/GConsent#ConsentStatusExplicitlyGiven", + "isProvidedTo":"https://id.test/agent", + "forPurpose":["https://purpose.test/Purpose1"], + "forPersonalData":["https://storage.test/data/"], + "verifiedRequest": "http://localhost:33367/access-request-5"}}, + + "proof":{ + "created":"2022-08-25T20:34:05.236Z", + "proofPurpose":"assertionMethod", + "proofValue":"nIeQF44XVik7onnAbdkbp8xxJ2C8JoTw6-VtCkAzxuWYRFsSfYpft5MuAJaivyeKDmaK82Lj_YsME2xgL2WIBQ", + "type":"Ed25519Signature2020", + "verificationMethod":"https://accessgrant.example/key/1e332728-4af5-46e4-a5db-4f7b89e3f378"} +} + diff --git a/access-grant/src/test/resources/vc-4.json b/access-grant/src/test/resources/vc-4.json index dbb008e45fb..e0903397001 100644 --- a/access-grant/src/test/resources/vc-4.json +++ b/access-grant/src/test/resources/vc-4.json @@ -21,7 +21,8 @@ "hasStatus":"https://w3id.org/GConsent#ConsentStatusExplicitlyGiven", "isProvidedTo":"https://id.test/agent", "forPurpose":["https://purpose.test/Purpose1"], - "forPersonalData":["https://storage.test/data/"]}}, + "forPersonalData":["https://storage.test/data/"], + "request": "http://localhost:33367/access-request-5"}}, "proof":{ "created":"2022-08-25T20:34:05.236Z", "proofPurpose":"assertionMethod",