Skip to content

Commit d0ae5f7

Browse files
authored
JCL-474: Link access grants and denials to originating requests (#1716)
1 parent 2b4e951 commit d0ae5f7

File tree

8 files changed

+72
-8
lines changed

8 files changed

+72
-8
lines changed

access-grant/src/main/java/com/inrupt/client/accessgrant/AccessCredential.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ public static class CredentialData {
282282
private final Set<URI> purposes;
283283
private final Set<URI> resources;
284284
private final URI recipient;
285+
private final URI accessRequest;
285286

286287
/**
287288
* Create a collection of user-managed credential data.
@@ -293,10 +294,25 @@ public static class CredentialData {
293294
*/
294295
public CredentialData(final Set<URI> resources, final Set<String> modes,
295296
final Set<URI> purposes, final URI recipient) {
297+
this(resources, modes, purposes, recipient, null);
298+
}
299+
300+
/**
301+
* Create a collection of user-managed credential data.
302+
*
303+
* @param resources the resources referenced by the credential
304+
* @param modes the access modes defined by this credential
305+
* @param purposes the purposes associated with this credential
306+
* @param recipient the recipient for this credential, may be {@code null}
307+
* @param accessRequest the access request identifier, may be {@code null}
308+
*/
309+
public CredentialData(final Set<URI> resources, final Set<String> modes,
310+
final Set<URI> purposes, final URI recipient, final URI accessRequest) {
296311
this.modes = Objects.requireNonNull(modes, "modes may not be null!");
297312
this.purposes = Objects.requireNonNull(purposes, "purposes may not be null!");
298313
this.resources = Objects.requireNonNull(resources, "resources may not be null!");
299314
this.recipient = recipient;
315+
this.accessRequest = accessRequest;
300316
}
301317

302318
/**
@@ -334,6 +350,15 @@ public Set<URI> getResources() {
334350
public URI getRecipient() {
335351
return recipient;
336352
}
353+
354+
/**
355+
* Get the access request identifier associated with this credential.
356+
*
357+
* @return the access request identifier, may be {@code null}
358+
*/
359+
public URI getAccessRequest() {
360+
return accessRequest;
361+
}
337362
}
338363

339364
static CredentialMetadata extractMetadata(final Map<String, Object> data) {

access-grant/src/main/java/com/inrupt/client/accessgrant/AccessDenial.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ public class AccessDenial extends AccessCredential {
4949
private static final Set<String> supportedTypes = getSupportedTypes();
5050
private static final JsonService jsonService = ServiceProvider.getJsonService();
5151

52+
private final URI accessRequest;
53+
5254
/**
5355
* Read a verifiable presentation as an AccessDenial.
5456
*
@@ -60,6 +62,16 @@ public class AccessDenial extends AccessCredential {
6062
protected AccessDenial(final URI identifier, final String credential, final CredentialData data,
6163
final CredentialMetadata metadata) {
6264
super(identifier, credential, data, metadata);
65+
this.accessRequest = data.getAccessRequest();
66+
}
67+
68+
/**
69+
* Get the corresponding access request identifier.
70+
*
71+
* @return the access request identifier, may be {@code null}
72+
*/
73+
public URI getAccessRequest() {
74+
return accessRequest;
6375
}
6476

6577
/**
@@ -127,12 +139,14 @@ static AccessDenial parse(final String serialization) throws IOException {
127139
final Optional<URI> other = asUri(consent.get("isProvidedTo"));
128140

129141
final URI recipient = person.orElseGet(() -> controller.orElseGet(() -> other.orElse(null)));
142+
final URI accessRequest = asUri(consent.get("request")).orElse(null);
130143
final Set<String> modes = asSet(consent.get("mode")).orElseGet(Collections::emptySet);
131144
final Set<URI> resources = asSet(consent.get("forPersonalData")).orElseGet(Collections::emptySet)
132145
.stream().map(URI::create).collect(Collectors.toSet());
133146
final Set<URI> purposes = asSet(consent.get("forPurpose")).orElseGet(Collections::emptySet)
134147
.stream().flatMap(AccessCredential::filterUris).collect(Collectors.toSet());
135-
final CredentialData credentialData = new CredentialData(resources, modes, purposes, recipient);
148+
final CredentialData credentialData = new CredentialData(resources, modes, purposes, recipient,
149+
accessRequest);
136150

137151
return new AccessDenial(identifier, serialization, credentialData, credentialMetadata);
138152
} else {

access-grant/src/main/java/com/inrupt/client/accessgrant/AccessGrant.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ public class AccessGrant extends AccessCredential {
4949
private static final Set<String> supportedTypes = getSupportedTypes();
5050
private static final JsonService jsonService = ServiceProvider.getJsonService();
5151

52+
private final URI accessRequest;
53+
5254
/**
5355
* Read a verifiable presentation as an AccessGrant.
5456
*
@@ -60,6 +62,16 @@ public class AccessGrant extends AccessCredential {
6062
protected AccessGrant(final URI identifier, final String credential, final CredentialData data,
6163
final CredentialMetadata metadata) {
6264
super(identifier, credential, data, metadata);
65+
this.accessRequest = data.getAccessRequest();
66+
}
67+
68+
/**
69+
* Get the corresponding access request identifier.
70+
*
71+
* @return the access request identifier, may be {@code null}
72+
*/
73+
public URI getAccessRequest() {
74+
return accessRequest;
6375
}
6476

6577
/**
@@ -126,12 +138,14 @@ static AccessGrant parse(final String serialization) throws IOException {
126138
final Optional<URI> other = asUri(consent.get("isProvidedTo"));
127139

128140
final URI recipient = person.orElseGet(() -> controller.orElseGet(() -> other.orElse(null)));
141+
final URI accessRequest = asUri(consent.get("request")).orElse(null);
129142
final Set<String> modes = asSet(consent.get("mode")).orElseGet(Collections::emptySet);
130143
final Set<URI> resources = asSet(consent.get("forPersonalData")).orElseGet(Collections::emptySet)
131144
.stream().map(URI::create).collect(Collectors.toSet());
132145
final Set<URI> purposes = asSet(consent.get("forPurpose")).orElseGet(Collections::emptySet)
133146
.stream().flatMap(AccessCredential::filterUris).collect(Collectors.toSet());
134-
final CredentialData credentialData = new CredentialData(resources, modes, purposes, recipient);
147+
final CredentialData credentialData = new CredentialData(resources, modes, purposes, recipient,
148+
accessRequest);
135149

136150
return new AccessGrant(identifier, serialization, credentialData, credentialMetadata);
137151
} else {

access-grant/src/main/java/com/inrupt/client/accessgrant/AccessGrantClient.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ public class AccessGrantClient {
106106
private static final String IS_CONSENT_FOR_DATA_SUBJECT = "isConsentForDataSubject";
107107
private static final String FOR_PERSONAL_DATA = "forPersonalData";
108108
private static final String HAS_STATUS = "hasStatus";
109+
private static final String REQUEST = "request";
109110
private static final String MODE = "mode";
110111
private static final String PROVIDED_CONSENT = "providedConsent";
111112
private static final String FOR_PURPOSE = "forPurpose";
@@ -260,7 +261,8 @@ public CompletionStage<AccessGrant> grantAccess(final AccessRequest request) {
260261
Objects.requireNonNull(request, "Request may not be null!");
261262
return v1Metadata().thenCompose(metadata -> {
262263
final Map<String, Object> data = buildAccessGrantv1(request.getCreator(), request.getResources(),
263-
request.getModes(), request.getPurposes(), request.getExpiration(), request.getIssuedAt());
264+
request.getModes(), request.getPurposes(), request.getExpiration(), request.getIssuedAt(),
265+
request.getIdentifier());
264266
final Request req = Request.newBuilder(metadata.issueEndpoint)
265267
.header(CONTENT_TYPE, APPLICATION_JSON)
266268
.POST(Request.BodyPublishers.ofByteArray(serialize(data))).build();
@@ -292,7 +294,8 @@ public CompletionStage<AccessDenial> denyAccess(final AccessRequest request) {
292294
Objects.requireNonNull(request, "Request may not be null!");
293295
return v1Metadata().thenCompose(metadata -> {
294296
final Map<String, Object> data = buildAccessDenialv1(request.getCreator(), request.getResources(),
295-
request.getModes(), request.getPurposes(), request.getExpiration(), request.getIssuedAt());
297+
request.getModes(), request.getPurposes(), request.getExpiration(), request.getIssuedAt(),
298+
request.getIdentifier());
296299
final Request req = Request.newBuilder(metadata.issueEndpoint)
297300
.header(CONTENT_TYPE, APPLICATION_JSON)
298301
.POST(Request.BodyPublishers.ofByteArray(serialize(data))).build();
@@ -799,13 +802,14 @@ static URI asUri(final Object value) {
799802
}
800803

801804
static Map<String, Object> buildAccessDenialv1(final URI agent, final Set<URI> resources, final Set<String> modes,
802-
final Set<URI> purposes, final Instant expiration, final Instant issuance) {
805+
final Set<URI> purposes, final Instant expiration, final Instant issuance, final URI accessRequest) {
803806
Objects.requireNonNull(agent, "Access denial agent may not be null!");
804807
final Map<String, Object> consent = new HashMap<>();
805808
consent.put(MODE, modes);
806809
consent.put(HAS_STATUS, "https://w3id.org/GConsent#ConsentStatusRefused");
807810
consent.put(FOR_PERSONAL_DATA, resources);
808811
consent.put(IS_PROVIDED_TO, agent);
812+
consent.put(REQUEST, accessRequest);
809813
if (!purposes.isEmpty()) {
810814
consent.put(FOR_PURPOSE, purposes);
811815
}
@@ -829,13 +833,14 @@ static Map<String, Object> buildAccessDenialv1(final URI agent, final Set<URI> r
829833
}
830834

831835
static Map<String, Object> buildAccessGrantv1(final URI agent, final Set<URI> resources, final Set<String> modes,
832-
final Set<URI> purposes, final Instant expiration, final Instant issuance) {
836+
final Set<URI> purposes, final Instant expiration, final Instant issuance, final URI accessRequest) {
833837
Objects.requireNonNull(agent, "Access grant agent may not be null!");
834838
final Map<String, Object> consent = new HashMap<>();
835839
consent.put(MODE, modes);
836840
consent.put(HAS_STATUS, "https://w3id.org/GConsent#ConsentStatusExplicitlyGiven");
837841
consent.put(FOR_PERSONAL_DATA, resources);
838842
consent.put(IS_PROVIDED_TO, agent);
843+
consent.put(REQUEST, accessRequest);
839844
if (!purposes.isEmpty()) {
840845
consent.put(FOR_PURPOSE, purposes);
841846
}

access-grant/src/test/java/com/inrupt/client/accessgrant/AccessDenialTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ void testReadAccessDenial() throws IOException {
5050
expectedTypes.add("VerifiableCredential");
5151
expectedTypes.add("SolidAccessDenial");
5252
assertEquals(expectedTypes, denial.getTypes());
53+
assertEquals(URI.create("https://accessrequest.test/5678"), denial.getAccessRequest());
5354
assertEquals(Instant.parse("2022-08-27T12:00:00Z"), denial.getExpiration());
5455
assertEquals(Instant.parse("2022-08-25T20:34:05.153Z"), denial.getIssuedAt());
5556
assertEquals(URI.create("https://accessgrant.test/credential/fc2dbcd9-81d4-4fa4-8fd4-239e16dd83ab"),
@@ -80,6 +81,7 @@ void testReadAccessDenialQualifiedName() throws IOException {
8081
final Set<String> expectedTypes = new HashSet<>();
8182
expectedTypes.add("VerifiableCredential");
8283
expectedTypes.add("vc:SolidAccessDenial");
84+
assertNull(denial.getAccessRequest());
8385
assertEquals(expectedTypes, denial.getTypes());
8486
assertEquals(Instant.parse("2022-08-27T12:00:00Z"), denial.getExpiration());
8587
assertEquals(Instant.parse("2022-08-25T20:34:05.153Z"), denial.getIssuedAt());

access-grant/src/test/java/com/inrupt/client/accessgrant/AccessGrantTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ void testReadAccessGrant() throws IOException {
5252
expectedTypes.add("VerifiableCredential");
5353
expectedTypes.add("SolidAccessGrant");
5454
assertEquals(expectedTypes, grant.getTypes());
55+
assertNull(grant.getAccessRequest());
5556
assertEquals(Instant.parse("2022-08-27T12:00:00Z"), grant.getExpiration());
5657
assertEquals(Instant.parse("2022-08-25T20:34:05.153Z"), grant.getIssuedAt());
5758
assertEquals(URI.create("https://accessgrant.example/credential/5c6060ad-2f16-4bc1-b022-dffb46bff626"),
@@ -83,6 +84,7 @@ void testReadAccessGrantQualifiedName() throws IOException {
8384
expectedTypes.add("VerifiableCredential");
8485
expectedTypes.add("vc:SolidAccessGrant");
8586
assertEquals(expectedTypes, grant.getTypes());
87+
assertEquals(URI.create("https://accessrequest.example/1234"), grant.getAccessRequest());
8688
assertEquals(Instant.parse("2022-08-27T12:00:00Z"), grant.getExpiration());
8789
assertEquals(Instant.parse("2022-08-25T20:34:05.153Z"), grant.getIssuedAt());
8890
assertEquals(URI.create("https://accessgrant.example/credential/5c6060ad-2f16-4bc1-b022-dffb46bff626"),

access-grant/src/test/resources/access_denial1.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"https://www.w3.org/2018/credentials/v1",
77
"https://w3id.org/security/suites/ed25519-2020/v1",
88
"https://w3id.org/vc-revocation-list-2020/v1",
9-
"https://schema.inrupt.com/credentials/v1.jsonld"],
9+
"https://schema.inrupt.com/credentials/v2.jsonld"],
1010
"id":"https://accessgrant.test/credential/fc2dbcd9-81d4-4fa4-8fd4-239e16dd83ab",
1111
"type":["VerifiableCredential","SolidAccessDenial"],
1212
"issuer":"https://accessgrant.test",
@@ -21,6 +21,7 @@
2121
"id":"https://id.test/grantor",
2222
"providedConsent":{
2323
"mode":["Read"],
24+
"request": "https://accessrequest.test/5678",
2425
"hasStatus":"https://w3id.org/GConsent#ConsentStatusRefused",
2526
"isProvidedTo":"https://id.test/grantee",
2627
"forPurpose":["https://purpose.test/Purpose1"],

access-grant/src/test/resources/access_grant4.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"https://www.w3.org/2018/credentials/v1",
77
"https://w3id.org/security/suites/ed25519-2020/v1",
88
"https://w3id.org/vc-revocation-list-2020/v1",
9-
"https://schema.inrupt.com/credentials/v1.jsonld"],
9+
"https://schema.inrupt.com/credentials/v2.jsonld"],
1010
"id":"https://accessgrant.example/credential/5c6060ad-2f16-4bc1-b022-dffb46bff626",
1111
"type":["VerifiableCredential","vc:SolidAccessGrant"],
1212
"issuer":"https://accessgrant.example",
@@ -20,6 +20,7 @@
2020
"credentialSubject":{
2121
"id":"https://id.example/grantor",
2222
"providedConsent":{
23+
"request":"https://accessrequest.example/1234",
2324
"mode":["Read"],
2425
"hasStatus":"https://w3id.org/GConsent#ConsentStatusExplicitlyGiven",
2526
"isProvidedTo":"https://id.example/grantee",

0 commit comments

Comments
 (0)