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
@@ -0,0 +1,214 @@
/*
* Copyright 2023 Inrupt Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
* PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.inrupt.client.accessgrant;

import static com.inrupt.client.accessgrant.Utils.*;
import static java.nio.charset.StandardCharsets.UTF_8;

import com.inrupt.client.spi.JsonService;
import com.inrupt.client.spi.ServiceProvider;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.time.Instant;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import org.apache.commons.io.IOUtils;

/**
* An Access Denial abstraction, for use when interacting with Solid resources.
*/
public class AccessDenial implements AccessCredential {

private static final String TYPE = "type";
private static final String REVOCATION_LIST_2020_STATUS = "RevocationList2020Status";
private static final Set<String> supportedTypes = getSupportedTypes();
private static final JsonService jsonService = ServiceProvider.getJsonService();

private final String credential;
private final URI issuer;
private final URI identifier;
private final Set<String> types;
private final Set<String> purposes;
private final Set<String> modes;
private final Set<URI> resources;
private final URI recipient;
private final URI creator;
private final Instant expiration;
private final Status status;

/**
* Read a verifiable presentation as an AccessDenial.
*
* @param serialization the Access Denial serialized as a verifiable presentation
*/
protected AccessDenial(final String serialization) throws IOException {
try (final InputStream in = new ByteArrayInputStream(serialization.getBytes())) {
// TODO process as JSON-LD
final Map<String, Object> data = jsonService.fromJson(in,
new HashMap<String, Object>(){}.getClass().getGenericSuperclass());

final List<Map> vcs = getCredentialsFromPresentation(data, supportedTypes);
if (vcs.size() != 1) {
throw new IllegalArgumentException(
"Invalid Access Denial: ambiguous number of verifiable credentials");
}
final Map vc = vcs.get(0);

if (asSet(data.get(TYPE)).orElseGet(Collections::emptySet).contains("VerifiablePresentation")) {
this.credential = serialization;
this.issuer = asUri(vc.get("issuer")).orElseThrow(() ->
new IllegalArgumentException("Missing or invalid issuer field"));
this.identifier = asUri(vc.get("id")).orElseThrow(() ->
new IllegalArgumentException("Missing or invalid id field"));

this.types = asSet(vc.get(TYPE)).orElseGet(Collections::emptySet);
this.expiration = asInstant(vc.get("expirationDate")).orElse(Instant.MAX);

final Map subject = asMap(vc.get("credentialSubject")).orElseThrow(() ->
new IllegalArgumentException("Missing or invalid credentialSubject field"));

this.creator = asUri(subject.get("id")).orElseThrow(() ->
new IllegalArgumentException("Missing or invalid credentialSubject.id field"));

// V1 Access Denial, using gConsent
final Map consent = asMap(subject.get("providedConsent")).orElseThrow(() ->
// Unsupported structure
new IllegalArgumentException("Invalid Access Denial: missing consent clause"));

final Optional<URI> person = asUri(consent.get("isProvidedToPerson"));
final Optional<URI> controller = asUri(consent.get("isProvidedToController"));
final Optional<URI> other = asUri(consent.get("isProvidedTo"));

this.recipient = person.orElseGet(() -> controller.orElseGet(() -> other.orElse(null)));
this.modes = asSet(consent.get("mode")).orElseGet(Collections::emptySet);
this.resources = asSet(consent.get("forPersonalData")).orElseGet(Collections::emptySet)
.stream().map(URI::create).collect(Collectors.toSet());
this.purposes = asSet(consent.get("forPurpose")).orElseGet(Collections::emptySet);
this.status = asMap(vc.get("credentialStatus")).flatMap(credentialStatus ->
asSet(credentialStatus.get(TYPE)).filter(statusTypes ->
statusTypes.contains(REVOCATION_LIST_2020_STATUS)).map(x ->
asRevocationList2020(credentialStatus))).orElse(null);
} else {
throw new IllegalArgumentException("Invalid Access Denial: missing VerifiablePresentation type");
}
}
}

/**
* Create an AccessDenial object from a serialized form.
*
* @param serialization the serialized access denial
* @return a parsed access denial
*/
public static AccessDenial of(final String serialization) {
try {
return new AccessDenial(serialization);
} catch (final IOException ex) {
throw new IllegalArgumentException("Unable to read access denial", ex);
}
}

/**
* Create an AccessDenial object from a serialized form.
*
* @param serialization the serialized access denial
* @return a parsed access denial
*/
public static AccessDenial of(final InputStream serialization) {
try {
return of(IOUtils.toString(serialization, UTF_8));
} catch (final IOException ex) {
throw new IllegalArgumentException("Unable to read access denial", ex);
}
}

@Override
public Set<String> getTypes() {
return types;
}

@Override
public Set<String> getModes() {
return modes;
}

@Override
public Optional<Status> getStatus() {
return Optional.ofNullable(status);
}

@Override
public Instant getExpiration() {
return expiration;
}

@Override
public URI getIssuer() {
return issuer;
}

@Override
public URI getIdentifier() {
return identifier;
}

@Override
public Set<String> getPurposes() {
return purposes;
}

@Override
public Set<URI> getResources() {
return resources;
}

@Override
public URI getCreator() {
return creator;
}

@Override
public Optional<URI> getRecipient() {
return Optional.ofNullable(recipient);
}

@Override
public String serialize() {
return credential;
}

static Set<String> getSupportedTypes() {
final Set<String> types = new HashSet<>();
types.add("SolidAccessDenial");
types.add("http://www.w3.org/ns/solid/vc#SolidAccessDenial");
return Collections.unmodifiableSet(types);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
import org.apache.commons.io.IOUtils;

/**
* An Access Grant abstraction, for use with interacting with Solid resources.
* An Access Grant abstraction, for use when interacting with Solid resources.
*/
public class AccessGrant implements AccessCredential {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,10 @@ public class AccessGrantClient {
private static final String MODE = "mode";
private static final URI ACCESS_GRANT = URI.create("http://www.w3.org/ns/solid/vc#SolidAccessGrant");
private static final URI ACCESS_REQUEST = URI.create("http://www.w3.org/ns/solid/vc#SolidAccessRequest");
private static final URI ACCESS_DENIAL = URI.create("http://www.w3.org/ns/solid/vc#SolidAccessDenial");
private static final Set<String> ACCESS_GRANT_TYPES = getAccessGrantTypes();
private static final Set<String> ACCESS_REQUEST_TYPES = getAccessRequestTypes();
private static final Set<String> ACCESS_DENIAL_TYPES = getAccessDenialTypes();

private final Client client;
private final ClientCache<URI, Metadata> metadataCache;
Expand Down Expand Up @@ -192,11 +194,11 @@ public CompletionStage<AccessRequest> requestAccess(final URI agent, final Set<U
if (isSuccess(status)) {
return processVerifiableCredential(input, ACCESS_REQUEST_TYPES, AccessRequest.class);
}
throw new AccessGrantException("Unable to issue Access Grant: HTTP error " + status,
throw new AccessGrantException("Unable to issue Access Request: HTTP error " + status,
status);
} catch (final IOException ex) {
throw new AccessGrantException(
"Unexpected I/O exception while processing Access Grant", ex);
"Unexpected I/O exception while processing Access Request", ex);
}
});
});
Expand Down Expand Up @@ -234,6 +236,38 @@ public CompletionStage<AccessGrant> grantAccess(final AccessRequest request) {
});
}

