requestAccess(final URI recipient, final
/**
* Issue an access grant based on an access request.
*
+ *
+ * The access request is not verified.
+ * Any templated URLs are ignored.
+ *
* @param request the access request
* @return the next stage of completion containing the issued access grant
*/
public CompletionStage grantAccess(final AccessRequest request) {
+ return grantAccess(request, templates -> Collections.emptySet(), false);
+ }
+
+ /**
+ * Issue an access grant based on an access request.
+ *
+ *
+ * The access request is verified.
+ * Any templated URLs are processed according to the provided mapping function.
+ *
+ * @param request the access request
+ * @param mapping a mapping function for template URLs
+ * @return the next stage of completion containing the issued access grant
+ */
+ public CompletionStage grantAccess(final AccessRequest request,
+ final Function, Set> mapping) {
+ return grantAccess(request, mapping, true);
+ }
+
+ /**
+ * Issue an access grant based on an access request.
+ *
+ *
+ * Any templated URLs are ignored.
+ *
+ * @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 grantAccess(final AccessRequest request, final boolean verifyRequest) {
+ return grantAccess(request, templates -> Collections.emptySet(), verifyRequest);
+ }
+
+ /**
+ * Issue an access grant based on an access request.
+ *
+ * @param request the access request
+ * @param mapping a mapping function for template URLs
+ * @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 grantAccess(final AccessRequest request,
+ final Function, Set> mapping, final boolean verifyRequest) {
Objects.requireNonNull(request, "Request may not be null!");
+ final var templated = mapping.apply(request.getTemplates());
+ if (templated.size() != request.getTemplates().size()) {
+ LOGGER.atDebug()
+ .setMessage("Unexpected number of mapped template values, found ({}) expected ({})")
+ .addArgument(templated::size)
+ .addArgument(() -> request.getTemplates().size())
+ .log();
+ }
+ final var resources = new HashSet(request.getResources());
+ resources.addAll(templated);
+ if (resources.isEmpty()) {
+ LOGGER.atWarn()
+ .setMessage("No data URLs supplied: {} resource URLs and {} mapped templates")
+ .addArgument(() -> request.getResources().size())
+ .addArgument(templated::size)
+ .log();
+ }
return v1Metadata().thenCompose(metadata -> {
- final Map data = buildAccessGrantv1(request.getCreator(), request.getResources(),
- request.getModes(), request.getPurposes(), request.getExpiration(), request.getIssuedAt());
+ final Map data = buildAccessGrantv1(request.getCreator(), resources,
+ request.getModes(), request.getPurposes(), request.getExpiration(), request.getIssuedAt(),
+ request.getIdentifier(), verifyRequest);
final Request req = Request.newBuilder(metadata.issueEndpoint)
.header(CONTENT_TYPE, APPLICATION_JSON)
.POST(Request.BodyPublishers.ofByteArray(serialize(data))).build();
@@ -270,16 +362,28 @@ public CompletionStage 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 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 denyAccess(final AccessRequest request, final boolean verifyRequest) {
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(), verifyRequest);
final Request req = Request.newBuilder(metadata.issueEndpoint)
.header(CONTENT_TYPE, APPLICATION_JSON)
.POST(Request.BodyPublishers.ofByteArray(serialize(data))).build();
@@ -314,8 +418,7 @@ public CompletionStage verify(final AccessCredenti
final Map presentation = new HashMap<>();
try (final InputStream is = new ByteArrayInputStream(credential.serialize().getBytes(UTF_8))) {
- final Map data = jsonService.fromJson(is,
- new HashMap(){}.getClass().getGenericSuperclass());
+ final Map data = jsonService.fromJson(is, JSON_TYPE_REF);
Utils.getCredentialsFromPresentation(data, credential.getTypes()).stream().findFirst()
.ifPresent(c -> presentation.put(VERIFIABLE_CREDENTIAL, c));
} catch (final IOException ex) {
@@ -345,85 +448,107 @@ public CompletionStage verify(final AccessCredenti
}
/**
- * Perform an Access Credentials query and returns 0 to N matching access credentials.
+ * Perform an Access Credentials query and return a page of access credentials.
*
- * @param the AccessCredential type
- * @param resource the resource identifier, may be {@code null}
- * @param creator the identifier for the agent who created the credential, may be {@code null}
- * @param recipient the identifier for the agent who is the recipient for the credential, may be {@code null}
- * @param purpose the access purpose, 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 Credentials
- */
- public CompletionStage> query(final URI resource, final URI creator,
- final URI recipient, final URI purpose, final String mode, final Class clazz) {
-
- final Set modes = mode != null ? Collections.singleton(mode) : Collections.emptySet();
- final Set purposes = purpose != null ? Collections.singleton(purpose) : Collections.emptySet();
-
- return query(resource, creator, recipient, purposes, modes, clazz);
- }
-
- /**
- * Perform an Access Credentials query and returns 0 to N matching access credentials.
- *
- * @param the AccessCredential type
- * @param query the access credential query, never {@code null}
- * @return the next stage of completion, including the matched Access Credentials
+ * @param the credential type
+ * @param filter the query filter
+ * @return the page of query results
*/
- public CompletionStage> query(final AccessCredentialQuery query) {
- Objects.requireNonNull(query, "The query may not be null!");
- return query(query.getResource(), query.getCreator(), query.getRecipient(), query.getPurposes(),
- query.getModes(), query.getAccessCredentialType());
- }
-
- private CompletionStage> query(final URI resource, final URI creator,
- final URI recipient, final Set purposes, final Set modes, final Class clazz) {
- Objects.requireNonNull(clazz, "The clazz parameter must not be null!");
-
- final URI type;
+ public CompletionStage> query(final CredentialFilter filter) {
+ final Class clazz = Objects.requireNonNull(filter, "filter may not be null!").getCredentialType();
final Set supportedTypes;
if (AccessGrant.class.isAssignableFrom(clazz)) {
- type = URI.create(SOLID_ACCESS_GRANT);
supportedTypes = ACCESS_GRANT_TYPES;
} else if (AccessRequest.class.isAssignableFrom(clazz)) {
- type = URI.create(SOLID_ACCESS_REQUEST);
supportedTypes = ACCESS_REQUEST_TYPES;
} else if (AccessDenial.class.isAssignableFrom(clazz)) {
- type = URI.create(SOLID_ACCESS_DENIAL);
supportedTypes = ACCESS_DENIAL_TYPES;
} else {
throw new AccessGrantException("Unsupported type " + clazz + " in query request");
}
return v1Metadata().thenCompose(metadata -> {
- final List>> responses = buildQuery(config.getIssuer(), type,
- resource, creator, recipient, purposes, modes).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())
- .toCompletableFuture())
- .collect(Collectors.toList());
-
- return CompletableFuture.allOf(responses.toArray(new CompletableFuture[0]))
- .thenApply(x -> responses.stream().map(CompletableFuture::join).map(response -> {
- try (final InputStream input = response.body()) {
- final int status = response.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);
+ if (metadata.queryEndpoint == null) {
+ throw new AccessGrantException("Server does not support CredentialFilter-based queries");
+ }
+ final Request req = Request.newBuilder(filter.asURI(metadata.queryEndpoint)).GET().build();
+ return client.send(req, Response.BodyHandlers.ofInputStream()).thenApply(response -> {
+ try (final InputStream input = response.body()) {
+ if (isSuccess(response.statusCode())) {
+ final Map> links = processFilterResponseHeaders(response.headers(),
+ filter);
+ final List items = processFilterResponseBody(input, supportedTypes, clazz);
+ return new CredentialResult<>(items, links.get(FIRST), links.get(PREV),
+ links.get(NEXT), links.get(LAST));
+ } else {
+ throw new AccessGrantException("Error querying access grant: HTTP response " +
+ response.statusCode());
}
- }).flatMap(List::stream).collect(Collectors.toList()));
+ } catch (final IOException ex) {
+ throw new AccessGrantException(
+ "Unexpected I/O exception while processing Access Grant query", ex);
+ }
+ });
});
}
+ Map> processFilterResponseHeaders(final Headers headers,
+ final CredentialFilter filter) {
+ final Map> links = new HashMap<>();
+ final List linkHeaders = headers.allValues("Link");
+ if (!linkHeaders.isEmpty()) {
+ Headers.Link.parse(linkHeaders.toArray(new String[0]))
+ .forEach(link -> {
+ final String rel = link.getParameter("rel");
+ final URI uri = link.getUri();
+ if (LINK_REL_VALUES.contains(rel) && uri != null) {
+ final String page = Utils.getQueryParam(uri, "page");
+ links.put(rel, CredentialFilter.newBuilder(filter).page(page)
+ .build(filter.getCredentialType()));
+ }
+ });
+ }
+ return links;
+ }
+
+ List processFilterResponseBody(final InputStream input,
+ final Set validTypes, final Class clazz) throws IOException {
+
+ final List page = new ArrayList<>();
+ final Map data = jsonService.fromJson(input, JSON_TYPE_REF);
+ Utils.asList(data.get("items")).ifPresent(items -> {
+ for (final Object item : items) {
+ 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(VERIFIABLE_PRESENTATION));
+ presentation.put(VERIFIABLE_CREDENTIAL, Arrays.asList(credential));
+ final T c = cast(presentation, clazz);
+ if (c != null) {
+ page.add(c);
+ }
+ }
+ }));
+ }
+ });
+ return page;
+ }
+
+ @SuppressWarnings("unchecked")
+ T cast(final Map data, final Class clazz) {
+ if (AccessGrant.class.isAssignableFrom(clazz)) {
+ return (T) AccessGrant.of(new String(serialize(data), UTF_8));
+ } else if (AccessRequest.class.isAssignableFrom(clazz)) {
+ return (T) AccessRequest.of(new String(serialize(data), UTF_8));
+ } else if (AccessDenial.class.isAssignableFrom(clazz)) {
+ return (T) AccessDenial.of(new String(serialize(data), UTF_8));
+ }
+ return null;
+ }
+
/**
* Revoke an access credential.
*
@@ -475,11 +600,11 @@ public CompletionStage fetch(final URI identifie
try (final InputStream input = res.body()) {
final int httpStatus = res.statusCode();
if (isSuccess(httpStatus)) {
- if (AccessGrant.class.equals(clazz)) {
+ if (AccessGrant.class.isAssignableFrom(clazz)) {
return (T) processVerifiableCredential(input, ACCESS_GRANT_TYPES, clazz);
- } else if (AccessRequest.class.equals(clazz)) {
+ } else if (AccessRequest.class.isAssignableFrom(clazz)) {
return (T) processVerifiableCredential(input, ACCESS_REQUEST_TYPES, clazz);
- } else if (AccessDenial.class.equals(clazz)) {
+ } else if (AccessDenial.class.isAssignableFrom(clazz)) {
return (T) processVerifiableCredential(input, ACCESS_DENIAL_TYPES, clazz);
}
throw new AccessGrantException("Unable to fetch credential as " + clazz);
@@ -493,35 +618,28 @@ public CompletionStage fetch(final URI identifie
});
}
- @SuppressWarnings("unchecked")
T processVerifiableCredential(final InputStream input, final Set validTypes,
final Class clazz) throws IOException {
- final Map data = jsonService.fromJson(input,
- new HashMap(){}.getClass().getGenericSuperclass());
+ final Map data = jsonService.fromJson(input, JSON_TYPE_REF);
final Set types = Utils.asSet(data.get(TYPE)).orElseThrow(() ->
new AccessGrantException("Invalid Access Grant: no 'type' field"));
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(TYPE, Arrays.asList(VERIFIABLE_PRESENTATION));
presentation.put(VERIFIABLE_CREDENTIAL, Arrays.asList(data));
- if (AccessGrant.class.isAssignableFrom(clazz)) {
- return (T) AccessGrant.of(new String(serialize(presentation), UTF_8));
- } else if (AccessRequest.class.isAssignableFrom(clazz)) {
- return (T) AccessRequest.of(new String(serialize(presentation), UTF_8));
- } else if (AccessDenial.class.isAssignableFrom(clazz)) {
- return (T) AccessDenial.of(new String(serialize(presentation), UTF_8));
+ final T credential = cast(presentation, clazz);
+ if (credential != null) {
+ return credential;
}
}
throw new AccessGrantException("Invalid Access Grant: missing supported type");
}
- @SuppressWarnings("unchecked")
List processQueryResponse(final InputStream input, final Set validTypes,
final Class clazz) throws IOException {
- final Map data = jsonService.fromJson(input,
- new HashMap(){}.getClass().getGenericSuperclass());
+ final Map data = jsonService.fromJson(input, JSON_TYPE_REF);
final List grants = new ArrayList<>();
for (final Object item : getCredentials(data)) {
Utils.asMap(item).ifPresent(credential ->
@@ -530,14 +648,11 @@ List processQueryResponse(final InputStream inpu
if (!types.isEmpty()) {
final Map presentation = new HashMap<>();
presentation.put(CONTEXT, Arrays.asList(VC_CONTEXT_URI));
- presentation.put(TYPE, Arrays.asList("VerifiablePresentation"));
+ presentation.put(TYPE, Arrays.asList(VERIFIABLE_PRESENTATION));
presentation.put(VERIFIABLE_CREDENTIAL, Arrays.asList(credential));
- 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)));
- } else if (AccessDenial.class.equals(clazz)) {
- grants.add((T) AccessDenial.of(new String(serialize(presentation), UTF_8)));
+ final T c = cast(presentation, clazz);
+ if (c != null) {
+ grants.add(c);
}
}
}));
@@ -559,8 +674,7 @@ CompletionStage v1Metadata() {
try (final InputStream input = res.body()) {
final int httpStatus = res.statusCode();
if (isSuccess(httpStatus)) {
- final Map data = jsonService.fromJson(input,
- new HashMap(){}.getClass().getGenericSuperclass());
+ final Map data = jsonService.fromJson(input, JSON_TYPE_REF);
return data;
}
throw new AccessGrantException(
@@ -572,7 +686,7 @@ CompletionStage v1Metadata() {
})
.thenApply(metadata -> {
final Metadata m = new Metadata();
- m.queryEndpoint = asUri(metadata.get("derivationService"));
+ m.queryEndpoint = asUri(metadata.get("queryService"));
m.issueEndpoint = asUri(metadata.get("issuerService"));
m.verifyEndpoint = asUri(metadata.get("verifierService"));
m.statusEndpoint = asUri(metadata.get("statusService"));
@@ -690,14 +804,22 @@ static URI asUri(final Object value) {
return null;
}
- static Map buildAccessDenialv1(final URI agent, final Set resources, final Set modes,
- final Set purposes, final Instant expiration, final Instant issuance) {
+ static Map buildAccessDenialv1(
+ final URI agent,
+ final Set resources,
+ final Set modes,
+ final Set 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 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);
+ linkRequest(consent, accessRequest, verifiedRequest);
if (!purposes.isEmpty()) {
consent.put(FOR_PURPOSE, purposes);
}
@@ -720,14 +842,22 @@ static Map buildAccessDenialv1(final URI agent, final Set r
return data;
}
- static Map buildAccessGrantv1(final URI agent, final Set resources, final Set modes,
- final Set purposes, final Instant expiration, final Instant issuance) {
+ static Map buildAccessGrantv1(
+ final URI agent,
+ final Set resources,
+ final Set modes,
+ final Set 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 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);
+ linkRequest(consent, accessRequest, verifiedRequest);
if (!purposes.isEmpty()) {
consent.put(FOR_PURPOSE, purposes);
}
@@ -750,12 +880,17 @@ static Map buildAccessGrantv1(final URI agent, final Set re
return data;
}
- static Map buildAccessRequestv1(final URI agent, final Set resources, final Set modes,
- final Set purposes, final Instant expiration, final Instant issuance) {
+ static Map buildAccessRequestv1(final URI agent, final Set resources,
+ final Set templates, final Set modes, final Set purposes,
+ final Instant expiration, final Instant issuance) {
final Map consent = new HashMap<>();
consent.put(HAS_STATUS, "https://w3id.org/GConsent#ConsentStatusRequested");
consent.put(MODE, modes);
- consent.put(FOR_PERSONAL_DATA, resources);
+ if (!resources.isEmpty()) {
+ consent.put(FOR_PERSONAL_DATA, resources);
+ } else {
+ consent.put(TEMPLATE, templates);
+ }
if (agent != null) {
consent.put(IS_CONSENT_FOR_DATA_SUBJECT, agent);
}
@@ -789,6 +924,7 @@ static boolean isSuccess(final int statusCode) {
static Set getAccessRequestTypes() {
final Set types = new HashSet<>();
types.add(SOLID_ACCESS_REQUEST);
+ types.add(QN_ACCESS_REQUEST.toString());
types.add(FQ_ACCESS_REQUEST.toString());
return Collections.unmodifiableSet(types);
}
@@ -796,6 +932,7 @@ static Set getAccessRequestTypes() {
static Set getAccessGrantTypes() {
final Set types = new HashSet<>();
types.add(SOLID_ACCESS_GRANT);
+ types.add(QN_ACCESS_GRANT.toString());
types.add(FQ_ACCESS_GRANT.toString());
return Collections.unmodifiableSet(types);
}
@@ -803,19 +940,40 @@ static Set getAccessGrantTypes() {
static Set getAccessDenialTypes() {
final Set types = new HashSet<>();
types.add(SOLID_ACCESS_DENIAL);
+ types.add(QN_ACCESS_DENIAL.toString());
types.add(FQ_ACCESS_DENIAL.toString());
return Collections.unmodifiableSet(types);
}
+ static Set getLinkPagingRelValues() {
+ final Set values = new HashSet<>();
+ values.add(FIRST);
+ values.add(LAST);
+ values.add(NEXT);
+ values.add(PREV);
+ return Collections.unmodifiableSet(values);
+ }
+
static boolean isAccessGrant(final URI type) {
- return SOLID_ACCESS_GRANT.equals(type.toString()) || FQ_ACCESS_GRANT.equals(type);
+ return SOLID_ACCESS_GRANT.equals(type.toString()) || QN_ACCESS_GRANT.equals(type)
+ || FQ_ACCESS_GRANT.equals(type);
}
static boolean isAccessRequest(final URI type) {
- return SOLID_ACCESS_REQUEST.equals(type.toString()) || FQ_ACCESS_REQUEST.equals(type);
+ return SOLID_ACCESS_REQUEST.equals(type.toString()) || QN_ACCESS_REQUEST.equals(type)
+ || FQ_ACCESS_REQUEST.equals(type);
}
static boolean isAccessDenial(final URI type) {
- return SOLID_ACCESS_DENIAL.equals(type.toString()) || FQ_ACCESS_DENIAL.equals(type);
+ return SOLID_ACCESS_DENIAL.equals(type.toString()) || QN_ACCESS_DENIAL.equals(type)
+ || FQ_ACCESS_DENIAL.equals(type);
+ }
+
+ private static void linkRequest(final Map consent, final URI request, final boolean verifiedLink) {
+ if (verifiedLink) {
+ consent.put(VERIFIED_REQUEST, request);
+ } else {
+ consent.put(REQUEST, request);
+ }
}
}
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 75c4f090b69..bb3db7dcd79 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
@@ -37,6 +37,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
@@ -94,18 +95,19 @@ public static AccessRequest of(final InputStream serialization) {
static Set getSupportedTypes() {
final Set types = new HashSet<>();
types.add("SolidAccessRequest");
+ types.add("vc:SolidAccessRequest");
types.add("http://www.w3.org/ns/solid/vc#SolidAccessRequest");
return Collections.unmodifiableSet(types);
}
static AccessRequest parse(final String serialization) throws IOException {
- try (final InputStream in = new ByteArrayInputStream(serialization.getBytes())) {
+ try (final InputStream in = new ByteArrayInputStream(serialization.getBytes(UTF_8))) {
// TODO process as JSON-LD
final Map data = jsonService.fromJson(in,
new HashMap(){}.getClass().getGenericSuperclass());
final List