Skip to content

Commit cae253c

Browse files
authored
JCL-359: Add denyAccess method to AccessGrantClient (#476)
1 parent 8d2d4a8 commit cae253c

File tree

8 files changed

+421
-13
lines changed

8 files changed

+421
-13
lines changed
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
/*
2+
* Copyright 2023 Inrupt Inc.
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy
5+
* of this software and associated documentation files (the "Software"), to deal in
6+
* the Software without restriction, including without limitation the rights to use,
7+
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
8+
* Software, and to permit persons to whom the Software is furnished to do so,
9+
* subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in
12+
* all copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
15+
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
16+
* PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
17+
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
18+
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
19+
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20+
*/
21+
package com.inrupt.client.accessgrant;
22+
23+
import static com.inrupt.client.accessgrant.Utils.*;
24+
import static java.nio.charset.StandardCharsets.UTF_8;
25+
26+
import com.inrupt.client.spi.JsonService;
27+
import com.inrupt.client.spi.ServiceProvider;
28+
29+
import java.io.ByteArrayInputStream;
30+
import java.io.IOException;
31+
import java.io.InputStream;
32+
import java.net.URI;
33+
import java.time.Instant;
34+
import java.util.Collections;
35+
import java.util.HashMap;
36+
import java.util.HashSet;
37+
import java.util.List;
38+
import java.util.Map;
39+
import java.util.Optional;
40+
import java.util.Set;
41+
import java.util.stream.Collectors;
42+
43+
import org.apache.commons.io.IOUtils;
44+
45+
/**
46+
* An Access Denial abstraction, for use when interacting with Solid resources.
47+
*/
48+
public class AccessDenial implements AccessCredential {
49+
50+
private static final String TYPE = "type";
51+
private static final String REVOCATION_LIST_2020_STATUS = "RevocationList2020Status";
52+
private static final Set<String> supportedTypes = getSupportedTypes();
53+
private static final JsonService jsonService = ServiceProvider.getJsonService();
54+
55+
private final String credential;
56+
private final URI issuer;
57+
private final URI identifier;
58+
private final Set<String> types;
59+
private final Set<String> purposes;
60+
private final Set<String> modes;
61+
private final Set<URI> resources;
62+
private final URI recipient;
63+
private final URI creator;
64+
private final Instant expiration;
65+
private final Status status;
66+
67+
/**
68+
* Read a verifiable presentation as an AccessDenial.
69+
*
70+
* @param serialization the Access Denial serialized as a verifiable presentation
71+
*/
72+
protected AccessDenial(final String serialization) throws IOException {
73+
try (final InputStream in = new ByteArrayInputStream(serialization.getBytes())) {
74+
// TODO process as JSON-LD
75+
final Map<String, Object> data = jsonService.fromJson(in,
76+
new HashMap<String, Object>(){}.getClass().getGenericSuperclass());
77+
78+
final List<Map> vcs = getCredentialsFromPresentation(data, supportedTypes);
79+
if (vcs.size() != 1) {
80+
throw new IllegalArgumentException(
81+
"Invalid Access Denial: ambiguous number of verifiable credentials");
82+
}
83+
final Map vc = vcs.get(0);
84+
85+
if (asSet(data.get(TYPE)).orElseGet(Collections::emptySet).contains("VerifiablePresentation")) {
86+
this.credential = serialization;
87+
this.issuer = asUri(vc.get("issuer")).orElseThrow(() ->
88+
new IllegalArgumentException("Missing or invalid issuer field"));
89+
this.identifier = asUri(vc.get("id")).orElseThrow(() ->
90+
new IllegalArgumentException("Missing or invalid id field"));
91+
92+
this.types = asSet(vc.get(TYPE)).orElseGet(Collections::emptySet);
93+
this.expiration = asInstant(vc.get("expirationDate")).orElse(Instant.MAX);
94+
95+
final Map subject = asMap(vc.get("credentialSubject")).orElseThrow(() ->
96+
new IllegalArgumentException("Missing or invalid credentialSubject field"));
97+
98+
this.creator = asUri(subject.get("id")).orElseThrow(() ->
99+
new IllegalArgumentException("Missing or invalid credentialSubject.id field"));
100+
101+
// V1 Access Denial, using gConsent
102+
final Map consent = asMap(subject.get("providedConsent")).orElseThrow(() ->
103+
// Unsupported structure
104+
new IllegalArgumentException("Invalid Access Denial: missing consent clause"));
105+
106+
final Optional<URI> person = asUri(consent.get("isProvidedToPerson"));
107+
final Optional<URI> controller = asUri(consent.get("isProvidedToController"));
108+
final Optional<URI> other = asUri(consent.get("isProvidedTo"));
109+
110+
this.recipient = person.orElseGet(() -> controller.orElseGet(() -> other.orElse(null)));
111+
this.modes = asSet(consent.get("mode")).orElseGet(Collections::emptySet);
112+
this.resources = asSet(consent.get("forPersonalData")).orElseGet(Collections::emptySet)
113+
.stream().map(URI::create).collect(Collectors.toSet());
114+
this.purposes = asSet(consent.get("forPurpose")).orElseGet(Collections::emptySet);
115+
this.status = asMap(vc.get("credentialStatus")).flatMap(credentialStatus ->
116+
asSet(credentialStatus.get(TYPE)).filter(statusTypes ->
117+
statusTypes.contains(REVOCATION_LIST_2020_STATUS)).map(x ->
118+
asRevocationList2020(credentialStatus))).orElse(null);
119+
} else {
120+
throw new IllegalArgumentException("Invalid Access Denial: missing VerifiablePresentation type");
121+
}
122+
}
123+
}
124+
125+
/**
126+
* Create an AccessDenial object from a serialized form.
127+
*
128+
* @param serialization the serialized access denial
129+
* @return a parsed access denial
130+
*/
131+
public static AccessDenial of(final String serialization) {
132+
try {
133+
return new AccessDenial(serialization);
134+
} catch (final IOException ex) {
135+
throw new IllegalArgumentException("Unable to read access denial", ex);
136+
}
137+
}
138+
139+
/**
140+
* Create an AccessDenial object from a serialized form.
141+
*
142+
* @param serialization the serialized access denial
143+
* @return a parsed access denial
144+
*/
145+
public static AccessDenial of(final InputStream serialization) {
146+
try {
147+
return of(IOUtils.toString(serialization, UTF_8));
148+
} catch (final IOException ex) {
149+
throw new IllegalArgumentException("Unable to read access denial", ex);
150+
}
151+
}
152+
153+
@Override
154+
public Set<String> getTypes() {
155+
return types;
156+
}
157+
158+
@Override
159+
public Set<String> getModes() {
160+
return modes;
161+
}
162+
163+
@Override
164+
public Optional<Status> getStatus() {
165+
return Optional.ofNullable(status);
166+
}
167+
168+
@Override
169+
public Instant getExpiration() {
170+
return expiration;
171+
}
172+
173+
@Override
174+
public URI getIssuer() {
175+
return issuer;
176+
}
177+
178+
@Override
179+
public URI getIdentifier() {
180+
return identifier;
181+
}
182+
183+
@Override
184+
public Set<String> getPurposes() {
185+
return purposes;
186+
}
187+
188+
@Override
189+
public Set<URI> getResources() {
190+
return resources;
191+
}
192+
193+
@Override
194+
public URI getCreator() {
195+
return creator;
196+
}
197+
198+
@Override
199+
public Optional<URI> getRecipient() {
200+
return Optional.ofNullable(recipient);
201+
}
202+
203+
@Override
204+
public String serialize() {
205+
return credential;
206+
}
207+
208+
static Set<String> getSupportedTypes() {
209+
final Set<String> types = new HashSet<>();
210+
types.add("SolidAccessDenial");
211+
types.add("http://www.w3.org/ns/solid/vc#SolidAccessDenial");
212+
return Collections.unmodifiableSet(types);
213+
}
214+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
import org.apache.commons.io.IOUtils;
4444

4545
/**
46-
* An Access Grant abstraction, for use with interacting with Solid resources.
46+
* An Access Grant abstraction, for use when interacting with Solid resources.
4747
*/
4848
public class AccessGrant implements AccessCredential {
4949

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

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,10 @@ public class AccessGrantClient {
9999
private static final String MODE = "mode";
100100
private static final URI ACCESS_GRANT = URI.create("http://www.w3.org/ns/solid/vc#SolidAccessGrant");
101101
private static final URI ACCESS_REQUEST = URI.create("http://www.w3.org/ns/solid/vc#SolidAccessRequest");
102+
private static final URI ACCESS_DENIAL = URI.create("http://www.w3.org/ns/solid/vc#SolidAccessDenial");
102103
private static final Set<String> ACCESS_GRANT_TYPES = getAccessGrantTypes();
103104
private static final Set<String> ACCESS_REQUEST_TYPES = getAccessRequestTypes();
105+
private static final Set<String> ACCESS_DENIAL_TYPES = getAccessDenialTypes();
104106

105107
private final Client client;
106108
private final ClientCache<URI, Metadata> metadataCache;
@@ -192,11 +194,11 @@ public CompletionStage<AccessRequest> requestAccess(final URI agent, final Set<U
192194
if (isSuccess(status)) {
193195
return processVerifiableCredential(input, ACCESS_REQUEST_TYPES, AccessRequest.class);
194196
}
195-
throw new AccessGrantException("Unable to issue Access Grant: HTTP error " + status,
197+
throw new AccessGrantException("Unable to issue Access Request: HTTP error " + status,
196198
status);
197199
} catch (final IOException ex) {
198200
throw new AccessGrantException(
199-
"Unexpected I/O exception while processing Access Grant", ex);
201+
"Unexpected I/O exception while processing Access Request", ex);
200202
}
201203
});
202204
});
@@ -234,6 +236,38 @@ public CompletionStage<AccessGrant> grantAccess(final AccessRequest request) {
234236
});
235237
}
236238

