From 9dc9742d42639daf910efa9ae4e7d4093405f3ce Mon Sep 17 00:00:00 2001 From: Aaron Coburn Date: Wed, 17 May 2023 15:38:27 -0400 Subject: [PATCH 1/3] JCL-335: Formalize type hierarchy in accessgrant module --- .../client/accessgrant/AccessCredential.java | 106 ++++++++ .../client/accessgrant/AccessGrant.java | 180 ++++++-------- .../client/accessgrant/AccessGrantClient.java | 231 +++++++++++++++--- .../client/accessgrant/AccessRequest.java | 207 ++++++++++++++++ .../com/inrupt/client/accessgrant/Utils.java | 117 +++++++++ .../accessgrant/AccessGrantClientTest.java | 27 +- .../accessgrant/AccessGrantSessionTest.java | 2 +- .../client/accessgrant/AccessGrantTest.java | 37 +-- .../base/AccessGrantScenarios.java | 81 +++--- .../base/MockAccessGrantServer.java | 16 ++ .../base/src/main/resources/vc-grant.json | 12 +- .../base/src/main/resources/vc-request.json | 31 +++ 12 files changed, 834 insertions(+), 213 deletions(-) create mode 100644 access-grant/src/main/java/com/inrupt/client/accessgrant/AccessCredential.java create mode 100644 access-grant/src/main/java/com/inrupt/client/accessgrant/AccessRequest.java create mode 100644 access-grant/src/main/java/com/inrupt/client/accessgrant/Utils.java create mode 100644 integration/base/src/main/resources/vc-request.json diff --git a/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessCredential.java b/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessCredential.java new file mode 100644 index 00000000000..a87bf02ea5e --- /dev/null +++ b/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessCredential.java @@ -0,0 +1,106 @@ +/* + * 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.time.Instant; +import java.util.Optional; +import java.util.Set; + +public interface AccessCredential { + + /** + * Get the types of the access credential. + * + * @return the access credential types + */ + Set getTypes(); + + /** + * Get the access modes of the access credential. + * + * @return the access credential types + */ + Set getModes(); + + /** + * Get the revocation status of the access credential. + * + * @return the revocation status, if present + */ + Optional getStatus(); + + /** + * Get the expiration time of the access credential. + * + * @return the expiration time + */ + Instant getExpiration(); + + /** + * Get the issuer of the access credential. + * + * @return the issuer + */ + URI getIssuer(); + + /** + * Get the identifier of the access credential. + * + * @return the identifier + */ + URI getIdentifier(); + + /** + * Get the collection of purposes associated with the access credential. + * + * @return the purposes + */ + Set getPurposes(); + + /** + * Get the resources associated with the access credential. + * + * @return the associated resource identifiers + */ + Set getResources(); + + /** + * Get the creator of this access credential. + * + * @return the creator + */ + URI getCreator(); + + /** + * Get the recipient of this access credential. + * + * @return the recipient, if present + */ + Optional getRecipient(); + + /** + * Serialize this access credential as a String. + * + * @return a serialized form of the credential + */ + String serialize(); +} 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 05e0d491929..c6a658d5c96 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 @@ -20,6 +20,7 @@ */ package com.inrupt.client.accessgrant; +import static com.inrupt.client.accessgrant.Utils.*; import static java.nio.charset.StandardCharsets.UTF_8; import com.inrupt.client.spi.JsonService; @@ -30,7 +31,6 @@ import java.io.InputStream; import java.net.URI; import java.time.Instant; -import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -44,11 +44,11 @@ /** * An Access Grant abstraction, for use with interacting with Solid resources. */ -public class AccessGrant { +public class AccessGrant implements AccessCredential { private static final String TYPE = "type"; private static final String REVOCATION_LIST_2020_STATUS = "RevocationList2020Status"; - private static final Set SUPPORTED_TYPES = supportedTypes(); + private static final Set supportedTypes = getSupportedTypes(); private static final JsonService jsonService = ServiceProvider.getJsonService(); private final String rawGrant; @@ -74,7 +74,7 @@ protected AccessGrant(final String grant) throws IOException { final Map data = jsonService.fromJson(in, new HashMap(){}.getClass().getGenericSuperclass()); - final Map vc = getCredentialFromPresentation(data).orElseThrow(() -> + final Map vc = getCredentialFromPresentation(data, supportedTypes).orElseThrow(() -> new IllegalArgumentException("Invalid Access Grant: missing verifiable credential")); if (asSet(data.get(TYPE)).orElseGet(Collections::emptySet).contains("VerifiablePresentation")) { @@ -94,21 +94,15 @@ protected AccessGrant(final String grant) throws IOException { new IllegalArgumentException("Missing or invalid credentialSubject.id field")); // V1 Access Grant, using gConsent - final Map consent = asMap(subject.get("providedConsent")).orElseGet(() -> - // V1 Access Request, using gConsent - asMap(subject.get("hasConsent")).orElseThrow(() -> - // Unsupported structure - new IllegalArgumentException("Invalid Access Grant: missing consent clause"))); + final Map consent = asMap(subject.get("providedConsent")).orElseThrow(() -> + // Unsupported structure + new IllegalArgumentException("Invalid Access Grant: missing consent clause")); final Optional person = asUri(consent.get("isProvidedToPerson")); final Optional controller = asUri(consent.get("isProvidedToController")); final Optional other = asUri(consent.get("isProvidedTo")); - final Optional dataSubject = asUri(consent.get("isConsentForDataSubject")); - if (subject.containsKey("hasConsent")) { - this.grantee = dataSubject.orElse(null); - } else { - this.grantee = person.orElseGet(() -> controller.orElseGet(() -> other.orElse(null))); - } + + this.grantee = person.orElseGet(() -> controller.orElseGet(() -> other.orElse(null))); this.modes = asSet(consent.get("mode")).orElseGet(Collections::emptySet); this.resources = asSet(consent.get("forPersonalData")).orElseGet(Collections::emptySet) .stream().map(URI::create).collect(Collectors.toSet()); @@ -150,7 +144,9 @@ static Status asRevocationList2020(final Map credentialStatus) { * * @param accessGrant the access grant * @return a parsed access grant + * @deprecated As of Beta3, please use the {@link AccessGrant#of} method */ + @Deprecated public static AccessGrant ofAccessGrant(final String accessGrant) { try { return new AccessGrant(accessGrant); @@ -164,65 +160,71 @@ public static AccessGrant ofAccessGrant(final String accessGrant) { * * @param accessGrant the access grant * @return a parsed access grant + * @deprecated As of Beta3, please use the {@link AccessGrant#of} method */ + @Deprecated public static AccessGrant ofAccessGrant(final InputStream accessGrant) { try { - return ofAccessGrant(IOUtils.toString(accessGrant, UTF_8)); + return of(IOUtils.toString(accessGrant, UTF_8)); } catch (final IOException ex) { throw new IllegalArgumentException("Unable to read access grant", ex); } } /** - * Get the types of the access grant. + * Create an AccessGrant object from a serialized form. * - * @return the access grant types + * @param serialization the serialized access grant + * @return a parsed access grant */ - public Set getTypes() { - return types; + public static AccessGrant of(final String serialization) { + try { + return new AccessGrant(serialization); + } catch (final IOException ex) { + throw new IllegalArgumentException("Unable to read access grant", ex); + } } /** - * Get the modes of the access grant. + * Create an AccessGrant object from a serialized form. * - * @return the access grant modes + * @param serialization the serialized access grant + * @return a parsed access grant */ + public static AccessGrant of(final InputStream serialization) { + try { + return of(IOUtils.toString(serialization, UTF_8)); + } catch (final IOException ex) { + throw new IllegalArgumentException("Unable to read access grant", ex); + } + } + + @Override + public Set getTypes() { + return types; + } + + @Override public Set getModes() { return modes; } - /** - * Get the access grant status information. - * - * @return the status information - */ + @Override public Optional getStatus() { return Optional.ofNullable(status); } - /** - * Get the expiration date of the access grant. - * - * @return the access grant expiration - */ + @Override public Instant getExpiration() { return expiration; } - /** - * Get the issuer of the access grant. - * - * @return the access grant issuer - */ + @Override public URI getIssuer() { return issuer; } - /** - * Get the identifier of the access grant. - * - * @return the access grant identifier - */ + @Override public URI getIdentifier() { return identifier; } @@ -238,20 +240,12 @@ public Set getPurpose() { return purposes; } - /** - * Get the purposes of the access grant. - * - * @return the access grant purposes - */ + @Override public Set getPurposes() { return purposes; } - /** - * Get the resources associated with the access grant. - * - * @return the access grant resources - */ + @Override public Set getResources() { return resources; } @@ -260,86 +254,52 @@ public Set getResources() { * Get the agent to whom access is granted. * * @return the agent that was granted access + * @deprecated As of Beta3, please use {@link #getRecipient} */ + @Deprecated public Optional getGrantee() { - return Optional.ofNullable(grantee); + return getRecipient(); } /** * Get the agent who granted access. * * @return the agent granting access + * @deprecated As of Beta3, please use {@link #getCreator} */ + @Deprecated public URI getGrantor() { + return getCreator(); + } + + @Override + public URI getCreator() { return grantor; } + @Override + public Optional getRecipient() { + return Optional.ofNullable(grantee); + } + + @Override + public String serialize() { + return rawGrant; + } + /** * Get the raw access grant. * * @return the access grant + * @deprecated as of Beta3, please use the {@link #serialize} method */ + @Deprecated public String getRawGrant() { - return rawGrant; - } - - static Optional asInstant(final Object value) { - if (value instanceof String) { - return Optional.of(Instant.parse((String) value)); - } - return Optional.empty(); - } - - static Optional asUri(final Object value) { - if (value instanceof String) { - return Optional.of(URI.create((String) value)); - } - return Optional.empty(); - } - - static Optional asMap(final Object value) { - if (value instanceof Map) { - return Optional.of((Map) value); - } - return Optional.empty(); - } - - static Optional> asSet(final Object value) { - if (value != null) { - final Set data = new HashSet<>(); - if (value instanceof String) { - data.add((String) value); - } else if (value instanceof Collection) { - for (final Object item : (Collection) value) { - if (item instanceof String) { - data.add((String) item); - } - } - } - return Optional.of(data); - } - return Optional.empty(); - } - - static Optional getCredentialFromPresentation(final Map data) { - if (data.get("verifiableCredential") instanceof Collection) { - for (final Object item : (Collection) data.get("verifiableCredential")) { - if (item instanceof Map) { - final Map vc = (Map) item; - if (asSet(vc.get(TYPE)).filter(types -> - types.stream().anyMatch(SUPPORTED_TYPES::contains)).isPresent()) { - return Optional.of(vc); - } - } - } - } - return Optional.empty(); + return serialize(); } - static Set supportedTypes() { + static Set getSupportedTypes() { final Set types = new HashSet<>(); - types.add("SolidAccessRequest"); - types.add("http://www.w3.org/ns/solid/vc#SolidAccessRequest"); types.add("SolidAccessGrant"); types.add("http://www.w3.org/ns/solid/vc#SolidAccessGrant"); return Collections.unmodifiableSet(types); 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 b745bc6c335..22703a78db9 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 @@ -32,6 +32,7 @@ import com.inrupt.client.spi.ServiceProvider; import com.inrupt.client.util.URIBuilder; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; @@ -98,6 +99,7 @@ public class AccessGrantClient { private static final URI ACCESS_GRANT = URI.create("http://www.w3.org/ns/solid/vc#SolidAccessGrant"); private static final URI ACCESS_REQUEST = URI.create("http://www.w3.org/ns/solid/vc#SolidAccessRequest"); private static final Set ACCESS_GRANT_TYPES = getAccessGrantTypes(); + private static final Set ACCESS_REQUEST_TYPES = getAccessRequestTypes(); private final Client client; private final ClientCache metadataCache; @@ -162,6 +164,77 @@ public AccessGrantClient session(final Session session) { return new AccessGrantClient(client.session(session), metadataCache, config); } + /** + * Issue an access request. + * + * @param agent the recipient of this credential + * @param resources the resources to which this credential applies + * @param modes the access modes for this credential + * @param purposes the purposes of this credential + * @param expiration the expiration time of this credential + * @return the next stage of completion containing the resulting access request + */ + public CompletionStage requestAccess(final URI agent, final Set resources, + final Set modes, final Set purposes, final Instant expiration) { + Objects.requireNonNull(resources, "Resources may not be null!"); + Objects.requireNonNull(modes, "Access modes may not be null!"); + return v1Metadata().thenCompose(metadata -> { + final Map data = buildAccessRequestv1(agent, resources, modes, expiration, purposes); + + final Request req = Request.newBuilder(metadata.issueEndpoint) + .header(CONTENT_TYPE, APPLICATION_JSON) + .POST(Request.BodyPublishers.ofByteArray(serialize(data))).build(); + + return client.send(req, Response.BodyHandlers.ofInputStream()) + .thenApply(res -> { + try (final InputStream input = res.body()) { + final int status = res.statusCode(); + if (isSuccess(status)) { + return processVerifiableCredential(input, ACCESS_REQUEST_TYPES, AccessRequest.class); + } + throw new AccessGrantException("Unable to issue Access Grant: HTTP error " + status, + status); + } catch (final IOException ex) { + throw new AccessGrantException( + "Unexpected I/O exception while processing Access Grant", ex); + } + }); + }); + } + + /** + * Issue an access grant based on an access request. + * + * @param request the access request + * @return the next stage of completion containing the issued access grant + */ + public CompletionStage grantAccess(final AccessRequest request) { + Objects.requireNonNull(request, "Request may not be null!"); + return v1Metadata().thenCompose(metadata -> { + final Map data = buildAccessGrantv1(request.getCreator(), request.getResources(), + request.getModes(), request.getExpiration(), request.getPurposes()); + final Request req = Request.newBuilder(metadata.issueEndpoint) + .header(CONTENT_TYPE, APPLICATION_JSON) + .POST(Request.BodyPublishers.ofByteArray(serialize(data))).build(); + + return client.send(req, Response.BodyHandlers.ofInputStream()) + .thenApply(res -> { + try (final InputStream input = res.body()) { + final int status = res.statusCode(); + if (isSuccess(status)) { + return processVerifiableCredential(input, ACCESS_GRANT_TYPES, AccessGrant.class); + } + throw new AccessGrantException("Unable to issue Access Grant: HTTP error " + status, + status); + } catch (final IOException ex) { + throw new AccessGrantException( + "Unexpected I/O exception while processing Access Grant", ex); + } + }); + }); + + } + /** * Issue an access grant or request. * @@ -172,7 +245,9 @@ public AccessGrantClient session(final Session session) { * @param purposes the purposes of this credential * @param expiration the expiration time of this credential * @return the next stage of completion containing the resulting credential + * @deprecated as of Beta3, please use the {@link #requestAccess} or {@link #grantAccess} methods */ + @Deprecated public CompletionStage issue(final URI type, final URI agent, final Set resources, final Set modes, final Set purposes, final Instant expiration) { Objects.requireNonNull(type, "Access Grant type may not be null!"); @@ -197,7 +272,7 @@ public CompletionStage issue(final URI type, final URI agent, final try (final InputStream input = res.body()) { final int status = res.statusCode(); if (isSuccess(status)) { - return processVerifiableCredential(input, ACCESS_GRANT_TYPES); + return processVerifiableCredential(input, ACCESS_GRANT_TYPES, AccessGrant.class); } throw new AccessGrantException("Unable to issue Access Grant: HTTP error " + status, status); @@ -212,14 +287,21 @@ public CompletionStage issue(final URI type, final URI agent, final /** * Verify an access grant or request. * - * @param accessGrant the access grant to verify - * @return the next stage of completion containing the resulting credential + * @param credential the credential to verify + * @return the next stage of completion containing the verification result */ - public CompletionStage verify(final AccessGrant accessGrant) { + public CompletionStage verify(final AccessCredential credential) { return v1Metadata().thenCompose(metadata -> { final Map presentation = new HashMap<>(); - presentation.put(VERIFIABLE_CREDENTIAL, accessGrant); + + try (final InputStream is = new ByteArrayInputStream(credential.serialize().getBytes(UTF_8))) { + final Map data = jsonService.fromJson(is, + new HashMap(){}.getClass().getGenericSuperclass()); + presentation.put(VERIFIABLE_CREDENTIAL, data); + } catch (final IOException ex) { + throw new AccessGrantException("Unable to serialize credential", ex); + } final Request req = Request.newBuilder(metadata.verifyEndpoint) .header(CONTENT_TYPE, APPLICATION_JSON) @@ -242,6 +324,60 @@ public CompletionStage verify(final AccessGrant accessGran }); } + /** + * Perform an Access Grant query. + * + * @param the AccessCredential type + * @param agent the agent identifier, may be {@code null} + * @param resource the resource identifier, 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) { + Objects.requireNonNull(clazz, "The clazz parameter must not be null!"); + + final URI type; + final Set supportedTypes; + if (AccessGrant.class.isAssignableFrom(clazz)) { + type = ACCESS_GRANT; + supportedTypes = ACCESS_GRANT_TYPES; + } else if (AccessRequest.class.isAssignableFrom(clazz)) { + type = ACCESS_REQUEST; + supportedTypes = ACCESS_REQUEST_TYPES; + } else { + throw new AccessGrantException("Unsupported type " + clazz + " in query request"); + } + + return v1Metadata().thenCompose(metadata -> { + final List>> futures = buildQuery(config.getIssuer(), type, + agent, resource, mode).stream() + .map(data -> Request.newBuilder(metadata.queryEndpoint) + .header(CONTENT_TYPE, APPLICATION_JSON) + .POST(Request.BodyPublishers.ofByteArray(serialize(data))).build()) + .map(req -> client.send(req, Response.BodyHandlers.ofInputStream()) + .thenApply(res -> { + try (final InputStream input = res.body()) { + final int status = res.statusCode(); + if (isSuccess(status)) { + return processQueryResponse(input, supportedTypes, clazz); + } + throw new AccessGrantException("Unable to perform Access Grant query: HTTP error " + + status, status); + } catch (final IOException ex) { + throw new AccessGrantException( + "Unexpected I/O exception while processing Access Grant query", ex); + } + }).toCompletableFuture()) + .collect(Collectors.toList()); + + return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) + .thenApply(x -> futures.stream().map(CompletableFuture::join).flatMap(List::stream) + .collect(Collectors.toList())); + }); + } + /** * Perform an Access Grant query. * @@ -254,7 +390,9 @@ public CompletionStage verify(final AccessGrant accessGran * @param resource the resource identifier, may be {@code null} * @param mode the access mode, may be {@code null} * @return the next stage of completion, including the matched Access Grants + * @deprecated as of Beta3, please use the alternative {@link #query} method */ + @Deprecated public CompletionStage> query(final URI type, final URI agent, final URI resource, final String mode) { Objects.requireNonNull(type, "The type parameter must not be null!"); @@ -269,7 +407,7 @@ public CompletionStage> query(final URI type, final URI agent, try (final InputStream input = res.body()) { final int status = res.statusCode(); if (isSuccess(status)) { - return processQueryResponse(input, ACCESS_GRANT_TYPES); + return processQueryResponse(input, ACCESS_GRANT_TYPES, AccessGrant.class); } throw new AccessGrantException("Unable to perform Access Grant query: HTTP error " + status, status); @@ -289,20 +427,20 @@ public CompletionStage> query(final URI type, final URI agent, /** * Revoke an access credential. * - * @param accessGrant the access grant + * @param credential the access credential * @return the next stage of completion */ - public CompletionStage revoke(final AccessGrant accessGrant) { + public CompletionStage revoke(final AccessCredential credential) { return v1Metadata().thenCompose(metadata -> { - final Status status = accessGrant.getStatus().orElseThrow(() -> - new AccessGrantException("Unable to revoke Access Grant: no credentialStatus data")); + final Status status = credential.getStatus().orElseThrow(() -> + new AccessGrantException("Unable to revoke Access Credential: no credentialStatus data")); final Map credentialStatus = new HashMap<>(); credentialStatus.put(TYPE, status.getType()); credentialStatus.put("status", Integer.toString(status.getIndex())); final Map data = new HashMap<>(); - data.put("credentialId", accessGrant.getIdentifier()); + data.put("credentialId", credential.getIdentifier()); data.put("credentialStatus", Arrays.asList(credentialStatus)); final Request req = Request.newBuilder(metadata.statusEndpoint) @@ -315,7 +453,7 @@ public CompletionStage revoke(final AccessGrant accessGrant) { final int code = res.statusCode(); if (!isSuccess(code)) { throw new AccessGrantException("Unable to revoke Access Grant: " + - accessGrant.getIdentifier(), code); + credential.getIdentifier(), code); } }); }); @@ -324,16 +462,16 @@ public CompletionStage revoke(final AccessGrant accessGrant) { /** * Delete an access credential. * - * @param accessGrant the access credential + * @param credential the access credential * @return the next stage of completion */ - public CompletionStage delete(final AccessGrant accessGrant) { - final Request req = Request.newBuilder(accessGrant.getIdentifier()).DELETE().build(); + public CompletionStage delete(final AccessCredential credential) { + final Request req = Request.newBuilder(credential.getIdentifier()).DELETE().build(); return client.send(req, Response.BodyHandlers.discarding()) .thenAccept(res -> { final int status = res.statusCode(); if (!isSuccess(status)) { - throw new AccessGrantException("Unable to delete Access Grant: " + accessGrant.getIdentifier(), + throw new AccessGrantException("Unable to delete Access Credential: " + credential.getIdentifier(), status); } }); @@ -344,8 +482,22 @@ public CompletionStage delete(final AccessGrant accessGrant) { * * @param identifier the access credential identifier * @return the next stage of completion, containing the access credential + * @deprecated as of Beta3, please use the {@link #fetch(URI, Class)} method */ + @Deprecated public CompletionStage fetch(final URI identifier) { + return fetch(identifier, AccessGrant.class); + } + + /** + * Fetch an access credential. + * + * @param the credential type + * @param identifier the access credential identifier + * @param clazz the credential type, such as {@link AccessGrant} or {@link AccessRequest} + * @return the next stage of completion, containing the access credential + */ + public CompletionStage fetch(final URI identifier, final Class clazz) { final Request req = Request.newBuilder(identifier) .header("Accept", "application/ld+json,application/json").build(); return client.send(req, Response.BodyHandlers.ofInputStream()) @@ -353,7 +505,12 @@ public CompletionStage fetch(final URI identifier) { try (final InputStream input = res.body()) { final int httpStatus = res.statusCode(); if (isSuccess(httpStatus)) { - return processVerifiableCredential(input, ACCESS_GRANT_TYPES); + if (AccessGrant.class.equals(clazz)) { + return (T) processVerifiableCredential(input, ACCESS_GRANT_TYPES, clazz); + } else if (AccessRequest.class.equals(clazz)) { + return (T) processVerifiableCredential(input, ACCESS_REQUEST_TYPES, clazz); + } + throw new AccessGrantException("Unable to fetch credential as " + clazz); } throw new AccessGrantException( "Unable to fetch the Access Grant: HTTP Error " + httpStatus, httpStatus); @@ -364,10 +521,13 @@ public CompletionStage fetch(final URI identifier) { }); } - AccessGrant processVerifiableCredential(final InputStream input, final Set validTypes) throws IOException { + + + T processVerifiableCredential(final InputStream input, final Set validTypes, + final Class clazz) throws IOException { final Map data = jsonService.fromJson(input, new HashMap(){}.getClass().getGenericSuperclass()); - final Set types = AccessGrant.asSet(data.get(TYPE)).orElseThrow(() -> + final Set types = Utils.asSet(data.get(TYPE)).orElseThrow(() -> new AccessGrantException("Invalid Access Grant: no 'type' field")); types.retainAll(validTypes); if (!types.isEmpty()) { @@ -375,31 +535,39 @@ AccessGrant processVerifiableCredential(final InputStream input, final Set processQueryResponse(final InputStream input, final Set validTypes) throws IOException { + List processQueryResponse(final InputStream input, final Set validTypes, + final Class clazz) throws IOException { final Map data = jsonService.fromJson(input, new HashMap(){}.getClass().getGenericSuperclass()); - final List grants = new ArrayList<>(); + final List grants = new ArrayList<>(); if (data.get(VERIFIABLE_CREDENTIAL) instanceof Collection) { for (final Object item : (Collection) data.get(VERIFIABLE_CREDENTIAL)) { - AccessGrant.asMap(item).ifPresent(credential -> - AccessGrant.asSet(credential.get(TYPE)).ifPresent(types -> { + Utils.asMap(item).ifPresent(credential -> + Utils.asSet(credential.get(TYPE)).ifPresent(types -> { types.retainAll(validTypes); if (!types.isEmpty()) { final Map presentation = new HashMap<>(); presentation.put(CONTEXT, Arrays.asList(VC_CONTEXT_URI)); presentation.put(TYPE, Arrays.asList("VerifiablePresentation")); presentation.put(VERIFIABLE_CREDENTIAL, Arrays.asList(credential)); - grants.add(AccessGrant.ofAccessGrant(new String(serialize(presentation), UTF_8))); + if (AccessGrant.class.equals(clazz)) { + grants.add((T) AccessGrant.of(new String(serialize(presentation), UTF_8))); + } else if (AccessRequest.class.equals(clazz)) { + grants.add((T) AccessRequest.of(new String(serialize(presentation), UTF_8))); + } } })); } @@ -582,12 +750,17 @@ static boolean isSuccess(final int statusCode) { return statusCode >= 200 && statusCode < 300; } + static Set getAccessRequestTypes() { + final Set types = new HashSet<>(); + types.add("SolidAccessRequest"); + types.add(ACCESS_REQUEST.toString()); + return Collections.unmodifiableSet(types); + } + static Set getAccessGrantTypes() { final Set types = new HashSet<>(); types.add("SolidAccessGrant"); types.add(ACCESS_GRANT.toString()); - types.add("SolidAccessRequest"); - types.add(ACCESS_REQUEST.toString()); return Collections.unmodifiableSet(types); } diff --git a/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessRequest.java b/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessRequest.java new file mode 100644 index 00000000000..762fdea7608 --- /dev/null +++ b/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessRequest.java @@ -0,0 +1,207 @@ +/* + * 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 static com.inrupt.client.accessgrant.Utils.*; +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.inrupt.client.spi.JsonService; +import com.inrupt.client.spi.ServiceProvider; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.time.Instant; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.commons.io.IOUtils; + +/** + * An Access Request abstraction, for use with interacting with Solid resources. + */ +public class AccessRequest implements AccessCredential { + + private static final String TYPE = "type"; + private static final String REVOCATION_LIST_2020_STATUS = "RevocationList2020Status"; + private static final JsonService jsonService = ServiceProvider.getJsonService(); + private static final Set supportedTypes = getSupportedTypes(); + + private final String rawGrant; + private final URI issuer; + private final URI identifier; + private final Set types; + private final Set purposes; + private final Set modes; + private final Set resources; + private final URI grantee; + private final URI grantor; + private final Instant expiration; + private final Status status; + + /** + * Read a verifiable presentation as an AccessRequest. + * + * @param grant the serialized form of an Access Request + */ + protected AccessRequest(final String grant) throws IOException { + try (final InputStream in = new ByteArrayInputStream(grant.getBytes())) { + // TODO process as JSON-LD + final Map data = jsonService.fromJson(in, + new HashMap(){}.getClass().getGenericSuperclass()); + + final Map vc = getCredentialFromPresentation(data, supportedTypes).orElseThrow(() -> + new IllegalArgumentException("Invalid Access Grant: missing verifiable credential")); + + if (asSet(data.get(TYPE)).orElseGet(Collections::emptySet).contains("VerifiablePresentation")) { + this.rawGrant = grant; + this.issuer = asUri(vc.get("issuer")).orElseThrow(() -> + new IllegalArgumentException("Missing or invalid issuer field")); + this.identifier = asUri(vc.get("id")).orElseThrow(() -> + new IllegalArgumentException("Missing or invalid id field")); + + this.types = asSet(vc.get(TYPE)).orElseGet(Collections::emptySet); + this.expiration = asInstant(vc.get("expirationDate")).orElse(Instant.MAX); + + final Map subject = asMap(vc.get("credentialSubject")).orElseThrow(() -> + new IllegalArgumentException("Missing or invalid credentialSubject field")); + + this.grantor = asUri(subject.get("id")).orElseThrow(() -> + new IllegalArgumentException("Missing or invalid credentialSubject.id field")); + + // V1 Access Request, using gConsent + final Map consent = asMap(subject.get("hasConsent")).orElseThrow(() -> + // Unsupported structure + new IllegalArgumentException("Invalid Access Request: missing consent clause")); + + final Optional dataSubject = asUri(consent.get("isConsentForDataSubject")); + this.grantee = dataSubject.orElse(null); + this.modes = asSet(consent.get("mode")).orElseGet(Collections::emptySet); + this.resources = asSet(consent.get("forPersonalData")).orElseGet(Collections::emptySet) + .stream().map(URI::create).collect(Collectors.toSet()); + this.purposes = asSet(consent.get("forPurpose")).orElseGet(Collections::emptySet); + this.status = asMap(vc.get("credentialStatus")).flatMap(credentialStatus -> + asSet(credentialStatus.get(TYPE)).filter(statusTypes -> + statusTypes.contains(REVOCATION_LIST_2020_STATUS)).map(x -> + asRevocationList2020(credentialStatus))).orElse(null); + } else { + throw new IllegalArgumentException("Invalid Access Request: missing VerifiablePresentation type"); + } + } + } + + + /** + * Create an AccessRequest object from a serialized form. + * + * @param serialization the serialized access grant + * @return a parsed access grant + */ + public static AccessRequest of(final String serialization) { + try { + return new AccessRequest(serialization); + } catch (final IOException ex) { + throw new IllegalArgumentException("Unable to read access grant", ex); + } + } + + /** + * Create an AccessRequest object from a serialized form. + * + * @param serialization the access request + * @return a parsed access grant + */ + public static AccessRequest of(final InputStream serialization) { + try { + return of(IOUtils.toString(serialization, UTF_8)); + } catch (final IOException ex) { + throw new IllegalArgumentException("Unable to read access grant", ex); + } + } + + @Override + public Set getTypes() { + return types; + } + + @Override + public Set getModes() { + return modes; + } + + @Override + public Optional getStatus() { + return Optional.ofNullable(status); + } + + @Override + public Instant getExpiration() { + return expiration; + } + + @Override + public URI getIssuer() { + return issuer; + } + + @Override + public URI getIdentifier() { + return identifier; + } + + @Override + public Set getPurposes() { + return purposes; + } + + @Override + public Set getResources() { + return resources; + } + + @Override + public URI getCreator() { + return grantor; + } + + @Override + public Optional getRecipient() { + return Optional.ofNullable(grantee); + } + + @Override + public String serialize() { + return rawGrant; + } + + static Set getSupportedTypes() { + final Set types = new HashSet<>(); + types.add("SolidAccessRequest"); + types.add("http://www.w3.org/ns/solid/vc#SolidAccessRequest"); + return Collections.unmodifiableSet(types); + } +} diff --git a/access-grant/src/main/java/com/inrupt/client/accessgrant/Utils.java b/access-grant/src/main/java/com/inrupt/client/accessgrant/Utils.java new file mode 100644 index 00000000000..0d3ba8183af --- /dev/null +++ b/access-grant/src/main/java/com/inrupt/client/accessgrant/Utils.java @@ -0,0 +1,117 @@ +/* + * 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.time.Instant; +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +/** Utility classes for the Access Grant module. **/ +final class Utils { + + private static final String TYPE = "type"; + private static final String REVOCATION_LIST_2020_STATUS = "RevocationList2020Status"; + + public static Status asRevocationList2020(final Map credentialStatus) { + try { + int idx = -1; + final Object index = credentialStatus.get("revocationListIndex"); + if (index instanceof String) { + idx = Integer.parseInt((String) index); + } else if (index instanceof Integer) { + idx = (Integer) index; + } + + final Object id = credentialStatus.get("id"); + final Object credential = credentialStatus.get("revocationListCredential"); + if (id instanceof String && credential instanceof String && idx >= 0) { + final URI uri = URI.create((String) credential); + return new Status(URI.create((String) id), REVOCATION_LIST_2020_STATUS, uri, idx); + } + throw new IllegalArgumentException("Unable to process credential status as Revocation List 2020"); + } catch (final Exception ex) { + throw new IllegalArgumentException("Unable to process credential status data", ex); + } + } + + public static Optional asInstant(final Object value) { + if (value instanceof String) { + return Optional.of(Instant.parse((String) value)); + } + return Optional.empty(); + } + + public static Optional asUri(final Object value) { + if (value instanceof String) { + return Optional.of(URI.create((String) value)); + } + return Optional.empty(); + } + + public static Optional asMap(final Object value) { + if (value instanceof Map) { + return Optional.of((Map) value); + } + return Optional.empty(); + } + + public static Optional> asSet(final Object value) { + if (value != null) { + final Set data = new HashSet<>(); + if (value instanceof String) { + data.add((String) value); + } else if (value instanceof Collection) { + for (final Object item : (Collection) value) { + if (item instanceof String) { + data.add((String) item); + } + } + } + return Optional.of(data); + } + return Optional.empty(); + } + + public static Optional getCredentialFromPresentation(final Map data, + final Set supportedTypes) { + if (data.get("verifiableCredential") instanceof Collection) { + for (final Object item : (Collection) data.get("verifiableCredential")) { + if (item instanceof Map) { + final Map vc = (Map) item; + if (asSet(vc.get(TYPE)).filter(types -> + types.stream().anyMatch(supportedTypes::contains)).isPresent()) { + return Optional.of(vc); + } + } + } + } + return Optional.empty(); + } + + + private Utils() { + // prevent instantiation + } +} 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 7914de25614..91f75c190c5 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 @@ -96,7 +96,7 @@ void testFetch1() { final String token = generateIdToken(claims); final URI uri = URIBuilder.newBuilder(baseUri).path("access-grant-1").build(); final AccessGrantClient client = agClient.session(OpenIdSession.ofIdToken(token)); - final AccessGrant grant = client.fetch(uri).toCompletableFuture().join(); + final AccessGrant grant = client.fetch(uri, AccessGrant.class).toCompletableFuture().join(); assertEquals(uri, grant.getIdentifier()); assertEquals(baseUri, grant.getIssuer()); @@ -118,7 +118,7 @@ void testFetch2() { final String token = generateIdToken(claims); final URI uri = URIBuilder.newBuilder(baseUri).path("access-grant-2").build(); final AccessGrantClient client = agClient.session(OpenIdSession.ofIdToken(token)); - final AccessGrant grant = client.fetch(uri).toCompletableFuture().join(); + final AccessGrant grant = client.fetch(uri, AccessGrant.class).toCompletableFuture().join(); assertEquals(uri, grant.getIdentifier()); assertEquals(baseUri, grant.getIssuer()); @@ -144,7 +144,7 @@ void testFetch6() { final String token = generateIdToken(claims); final URI uri = URIBuilder.newBuilder(baseUri).path("access-grant-6").build(); final AccessGrantClient client = agClient.session(OpenIdSession.ofIdToken(token)); - final AccessGrant grant = client.fetch(uri).toCompletableFuture().join(); + final AccessGrant grant = client.fetch(uri, AccessGrant.class).toCompletableFuture().join(); assertEquals(uri, grant.getIdentifier()); assertEquals(baseUri, grant.getIssuer()); @@ -169,14 +169,14 @@ void testNotAccessGrant() { final URI uri = URIBuilder.newBuilder(baseUri).path("vc-3").build(); final AccessGrantClient client = agClient.session(OpenIdSession.ofIdToken(token)); final CompletionException err = assertThrows(CompletionException.class, - client.fetch(uri).toCompletableFuture()::join); + client.fetch(uri, AccessGrant.class).toCompletableFuture()::join); } @Test void testFetchInvalid() { final URI uri = URIBuilder.newBuilder(baseUri).path(".well-known/vc-configuration").build(); final CompletionException err = assertThrows(CompletionException.class, - agClient.fetch(uri).toCompletableFuture()::join); + agClient.fetch(uri, AccessRequest.class).toCompletableFuture()::join); assertTrue(err.getCause() instanceof AccessGrantException); } @@ -185,7 +185,7 @@ void testFetchInvalid() { void testFetchNotFound() { final URI uri = URIBuilder.newBuilder(baseUri).path("not-found").build(); final CompletionException err1 = assertThrows(CompletionException.class, - agClient.fetch(uri).toCompletableFuture()::join); + agClient.fetch(uri, AccessRequest.class).toCompletableFuture()::join); assertTrue(err1.getCause() instanceof AccessGrantException); @@ -242,15 +242,14 @@ void testIssueRequest() { final Set purposes = Collections.singleton("https://purpose.test/Purpose1"); final Set resources = Collections.singleton(URI.create("https://storage.test/data/")); - final AccessGrant request = client.issue(ACCESS_REQUEST, agent, resources, modes, purposes, expiration) + final AccessRequest request = client.requestAccess(agent, resources, modes, purposes, expiration) .toCompletableFuture().join(); assertTrue(request.getTypes().contains("SolidAccessRequest")); - assertEquals(Optional.of(agent), request.getGrantee()); + assertEquals(Optional.of(agent), request.getRecipient()); assertEquals(modes, request.getModes()); assertEquals(expiration, request.getExpiration()); assertEquals(baseUri, request.getIssuer()); - assertEquals(purposes, request.getPurpose()); assertEquals(purposes, request.getPurposes()); assertEquals(resources, request.getResources()); } @@ -317,16 +316,18 @@ void testQueryRequest() { final String token = generateIdToken(claims); final AccessGrantClient client = agClient.session(OpenIdSession.ofIdToken(token)); - final List grants = client.query(URI.create("SolidAccessRequest"), null, - URI.create("https://storage.example/f1759e6d-4dda-4401-be61-d90d070a5474/a/b/c"), "Read") + final List requests = client.query(null, + URI.create("https://storage.example/f1759e6d-4dda-4401-be61-d90d070a5474/a/b/c"), "Read", + AccessRequest.class) .toCompletableFuture().join(); - assertEquals(1, grants.size()); + assertEquals(1, requests.size()); } @Test void testQueryInvalidAuth() { final CompletionException err = assertThrows(CompletionException.class, - agClient.query(URI.create("SolidAccessGrant"), null, null, null).toCompletableFuture()::join); + agClient.query(URI.create("SolidAccessGrant"), (URI) null, (URI) null, null) + .toCompletableFuture()::join); assertTrue(err.getCause() instanceof AccessGrantException); } diff --git a/access-grant/src/test/java/com/inrupt/client/accessgrant/AccessGrantSessionTest.java b/access-grant/src/test/java/com/inrupt/client/accessgrant/AccessGrantSessionTest.java index 64211810cec..199ba0bcb55 100644 --- a/access-grant/src/test/java/com/inrupt/client/accessgrant/AccessGrantSessionTest.java +++ b/access-grant/src/test/java/com/inrupt/client/accessgrant/AccessGrantSessionTest.java @@ -93,7 +93,7 @@ void testAccessGrantSession() throws IOException { assertFalse(grant.getResources().isEmpty()); for (final URI uri : grant.getResources()) { final String encoded = Base64.getUrlEncoder().withoutPadding() - .encodeToString(grant.getRawGrant().getBytes(UTF_8)); + .encodeToString(grant.serialize().getBytes(UTF_8)); assertEquals(Optional.of(encoded), session.getCredential(AccessGrantSession.VERIFIABLE_CREDENTIAL, uri).map(Credential::getToken)); final URI child = URIBuilder.newBuilder(uri).path("a").path("b").build(); diff --git a/access-grant/src/test/java/com/inrupt/client/accessgrant/AccessGrantTest.java b/access-grant/src/test/java/com/inrupt/client/accessgrant/AccessGrantTest.java index 0a5334849f5..242b0478f5d 100644 --- a/access-grant/src/test/java/com/inrupt/client/accessgrant/AccessGrantTest.java +++ b/access-grant/src/test/java/com/inrupt/client/accessgrant/AccessGrantTest.java @@ -47,7 +47,7 @@ class AccessGrantTest { @Test void testReadAccessGrant() throws IOException { try (final InputStream resource = AccessGrantTest.class.getResourceAsStream("/access_grant1.json")) { - final AccessGrant grant = AccessGrant.ofAccessGrant(resource); + final AccessGrant grant = AccessGrant.of(resource); assertEquals(Collections.singleton("Read"), grant.getModes()); assertEquals(URI.create("https://accessgrant.example"), grant.getIssuer()); final Set expectedTypes = new HashSet<>(); @@ -78,7 +78,7 @@ void testReadAccessGrant() throws IOException { @Test void testReadAccessGrantSingletons() throws IOException { try (final InputStream resource = AccessGrantTest.class.getResourceAsStream("/access_grant2.json")) { - final AccessGrant grant = AccessGrant.ofAccessGrant(resource); + final AccessGrant grant = AccessGrant.of(resource); assertEquals(Collections.singleton("Read"), grant.getModes()); assertEquals(URI.create("https://accessgrant.example"), grant.getIssuer()); final Set expectedTypes = new HashSet<>(); @@ -104,9 +104,10 @@ void testReadAccessGrantSingletons() throws IOException { void testRawAccessGrant() throws IOException { try (final InputStream resource = AccessGrantTest.class.getResourceAsStream("/access_grant1.json")) { final String raw = IOUtils.toString(resource, UTF_8); - final AccessGrant grant = AccessGrant.ofAccessGrant(raw); + final AccessGrant grant = AccessGrant.of(raw); - assertEquals(raw, grant.getRawGrant()); + assertEquals(raw, grant.serialize()); + assertEquals(grant.serialize(), grant.getRawGrant()); } } @@ -165,84 +166,84 @@ void testRevocationList2020CredentialNotString() throws IOException { @Test void testBareAccessGrant() throws IOException { try (final InputStream resource = AccessGrantTest.class.getResourceAsStream("/invalid_access_grant1.json")) { - assertThrows(IllegalArgumentException.class, () -> AccessGrant.ofAccessGrant(resource)); + assertThrows(IllegalArgumentException.class, () -> AccessGrant.of(resource)); } } @Test void testAccessGrantMissingIssuer() throws IOException { try (final InputStream resource = AccessGrantTest.class.getResourceAsStream("/invalid_access_grant2.json")) { - assertThrows(IllegalArgumentException.class, () -> AccessGrant.ofAccessGrant(resource)); + assertThrows(IllegalArgumentException.class, () -> AccessGrant.of(resource)); } } @Test void testAccessGrantMissingCredentialSubject() throws IOException { try (final InputStream resource = AccessGrantTest.class.getResourceAsStream("/invalid_access_grant3.json")) { - assertThrows(IllegalArgumentException.class, () -> AccessGrant.ofAccessGrant(resource)); + assertThrows(IllegalArgumentException.class, () -> AccessGrant.of(resource)); } } @Test void testAccessGrantMissingPresentationType() throws IOException { try (final InputStream resource = AccessGrantTest.class.getResourceAsStream("/invalid_access_grant4.json")) { - assertThrows(IllegalArgumentException.class, () -> AccessGrant.ofAccessGrant(resource)); + assertThrows(IllegalArgumentException.class, () -> AccessGrant.of(resource)); } } @Test void testIrrelevantCredential() throws IOException { try (final InputStream resource = AccessGrantTest.class.getResourceAsStream("/invalid_access_grant5.json")) { - assertThrows(IllegalArgumentException.class, () -> AccessGrant.ofAccessGrant(resource)); + assertThrows(IllegalArgumentException.class, () -> AccessGrant.of(resource)); } } @Test void testAccessGrantMissingId() throws IOException { try (final InputStream resource = AccessGrantTest.class.getResourceAsStream("/invalid_access_grant6.json")) { - assertThrows(IllegalArgumentException.class, () -> AccessGrant.ofAccessGrant(resource)); + assertThrows(IllegalArgumentException.class, () -> AccessGrant.of(resource)); } } @Test void testAccessGrantMissingCredentialSubjectId() throws IOException { try (final InputStream resource = AccessGrantTest.class.getResourceAsStream("/invalid_access_grant7.json")) { - assertThrows(IllegalArgumentException.class, () -> AccessGrant.ofAccessGrant(resource)); + assertThrows(IllegalArgumentException.class, () -> AccessGrant.of(resource)); } } @Test void testAccessGrantMissingConsent() throws IOException { try (final InputStream resource = AccessGrantTest.class.getResourceAsStream("/invalid_access_grant8.json")) { - assertThrows(IllegalArgumentException.class, () -> AccessGrant.ofAccessGrant(resource)); + assertThrows(IllegalArgumentException.class, () -> AccessGrant.of(resource)); } } @Test void testAccessGrantTypeComplexListStructure() throws IOException { try (final InputStream resource = AccessGrantTest.class.getResourceAsStream("/invalid_access_grant9.json")) { - assertThrows(IllegalArgumentException.class, () -> AccessGrant.ofAccessGrant(resource)); + assertThrows(IllegalArgumentException.class, () -> AccessGrant.of(resource)); } } @Test void testAccessGrantTypeObjectStructure() throws IOException { try (final InputStream resource = AccessGrantTest.class.getResourceAsStream("/invalid_access_grant10.json")) { - assertThrows(IllegalArgumentException.class, () -> AccessGrant.ofAccessGrant(resource)); + assertThrows(IllegalArgumentException.class, () -> AccessGrant.of(resource)); } } @Test void testAccessGrantInvalidStatusNoId() throws IOException { try (final InputStream resource = AccessGrantTest.class.getResourceAsStream("/invalid_access_grant12.json")) { - assertThrows(IllegalArgumentException.class, () -> AccessGrant.ofAccessGrant(resource)); + assertThrows(IllegalArgumentException.class, () -> AccessGrant.of(resource)); } } @Test void testAccessGrantInvalidStatusBadCredential() throws IOException { try (final InputStream resource = AccessGrantTest.class.getResourceAsStream("/invalid_access_grant13.json")) { - assertThrows(IllegalArgumentException.class, () -> AccessGrant.ofAccessGrant(resource)); + assertThrows(IllegalArgumentException.class, () -> AccessGrant.of(resource)); } } @@ -250,11 +251,11 @@ void testAccessGrantInvalidStatusBadCredential() throws IOException { void testInvalidStream() throws IOException { final InputStream resource = AccessGrantTest.class.getResourceAsStream("/access_grant2.json"); resource.close(); - assertThrows(IllegalArgumentException.class, () -> AccessGrant.ofAccessGrant(resource)); + assertThrows(IllegalArgumentException.class, () -> AccessGrant.of(resource)); } @Test void testInvalidString() throws IOException { - assertThrows(IllegalArgumentException.class, () -> AccessGrant.ofAccessGrant("not json")); + assertThrows(IllegalArgumentException.class, () -> AccessGrant.of("not json")); } } 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 f3b554e25a8..0b7c4b84992 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 @@ -31,6 +31,7 @@ import com.inrupt.client.accessgrant.AccessGrant; import com.inrupt.client.accessgrant.AccessGrantClient; import com.inrupt.client.accessgrant.AccessGrantSession; +import com.inrupt.client.accessgrant.AccessRequest; import com.inrupt.client.auth.Credential; import com.inrupt.client.auth.Session; import com.inrupt.client.openid.OpenIdException; @@ -224,14 +225,17 @@ void accessGrantIssuanceLifecycleTest(final Session session) { final Set modes = new HashSet<>(Arrays.asList(GRANT_MODE_READ)); final Instant expiration = Instant.parse(GRANT_EXPIRATION); - final AccessGrant grant = accessGrantClient.issue(ACCESS_REQUEST, URI.create(webidUrl), + final AccessRequest request = accessGrantClient.requestAccess(URI.create(webidUrl), new HashSet<>(Arrays.asList(sharedTextFileURI)), modes, PURPOSES, expiration) .toCompletableFuture().join(); + final AccessGrant grant = accessGrantClient.grantAccess(request) + .toCompletableFuture().join(); //2. call verify endpoint to verify grant final URI uri = URIBuilder.newBuilder(URI.create(VC_PROVIDER)).path(grant.getIdentifier().toString()).build(); - final AccessGrant grantFromVcProvider = accessGrantClient.fetch(uri).toCompletableFuture().join(); + final AccessGrant grantFromVcProvider = accessGrantClient.fetch(uri, AccessGrant.class) + .toCompletableFuture().join(); assertEquals(grant.getPurpose(), grantFromVcProvider.getPurpose()); //unauthorized request test @@ -260,7 +264,6 @@ void accessGrantIssuanceLifecycleTest(final Session session) { //6. call verify endpoint to check the grant is not valid - assertDoesNotThrow(accessGrantClient.delete(grant).toCompletableFuture()::join); } @ParameterizedTest @@ -273,9 +276,11 @@ void accessGrantWithRequestOverridesTest(final Session session) { final Set modes = new HashSet<>(Arrays.asList(GRANT_MODE_READ, GRANT_MODE_APPEND)); final Instant expiration = Instant.now().plus(90, ChronoUnit.DAYS); - final AccessGrant grant = accessGrantClient.issue(ACCESS_GRANT, URI.create(webidUrl), + final AccessRequest request = accessGrantClient.requestAccess(URI.create(webidUrl), new HashSet<>(Arrays.asList(sharedTextFileURI)), modes, PURPOSES, expiration) .toCompletableFuture().join(); + final AccessGrant grant = accessGrantClient.grantAccess(request) + .toCompletableFuture().join(); //2. call verify endpoint to verify grant @@ -292,9 +297,11 @@ void accessGrantNonRecursiveTest(final Session session) { final Set modes = new HashSet<>(Arrays.asList(GRANT_MODE_READ)); final Instant expiration = Instant.parse(GRANT_EXPIRATION); - final AccessGrant grant = accessGrantClient.issue(ACCESS_REQUEST, URI.create(webidUrl), + final AccessRequest request = accessGrantClient.requestAccess(URI.create(webidUrl), new HashSet<>(Arrays.asList(sharedTextFileURI)), modes, PURPOSES, expiration) .toCompletableFuture().join(); + final AccessGrant grant = accessGrantClient.grantAccess(request) + .toCompletableFuture().join(); //Steps //1. call verify endpoint to verify grant @@ -311,9 +318,8 @@ void accessGrantQueryByRequestorTest(final Session session) { final AccessGrantClient accessGrantClient = new AccessGrantClient(URI.create(VC_PROVIDER)).session(session); //query for all grants issued by the user - final List grants = accessGrantClient.query( - ACCESS_REQUEST, URI.create(webidUrl), - sharedTextFileURI, GRANT_MODE_READ) + final List grants = accessGrantClient.query(URI.create(webidUrl), + sharedTextFileURI, GRANT_MODE_READ, AccessRequest.class) .toCompletableFuture().join(); // result is 4 because we retrieve the grants for each path // sharedTextFileURI = @@ -321,9 +327,8 @@ void accessGrantQueryByRequestorTest(final Session session) { assertEquals(1, grants.size()); //query for all grants issued by a random user - final List randomGrants = accessGrantClient.query( - ACCESS_REQUEST, URI.create("https://someuser.test"), - sharedTextFileURI, GRANT_MODE_READ) + final List randomGrants = accessGrantClient.query(URI.create("https://someuser.test"), + sharedTextFileURI, GRANT_MODE_READ, AccessRequest.class) .toCompletableFuture().join(); assertEquals(0, randomGrants.size()); } @@ -337,16 +342,14 @@ void accessGrantQueryByResourceTest(final Session session) { final AccessGrantClient accessGrantClient = new AccessGrantClient(URI.create(VC_PROVIDER)).session(session); //query for all grants of a dedicated resource - final List grants = accessGrantClient.query( - ACCESS_REQUEST, URI.create(webidUrl), - sharedTextFileURI, GRANT_MODE_READ) + final List requests = accessGrantClient.query(URI.create(webidUrl), + sharedTextFileURI, GRANT_MODE_READ, AccessRequest.class) .toCompletableFuture().join(); - assertEquals(1, grants.size()); + assertEquals(1, requests.size()); //query for all grants of a random resource - final List randomGrants = accessGrantClient.query( - ACCESS_REQUEST, URI.create(webidUrl), - URI.create("https://somerandom.test"), GRANT_MODE_READ) + final List randomGrants = accessGrantClient.query(URI.create(webidUrl), + URI.create("https://somerandom.test"), GRANT_MODE_READ, AccessRequest.class) .toCompletableFuture().join(); assertEquals(0, randomGrants.size()); } @@ -360,16 +363,14 @@ void accessGrantQueryByPurposeTest(final Session session) { final AccessGrantClient accessGrantClient = new AccessGrantClient(URI.create(VC_PROVIDER)).session(session); //query for all grants of existent purposes - final List grants = accessGrantClient.query( - ACCESS_REQUEST, URI.create(webidUrl), - sharedTextFileURI, GRANT_MODE_READ) + final List requests = accessGrantClient.query(URI.create(webidUrl), + sharedTextFileURI, GRANT_MODE_READ, AccessRequest.class) .toCompletableFuture().join(); - assertEquals(1, grants.size()); + assertEquals(1, requests.size()); //query for all grants of dedicated purpose combinations - final List randomGrants = accessGrantClient.query( - ACCESS_REQUEST, URI.create(webidUrl), - sharedTextFileURI, GRANT_MODE_WRITE) + final List randomGrants = accessGrantClient.query(URI.create(webidUrl), + sharedTextFileURI, GRANT_MODE_WRITE, AccessRequest.class) .toCompletableFuture().join(); assertEquals(0, randomGrants.size()); //our grant is actually a Read } @@ -392,9 +393,11 @@ void accessGrantGetRdfTest(final Session session) { final Set modes = new HashSet<>(Arrays.asList(GRANT_MODE_READ, GRANT_MODE_WRITE, GRANT_MODE_APPEND)); final Instant expiration = Instant.parse(GRANT_EXPIRATION); - final AccessGrant grant = accessGrantClient.issue(ACCESS_REQUEST, URI.create(webidUrl), + final AccessRequest request = accessGrantClient.requestAccess(URI.create(webidUrl), new HashSet<>(Arrays.asList(testRDFresourceURI)), modes, PURPOSES, expiration) .toCompletableFuture().join(); + final AccessGrant grant = accessGrantClient.grantAccess(request) + .toCompletableFuture().join(); final Session newSession = AccessGrantSession.ofAccessGrant(session, grant); final SolidSyncClient newClient = SolidSyncClient.getClientBuilder() .build().session(newSession); @@ -405,7 +408,6 @@ void accessGrantGetRdfTest(final Session session) { newClient.delete(testRDFresourceURI); assertDoesNotThrow(accessGrantClient.revoke(grant).toCompletableFuture()::join); - assertDoesNotThrow(accessGrantClient.delete(grant).toCompletableFuture()::join); } @ParameterizedTest @@ -425,9 +427,11 @@ void accessGrantSetRdfTest(final Session session) { final Set modes = new HashSet<>(Arrays.asList( GRANT_MODE_READ, GRANT_MODE_WRITE, GRANT_MODE_APPEND)); final Instant expiration = Instant.parse(GRANT_EXPIRATION); - final AccessGrant grant = accessGrantClient.issue(ACCESS_REQUEST, URI.create(webidUrl), + final AccessRequest request = accessGrantClient.requestAccess(URI.create(webidUrl), new HashSet<>(Arrays.asList(testRDFresourceURI)), modes, PURPOSES, expiration) .toCompletableFuture().join(); + final AccessGrant grant = accessGrantClient.grantAccess(request) + .toCompletableFuture().join(); final Session newSession = AccessGrantSession.ofAccessGrant(session, grant); final SolidSyncClient authClient = SolidSyncClient.getClientBuilder() .build().session(newSession); @@ -465,9 +469,11 @@ void accessGrantCreateRdfTest(final Session session) { final Set modes = new HashSet<>(Arrays.asList(GRANT_MODE_READ, GRANT_MODE_WRITE, GRANT_MODE_APPEND)); final Instant expiration = Instant.parse(GRANT_EXPIRATION); - final AccessGrant grant = accessGrantClient.issue(ACCESS_REQUEST, URI.create(webidUrl), + final AccessRequest request = accessGrantClient.requestAccess(URI.create(webidUrl), new HashSet<>(Arrays.asList(newTestFileURI)), modes, PURPOSES, expiration) .toCompletableFuture().join(); + final AccessGrant grant = accessGrantClient.grantAccess(request) + .toCompletableFuture().join(); final Session newSession = AccessGrantSession.ofAccessGrant(session, grant); final SolidSyncClient authClient = SolidSyncClient.getClientBuilder() @@ -479,7 +485,6 @@ void accessGrantCreateRdfTest(final Session session) { authClient.delete(newTestFileURI); assertDoesNotThrow(accessGrantClient.revoke(grant).toCompletableFuture()::join); - assertDoesNotThrow(accessGrantClient.delete(grant).toCompletableFuture()::join); } @ParameterizedTest @@ -505,9 +510,11 @@ void accessGrantGetNonRdfTest(final Session session) throws IOException { final Set modes = new HashSet<>(Arrays.asList(GRANT_MODE_READ, GRANT_MODE_WRITE, GRANT_MODE_APPEND)); final Instant expiration = Instant.parse(GRANT_EXPIRATION); - final AccessGrant grant = accessGrantClient.issue(ACCESS_REQUEST, URI.create(webidUrl), + final AccessRequest request = accessGrantClient.requestAccess(URI.create(webidUrl), new HashSet<>(Arrays.asList(newTestFileURI)), modes, PURPOSES, expiration) .toCompletableFuture().join(); + final AccessGrant grant = accessGrantClient.grantAccess(request) + .toCompletableFuture().join(); final Session newSession = AccessGrantSession.ofAccessGrant(session, grant); final SolidSyncClient authClient = SolidSyncClient.getClientBuilder() .build().session(newSession); @@ -549,9 +556,11 @@ void accessGrantSetNonRdfTest(final Session session) throws IOException { final Set modes = new HashSet<>(Arrays.asList(GRANT_MODE_READ, GRANT_MODE_WRITE, GRANT_MODE_APPEND)); final Instant expiration = Instant.parse(GRANT_EXPIRATION); - final AccessGrant grant = accessGrantClient.issue(ACCESS_REQUEST, URI.create(webidUrl), + final AccessRequest request = accessGrantClient.requestAccess(URI.create(webidUrl), new HashSet<>(Arrays.asList(newTestFileURI)), modes, PURPOSES, expiration) .toCompletableFuture().join(); + final AccessGrant grant = accessGrantClient.grantAccess(request) + .toCompletableFuture().join(); final Session newSession = AccessGrantSession.ofAccessGrant(session, grant); final SolidSyncClient authClient = SolidSyncClient.getClientBuilder() .build().session(newSession); @@ -592,12 +601,13 @@ void accessGrantCreateNonRdfTest(final Session session) throws IOException { final Set modes = new HashSet<>(Arrays.asList(GRANT_MODE_READ, GRANT_MODE_WRITE, GRANT_MODE_APPEND)); final Instant expiration = Instant.parse(GRANT_EXPIRATION); - final AccessGrant grant = accessGrantClient.issue(ACCESS_REQUEST, URI.create(webidUrl), + final AccessRequest request = accessGrantClient.requestAccess(URI.create(webidUrl), new HashSet<>(Arrays.asList(newTestFileURI)), modes, PURPOSES, expiration) .toCompletableFuture().join(); + final AccessGrant grant = accessGrantClient.grantAccess(request) + .toCompletableFuture().join(); final Session newSession = AccessGrantSession.ofAccessGrant(session, grant); - final SolidSyncClient authClient = SolidSyncClient.getClientBuilder() - .build().session(newSession); + final SolidSyncClient authClient = SolidSyncClient.getClient().session(newSession); try (final InputStream is = new ByteArrayInputStream( StandardCharsets.UTF_8.encode("Test test test text").array())) { @@ -608,7 +618,6 @@ void accessGrantCreateNonRdfTest(final Session session) throws IOException { authClient.delete(newTestFileURI); assertDoesNotThrow(accessGrantClient.revoke(grant).toCompletableFuture()::join); - assertDoesNotThrow(accessGrantClient.delete(grant).toCompletableFuture()::join); } private static void prepareACPofResource(final SolidSyncClient authClient, final URI resourceURI) { 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 04b314c76ee..d450b10a0de 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 @@ -59,11 +59,27 @@ private void setupMocks() { .withBody(getResource("/vc-grant.json", wireMockServer.baseUrl(), this.webId, this.sharedFile)))); + wireMockServer.stubFor(get(urlPathMatching(".+:(\\d*)/vc-request")) + .willReturn(aResponse() + .withStatus(Utils.SUCCESS) + .withHeader(Utils.CONTENT_TYPE, Utils.APPLICATION_JSON) + .withBody(getResource("/vc-request.json", wireMockServer.baseUrl(), + this.webId, this.sharedFile)))); + wireMockServer.stubFor(delete(urlPathMatching("/vc-grant")) .willReturn(aResponse() .withStatus(204))); wireMockServer.stubFor(post(urlEqualTo("/issue")) + .withRequestBody(containing("hasConsent")) + .willReturn(aResponse() + .withStatus(Utils.SUCCESS) + .withHeader(Utils.CONTENT_TYPE, Utils.APPLICATION_JSON) + .withBody(getResource("/vc-request.json", wireMockServer.baseUrl(), + this.webId, this.sharedFile)))); + + wireMockServer.stubFor(post(urlEqualTo("/issue")) + .withRequestBody(containing("providedConsent")) .willReturn(aResponse() .withStatus(Utils.SUCCESS) .withHeader(Utils.CONTENT_TYPE, Utils.APPLICATION_JSON) diff --git a/integration/base/src/main/resources/vc-grant.json b/integration/base/src/main/resources/vc-grant.json index e76e31d78b3..bdedc777dc1 100644 --- a/integration/base/src/main/resources/vc-grant.json +++ b/integration/base/src/main/resources/vc-grant.json @@ -5,7 +5,7 @@ "https://w3id.org/vc-revocation-list-2020/v1", "https://schema.inrupt.com/credentials/v1.jsonld"], "id":"{{baseUrl}}/vc-grant", - "type":["VerifiableCredential","SolidAccessRequest"], + "type":["VerifiableCredential","SolidAccessGrant"], "issuer":"{{baseUrl}}", "expirationDate":"2023-04-03T12:00:00Z", "issuanceDate":"2022-08-25T20:34:05.153Z", @@ -16,11 +16,11 @@ "type":"RevocationList2020Status"}, "credentialSubject":{ "id":"{{webId}}", - "hasConsent":{ + "providedConsent":{ "mode":["Read"], - "hasStatus":"https://w3id.org/GConsent#ConsentStatusRequested", - "isConsentForDataSubject":"{{webId}}", - "forPurpose":["https://some.purpose/not-a-nefarious-one/i-promise", "https://some.other.purpose/"], + "hasStatus":"https://w3id.org/GConsent#ConsentStatusExplicitlyGiven", + "isProvidedToPerson":"{{webId}}", + "forPurpose":["https://purpose.example/1", "https://purpose.example/2"], "forPersonalData":["{{sharedFile}}"]}}, "proof":{ "created":"2022-08-25T20:34:05.236Z", @@ -28,4 +28,4 @@ "proofValue":"nIeQF44XVik7onnAbdkbp8xxJ2C8JoTw6-VtCkAzxuWYRFsSfYpft5MuAJaivyeKDmaK82Lj_YsME2xgL2WIBQ", "type":"Ed25519Signature2020", "verificationMethod":"https://accessgrant.example/key/1e332728-4af5-46e4-a5db-4f7b89e3f378"} -} \ No newline at end of file +} diff --git a/integration/base/src/main/resources/vc-request.json b/integration/base/src/main/resources/vc-request.json new file mode 100644 index 00000000000..041726fa9da --- /dev/null +++ b/integration/base/src/main/resources/vc-request.json @@ -0,0 +1,31 @@ +{ + "@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/v1.jsonld"], + "id":"{{baseUrl}}/vc-request", + "type":["VerifiableCredential","SolidAccessRequest"], + "issuer":"{{baseUrl}}", + "expirationDate":"2023-04-03T12: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":"{{webId}}", + "hasConsent":{ + "mode":["Read"], + "hasStatus":"https://w3id.org/GConsent#ConsentStatusRequested", + "isConsentForDataSubject":"{{webId}}", + "forPurpose":["https://some.purpose/not-a-nefarious-one/i-promise", "https://some.other.purpose/"], + "forPersonalData":["{{sharedFile}}"]}}, + "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"} +} From a48c8e654e7ad0ee5408de19f1b7d37ce4a9164a Mon Sep 17 00:00:00 2001 From: Aaron Coburn Date: Fri, 19 May 2023 06:51:57 -0400 Subject: [PATCH 2/3] Code review --- .../inrupt/client/accessgrant/AccessGrant.java | 18 +++++++++--------- .../client/accessgrant/AccessRequest.java | 18 +++++++++--------- .../accessgrant/AccessGrantClientTest.java | 1 + .../client/accessgrant/AccessGrantTest.java | 4 ++++ 4 files changed, 23 insertions(+), 18 deletions(-) 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 c6a658d5c96..9701db44d45 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 @@ -51,15 +51,15 @@ public class AccessGrant implements AccessCredential { private static final Set supportedTypes = getSupportedTypes(); private static final JsonService jsonService = ServiceProvider.getJsonService(); - private final String rawGrant; + private final String credential; private final URI issuer; private final URI identifier; private final Set types; private final Set purposes; private final Set modes; private final Set resources; - private final URI grantee; - private final URI grantor; + private final URI recipient; + private final URI creator; private final Instant expiration; private final Status status; @@ -78,7 +78,7 @@ protected AccessGrant(final String grant) throws IOException { new IllegalArgumentException("Invalid Access Grant: missing verifiable credential")); if (asSet(data.get(TYPE)).orElseGet(Collections::emptySet).contains("VerifiablePresentation")) { - this.rawGrant = grant; + this.credential = grant; this.issuer = asUri(vc.get("issuer")).orElseThrow(() -> new IllegalArgumentException("Missing or invalid issuer field")); this.identifier = asUri(vc.get("id")).orElseThrow(() -> @@ -90,7 +90,7 @@ protected AccessGrant(final String grant) throws IOException { final Map subject = asMap(vc.get("credentialSubject")).orElseThrow(() -> new IllegalArgumentException("Missing or invalid credentialSubject field")); - this.grantor = asUri(subject.get("id")).orElseThrow(() -> + this.creator = asUri(subject.get("id")).orElseThrow(() -> new IllegalArgumentException("Missing or invalid credentialSubject.id field")); // V1 Access Grant, using gConsent @@ -102,7 +102,7 @@ protected AccessGrant(final String grant) throws IOException { final Optional controller = asUri(consent.get("isProvidedToController")); final Optional other = asUri(consent.get("isProvidedTo")); - this.grantee = person.orElseGet(() -> controller.orElseGet(() -> other.orElse(null))); + this.recipient = person.orElseGet(() -> controller.orElseGet(() -> other.orElse(null))); this.modes = asSet(consent.get("mode")).orElseGet(Collections::emptySet); this.resources = asSet(consent.get("forPersonalData")).orElseGet(Collections::emptySet) .stream().map(URI::create).collect(Collectors.toSet()); @@ -274,17 +274,17 @@ public URI getGrantor() { @Override public URI getCreator() { - return grantor; + return creator; } @Override public Optional getRecipient() { - return Optional.ofNullable(grantee); + return Optional.ofNullable(recipient); } @Override public String serialize() { - return rawGrant; + return credential; } /** diff --git a/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessRequest.java b/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessRequest.java index 762fdea7608..8d8103d80e8 100644 --- a/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessRequest.java +++ b/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessRequest.java @@ -51,15 +51,15 @@ public class AccessRequest implements AccessCredential { private static final JsonService jsonService = ServiceProvider.getJsonService(); private static final Set supportedTypes = getSupportedTypes(); - private final String rawGrant; + private final String credential; private final URI issuer; private final URI identifier; private final Set types; private final Set purposes; private final Set modes; private final Set resources; - private final URI grantee; - private final URI grantor; + private final URI recipient; + private final URI creator; private final Instant expiration; private final Status status; @@ -78,7 +78,7 @@ protected AccessRequest(final String grant) throws IOException { new IllegalArgumentException("Invalid Access Grant: missing verifiable credential")); if (asSet(data.get(TYPE)).orElseGet(Collections::emptySet).contains("VerifiablePresentation")) { - this.rawGrant = grant; + this.credential = grant; this.issuer = asUri(vc.get("issuer")).orElseThrow(() -> new IllegalArgumentException("Missing or invalid issuer field")); this.identifier = asUri(vc.get("id")).orElseThrow(() -> @@ -90,7 +90,7 @@ protected AccessRequest(final String grant) throws IOException { final Map subject = asMap(vc.get("credentialSubject")).orElseThrow(() -> new IllegalArgumentException("Missing or invalid credentialSubject field")); - this.grantor = asUri(subject.get("id")).orElseThrow(() -> + this.creator = asUri(subject.get("id")).orElseThrow(() -> new IllegalArgumentException("Missing or invalid credentialSubject.id field")); // V1 Access Request, using gConsent @@ -99,7 +99,7 @@ protected AccessRequest(final String grant) throws IOException { new IllegalArgumentException("Invalid Access Request: missing consent clause")); final Optional dataSubject = asUri(consent.get("isConsentForDataSubject")); - this.grantee = dataSubject.orElse(null); + this.recipient = dataSubject.orElse(null); this.modes = asSet(consent.get("mode")).orElseGet(Collections::emptySet); this.resources = asSet(consent.get("forPersonalData")).orElseGet(Collections::emptySet) .stream().map(URI::create).collect(Collectors.toSet()); @@ -185,17 +185,17 @@ public Set getResources() { @Override public URI getCreator() { - return grantor; + return creator; } @Override public Optional getRecipient() { - return Optional.ofNullable(grantee); + return Optional.ofNullable(recipient); } @Override public String serialize() { - return rawGrant; + return credential; } static Set getSupportedTypes() { 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 91f75c190c5..9baab0c04d2 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 @@ -218,6 +218,7 @@ void testIssueGrant() { assertTrue(grant.getTypes().contains("SolidAccessGrant")); assertEquals(Optional.of(agent), grant.getGrantee()); + assertEquals(Optional.of(agent), grant.getRecipient()); assertEquals(modes, grant.getModes()); assertEquals(expiration, grant.getExpiration()); assertEquals(baseUri, grant.getIssuer()); diff --git a/access-grant/src/test/java/com/inrupt/client/accessgrant/AccessGrantTest.java b/access-grant/src/test/java/com/inrupt/client/accessgrant/AccessGrantTest.java index 242b0478f5d..598a8d665bc 100644 --- a/access-grant/src/test/java/com/inrupt/client/accessgrant/AccessGrantTest.java +++ b/access-grant/src/test/java/com/inrupt/client/accessgrant/AccessGrantTest.java @@ -63,7 +63,9 @@ void testReadAccessGrant() throws IOException { URI.create("https://storage.example/e973cc3d-5c28-4a10-98c5-e40079289358/")), grant.getResources()); assertEquals(URI.create("https://id.example/grantor"), grant.getGrantor()); + assertEquals(URI.create("https://id.example/grantor"), grant.getCreator()); assertEquals(Optional.of(URI.create("https://id.example/grantee")), grant.getGrantee()); + assertEquals(Optional.of(URI.create("https://id.example/grantee")), grant.getRecipient()); final Optional status = grant.getStatus(); assertTrue(status.isPresent()); status.ifPresent(s -> { @@ -94,7 +96,9 @@ void testReadAccessGrantSingletons() throws IOException { URI.create("https://storage.example/e973cc3d-5c28-4a10-98c5-e40079289358/")), grant.getResources()); assertEquals(URI.create("https://id.example/grantor"), grant.getGrantor()); + assertEquals(URI.create("https://id.example/grantor"), grant.getCreator()); assertEquals(Optional.of(URI.create("https://id.example/grantee")), grant.getGrantee()); + assertEquals(Optional.of(URI.create("https://id.example/grantee")), grant.getRecipient()); final Optional status = grant.getStatus(); assertFalse(status.isPresent()); } From a44659c2c27f6a7cab8d5c2304500ff5c341e617 Mon Sep 17 00:00:00 2001 From: Aaron Coburn Date: Fri, 19 May 2023 08:26:26 -0400 Subject: [PATCH 3/3] Code review --- .../java/com/inrupt/client/accessgrant/AccessGrant.java | 9 +++++++-- .../com/inrupt/client/accessgrant/AccessRequest.java | 9 +++++++-- .../main/java/com/inrupt/client/accessgrant/Utils.java | 9 ++++++--- 3 files changed, 20 insertions(+), 7 deletions(-) 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 9701db44d45..f464073a2b4 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 @@ -34,6 +34,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -74,8 +75,12 @@ protected AccessGrant(final String grant) throws IOException { final Map data = jsonService.fromJson(in, new HashMap(){}.getClass().getGenericSuperclass()); - final Map vc = getCredentialFromPresentation(data, supportedTypes).orElseThrow(() -> - new IllegalArgumentException("Invalid Access Grant: missing verifiable credential")); + final List vcs = getCredentialsFromPresentation(data, supportedTypes); + if (vcs.size() != 1) { + throw new IllegalArgumentException( + "Invalid Access Grant: ambiguous number of verifiable credentials"); + } + final Map vc = vcs.get(0); if (asSet(data.get(TYPE)).orElseGet(Collections::emptySet).contains("VerifiablePresentation")) { this.credential = grant; diff --git a/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessRequest.java b/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessRequest.java index 8d8103d80e8..4647adf103a 100644 --- a/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessRequest.java +++ b/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessRequest.java @@ -34,6 +34,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -74,8 +75,12 @@ protected AccessRequest(final String grant) throws IOException { final Map data = jsonService.fromJson(in, new HashMap(){}.getClass().getGenericSuperclass()); - final Map vc = getCredentialFromPresentation(data, supportedTypes).orElseThrow(() -> - new IllegalArgumentException("Invalid Access Grant: missing verifiable credential")); + final List vcs = getCredentialsFromPresentation(data, supportedTypes); + if (vcs.size() != 1) { + throw new IllegalArgumentException( + "Invalid Access Request: ambiguous number of verifiable credentials"); + } + final Map vc = vcs.get(0); if (asSet(data.get(TYPE)).orElseGet(Collections::emptySet).contains("VerifiablePresentation")) { this.credential = grant; diff --git a/access-grant/src/main/java/com/inrupt/client/accessgrant/Utils.java b/access-grant/src/main/java/com/inrupt/client/accessgrant/Utils.java index 0d3ba8183af..e9887df7ba4 100644 --- a/access-grant/src/main/java/com/inrupt/client/accessgrant/Utils.java +++ b/access-grant/src/main/java/com/inrupt/client/accessgrant/Utils.java @@ -22,8 +22,10 @@ import java.net.URI; import java.time.Instant; +import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -94,20 +96,21 @@ public static Optional> asSet(final Object value) { return Optional.empty(); } - public static Optional getCredentialFromPresentation(final Map data, + public static List getCredentialsFromPresentation(final Map data, final Set supportedTypes) { + final List credentials = new ArrayList<>(); if (data.get("verifiableCredential") instanceof Collection) { for (final Object item : (Collection) data.get("verifiableCredential")) { if (item instanceof Map) { final Map vc = (Map) item; if (asSet(vc.get(TYPE)).filter(types -> types.stream().anyMatch(supportedTypes::contains)).isPresent()) { - return Optional.of(vc); + credentials.add(vc); } } } } - return Optional.empty(); + return credentials; }