From babd281288eef89598c636b9c1626f3e323f0d32 Mon Sep 17 00:00:00 2001 From: Aaron Coburn Date: Tue, 30 May 2023 14:00:56 -0400 Subject: [PATCH] JCL-370: Add purpose argument to query API --- .../client/accessgrant/AccessGrantClient.java | 21 ++++++++++------- .../accessgrant/AccessGrantClientTest.java | 14 +++++------ .../base/AccessGrantScenarios.java | 23 ++++++++++--------- .../base/MockAccessGrantServer.java | 1 + .../src/main/resources/query_response.json | 4 +++- .../base/src/main/resources/vc-grant.json | 4 +++- .../base/src/main/resources/vc-request.json | 4 +++- 7 files changed, 42 insertions(+), 29 deletions(-) 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 7a8bd80b05c..e5a86203e42 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 @@ -72,7 +72,8 @@ AccessGrantClient client = new AccessGrantClient(issuer).session(session); URI resource = URI.create("https://storage.example/data/resource"); - client.query(null, resource, null, AccessGrant.class) + URI purpose = URI.create("https://purpose.example/1"); + client.query(null, resource, purpose, "Read", AccessGrant.class) .thenApply(grants -> AccessGrantSession.ofAccessGrant(openid, grants.toArray(new AccessGrant[0]))) .thenApply(session -> SolidClient.getClient().session(session)) .thenAccept(cl -> { @@ -378,12 +379,13 @@ public CompletionStage verify(final AccessCredenti * @param the AccessCredential type * @param agent the agent identifier, may be {@code null} * @param resource the resource identifier, may be {@code null} + * @param purpose the access purpose, may be {@code null} * @param mode the access mode, may be {@code null} * @param clazz the AccessCredential type, either {@link AccessGrant} or {@link AccessRequest} * @return the next stage of completion, including the matched Access Grants */ public CompletionStage> query(final URI agent, final URI resource, - final String mode, final Class clazz) { + final URI purpose, final String mode, final Class clazz) { Objects.requireNonNull(clazz, "The clazz parameter must not be null!"); final URI type; @@ -403,7 +405,7 @@ public CompletionStage> query(final URI age return v1Metadata().thenCompose(metadata -> { final List>> futures = buildQuery(config.getIssuer(), type, - agent, resource, mode).stream() + agent, resource, purpose, mode).stream() .map(data -> Request.newBuilder(metadata.queryEndpoint) .header(CONTENT_TYPE, APPLICATION_JSON) .POST(Request.BodyPublishers.ofByteArray(serialize(data))).build()) @@ -449,7 +451,7 @@ public CompletionStage> query(final URI type, final URI agent, Objects.requireNonNull(type, "The type parameter must not be null!"); return v1Metadata().thenCompose(metadata -> { final List>> futures = buildQuery(config.getIssuer(), type, - agent, resource, mode).stream() + agent, resource, null, mode).stream() .map(data -> Request.newBuilder(metadata.queryEndpoint) .header(CONTENT_TYPE, APPLICATION_JSON) .POST(Request.BodyPublishers.ofByteArray(serialize(data))).build()) @@ -680,14 +682,14 @@ static Collection getCredentials(final Map data) { } static List> buildQuery(final URI issuer, final URI type, final URI agent, final URI resource, - final String mode) { + final URI purpose, final String mode) { final List> queries = new ArrayList<>(); - buildQuery(queries, issuer, type, agent, resource, mode); + buildQuery(queries, issuer, type, agent, resource, purpose, mode); return queries; } static void buildQuery(final List> queries, final URI issuer, final URI type, final URI agent, - final URI resource, final String mode) { + final URI resource, final URI purpose, final String mode) { final Map credential = new HashMap<>(); credential.put(CONTEXT, Arrays.asList(VC_CONTEXT_URI, INRUPT_CONTEXT_URI)); credential.put("issuer", issuer); @@ -704,6 +706,9 @@ static void buildQuery(final List> queries, final URI issuer if (resource != null) { consent.put(FOR_PERSONAL_DATA, resource); } + if (purpose != null) { + consent.put(FOR_PURPOSE, purpose); + } if (mode != null) { consent.put(MODE, mode); } @@ -726,7 +731,7 @@ static void buildQuery(final List> queries, final URI issuer // Recurse final URI parent = getParent(resource); if (parent != null) { - buildQuery(queries, issuer, type, agent, parent, mode); + buildQuery(queries, issuer, type, agent, parent, purpose, mode); } } 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 275ba641be3..05c5664dd4a 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 @@ -509,7 +509,7 @@ void testQueryGrant() { final AccessGrantClient client = agClient.session(OpenIdSession.ofIdToken(token)); final List grants = client.query(null, - URI.create("https://storage.example/e973cc3d-5c28-4a10-98c5-e40079289358/a/b/c"), "Read", + URI.create("https://storage.example/e973cc3d-5c28-4a10-98c5-e40079289358/a/b/c"), null, "Read", AccessGrant.class) .toCompletableFuture().join(); assertEquals(1, grants.size()); @@ -526,7 +526,7 @@ void testQueryGrantAgent() { final AccessGrantClient client = agClient.session(OpenIdSession.ofIdToken(token)); final List grants = client.query(URI.create("https://id.test/user"), - null, "Read", AccessGrant.class) + null, null, "Read", AccessGrant.class) .toCompletableFuture().join(); assertEquals(1, grants.size()); } @@ -542,7 +542,7 @@ void testQueryRequestAgent() { final AccessGrantClient client = agClient.session(OpenIdSession.ofIdToken(token)); final List requests = client.query(URI.create("https://id.test/user"), - null, "Read", AccessRequest.class) + null, null, "Read", AccessRequest.class) .toCompletableFuture().join(); assertEquals(1, requests.size()); } @@ -558,7 +558,7 @@ void testQueryRequest() { final AccessGrantClient client = agClient.session(OpenIdSession.ofIdToken(token)); final List requests = client.query(null, - URI.create("https://storage.example/f1759e6d-4dda-4401-be61-d90d070a5474/a/b/c"), "Read", + URI.create("https://storage.example/f1759e6d-4dda-4401-be61-d90d070a5474/a/b/c"), null, "Read", AccessRequest.class) .toCompletableFuture().join(); assertEquals(1, requests.size()); @@ -575,7 +575,7 @@ void testQueryDenial() { final AccessGrantClient client = agClient.session(OpenIdSession.ofIdToken(token)); final List grants = client.query(null, - URI.create("https://storage.example/ef9c4b90-0459-408d-bfa9-1c61d46e1eaf/e/f/g"), "Read", + URI.create("https://storage.example/ef9c4b90-0459-408d-bfa9-1c61d46e1eaf/e/f/g"), null, "Read", AccessDenial.class) .toCompletableFuture().join(); assertEquals(1, grants.size()); @@ -592,14 +592,14 @@ void testQueryInvalidType() { final AccessGrantClient client = agClient.session(OpenIdSession.ofIdToken(token)); final URI uri = URI.create("https://storage.example/f1759e6d-4dda-4401-be61-d90d070a5474/a/b/c"); - assertThrows(AccessGrantException.class, () -> client.query(null, uri, "Read", AccessCredential.class)); + assertThrows(AccessGrantException.class, () -> client.query(null, uri, null, "Read", AccessCredential.class)); } @Test void testQueryInvalidAuth() { final CompletionException err = assertThrows(CompletionException.class, - agClient.query(URI.create("SolidAccessGrant"), (URI) null, (URI) null, null) + agClient.query(URI.create("SolidAccessGrant"), null, null, null) .toCompletableFuture()::join); assertInstanceOf(AccessGrantException.class, err.getCause()); diff --git a/integration/base/src/main/java/com/inrupt/client/integration/base/AccessGrantScenarios.java b/integration/base/src/main/java/com/inrupt/client/integration/base/AccessGrantScenarios.java index 77c1944f81f..91677610d1c 100644 --- a/integration/base/src/main/java/com/inrupt/client/integration/base/AccessGrantScenarios.java +++ b/integration/base/src/main/java/com/inrupt/client/integration/base/AccessGrantScenarios.java @@ -104,9 +104,9 @@ public class AccessGrantScenarios { private static final String GRANT_MODE_READ = "Read"; private static final String GRANT_MODE_APPEND = "Append"; private static final String GRANT_MODE_WRITE = "Write"; - private static final Set PURPOSES = new HashSet<>(Arrays.asList( - URI.create("https://some.purpose/not-a-nefarious-one/i-promise"), - URI.create("https://some.other.purpose/"))); + private static final URI PURPOSE1 = URI.create("https://purpose.example/212efdf4-e1a4-4dcd-9d3b-d6eb92e0205f"); + private static final URI PURPOSE2 = URI.create("https://purpose.example/de605b08-76c7-4f04-9cec-a438810b0c03"); + private static final Set PURPOSES = new HashSet<>(Arrays.asList(PURPOSE1, PURPOSE2)); private static final String GRANT_EXPIRATION = "2024-04-03T12:00:00Z"; private static URI testContainerURI; @@ -316,7 +316,7 @@ void accessGrantQueryByRequestorTest(final Session session) { //query for all grants issued by the user final List grants = accessGrantClient.query(URI.create(webidUrl), - sharedTextFileURI, GRANT_MODE_READ, AccessRequest.class) + sharedTextFileURI, PURPOSE1, GRANT_MODE_READ, AccessRequest.class) .toCompletableFuture().join(); // result is 4 because we retrieve the grants for each path // sharedTextFileURI = @@ -325,7 +325,7 @@ void accessGrantQueryByRequestorTest(final Session session) { //query for all grants issued by a random user final List randomGrants = accessGrantClient.query(URI.create("https://someuser.test"), - sharedTextFileURI, GRANT_MODE_READ, AccessRequest.class) + sharedTextFileURI, PURPOSE1, GRANT_MODE_READ, AccessRequest.class) .toCompletableFuture().join(); assertEquals(0, randomGrants.size()); } @@ -340,13 +340,13 @@ void accessGrantQueryByResourceTest(final Session session) { //query for all grants of a dedicated resource final List requests = accessGrantClient.query(URI.create(webidUrl), - sharedTextFileURI, GRANT_MODE_READ, AccessRequest.class) + sharedTextFileURI, PURPOSE1, GRANT_MODE_READ, AccessRequest.class) .toCompletableFuture().join(); assertEquals(1, requests.size()); //query for all grants of a random resource final List randomGrants = accessGrantClient.query(URI.create(webidUrl), - URI.create("https://somerandom.test"), GRANT_MODE_READ, AccessRequest.class) + URI.create("https://somerandom.test"), PURPOSE1, GRANT_MODE_READ, AccessRequest.class) .toCompletableFuture().join(); assertEquals(0, randomGrants.size()); } @@ -359,15 +359,16 @@ void accessGrantQueryByPurposeTest(final Session session) { final AccessGrantClient accessGrantClient = new AccessGrantClient(URI.create(VC_PROVIDER)).session(session); - //query for all grants of existent purposes + //query for all grants with a dedicated purpose final List requests = accessGrantClient.query(URI.create(webidUrl), - sharedTextFileURI, GRANT_MODE_READ, AccessRequest.class) + sharedTextFileURI, PURPOSE1, GRANT_MODE_READ, AccessRequest.class) .toCompletableFuture().join(); assertEquals(1, requests.size()); - //query for all grants of dedicated purpose combinations + //query for all grants of an unsupported purpose + final URI purpose = URI.create("https://example.com/12"); final List randomGrants = accessGrantClient.query(URI.create(webidUrl), - sharedTextFileURI, GRANT_MODE_WRITE, AccessRequest.class) + sharedTextFileURI, purpose, GRANT_MODE_READ, AccessRequest.class) .toCompletableFuture().join(); assertEquals(0, randomGrants.size()); //our grant is actually a Read } diff --git a/integration/base/src/main/java/com/inrupt/client/integration/base/MockAccessGrantServer.java b/integration/base/src/main/java/com/inrupt/client/integration/base/MockAccessGrantServer.java index d450b10a0de..a4d1ddfbc94 100644 --- a/integration/base/src/main/java/com/inrupt/client/integration/base/MockAccessGrantServer.java +++ b/integration/base/src/main/java/com/inrupt/client/integration/base/MockAccessGrantServer.java @@ -94,6 +94,7 @@ private void setupMocks() { wireMockServer.stubFor(post(urlEqualTo(DERIVE)) .atPriority(1) .withRequestBody(containing("\"Read\"")) + .withRequestBody(containing("\"https://purpose.example/212efdf4-e1a4-4dcd-9d3b-d6eb92e0205f\"")) .withRequestBody(containing("\"" + this.webId + "\"")) .withRequestBody(containing("\"" + this.sharedFile + "\"")) .willReturn(aResponse() diff --git a/integration/base/src/main/resources/query_response.json b/integration/base/src/main/resources/query_response.json index deb2866d3a2..738630f0754 100644 --- a/integration/base/src/main/resources/query_response.json +++ b/integration/base/src/main/resources/query_response.json @@ -21,7 +21,9 @@ "mode":["Read"], "hasStatus":"https://w3id.org/GConsent#ConsentStatusRequested", "isProvidedToPerson":"{{webId}}", - "forPurpose":["https://some.purpose/not-a-nefarious-one/i-promise", "https://some.other.purpose/"], + "forPurpose":[ + "https://purpose.example/212efdf4-e1a4-4dcd-9d3b-d6eb92e0205f", + "https://purpose.example/de605b08-76c7-4f04-9cec-a438810b0c03"], "forPersonalData":["{{sharedFile}}"]}}, "proof":{ "created":"2022-08-25T20:34:05.236Z", diff --git a/integration/base/src/main/resources/vc-grant.json b/integration/base/src/main/resources/vc-grant.json index bdedc777dc1..af962cbe520 100644 --- a/integration/base/src/main/resources/vc-grant.json +++ b/integration/base/src/main/resources/vc-grant.json @@ -20,7 +20,9 @@ "mode":["Read"], "hasStatus":"https://w3id.org/GConsent#ConsentStatusExplicitlyGiven", "isProvidedToPerson":"{{webId}}", - "forPurpose":["https://purpose.example/1", "https://purpose.example/2"], + "forPurpose":[ + "https://purpose.example/212efdf4-e1a4-4dcd-9d3b-d6eb92e0205f", + "https://purpose.example/de605b08-76c7-4f04-9cec-a438810b0c03"], "forPersonalData":["{{sharedFile}}"]}}, "proof":{ "created":"2022-08-25T20:34:05.236Z", diff --git a/integration/base/src/main/resources/vc-request.json b/integration/base/src/main/resources/vc-request.json index 041726fa9da..64caac7851f 100644 --- a/integration/base/src/main/resources/vc-request.json +++ b/integration/base/src/main/resources/vc-request.json @@ -20,7 +20,9 @@ "mode":["Read"], "hasStatus":"https://w3id.org/GConsent#ConsentStatusRequested", "isConsentForDataSubject":"{{webId}}", - "forPurpose":["https://some.purpose/not-a-nefarious-one/i-promise", "https://some.other.purpose/"], + "forPurpose":[ + "https://purpose.example/212efdf4-e1a4-4dcd-9d3b-d6eb92e0205f", + "https://purpose.example/de605b08-76c7-4f04-9cec-a438810b0c03"], "forPersonalData":["{{sharedFile}}"]}}, "proof":{ "created":"2022-08-25T20:34:05.236Z",