Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,9 @@ 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 URI accessRequest = asUri(consent.get("verifiedRequest")).orElse(
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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ public class AccessGrantClient {
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 VERIFIED_REQUEST = "verifiedRequest";
private static final String MODE = "mode";
private static final String PROVIDED_CONSENT = "providedConsent";
private static final String FOR_PURPOSE = "forPurpose";
Expand Down Expand Up @@ -252,17 +253,28 @@ private CompletionStage<AccessRequest> requestAccess(final URI recipient, final
}

/**
* Issue an access grant based on an access request.
* Issue an access grant based on an access request. The access request is not verified.
*
* @param request the access request
* @return the next stage of completion containing the issued access grant
*/
public CompletionStage<AccessGrant> grantAccess(final AccessRequest request) {
return grantAccess(request, false);
}

/**
* Issue an access grant based on an access request.
*
* @param request the access request
* @param verifyRequest whether the request should be verified before issuing the access grant
* @return the next stage of completion containing the issued access grant
*/
public CompletionStage<AccessGrant> grantAccess(final AccessRequest request, final boolean verifyRequest) {
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.getIdentifier());
request.getIdentifier(), verifyRequest);
final Request req = Request.newBuilder(metadata.issueEndpoint)
.header(CONTENT_TYPE, APPLICATION_JSON)
.POST(Request.BodyPublishers.ofByteArray(serialize(data))).build();
Expand All @@ -285,17 +297,28 @@ public CompletionStage<AccessGrant> grantAccess(final AccessRequest request) {
}

/**
* Issue an access denial receipt based on an access request.
* Issue an access denial receipt based on an access request. The access request is not verified.
*
* @param request the access request
* @return the next stage of completion containing the issued access denial
*/
public CompletionStage<AccessDenial> denyAccess(final AccessRequest request) {
return denyAccess(request, false);
}

/**
* Issue an access denial receipt based on an access request.
*
* @param request the access request
* @param verifyRequest whether the request should be verified before issuing the access denial
* @return the next stage of completion containing the issued access denial
*/
public CompletionStage<AccessDenial> denyAccess(final AccessRequest request, final boolean verifyRequest) {
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.getIdentifier());
request.getIdentifier(), verifyRequest);
final Request req = Request.newBuilder(metadata.issueEndpoint)
.header(CONTENT_TYPE, APPLICATION_JSON)
.POST(Request.BodyPublishers.ofByteArray(serialize(data))).build();
Expand Down Expand Up @@ -716,15 +739,22 @@ static URI asUri(final Object value) {
return null;
}

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 URI accessRequest) {
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 URI accessRequest,
final boolean verifiedRequest) {
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);
linkRequest(consent, accessRequest, verifiedRequest);
if (!purposes.isEmpty()) {
consent.put(FOR_PURPOSE, purposes);
}
Expand All @@ -747,15 +777,22 @@ static Map<String, Object> buildAccessDenialv1(final URI agent, final Set<URI> r
return data;
}

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 URI accessRequest) {
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 URI accessRequest,
final boolean verifiedRequest) {
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);
linkRequest(consent, accessRequest, verifiedRequest);
if (!purposes.isEmpty()) {
consent.put(FOR_PURPOSE, purposes);
}
Expand Down Expand Up @@ -861,4 +898,12 @@ static boolean isAccessDenial(final URI type) {
return SOLID_ACCESS_DENIAL.equals(type.toString()) || QN_ACCESS_DENIAL.equals(type)
|| FQ_ACCESS_DENIAL.equals(type);
}

