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 @@ -65,15 +65,14 @@
* {@link Session} object, typically an OpenID-based session:
*
* <pre>{@code
URI SOLID_ACCESS_GRANT = URI.create("http://www.w3.org/ns/solid/vc#SolidAccessGrant");
URI issuer = URI.create("https://issuer.example");
Session openid = OpenIdSession.ofIdToken(idToken);

AccessGrantClient client = new AccessGrantClient(issuer).session(session);

URI resource = URI.create("https://storage.example/data/resource");
URI purpose = URI.create("https://purpose.example/1");
client.query(null, resource, purpose, "Read", AccessGrant.class)
client.query(resource, null, openid.getPrincipal().orElse(null), purpose, "Read", AccessGrant.class)
.thenApply(grants -> AccessGrantSession.ofAccessGrant(openid, grants.toArray(new AccessGrant[0])))
.thenApply(session -> SolidClient.getClient().session(session))
.thenAccept(cl -> {
Expand All @@ -89,6 +88,7 @@ public class AccessGrantClient {
private static final String VC_CONTEXT_URI = "https://www.w3.org/2018/credentials/v1";
private static final String INRUPT_CONTEXT_URI = "https://schema.inrupt.com/credentials/v1.jsonld";
private static final String VERIFIABLE_CREDENTIAL = "verifiableCredential";
private static final String SOLID_VC_NAMESPACE = "http://www.w3.org/ns/solid/vc#";
private static final String TYPE = "type";
private static final String APPLICATION_JSON = "application/json";
private static final String CONTENT_TYPE = "Content-Type";
Expand All @@ -102,9 +102,12 @@ public class AccessGrantClient {
private static final String FOR_PURPOSE = "forPurpose";
private static final String EXPIRATION_DATE = "expirationDate";
private static final String CREDENTIAL = "credential";
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 String SOLID_ACCESS_GRANT = "SolidAccessGrant";
private static final String SOLID_ACCESS_REQUEST = "SolidAccessRequest";
private static final String SOLID_ACCESS_DENIAL = "SolidAccessDenial";
private static final URI FQ_ACCESS_GRANT = URI.create(SOLID_VC_NAMESPACE + SOLID_ACCESS_GRANT);
private static final URI FQ_ACCESS_REQUEST = URI.create(SOLID_VC_NAMESPACE + SOLID_ACCESS_REQUEST);
private static final URI FQ_ACCESS_DENIAL = URI.create(SOLID_VC_NAMESPACE + SOLID_ACCESS_DENIAL);
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();
Expand Down Expand Up @@ -301,9 +304,9 @@ public CompletionStage<AccessGrant> issue(final URI type, final URI agent, final
}
return v1Metadata().thenCompose(metadata -> {
final Map<String, Object> data;
if (ACCESS_GRANT.equals(type)) {
if (FQ_ACCESS_GRANT.equals(type)) {
data = buildAccessGrantv1(agent, resources, modes, expiration, uriPurposes);
} else if (ACCESS_REQUEST.equals(type)) {
} else if (FQ_ACCESS_REQUEST.equals(type)) {
data = buildAccessRequestv1(agent, resources, modes, expiration, uriPurposes);
} else {
throw new AccessGrantException("Unsupported grant type: " + type);
Expand Down Expand Up @@ -377,35 +380,36 @@ public CompletionStage<AccessCredentialVerification> verify(final AccessCredenti
* Perform an Access Grant query.
*
* @param <T> the AccessCredential type
* @param agent the agent identifier, may be {@code null}
* @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 Grants
* @return the next stage of completion, including the matched Access Credentials
*/
public <T extends AccessCredential> CompletionStage<List<T>> query(final URI agent, final URI resource,
final URI purpose, final String mode, final Class<T> clazz) {
public <T extends AccessCredential> CompletionStage<List<T>> query(final URI resource, final URI creator,
final URI recipient, final URI purpose, final String mode, final Class<T> clazz) {
Objects.requireNonNull(clazz, "The clazz parameter must not be null!");

final URI type;
final Set<String> supportedTypes;
if (AccessGrant.class.isAssignableFrom(clazz)) {
type = URI.create("SolidAccessGrant");
type = URI.create(SOLID_ACCESS_GRANT);
supportedTypes = ACCESS_GRANT_TYPES;
} else if (AccessRequest.class.isAssignableFrom(clazz)) {
type = URI.create("SolidAccessRequest");
type = URI.create(SOLID_ACCESS_REQUEST);
supportedTypes = ACCESS_REQUEST_TYPES;
} else if (AccessDenial.class.isAssignableFrom(clazz)) {
type = URI.create("SolidAccessDenial");
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<CompletableFuture<List<T>>> futures = buildQuery(config.getIssuer(), type,
agent, resource, purpose, mode).stream()
resource, creator, recipient, purpose, mode).stream()
.map(data -> Request.newBuilder(metadata.queryEndpoint)
.header(CONTENT_TYPE, APPLICATION_JSON)
.POST(Request.BodyPublishers.ofByteArray(serialize(data))).build())
Expand Down Expand Up @@ -451,7 +455,7 @@ public CompletionStage<List<AccessGrant>> query(final URI type, final URI agent,
Objects.requireNonNull(type, "The type parameter must not be null!");
return v1Metadata().thenCompose(metadata -> {
final List<CompletableFuture<List<AccessGrant>>> futures = buildQuery(config.getIssuer(), type,
agent, resource, null, mode).stream()
resource, null, agent, null, mode).stream()
.map(data -> Request.newBuilder(metadata.queryEndpoint)
.header(CONTENT_TYPE, APPLICATION_JSON)
.POST(Request.BodyPublishers.ofByteArray(serialize(data))).build())
Expand Down Expand Up @@ -683,26 +687,26 @@ static Collection<Object> getCredentials(final Map<String, Object> data) {
return Collections.emptyList();
}

static List<Map<String, Object>> buildQuery(final URI issuer, final URI type, final URI agent, final URI resource,
final URI purpose, final String mode) {
static List<Map<String, Object>> buildQuery(final URI issuer, final URI type, final URI resource, final URI creator,
final URI recipient, final URI purpose, final String mode) {
final List<Map<String, Object>> queries = new ArrayList<>();
buildQuery(queries, issuer, type, agent, resource, purpose, mode);
buildQuery(queries, issuer, type, resource, creator, recipient, purpose, mode);
return queries;
}

static void buildQuery(final List<Map<String, Object>> queries, final URI issuer, final URI type, final URI agent,
final URI resource, final URI purpose, final String mode) {
static void buildQuery(final List<Map<String, Object>> queries, final URI issuer, final URI type,
final URI resource, final URI creator, final URI recipient, final URI purpose, final String mode) {
final Map<String, Object> credential = new HashMap<>();
credential.put(CONTEXT, Arrays.asList(VC_CONTEXT_URI, INRUPT_CONTEXT_URI));
credential.put("issuer", issuer);
credential.put(TYPE, Arrays.asList(type));

final Map<String, Object> consent = new HashMap<>();
if (agent != null) {
if (recipient != null) {
if (isAccessGrant(type) || isAccessDenial(type)) {
consent.put(IS_PROVIDED_TO, agent);
consent.put(IS_PROVIDED_TO, recipient);
} else if (isAccessRequest(type)) {
consent.put(IS_CONSENT_FOR_DATA_SUBJECT, agent);
consent.put(IS_CONSENT_FOR_DATA_SUBJECT, recipient);
}
}
if (resource != null) {
Expand All @@ -716,13 +720,18 @@ static void buildQuery(final List<Map<String, Object>> queries, final URI issuer
}

final Map<String, Object> subject = new HashMap<>();
if (creator != null) {
subject.put("id", creator);
}
if (!consent.isEmpty()) {
if (isAccessGrant(type) || isAccessDenial(type)) {
subject.put(PROVIDED_CONSENT, consent);
} else if (isAccessRequest(type)) {
subject.put("hasConsent", consent);
}
credential.put(CREDENTIAL_SUBJECT, subject);
} else if (!subject.isEmpty()) {
credential.put(CREDENTIAL_SUBJECT, subject);
}

final Map<String, Object> data = new HashMap<>();
Expand All @@ -733,7 +742,7 @@ static void buildQuery(final List<Map<String, Object>> queries, final URI issuer
// Recurse
final URI parent = getParent(resource);
if (parent != null) {
buildQuery(queries, issuer, type, agent, parent, purpose, mode);
buildQuery(queries, issuer, type, parent, creator, recipient, purpose, mode);
}
}

Expand Down Expand Up @@ -849,35 +858,35 @@ static boolean isSuccess(final int statusCode) {

static Set<String> getAccessRequestTypes() {
final Set<String> types = new HashSet<>();
types.add("SolidAccessRequest");
types.add(ACCESS_REQUEST.toString());
types.add(SOLID_ACCESS_REQUEST);
types.add(FQ_ACCESS_REQUEST.toString());
return Collections.unmodifiableSet(types);
}

static Set<String> getAccessGrantTypes() {
final Set<String> types = new HashSet<>();
types.add("SolidAccessGrant");
types.add(ACCESS_GRANT.toString());
types.add(SOLID_ACCESS_GRANT);
types.add(FQ_ACCESS_GRANT.toString());
return Collections.unmodifiableSet(types);
}

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

static boolean isAccessGrant(final URI type) {
return "SolidAccessGrant".equals(type.toString()) || ACCESS_GRANT.equals(type);
return SOLID_ACCESS_GRANT.equals(type.toString()) || FQ_ACCESS_GRANT.equals(type);
}

static boolean isAccessRequest(final URI type) {
return "SolidAccessRequest".equals(type.toString()) || ACCESS_REQUEST.equals(type);
return SOLID_ACCESS_REQUEST.equals(type.toString()) || FQ_ACCESS_REQUEST.equals(type);
}

static boolean isAccessDenial(final URI type) {
return "SolidAccessDenial".equals(type.toString()) || ACCESS_DENIAL.equals(type);
return SOLID_ACCESS_DENIAL.equals(type.toString()) || FQ_ACCESS_DENIAL.equals(type);
}

/**
Expand Down
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests referencing the notion of Agent (e.g. testQueryGrantAgent) could be renamed to the appropriate role, i.e. Recipient or Creator.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a very good idea. I'll create a ticket to make that adjustment in the tests

Original file line number Diff line number Diff line change
Expand Up @@ -508,9 +508,8 @@ void testQueryGrant() {
final String token = generateIdToken(claims);
final AccessGrantClient client = agClient.session(OpenIdSession.ofIdToken(token));

final List<AccessGrant> grants = client.query(null,
URI.create("https://storage.example/e973cc3d-5c28-4a10-98c5-e40079289358/a/b/c"), null, "Read",
AccessGrant.class)
final URI resource = URI.create("https://storage.example/e973cc3d-5c28-4a10-98c5-e40079289358/a/b/c");
final List<AccessGrant> grants = client.query(resource, null, null, null, "Read", AccessGrant.class)
.toCompletableFuture().join();
assertEquals(1, grants.size());
}
Expand All @@ -525,9 +524,8 @@ void testQueryGrantAgent() {
final String token = generateIdToken(claims);
final AccessGrantClient client = agClient.session(OpenIdSession.ofIdToken(token));

final List<AccessGrant> grants = client.query(URI.create("https://id.test/user"),
null, null, "Read", AccessGrant.class)
.toCompletableFuture().join();
final List<AccessGrant> grants = client.query(null, null, URI.create("https://id.test/user"),
null, "Read", AccessGrant.class).toCompletableFuture().join();
assertEquals(1, grants.size());
}

Expand All @@ -541,9 +539,8 @@ void testQueryRequestAgent() {
final String token = generateIdToken(claims);
final AccessGrantClient client = agClient.session(OpenIdSession.ofIdToken(token));

final List<AccessRequest> requests = client.query(URI.create("https://id.test/user"),
null, null, "Read", AccessRequest.class)
.toCompletableFuture().join();
final List<AccessRequest> requests = client.query(null, null, URI.create("https://id.test/user"),
null, "Read", AccessRequest.class).toCompletableFuture().join();
assertEquals(1, requests.size());
}

Expand All @@ -557,10 +554,9 @@ void testQueryRequest() {
final String token = generateIdToken(claims);
final AccessGrantClient client = agClient.session(OpenIdSession.ofIdToken(token));

final List<AccessRequest> requests = client.query(null,
URI.create("https://storage.example/f1759e6d-4dda-4401-be61-d90d070a5474/a/b/c"), null, "Read",
AccessRequest.class)
.toCompletableFuture().join();
final URI resource = URI.create("https://storage.example/f1759e6d-4dda-4401-be61-d90d070a5474/a/b/c");
final List<AccessRequest> requests = client.query(resource, null, null, null, "Read", AccessRequest.class)
.toCompletableFuture().join();
assertEquals(1, requests.size());
}

Expand All @@ -574,10 +570,9 @@ void testQueryDenial() {
final String token = generateIdToken(claims);
final AccessGrantClient client = agClient.session(OpenIdSession.ofIdToken(token));

final List<AccessDenial> grants = client.query(null,
URI.create("https://storage.example/ef9c4b90-0459-408d-bfa9-1c61d46e1eaf/e/f/g"), null, "Read",
AccessDenial.class)
.toCompletableFuture().join();
final URI resource = URI.create("https://storage.example/ef9c4b90-0459-408d-bfa9-1c61d46e1eaf/e/f/g");
final List<AccessDenial> grants = client.query(resource, null, null, null, "Read", AccessDenial.class)
.toCompletableFuture().join();
assertEquals(1, grants.size());
}

Expand All @@ -592,7 +587,8 @@ void testQueryInvalidType() {
final AccessGrantClient client = agClient.session(OpenIdSession.ofIdToken(token));

final URI uri = URI.create("https://storage.example/f1759e6d-4dda-4401-be61-d90d070a5474/a/b/c");
assertThrows(AccessGrantException.class, () -> client.query(null, uri, null, "Read", AccessCredential.class));
assertThrows(AccessGrantException.class, () ->
client.query(uri, null, null, null, "Read", AccessCredential.class));
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -323,17 +323,17 @@ void accessGrantQueryByRequestorTest(final Session session) {
final AccessGrantClient accessGrantClient = new AccessGrantClient(URI.create(VC_PROVIDER)).session(session);

//query for all grants issued by the user
final List<AccessRequest> grants = accessGrantClient.query(URI.create(webidUrl),
sharedResource, PURPOSE1, GRANT_MODE_READ, AccessRequest.class)
final List<AccessRequest> grants = accessGrantClient.query(sharedResource, null, URI.create(webidUrl),
PURPOSE1, GRANT_MODE_READ, AccessRequest.class)
.toCompletableFuture().join();
// result is 4 because we retrieve the grants for each path
// sharedTextFileURI =
// http://localhost:57577/private/accessgrant-test-2c82772f-7c0a-4e39-9466-abf9756b59c7/sharedFile.txt
assertEquals(1, grants.size());

//query for all grants issued by a random user
final List<AccessRequest> randomGrants = accessGrantClient.query(URI.create("https://someuser.test"),
sharedResource, PURPOSE1, GRANT_MODE_READ, AccessRequest.class)
final List<AccessRequest> randomGrants = accessGrantClient.query(sharedResource, null,
URI.create("https://someuser.test"), PURPOSE1, GRANT_MODE_READ, AccessRequest.class)
.toCompletableFuture().join();
assertEquals(0, randomGrants.size());
}
Expand All @@ -347,14 +347,14 @@ void accessGrantQueryByResourceTest(final Session session) {
final AccessGrantClient accessGrantClient = new AccessGrantClient(URI.create(VC_PROVIDER)).session(session);

//query for all grants of a dedicated resource
final List<AccessRequest> requests = accessGrantClient.query(URI.create(webidUrl),
sharedResource, PURPOSE1, GRANT_MODE_READ, AccessRequest.class)
final List<AccessRequest> requests = accessGrantClient.query(sharedResource, null, URI.create(webidUrl),
PURPOSE1, GRANT_MODE_READ, AccessRequest.class)
.toCompletableFuture().join();
assertEquals(1, requests.size());

//query for all grants of a random resource
final List<AccessRequest> randomGrants = accessGrantClient.query(URI.create(webidUrl),
URI.create("https://somerandom.test"), PURPOSE1, GRANT_MODE_READ, AccessRequest.class)
final List<AccessRequest> randomGrants = accessGrantClient.query(URI.create("https://somerandom.test"),
null, URI.create(webidUrl), PURPOSE1, GRANT_MODE_READ, AccessRequest.class)
.toCompletableFuture().join();
assertEquals(0, randomGrants.size());
}
Expand All @@ -368,14 +368,14 @@ void accessGrantQueryByPurposeTest(final Session session) {
final AccessGrantClient accessGrantClient = new AccessGrantClient(URI.create(VC_PROVIDER)).session(session);

//query for all grants with a dedicated purpose
final List<AccessRequest> requests = accessGrantClient.query(URI.create(webidUrl),
sharedResource, PURPOSE1, GRANT_MODE_READ, AccessRequest.class)
final List<AccessRequest> requests = accessGrantClient.query(sharedResource, null, URI.create(webidUrl),
PURPOSE1, GRANT_MODE_READ, AccessRequest.class)
.toCompletableFuture().join();
assertEquals(1, requests.size());

//query for all grants of dedicated purpose combinations
final List<AccessGrant> randomGrants = accessGrantClient.query(URI.create(webidUrl),
sharedResource, PURPOSE1, GRANT_MODE_WRITE, AccessGrant.class)
final List<AccessGrant> randomGrants = accessGrantClient.query(sharedResource, null, URI.create(webidUrl),
PURPOSE1, GRANT_MODE_WRITE, AccessGrant.class)
.toCompletableFuture().join();
assertEquals(0, randomGrants.size()); //our grant is actually a Read
}
Expand Down