Skip to content

Commit

Permalink
JCL-474: Link access grants and denials to originating requests (#1716)
Browse files Browse the repository at this point in the history
  • Loading branch information
acoburn authored Nov 22, 2024
1 parent 2b4e951 commit d0ae5f7
Show file tree
Hide file tree
Showing 8 changed files with 72 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ public static class CredentialData {
private final Set<URI> purposes;
private final Set<URI> resources;
private final URI recipient;
private final URI accessRequest;

/**
* Create a collection of user-managed credential data.
Expand All @@ -293,10 +294,25 @@ public static class CredentialData {
*/
public CredentialData(final Set<URI> resources, final Set<String> modes,
final Set<URI> 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<URI> resources, final Set<String> modes,
final Set<URI> 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;
}

/**
Expand Down Expand Up @@ -334,6 +350,15 @@ public Set<URI> 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<String, Object> data) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ public class AccessDenial extends AccessCredential {
private static final Set<String> supportedTypes = getSupportedTypes();
private static final JsonService jsonService = ServiceProvider.getJsonService();

private final URI accessRequest;

/**
* Read a verifiable presentation as an AccessDenial.
*
Expand All @@ -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;
}

/**
Expand Down Expand Up @@ -127,12 +139,14 @@ static AccessDenial parse(final String serialization) throws IOException {
final Optional<URI> 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<String> modes = asSet(consent.get("mode")).orElseGet(Collections::emptySet);
final Set<URI> resources = asSet(consent.get("forPersonalData")).orElseGet(Collections::emptySet)
.stream().map(URI::create).collect(Collectors.toSet());
final Set<URI> 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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ public class AccessGrant extends AccessCredential {
private static final Set<String> supportedTypes = getSupportedTypes();
private static final JsonService jsonService = ServiceProvider.getJsonService();

private final URI accessRequest;

/**
* Read a verifiable presentation as an AccessGrant.
*
Expand All @@ -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;
}

/**
Expand Down Expand Up @@ -126,12 +138,14 @@ static AccessGrant parse(final String serialization) throws IOException {
final Optional<URI> 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<String> modes = asSet(consent.get("mode")).orElseGet(Collections::emptySet);
final Set<URI> resources = asSet(consent.get("forPersonalData")).orElseGet(Collections::emptySet)
.stream().map(URI::create).collect(Collectors.toSet());
final Set<URI> 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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -260,7 +261,8 @@ public CompletionStage<AccessGrant> grantAccess(final AccessRequest request) {
Objects.requireNonNull(request, "Request may not be null!");
return v1Metadata().thenCompose(metadata -> {
final Map<String, Object> 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();
Expand Down Expand Up @@ -292,7 +294,8 @@ public CompletionStage<AccessDenial> denyAccess(final AccessRequest request) {
Objects.requireNonNull(request, "Request may not be null!");
return v1Metadata().thenCompose(metadata -> {
final Map<String, Object> 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();
Expand Down Expand Up @@ -799,13 +802,14 @@ static URI asUri(final Object value) {
}

static Map<String, Object> buildAccessDenialv1(final URI agent, final Set<URI> resources, final Set<String> modes,
final Set<URI> purposes, final Instant expiration, final Instant issuance) {
final Set<URI> purposes, final Instant expiration, final Instant issuance, final URI accessRequest) {
Objects.requireNonNull(agent, "Access denial agent may not be null!");
final Map<String, Object> 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);
}
Expand All @@ -829,13 +833,14 @@ static Map<String, Object> buildAccessDenialv1(final URI agent, final Set<URI> r
}

static Map<String, Object> buildAccessGrantv1(final URI agent, final Set<URI> resources, final Set<String> modes,
final Set<URI> purposes, final Instant expiration, final Instant issuance) {
final Set<URI> purposes, final Instant expiration, final Instant issuance, final URI accessRequest) {
Objects.requireNonNull(agent, "Access grant agent may not be null!");
final Map<String, Object> 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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down Expand Up @@ -80,6 +81,7 @@ void testReadAccessDenialQualifiedName() throws IOException {
final Set<String> 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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down Expand Up @@ -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"),
Expand Down
3 changes: 2 additions & 1 deletion access-grant/src/test/resources/access_denial1.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"],
Expand Down
3 changes: 2 additions & 1 deletion access-grant/src/test/resources/access_grant4.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down

0 comments on commit d0ae5f7

Please sign in to comment.