private static void linkRequest(final Map<String, Object> consent, final URI request, final boolean verifiedLink) {
if (verifiedLink) {
consent.put(VERIFIED_REQUEST, request);
} else {
consent.put(REQUEST, request);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

class AccessGrantClientTest {

Expand Down Expand Up @@ -299,8 +301,9 @@ void testRequestAccessNoAuth() {
assertInstanceOf(AccessGrantException.class, err.getCause());
}

@Test
void testGrantAccess() {
@ParameterizedTest
@ValueSource(booleans = {true, false})
void testGrantAccess(final boolean verifyRequest) {
final Map<String, Object> claims = new HashMap<>();
claims.put("webid", WEBID);
claims.put("sub", SUB);
Expand All @@ -318,7 +321,7 @@ void testGrantAccess() {
final AccessRequest request = client.requestAccess(recipient, resources, modes, purposes, expiration)
.toCompletableFuture().join();

final AccessGrant grant = client.grantAccess(request).toCompletableFuture().join();
final AccessGrant grant = client.grantAccess(request, verifyRequest).toCompletableFuture().join();

assertTrue(grant.getTypes().contains("SolidAccessGrant"));
assertEquals(Optional.of(recipient), grant.getRecipient());
Expand All @@ -327,10 +330,14 @@ void testGrantAccess() {
assertEquals(baseUri, grant.getIssuer());
assertEquals(purposes, grant.getPurposes());
assertEquals(resources, grant.getResources());
// The request URL is static in the mock response, but it is dynamic in the request, so they will mismatch
// if compared.
assertNotNull(grant.getAccessRequest());
}

@Test
void testDenyAccess() {
@ParameterizedTest
@ValueSource(booleans = {true, false})
void testDenyAccess(final boolean verifyRequest) {
final Map<String, Object> claims = new HashMap<>();
claims.put("webid", WEBID);
claims.put("sub", SUB);
Expand All @@ -348,7 +355,7 @@ void testDenyAccess() {
final AccessRequest request = client.requestAccess(recipient, resources, modes, purposes, expiration)
.toCompletableFuture().join();

final AccessDenial denial = client.denyAccess(request).toCompletableFuture().join();
final AccessDenial denial = client.denyAccess(request, verifyRequest).toCompletableFuture().join();

assertTrue(denial.getTypes().contains("SolidAccessDenial"));
assertEquals(Optional.of(recipient), denial.getRecipient());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,17 @@ private void setupMocks() {
.withHeader("Content-Type", "application/json")
.withBody(getResource("/vc-4.json", wireMockServer.baseUrl()))));

wireMockServer.stubFor(post(urlEqualTo("/issue"))
.atPriority(1)
.withHeader("Authorization", containing("Bearer eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9."))
.withRequestBody(containing("\"providedConsent\""))
.withRequestBody(containing("\"2022-08-27T12:00:00Z\""))
.withRequestBody(containing("\"verifiedRequest\""))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody(getResource("/vc-4-verified.json", wireMockServer.baseUrl()))));

// Access Request
wireMockServer.stubFor(post(urlEqualTo("/issue"))
.atPriority(1)
Expand Down
34 changes: 34 additions & 0 deletions access-grant/src/test/resources/vc-4-verified.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"@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/v2.jsonld"],
"id":"{{baseUrl}}/access-grant-4",
"type":["VerifiableCredential","SolidAccessGrant"],
"issuer":"{{baseUrl}}",
"expirationDate":"2022-08-27T12: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":"https://id.test/username",
"providedConsent":{
"mode":["Read","Append"],
"hasStatus":"https://w3id.org/GConsent#ConsentStatusExplicitlyGiven",
"isProvidedTo":"https://id.test/agent",
"forPurpose":["https://purpose.test/Purpose1"],
"forPersonalData":["https://storage.test/data/"],
"verifiedRequest": "http://localhost:33367/access-request-5"}},

"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"}
}

3 changes: 2 additions & 1 deletion access-grant/src/test/resources/vc-4.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
"hasStatus":"https://w3id.org/GConsent#ConsentStatusExplicitlyGiven",
"isProvidedTo":"https://id.test/agent",
"forPurpose":["https://purpose.test/Purpose1"],
"forPersonalData":["https://storage.test/data/"]}},
"forPersonalData":["https://storage.test/data/"],
"request": "http://localhost:33367/access-request-5"}},
"proof":{
"created":"2022-08-25T20:34:05.236Z",
"proofPurpose":"assertionMethod",
Expand Down
Loading