239+
/**
240+
* Issue an access denial receipt based on an access request.
241+
*
242+
* @param request the access request
243+
* @return the next stage of completion containing the issued access denial
244+
*/
245+
public CompletionStage<AccessDenial> denyAccess(final AccessRequest request) {
246+
Objects.requireNonNull(request, "Request may not be null!");
247+
return v1Metadata().thenCompose(metadata -> {
248+
final Map<String, Object> data = buildAccessDenialv1(request.getCreator(), request.getResources(),
249+
request.getModes(), request.getExpiration(), request.getPurposes());
250+
final Request req = Request.newBuilder(metadata.issueEndpoint)
251+
.header(CONTENT_TYPE, APPLICATION_JSON)
252+
.POST(Request.BodyPublishers.ofByteArray(serialize(data))).build();
253+
254+
return client.send(req, Response.BodyHandlers.ofInputStream())
255+
.thenApply(res -> {
256+
try (final InputStream input = res.body()) {
257+
final int status = res.statusCode();
258+
if (isSuccess(status)) {
259+
return processVerifiableCredential(input, ACCESS_DENIAL_TYPES, AccessDenial.class);
260+
}
261+
throw new AccessGrantException("Unable to issue Access Denial: HTTP error " + status,
262+
status);
263+
} catch (final IOException ex) {
264+
throw new AccessGrantException(
265+
"Unexpected I/O exception while processing Access Denial", ex);
266+
}
267+
});
268+
});
269+
}
270+
237271
/**
238272
* Issue an access grant or request.
239273
*
@@ -511,6 +545,8 @@ public <T extends AccessCredential> CompletionStage<T> fetch(final URI identifie
511545
return (T) processVerifiableCredential(input, ACCESS_GRANT_TYPES, clazz);
512546
} else if (AccessRequest.class.equals(clazz)) {
513547
return (T) processVerifiableCredential(input, ACCESS_REQUEST_TYPES, clazz);
548+
} else if (AccessDenial.class.equals(clazz)) {
549+
return (T) processVerifiableCredential(input, ACCESS_DENIAL_TYPES, clazz);
514550
}
515551
throw new AccessGrantException("Unable to fetch credential as " + clazz);
516552
}
@@ -541,6 +577,8 @@ <T extends AccessCredential> T processVerifiableCredential(final InputStream inp
541577
return (T) AccessGrant.of(new String(serialize(presentation), UTF_8));
542578
} else if (AccessRequest.class.isAssignableFrom(clazz)) {
543579
return (T) AccessRequest.of(new String(serialize(presentation), UTF_8));
580+
} else if (AccessDenial.class.isAssignableFrom(clazz)) {
581+
return (T) AccessDenial.of(new String(serialize(presentation), UTF_8));
544582
}
545583
}
546584
throw new AccessGrantException("Invalid Access Grant: missing supported type");
@@ -693,6 +731,33 @@ static URI asUri(final Object value) {
693731
return null;
694732
}
695733

734+
static Map<String, Object> buildAccessDenialv1(final URI agent, final Set<URI> resources, final Set<String> modes,
735+
final Instant expiration, final Set<String> purposes) {
736+
Objects.requireNonNull(agent, "Access denial agent may not be null!");
737+
final Map<String, Object> consent = new HashMap<>();
738+
consent.put(MODE, modes);
739+
consent.put(HAS_STATUS, "https://w3id.org/GConsent#ConsentStatusRefused");
740+
consent.put(FOR_PERSONAL_DATA, resources);
741+
consent.put(IS_PROVIDED_TO_PERSON, agent);
742+
if (!purposes.isEmpty()) {
743+
consent.put("forPurpose", purposes);
744+
}
745+
746+
final Map<String, Object> subject = new HashMap<>();
747+
subject.put("providedConsent", consent);
748+
749+
final Map<String, Object> credential = new HashMap<>();
750+
credential.put(CONTEXT, Arrays.asList(VC_CONTEXT_URI, INRUPT_CONTEXT_URI));
751+
if (expiration != null) {
752+
credential.put("expirationDate", expiration.truncatedTo(ChronoUnit.SECONDS).toString());
753+
}
754+
credential.put(CREDENTIAL_SUBJECT, subject);
755+
756+
final Map<String, Object> data = new HashMap<>();
757+
data.put("credential", credential);
758+
return data;
759+
}
760+
696761
static Map<String, Object> buildAccessGrantv1(final URI agent, final Set<URI> resources, final Set<String> modes,
697762
final Instant expiration, final Set<String> purposes) {
698763
Objects.requireNonNull(agent, "Access grant agent may not be null!");
@@ -766,6 +831,13 @@ static Set<String> getAccessGrantTypes() {
766831
return Collections.unmodifiableSet(types);
767832
}
768833

834+
static Set<String> getAccessDenialTypes() {
835+
final Set<String> types = new HashSet<>();
836+
types.add("SolidAccessDenial");
837+
types.add(ACCESS_DENIAL.toString());
838+
return Collections.unmodifiableSet(types);
839+
}
840+
769841
static boolean isAccessGrant(final URI type) {
770842
return "SolidAccessGrant".equals(type.toString()) || ACCESS_GRANT.equals(type);
771843
}

0 commit comments

Comments
 (0)