Skip to content

Commit 7dbe865

Browse files
authored
Support templated access requests (#2446)
1 parent 02e31a7 commit 7dbe865

File tree

10 files changed

+495
-26
lines changed

10 files changed

+495
-26
lines changed

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

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
/** A base class for access credentials. **/
3838
public class AccessCredential {
3939

40+
static final int SINGLETON = 1;
4041
private static final Logger LOGGER = LoggerFactory.getLogger(AccessCredential.class);
4142

4243
protected static final String TYPE = "type";
@@ -49,6 +50,7 @@ public class AccessCredential {
4950
private final Set<String> modes;
5051
private final Set<URI> purposes;
5152
private final Set<URI> resources;
53+
private final Set<String> templates;
5254
private final URI recipient;
5355
private final URI creator;
5456
private final Instant expiration;
@@ -71,6 +73,7 @@ protected AccessCredential(final URI identifier, final String credential,
7173

7274
this.purposes = data.getPurposes();
7375
this.resources = data.getResources();
76+
this.templates = data.getTemplates();
7477
this.modes = data.getModes();
7578
this.recipient = data.getRecipient();
7679

@@ -165,6 +168,15 @@ public Set<URI> getResources() {
165168
return resources;
166169
}
167170

171+
/**
172+
* Get the templates associated with the access credential.
173+
*
174+
* @return the associated templates
175+
*/
176+
public Set<String> getTemplates() {
177+
return templates;
178+
}
179+
168180
/**
169181
* Get the creator of this access credential.
170182
*
@@ -281,6 +293,7 @@ public static class CredentialData {
281293
private final Set<String> modes;
282294
private final Set<URI> purposes;
283295
private final Set<URI> resources;
296+
private final Set<String> templates;
284297
private final URI recipient;
285298
private final URI accessRequest;
286299

@@ -294,7 +307,7 @@ public static class CredentialData {
294307
*/
295308
public CredentialData(final Set<URI> resources, final Set<String> modes,
296309
final Set<URI> purposes, final URI recipient) {
297-
this(resources, modes, purposes, recipient, null);
310+
this(resources, Collections.emptySet(), modes, purposes, recipient, null);
298311
}
299312

300313
/**
@@ -308,6 +321,22 @@ public CredentialData(final Set<URI> resources, final Set<String> modes,
308321
*/
309322
public CredentialData(final Set<URI> resources, final Set<String> modes,
310323
final Set<URI> purposes, final URI recipient, final URI accessRequest) {
324+
this(resources, Collections.emptySet(), modes, purposes, recipient, accessRequest);
325+
}
326+
327+
/**
328+
* Create a collection of user-managed credential data.
329+
*
330+
* @param resources the resources referenced by the credential
331+
* @param templates the resource templates referenced by the credential
332+
* @param modes the access modes defined by this credential
333+
* @param purposes the purposes associated with this credential
334+
* @param recipient the recipient for this credential, may be {@code null}
335+
* @param accessRequest the access request identifier, may be {@code null}
336+
*/
337+
public CredentialData(final Set<URI> resources, final Set<String> templates, final Set<String> modes,
338+
final Set<URI> purposes, final URI recipient, final URI accessRequest) {
339+
this.templates = Objects.requireNonNull(templates, "templates may not be null!");
311340
this.modes = Objects.requireNonNull(modes, "modes may not be null!");
312341
this.purposes = Objects.requireNonNull(purposes, "purposes may not be null!");
313342
this.resources = Objects.requireNonNull(resources, "resources may not be null!");
@@ -342,6 +371,15 @@ public Set<URI> getResources() {
342371
return resources;
343372
}
344373

374+
/**
375+
* Get the URL templates associated with this credential.
376+
*
377+
* @return the URL templates
378+
*/
379+
public Set<String> getTemplates() {
380+
return templates;
381+
}
382+
345383
/**
346384
* Get the recipient associated with this credential.
347385
*
@@ -393,7 +431,7 @@ static Stream<URI> filterUris(final String uri) {
393431
try {
394432
return Stream.of(URI.create(uri));
395433
} catch (final IllegalArgumentException ex) {
396-
LOGGER.debug("Ignoring non-URI purpose: {}", ex.getMessage());
434+
LOGGER.atDebug().setMessage("Ignoring non-URI purpose: {}").addArgument(ex::getMessage).log();
397435
}
398436
return Stream.empty();
399437
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,13 +111,13 @@ static Set<String> getSupportedTypes() {
111111
}
112112

113113
static AccessDenial parse(final String serialization) throws IOException {
114-
try (final InputStream in = new ByteArrayInputStream(serialization.getBytes())) {
114+
try (final InputStream in = new ByteArrayInputStream(serialization.getBytes(UTF_8))) {
115115
// TODO process as JSON-LD
116116
final Map<String, Object> data = jsonService.fromJson(in,
117117
new HashMap<String, Object>(){}.getClass().getGenericSuperclass());
118118

119119
final List<Map<String, Object>> vcs = getCredentialsFromPresentation(data, supportedTypes);
120-
if (vcs.size() != 1) {
120+
if (vcs.size() != SINGLETON) {
121121
throw new IllegalArgumentException(
122122
"Invalid Access Denial: ambiguous number of verifiable credentials");
123123
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,13 +111,13 @@ static Set<String> getSupportedTypes() {
111111
}
112112

113113
static AccessGrant parse(final String serialization) throws IOException {
114-
try (final InputStream in = new ByteArrayInputStream(serialization.getBytes())) {
114+
try (final InputStream in = new ByteArrayInputStream(serialization.getBytes(UTF_8))) {
115115
// TODO process as JSON-LD
116116
final Map<String, Object> data = jsonService.fromJson(in,
117117
new HashMap<String, Object>(){}.getClass().getGenericSuperclass());
118118

119119
final List<Map<String, Object>> vcs = getCredentialsFromPresentation(data, supportedTypes);
120-
if (vcs.size() != 1) {
120+
if (vcs.size() != SINGLETON) {
121121
throw new IllegalArgumentException(
122122
"Invalid Access Grant: ambiguous number of verifiable credentials");
123123
}

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

Lines changed: 82 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
import java.util.Set;
5656
import java.util.concurrent.CompletableFuture;
5757
import java.util.concurrent.CompletionStage;
58+
import java.util.function.Function;
5859

5960
import org.slf4j.Logger;
6061
import org.slf4j.LoggerFactory;
@@ -105,6 +106,7 @@ public class AccessGrantClient {
105106
private static final String IS_PROVIDED_TO = "isProvidedTo";
106107
private static final String IS_CONSENT_FOR_DATA_SUBJECT = "isConsentForDataSubject";
107108
private static final String FOR_PERSONAL_DATA = "forPersonalData";
109+
private static final String TEMPLATE = "template";
108110
private static final String HAS_STATUS = "hasStatus";
109111
private static final String REQUEST = "request";
110112
private static final String VERIFIED_REQUEST = "verifiedRequest";
@@ -183,7 +185,10 @@ private AccessGrantClient(final Client client, final ClientCache<URI, Metadata>
183185
this.config = Objects.requireNonNull(config, "config may not be null!");
184186
this.metadataCache = Objects.requireNonNull(metadataCache, "metadataCache may not be null!");
185187
this.jsonService = ServiceProvider.getJsonService();
186-
LOGGER.debug("Initializing Access Grant client with issuer: {}", config.getIssuer());
188+
LOGGER.atDebug()
189+
.setMessage("Initializing Access Grant client with issuer: {}")
190+
.addArgument(config::getIssuer)
191+
.log();
187192
}
188193

189194
/**
@@ -204,7 +209,7 @@ public AccessGrantClient session(final Session session) {
204209
* @return the next stage of completion containing the resulting access request
205210
*/
206211
public CompletionStage<AccessRequest> requestAccess(final AccessRequest.RequestParameters request) {
207-
return requestAccess(request.getRecipient(), request.getResources(),
212+
return requestAccess(request.getRecipient(), request.getResources(), request.getTemplates(),
208213
request.getModes(), request.getPurposes(), request.getExpiration(), request.getIssuedAt());
209214
}
210215

@@ -220,16 +225,23 @@ public CompletionStage<AccessRequest> requestAccess(final AccessRequest.RequestP
220225
*/
221226
public CompletionStage<AccessRequest> requestAccess(final URI recipient, final Set<URI> resources,
222227
final Set<String> modes, final Set<URI> purposes, final Instant expiration) {
223-
return requestAccess(recipient, resources, modes, purposes, expiration, null);
228+
return requestAccess(recipient, resources, Collections.emptySet(), modes, purposes, expiration, null);
224229
}
225230

226231
private CompletionStage<AccessRequest> requestAccess(final URI recipient, final Set<URI> resources,
227-
final Set<String> modes, final Set<URI> purposes, final Instant expiration, final Instant issuance) {
232+
final Set<String> templates, final Set<String> modes, final Set<URI> purposes, final Instant expiration,
233+
final Instant issuance) {
228234
Objects.requireNonNull(resources, "Resources may not be null!");
235+
Objects.requireNonNull(templates, "Templates may not be null!");
229236
Objects.requireNonNull(modes, "Access modes may not be null!");
237+
if (templates.isEmpty() && resources.isEmpty()) {
238+
LOGGER.warn("Both resources and templates are empty in access request");
239+
} else if (!templates.isEmpty() && !resources.isEmpty()) {
240+
LOGGER.warn("Both resources and templates are non-empty in access request");
241+
}
230242
return v1Metadata().thenCompose(metadata -> {
231-
final Map<String, Object> data = buildAccessRequestv1(recipient, resources, modes, purposes, expiration,
232-
issuance);
243+
final Map<String, Object> data = buildAccessRequestv1(recipient, resources, templates, modes, purposes,
244+
expiration, issuance);
233245

234246
final Request req = Request.newBuilder(metadata.issueEndpoint)
235247
.header(CONTENT_TYPE, APPLICATION_JSON)
@@ -253,26 +265,79 @@ private CompletionStage<AccessRequest> requestAccess(final URI recipient, final
253265
}
254266

255267
/**
256-
* Issue an access grant based on an access request. The access request is not verified.
268+
* Issue an access grant based on an access request.
269+
*
270+
* <p>
271+
* The access request is not verified.
272+
* Any templated URLs are ignored.
257273
*
258274
* @param request the access request
259275
* @return the next stage of completion containing the issued access grant
260276
*/
261277
public CompletionStage<AccessGrant> grantAccess(final AccessRequest request) {
262-
return grantAccess(request, false);
278+
return grantAccess(request, templates -> Collections.emptySet(), false);
279+
}
280+
281+
/**
282+
* Issue an access grant based on an access request.
283+
*
284+
* <p>
285+
* The access request is verified.
286+
* Any templated URLs are processed according to the provided mapping function.
287+
*
288+
* @param request the access request
289+
* @param mapping a mapping function for template URLs
290+
* @return the next stage of completion containing the issued access grant
291+
*/
292+
public CompletionStage<AccessGrant> grantAccess(final AccessRequest request,
293+
final Function<Set<String>, Set<URI>> mapping) {
294+
return grantAccess(request, mapping, true);
263295
}
264296

265297
/**
266298
* Issue an access grant based on an access request.
267299
*
300+
* <p>
301+
* Any templated URLs are ignored.
302+
*
268303
* @param request the access request
269304
* @param verifyRequest whether the request should be verified before issuing the access grant
270305
* @return the next stage of completion containing the issued access grant
271306
*/
272307
public CompletionStage<AccessGrant> grantAccess(final AccessRequest request, final boolean verifyRequest) {
308+
return grantAccess(request, templates -> Collections.emptySet(), verifyRequest);
309+
}
310+
311+
/**
312+
* Issue an access grant based on an access request.
313+
*
314+
* @param request the access request
315+
* @param mapping a mapping function for template URLs
316+
* @param verifyRequest whether the request should be verified before issuing the access grant
317+
* @return the next stage of completion containing the issued access grant
318+
*/
319+
public CompletionStage<AccessGrant> grantAccess(final AccessRequest request,
320+
final Function<Set<String>, Set<URI>> mapping, final boolean verifyRequest) {
273321
Objects.requireNonNull(request, "Request may not be null!");
322+
final var templated = mapping.apply(request.getTemplates());
323+
if (templated.size() != request.getTemplates().size()) {
324+
LOGGER.atDebug()
325+
.setMessage("Unexpected number of mapped template values, found ({}) expected ({})")
326+
.addArgument(templated::size)
327+
.addArgument(() -> request.getTemplates().size())
328+
.log();
329+
}
330+
final var resources = new HashSet<URI>(request.getResources());
331+
resources.addAll(templated);
332+
if (resources.isEmpty()) {
333+
LOGGER.atWarn()
334+
.setMessage("No data URLs supplied: {} resource URLs and {} mapped templates")
335+
.addArgument(() -> request.getResources().size())
336+
.addArgument(templated::size)
337+
.log();
338+
}
274339
return v1Metadata().thenCompose(metadata -> {
275-
final Map<String, Object> data = buildAccessGrantv1(request.getCreator(), request.getResources(),
340+
final Map<String, Object> data = buildAccessGrantv1(request.getCreator(), resources,
276341
request.getModes(), request.getPurposes(), request.getExpiration(), request.getIssuedAt(),
277342
request.getIdentifier(), verifyRequest);
278343
final Request req = Request.newBuilder(metadata.issueEndpoint)
@@ -815,12 +880,17 @@ static Map<String, Object> buildAccessGrantv1(
815880
return data;
816881
}
817882

818-
static Map<String, Object> buildAccessRequestv1(final URI agent, final Set<URI> resources, final Set<String> modes,
819-
final Set<URI> purposes, final Instant expiration, final Instant issuance) {
883+
static Map<String, Object> buildAccessRequestv1(final URI agent, final Set<URI> resources,
884+
final Set<String> templates, final Set<String> modes, final Set<URI> purposes,
885+
final Instant expiration, final Instant issuance) {
820886
final Map<String, Object> consent = new HashMap<>();
821887
consent.put(HAS_STATUS, "https://w3id.org/GConsent#ConsentStatusRequested");
822888
consent.put(MODE, modes);
823-
consent.put(FOR_PERSONAL_DATA, resources);
889+
if (!resources.isEmpty()) {
890+
consent.put(FOR_PERSONAL_DATA, resources);
891+
} else {
892+
consent.put(TEMPLATE, templates);
893+
}
824894
if (agent != null) {
825895
consent.put(IS_CONSENT_FOR_DATA_SUBJECT, agent);
826896
}

0 commit comments

Comments
 (0)