From d0ae5f73f8f6a3def118ea52e40554437c3a8067 Mon Sep 17 00:00:00 2001 From: Aaron Coburn Date: Fri, 22 Nov 2024 04:44:37 -0600 Subject: [PATCH] JCL-474: Link access grants and denials to originating requests (#1716) --- .../client/accessgrant/AccessCredential.java | 25 +++++++++++++++++++ .../client/accessgrant/AccessDenial.java | 16 +++++++++++- .../client/accessgrant/AccessGrant.java | 16 +++++++++++- .../client/accessgrant/AccessGrantClient.java | 13 +++++++--- .../client/accessgrant/AccessDenialTest.java | 2 ++ .../client/accessgrant/AccessGrantTest.java | 2 ++ .../src/test/resources/access_denial1.json | 3 ++- .../src/test/resources/access_grant4.json | 3 ++- 8 files changed, 72 insertions(+), 8 deletions(-) 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 index d376ce5e040..a520fa6be22 100644 --- a/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessCredential.java +++ b/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessCredential.java @@ -282,6 +282,7 @@ public static class CredentialData { private final Set purposes; private final Set resources; private final URI recipient; + private final URI accessRequest; /** * Create a collection of user-managed credential data. @@ -293,10 +294,25 @@ public static class CredentialData { */ public CredentialData(final Set resources, final Set modes, final Set purposes, final URI recipient) { + this(resources, modes, purposes, recipient, null); + } + + /** + * Create a collection of user-managed credential data. + * + * @param resources the resources referenced by the credential + * @param modes the access modes defined by this credential + * @param purposes the purposes associated with this credential + * @param recipient the recipient for this credential, may be {@code null} + * @param accessRequest the access request identifier, may be {@code null} + */ + public CredentialData(final Set resources, final Set modes, + final Set purposes, final URI recipient, final URI accessRequest) { this.modes = Objects.requireNonNull(modes, "modes may not be null!"); this.purposes = Objects.requireNonNull(purposes, "purposes may not be null!"); this.resources = Objects.requireNonNull(resources, "resources may not be null!"); this.recipient = recipient; + this.accessRequest = accessRequest; } /** @@ -334,6 +350,15 @@ public Set getResources() { public URI getRecipient() { return recipient; } + + /** + * Get the access request identifier associated with this credential. + * + * @return the access request identifier, may be {@code null} + */ + public URI getAccessRequest() { + return accessRequest; + } } static CredentialMetadata extractMetadata(final Map data) { diff --git a/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessDenial.java b/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessDenial.java index 939e93b1559..7259a9c792f 100644 --- a/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessDenial.java +++ b/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessDenial.java @@ -49,6 +49,8 @@ public class AccessDenial extends AccessCredential { private static final Set supportedTypes = getSupportedTypes(); private static final JsonService jsonService = ServiceProvider.getJsonService(); + private final URI accessRequest; + /** * Read a verifiable presentation as an AccessDenial. * @@ -60,6 +62,16 @@ public class AccessDenial extends AccessCredential { protected AccessDenial(final URI identifier, final String credential, final CredentialData data, final CredentialMetadata metadata) { super(identifier, credential, data, metadata); + this.accessRequest = data.getAccessRequest(); + } + + /** + * Get the corresponding access request identifier. + * + * @return the access request identifier, may be {@code null} + */ + public URI getAccessRequest() { + return accessRequest; } /** @@ -127,12 +139,14 @@ static AccessDenial parse(final String serialization) throws IOException { final Optional other = asUri(consent.get("isProvidedTo")); final URI recipient = person.orElseGet(() -> controller.orElseGet(() -> other.orElse(null))); + final URI accessRequest = asUri(consent.get("request")).orElse(null); final Set modes = asSet(consent.get("mode")).orElseGet(Collections::emptySet); final Set resources = asSet(consent.get("forPersonalData")).orElseGet(Collections::emptySet) .stream().map(URI::create).collect(Collectors.toSet()); final Set purposes = asSet(consent.get("forPurpose")).orElseGet(Collections::emptySet) .stream().flatMap(AccessCredential::filterUris).collect(Collectors.toSet()); - final CredentialData credentialData = new CredentialData(resources, modes, purposes, recipient); + final CredentialData credentialData = new CredentialData(resources, modes, purposes, recipient, + accessRequest); return new AccessDenial(identifier, serialization, credentialData, credentialMetadata); } else { 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 9728d817535..101dea1c89e 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 @@ -49,6 +49,8 @@ public class AccessGrant extends AccessCredential { private static final Set supportedTypes = getSupportedTypes(); private static final JsonService jsonService = ServiceProvider.getJsonService(); + private final URI accessRequest; + /** * Read a verifiable presentation as an AccessGrant. * @@ -60,6 +62,16 @@ public class AccessGrant extends AccessCredential { protected AccessGrant(final URI identifier, final String credential, final CredentialData data, final CredentialMetadata metadata) { super(identifier, credential, data, metadata); + this.accessRequest = data.getAccessRequest(); + } + + /** + * Get the corresponding access request identifier. + * + * @return the access request identifier, may be {@code null} + */ + public URI getAccessRequest() { + return accessRequest; } /** @@ -126,12 +138,14 @@ static AccessGrant parse(final String serialization) throws IOException { final Optional other = asUri(consent.get("isProvidedTo")); final URI recipient = person.orElseGet(() -> controller.orElseGet(() -> other.orElse(null))); + final URI accessRequest = asUri(consent.get("request")).orElse(null); final Set modes = asSet(consent.get("mode")).orElseGet(Collections::emptySet); final Set resources = asSet(consent.get("forPersonalData")).orElseGet(Collections::emptySet) .stream().map(URI::create).collect(Collectors.toSet()); final Set purposes = asSet(consent.get("forPurpose")).orElseGet(Collections::emptySet) .stream().flatMap(AccessCredential::filterUris).collect(Collectors.toSet()); - final CredentialData credentialData = new CredentialData(resources, modes, purposes, recipient); + final CredentialData credentialData = new CredentialData(resources, modes, purposes, recipient, + accessRequest); return new AccessGrant(identifier, serialization, credentialData, credentialMetadata); } else { 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 bff1bc407e8..bc144db2a65 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 @@ -106,6 +106,7 @@ public class AccessGrantClient { private static final String IS_CONSENT_FOR_DATA_SUBJECT = "isConsentForDataSubject"; private static final String FOR_PERSONAL_DATA = "forPersonalData"; private static final String HAS_STATUS = "hasStatus"; + private static final String REQUEST = "request"; private static final String MODE = "mode"; private static final String PROVIDED_CONSENT = "providedConsent"; private static final String FOR_PURPOSE = "forPurpose"; @@ -260,7 +261,8 @@ 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.getPurposes(), request.getExpiration(), request.getIssuedAt()); + request.getModes(), request.getPurposes(), request.getExpiration(), request.getIssuedAt(), + request.getIdentifier()); final Request req = Request.newBuilder(metadata.issueEndpoint) .header(CONTENT_TYPE, APPLICATION_JSON) .POST(Request.BodyPublishers.ofByteArray(serialize(data))).build(); @@ -292,7 +294,8 @@ public CompletionStage denyAccess(final AccessRequest request) { Objects.requireNonNull(request, "Request may not be null!"); return v1Metadata().thenCompose(metadata -> { final Map data = buildAccessDenialv1(request.getCreator(), request.getResources(), - request.getModes(), request.getPurposes(), request.getExpiration(), request.getIssuedAt()); + request.getModes(), request.getPurposes(), request.getExpiration(), request.getIssuedAt(), + request.getIdentifier()); final Request req = Request.newBuilder(metadata.issueEndpoint) .header(CONTENT_TYPE, APPLICATION_JSON) .POST(Request.BodyPublishers.ofByteArray(serialize(data))).build(); @@ -799,13 +802,14 @@ static URI asUri(final Object value) { } static Map buildAccessDenialv1(final URI agent, final Set resources, final Set modes, - final Set purposes, final Instant expiration, final Instant issuance) { + final Set purposes, final Instant expiration, final Instant issuance, final URI accessRequest) { Objects.requireNonNull(agent, "Access denial agent may not be null!"); final Map consent = new HashMap<>(); consent.put(MODE, modes); consent.put(HAS_STATUS, "https://w3id.org/GConsent#ConsentStatusRefused"); consent.put(FOR_PERSONAL_DATA, resources); consent.put(IS_PROVIDED_TO, agent); + consent.put(REQUEST, accessRequest); if (!purposes.isEmpty()) { consent.put(FOR_PURPOSE, purposes); } @@ -829,13 +833,14 @@ static Map buildAccessDenialv1(final URI agent, final Set r } static Map buildAccessGrantv1(final URI agent, final Set resources, final Set modes, - final Set purposes, final Instant expiration, final Instant issuance) { + final Set purposes, final Instant expiration, final Instant issuance, final URI accessRequest) { Objects.requireNonNull(agent, "Access grant agent may not be null!"); final Map consent = new HashMap<>(); consent.put(MODE, modes); consent.put(HAS_STATUS, "https://w3id.org/GConsent#ConsentStatusExplicitlyGiven"); consent.put(FOR_PERSONAL_DATA, resources); consent.put(IS_PROVIDED_TO, agent); + consent.put(REQUEST, accessRequest); if (!purposes.isEmpty()) { consent.put(FOR_PURPOSE, purposes); } diff --git a/access-grant/src/test/java/com/inrupt/client/accessgrant/AccessDenialTest.java b/access-grant/src/test/java/com/inrupt/client/accessgrant/AccessDenialTest.java index e8fd27536a4..ec2c85a8e27 100644 --- a/access-grant/src/test/java/com/inrupt/client/accessgrant/AccessDenialTest.java +++ b/access-grant/src/test/java/com/inrupt/client/accessgrant/AccessDenialTest.java @@ -50,6 +50,7 @@ void testReadAccessDenial() throws IOException { expectedTypes.add("VerifiableCredential"); expectedTypes.add("SolidAccessDenial"); assertEquals(expectedTypes, denial.getTypes()); + assertEquals(URI.create("https://accessrequest.test/5678"), denial.getAccessRequest()); assertEquals(Instant.parse("2022-08-27T12:00:00Z"), denial.getExpiration()); assertEquals(Instant.parse("2022-08-25T20:34:05.153Z"), denial.getIssuedAt()); assertEquals(URI.create("https://accessgrant.test/credential/fc2dbcd9-81d4-4fa4-8fd4-239e16dd83ab"), @@ -80,6 +81,7 @@ void testReadAccessDenialQualifiedName() throws IOException { final Set expectedTypes = new HashSet<>(); expectedTypes.add("VerifiableCredential"); expectedTypes.add("vc:SolidAccessDenial"); + assertNull(denial.getAccessRequest()); assertEquals(expectedTypes, denial.getTypes()); assertEquals(Instant.parse("2022-08-27T12:00:00Z"), denial.getExpiration()); assertEquals(Instant.parse("2022-08-25T20:34:05.153Z"), denial.getIssuedAt()); 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 23fd54d864a..966eaa1c0bb 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 @@ -52,6 +52,7 @@ void testReadAccessGrant() throws IOException { expectedTypes.add("VerifiableCredential"); expectedTypes.add("SolidAccessGrant"); assertEquals(expectedTypes, grant.getTypes()); + assertNull(grant.getAccessRequest()); assertEquals(Instant.parse("2022-08-27T12:00:00Z"), grant.getExpiration()); assertEquals(Instant.parse("2022-08-25T20:34:05.153Z"), grant.getIssuedAt()); assertEquals(URI.create("https://accessgrant.example/credential/5c6060ad-2f16-4bc1-b022-dffb46bff626"), @@ -83,6 +84,7 @@ void testReadAccessGrantQualifiedName() throws IOException { expectedTypes.add("VerifiableCredential"); expectedTypes.add("vc:SolidAccessGrant"); assertEquals(expectedTypes, grant.getTypes()); + assertEquals(URI.create("https://accessrequest.example/1234"), grant.getAccessRequest()); assertEquals(Instant.parse("2022-08-27T12:00:00Z"), grant.getExpiration()); assertEquals(Instant.parse("2022-08-25T20:34:05.153Z"), grant.getIssuedAt()); assertEquals(URI.create("https://accessgrant.example/credential/5c6060ad-2f16-4bc1-b022-dffb46bff626"), diff --git a/access-grant/src/test/resources/access_denial1.json b/access-grant/src/test/resources/access_denial1.json index 872e786481f..f7781507540 100644 --- a/access-grant/src/test/resources/access_denial1.json +++ b/access-grant/src/test/resources/access_denial1.json @@ -6,7 +6,7 @@ "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"], + "https://schema.inrupt.com/credentials/v2.jsonld"], "id":"https://accessgrant.test/credential/fc2dbcd9-81d4-4fa4-8fd4-239e16dd83ab", "type":["VerifiableCredential","SolidAccessDenial"], "issuer":"https://accessgrant.test", @@ -21,6 +21,7 @@ "id":"https://id.test/grantor", "providedConsent":{ "mode":["Read"], + "request": "https://accessrequest.test/5678", "hasStatus":"https://w3id.org/GConsent#ConsentStatusRefused", "isProvidedTo":"https://id.test/grantee", "forPurpose":["https://purpose.test/Purpose1"], diff --git a/access-grant/src/test/resources/access_grant4.json b/access-grant/src/test/resources/access_grant4.json index ca14c72251e..d48d126204b 100644 --- a/access-grant/src/test/resources/access_grant4.json +++ b/access-grant/src/test/resources/access_grant4.json @@ -6,7 +6,7 @@ "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"], + "https://schema.inrupt.com/credentials/v2.jsonld"], "id":"https://accessgrant.example/credential/5c6060ad-2f16-4bc1-b022-dffb46bff626", "type":["VerifiableCredential","vc:SolidAccessGrant"], "issuer":"https://accessgrant.example", @@ -20,6 +20,7 @@ "credentialSubject":{ "id":"https://id.example/grantor", "providedConsent":{ + "request":"https://accessrequest.example/1234", "mode":["Read"], "hasStatus":"https://w3id.org/GConsent#ConsentStatusExplicitlyGiven", "isProvidedTo":"https://id.example/grantee",