From 176b9f6058cc5f82482a6aa0fffe79da854be084 Mon Sep 17 00:00:00 2001 From: Aaron Coburn Date: Wed, 14 Jun 2023 16:49:12 -0400 Subject: [PATCH] JCL-382: Access Credential Query builder --- .../accessgrant/AccessCredentialQuery.java | 228 ++++++++++++++++++ .../client/accessgrant/AccessGrantClient.java | 49 +++- .../accessgrant/AccessGrantClientTest.java | 93 +++++++ 3 files changed, 357 insertions(+), 13 deletions(-) create mode 100644 access-grant/src/main/java/com/inrupt/client/accessgrant/AccessCredentialQuery.java diff --git a/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessCredentialQuery.java b/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessCredentialQuery.java new file mode 100644 index 00000000000..df187cfbd21 --- /dev/null +++ b/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessCredentialQuery.java @@ -0,0 +1,228 @@ +/* + * 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 java.net.URI; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +/** + * An object to represent an access credential query. + * + * @param The access credential type + */ +public class AccessCredentialQuery { + + private static final URI SOLID_ACCESS_GRANT = URI.create("SolidAccessGrant"); + private static final URI SOLID_ACCESS_REQUEST = URI.create("SolidAccessRequest"); + private static final URI SOLID_ACCESS_DENIAL = URI.create("SolidAccessDenial"); + + private final URI type; + private final Set purposes; + private final Set modes; + private final URI resource; + private final URI creator; + private final URI recipient; + private final Class clazz; + + /** + * Create an access credential query. + * + * @param resource the resource, may be {@code null} + * @param creator the creator, may be {@code null} + * @param recipient the recipient, may be {@code null} + * @param purposes the purposes, never {@code null} + * @param modes the access modes, never {@code null} + * @param clazz the credential type, never {@code null} + */ + AccessCredentialQuery(final URI resource, final URI creator, final URI recipient, + final Set purposes, final Set modes, final Class clazz) { + this.clazz = Objects.requireNonNull(clazz, "The clazz parameter must not be null!"); + + if (AccessGrant.class.isAssignableFrom(clazz)) { + this.type = SOLID_ACCESS_GRANT; + } else if (AccessRequest.class.isAssignableFrom(clazz)) { + this.type = SOLID_ACCESS_REQUEST; + } else if (AccessDenial.class.isAssignableFrom(clazz)) { + this.type = SOLID_ACCESS_DENIAL; + } else { + throw new AccessGrantException("Unsupported type " + clazz + " in query request"); + } + + this.resource = resource; + this.creator = creator; + this.recipient = recipient; + this.purposes = purposes; + this.modes = modes; + } + + /** + * Get the access credential type value. + * + * @return the type, never {@code null} + */ + public URI getType() { + return type; + } + + /** + * Get the requested resource. + * + * @return the resource, may be {@code null} + */ + public URI getResource() { + return resource; + } + + /** + * Get the requested creator. + * + * @return the creator, may be {@code null} + */ + public URI getCreator() { + return creator; + } + + /** + * Get the requested recipient. + * + * @return the recipient, may be {@code null} + */ + public URI getRecipient() { + return recipient; + } + + /** + * Get the requested purposes. + * + * @return the purpose identifiers, never {@code null} + */ + public Set getPurposes() { + return purposes; + } + + /** + * Get the requested access modes. + * + * @return the access modes, never {@code null} + */ + public Set getModes() { + return modes; + } + + /* package private */ + Class getAccessCredentialType() { + return clazz; + } + + /** + * Create a new access credential query builder. + * + * @return the builder + */ + public static Builder newBuilder() { + return new Builder(); + } + + /** + * A builder class for access credential queries. + */ + public static class Builder { + + private final Set purposes = new HashSet<>(); + private final Set modes = new HashSet<>(); + private URI builderResource; + private URI builderCreator; + private URI builderRecipient; + + /** + * Set the resource identifier. + * + * @param resource the resource identifier, may be {@code null} + * @return this builder + */ + public Builder resource(final URI resource) { + builderResource = resource; + return this; + } + + /** + * Add a purpose identifier. + * + * @param purpose a purpose identifier; {@code null} values have no effect. + * @return this builder + */ + public Builder purpose(final URI purpose) { + if (purpose != null) { + purposes.add(purpose); + } + return this; + } + + /** + * Add an access mode value. + * + * @param mode a mode value; {@code null} values have no effect. + * @return this builder + */ + public Builder mode(final String mode) { + if (mode != null) { + modes.add(mode); + } + return this; + } + + /** + * Set the creator identifier. + * + * @param creator the creator identifier, may be {@code null} + * @return this builder + */ + public Builder creator(final URI creator) { + builderCreator = creator; + return this; + } + + /** + * Set the recipient identifier. + * + * @param recipient the recipient identifier, may be {@code null} + * @return this builder + */ + public Builder recipient(final URI recipient) { + builderRecipient = recipient; + return this; + } + + /** + * Build the access credential query. + * + * @param the credential type + * @param clazz the credential type + * @return the query object + */ + public AccessCredentialQuery build(final Class clazz) { + return new AccessCredentialQuery(builderResource, builderCreator, builderRecipient, purposes, modes, + clazz); + } + } +} 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 c9aec3e3436..bffd3ad448b 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 @@ -390,6 +390,28 @@ public CompletionStage verify(final AccessCredenti */ public CompletionStage> query(final URI resource, final URI creator, final URI recipient, final URI purpose, final String mode, final Class clazz) { + + final Set modes = mode != null ? Collections.singleton(mode) : Collections.emptySet(); + final Set purposes = purpose != null ? Collections.singleton(purpose) : Collections.emptySet(); + + return query(resource, creator, recipient, purposes, modes, clazz); + } + + /** + * Perform an Access Grant query. + * + * @param the AccessCredential type + * @param query the access credential query, never {@code null} + * @return the next stage of completion, including the matched Access Credentials + */ + public CompletionStage> query(final AccessCredentialQuery query) { + Objects.requireNonNull(query, "The query may not be null!"); + return query(query.getResource(), query.getCreator(), query.getRecipient(), query.getPurposes(), + query.getModes(), query.getAccessCredentialType()); + } + + private CompletionStage> query(final URI resource, final URI creator, + final URI recipient, final Set purposes, final Set modes, final Class clazz) { Objects.requireNonNull(clazz, "The clazz parameter must not be null!"); final URI type; @@ -409,7 +431,7 @@ public CompletionStage> query(final URI res return v1Metadata().thenCompose(metadata -> { final List>> futures = buildQuery(config.getIssuer(), type, - resource, creator, recipient, purpose, mode).stream() + resource, creator, recipient, purposes, modes).stream() .map(data -> Request.newBuilder(metadata.queryEndpoint) .header(CONTENT_TYPE, APPLICATION_JSON) .POST(Request.BodyPublishers.ofByteArray(serialize(data))).build()) @@ -455,7 +477,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, - resource, null, agent, null, mode).stream() + resource, null, agent, Collections.emptySet(), Collections.singleton(mode)).stream() .map(data -> Request.newBuilder(metadata.queryEndpoint) .header(CONTENT_TYPE, APPLICATION_JSON) .POST(Request.BodyPublishers.ofByteArray(serialize(data))).build()) @@ -688,14 +710,15 @@ static Collection getCredentials(final Map data) { } static List> buildQuery(final URI issuer, final URI type, final URI resource, final URI creator, - final URI recipient, final URI purpose, final String mode) { + final URI recipient, final Set purposes, final Set modes) { final List> queries = new ArrayList<>(); - buildQuery(queries, issuer, type, resource, creator, recipient, purpose, mode); + buildQuery(queries, issuer, type, resource, creator, recipient, purposes, modes); return queries; } static void buildQuery(final List> queries, final URI issuer, final URI type, - final URI resource, final URI creator, final URI recipient, final URI purpose, final String mode) { + final URI resource, final URI creator, final URI recipient, final Set purposes, + final Set modes) { final Map credential = new HashMap<>(); credential.put(CONTEXT, Arrays.asList(VC_CONTEXT_URI, INRUPT_CONTEXT_URI)); credential.put("issuer", issuer); @@ -706,7 +729,7 @@ static void buildQuery(final List> queries, final URI issuer subject.put("id", creator); } - final Map consent = buildConsent(type, resource, recipient, purpose, mode); + final Map consent = buildConsent(type, resource, recipient, purposes, modes); if (!consent.isEmpty()) { if (isAccessGrant(type) || isAccessDenial(type)) { subject.put(PROVIDED_CONSENT, consent); @@ -726,12 +749,12 @@ static void buildQuery(final List> queries, final URI issuer // Recurse final URI parent = getParent(resource); if (parent != null) { - buildQuery(queries, issuer, type, parent, creator, recipient, purpose, mode); + buildQuery(queries, issuer, type, parent, creator, recipient, purposes, modes); } } - static Map buildConsent(final URI type, final URI resource, final URI recipient, final URI purpose, - final String mode) { + static Map buildConsent(final URI type, final URI resource, final URI recipient, + final Set purposes, final Set modes) { final Map consent = new HashMap<>(); if (recipient != null) { if (isAccessGrant(type) || isAccessDenial(type)) { @@ -743,11 +766,11 @@ static Map buildConsent(final URI type, final URI resource, fina if (resource != null) { consent.put(FOR_PERSONAL_DATA, resource); } - if (purpose != null) { - consent.put(FOR_PURPOSE, purpose); + if (!purposes.isEmpty()) { + consent.put(FOR_PURPOSE, purposes); } - if (mode != null) { - consent.put(MODE, mode); + if (!modes.isEmpty()) { + consent.put(MODE, modes); } return consent; } 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 8757a8be196..f3008883bc2 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 @@ -514,6 +514,23 @@ void testQueryGrant() { assertEquals(1, grants.size()); } + @Test + void testQueryGrantBuilder() { + final Map claims = new HashMap<>(); + claims.put("webid", WEBID); + claims.put("sub", SUB); + claims.put("iss", ISS); + claims.put("azp", AZP); + final String token = generateIdToken(claims); + final AccessGrantClient client = agClient.session(OpenIdSession.ofIdToken(token)); + + final URI resource = URI.create("https://storage.example/e973cc3d-5c28-4a10-98c5-e40079289358/a/b/c"); + final AccessCredentialQuery query = AccessCredentialQuery.newBuilder() + .resource(resource).mode("Read").build(AccessGrant.class); + final List grants = client.query(query).toCompletableFuture().join(); + assertEquals(1, grants.size()); + } + @Test void testQueryGrantAgent() { final Map claims = new HashMap<>(); @@ -529,6 +546,22 @@ void testQueryGrantAgent() { assertEquals(1, grants.size()); } + @Test + void testQueryGrantAgentBuilder() { + final Map claims = new HashMap<>(); + claims.put("webid", WEBID); + claims.put("sub", SUB); + claims.put("iss", ISS); + claims.put("azp", AZP); + final String token = generateIdToken(claims); + final AccessGrantClient client = agClient.session(OpenIdSession.ofIdToken(token)); + + final AccessCredentialQuery query = AccessCredentialQuery.newBuilder() + .recipient(URI.create("https://id.test/user")).mode("Read").build(AccessGrant.class); + final List grants = client.query(query).toCompletableFuture().join(); + assertEquals(1, grants.size()); + } + @Test void testQueryRequestAgent() { final Map claims = new HashMap<>(); @@ -544,6 +577,22 @@ void testQueryRequestAgent() { assertEquals(1, requests.size()); } + @Test + void testQueryRequestAgentBuilder() { + final Map claims = new HashMap<>(); + claims.put("webid", WEBID); + claims.put("sub", SUB); + claims.put("iss", ISS); + claims.put("azp", AZP); + final String token = generateIdToken(claims); + final AccessGrantClient client = agClient.session(OpenIdSession.ofIdToken(token)); + + final AccessCredentialQuery query = AccessCredentialQuery.newBuilder() + .recipient(URI.create("https://id.test/user")).mode("Read").build(AccessRequest.class); + final List requests = client.query(query).toCompletableFuture().join(); + assertEquals(1, requests.size()); + } + @Test void testQueryRequest() { final Map claims = new HashMap<>(); @@ -560,6 +609,23 @@ void testQueryRequest() { assertEquals(1, requests.size()); } + @Test + void testQueryRequestBuilder() { + final Map claims = new HashMap<>(); + claims.put("webid", WEBID); + claims.put("sub", SUB); + claims.put("iss", ISS); + claims.put("azp", AZP); + final String token = generateIdToken(claims); + final AccessGrantClient client = agClient.session(OpenIdSession.ofIdToken(token)); + + final URI resource = URI.create("https://storage.example/f1759e6d-4dda-4401-be61-d90d070a5474/a/b/c"); + final AccessCredentialQuery query = AccessCredentialQuery.newBuilder() + .resource(resource).mode("Read").build(AccessRequest.class); + final List requests = client.query(query).toCompletableFuture().join(); + assertEquals(1, requests.size()); + } + @Test void testQueryDenial() { final Map claims = new HashMap<>(); @@ -576,6 +642,33 @@ void testQueryDenial() { assertEquals(1, grants.size()); } + @Test + void testQueryDenialBuilder() { + final Map claims = new HashMap<>(); + claims.put("webid", WEBID); + claims.put("sub", SUB); + claims.put("iss", ISS); + claims.put("azp", AZP); + final String token = generateIdToken(claims); + final AccessGrantClient client = agClient.session(OpenIdSession.ofIdToken(token)); + + final URI resource = URI.create("https://storage.example/ef9c4b90-0459-408d-bfa9-1c61d46e1eaf/e/f/g"); + final AccessCredentialQuery query = AccessCredentialQuery.newBuilder() + .resource(resource).mode("Read").build(AccessDenial.class); + final List grants = client.query(query).toCompletableFuture().join(); + assertEquals(1, grants.size()); + } + + @Test + void testQueryInvalidTypeBuilder() { + final URI uri = URI.create("https://storage.example/f1759e6d-4dda-4401-be61-d90d070a5474/a/b/c"); + final AccessCredentialQuery.Builder builder = AccessCredentialQuery.newBuilder() + .resource(uri).mode("Read"); + + assertThrows(AccessGrantException.class, () -> builder.build(AccessCredential.class)); + } + + @Test void testQueryInvalidType() { final Map claims = new HashMap<>();