/**
* Issue an access denial receipt based on an access request.
*
* @param request the access request
* @return the next stage of completion containing the issued access denial
*/
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.getExpiration(), request.getPurposes());
final Request req = Request.newBuilder(metadata.issueEndpoint)
.header(CONTENT_TYPE, APPLICATION_JSON)
.POST(Request.BodyPublishers.ofByteArray(serialize(data))).build();

return client.send(req, Response.BodyHandlers.ofInputStream())
.thenApply(res -> {
try (final InputStream input = res.body()) {
final int status = res.statusCode();
if (isSuccess(status)) {
return processVerifiableCredential(input, ACCESS_DENIAL_TYPES, AccessDenial.class);
}
throw new AccessGrantException("Unable to issue Access Denial: HTTP error " + status,
status);
} catch (final IOException ex) {
throw new AccessGrantException(
"Unexpected I/O exception while processing Access Denial", ex);
}
});
});
}

/**
* Issue an access grant or request.
*
Expand Down Expand Up @@ -511,6 +545,8 @@ public <T extends AccessCredential> CompletionStage<T> fetch(final URI identifie
return (T) processVerifiableCredential(input, ACCESS_GRANT_TYPES, clazz);
} else if (AccessRequest.class.equals(clazz)) {
return (T) processVerifiableCredential(input, ACCESS_REQUEST_TYPES, clazz);
} else if (AccessDenial.class.equals(clazz)) {
return (T) processVerifiableCredential(input, ACCESS_DENIAL_TYPES, clazz);
}
throw new AccessGrantException("Unable to fetch credential as " + clazz);
}
Expand Down Expand Up @@ -541,6 +577,8 @@ <T extends AccessCredential> T processVerifiableCredential(final InputStream inp
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));
}
}
throw new AccessGrantException("Invalid Access Grant: missing supported type");
Expand Down Expand Up @@ -693,6 +731,33 @@ 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 Instant expiration, final Set<String> purposes) {
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_PERSON, agent);
if (!purposes.isEmpty()) {
consent.put("forPurpose", purposes);
}

final Map<String, Object> subject = new HashMap<>();
subject.put("providedConsent", consent);

final Map<String, Object> credential = new HashMap<>();
credential.put(CONTEXT, Arrays.asList(VC_CONTEXT_URI, INRUPT_CONTEXT_URI));
if (expiration != null) {
credential.put("expirationDate", expiration.truncatedTo(ChronoUnit.SECONDS).toString());
}
credential.put(CREDENTIAL_SUBJECT, subject);

final Map<String, Object> data = new HashMap<>();
data.put("credential", credential);
return data;
}

static Map<String, Object> buildAccessGrantv1(final URI agent, final Set<URI> resources, final Set<String> modes,
final Instant expiration, final Set<String> purposes) {
Objects.requireNonNull(agent, "Access grant agent may not be null!");
Expand Down Expand Up @@ -766,6 +831,13 @@ static Set<String> getAccessGrantTypes() {
return Collections.unmodifiableSet(types);
}

static Set<String> getAccessDenialTypes() {
final Set<String> types = new HashSet<>();
types.add("SolidAccessDenial");
types.add(ACCESS_DENIAL.toString());
return Collections.unmodifiableSet(types);
}

static boolean isAccessGrant(final URI type) {
return "SolidAccessGrant".equals(type.toString()) || ACCESS_GRANT.equals(type);
}
Expand Down
Loading