From f05989d0f6ba435ca3f90c81078137f9cea6ae4c Mon Sep 17 00:00:00 2001 From: Nicolas Ayral Seydoux Date: Sun, 22 Dec 2024 23:56:37 +0100 Subject: [PATCH] Add restrictions on Access Crendentials queries Queries must now be on Access Grants or Access Requests, rather than generically on Acces Credentials. This is aligned with the JCL general behavior. --- e2e/node/e2e.test.ts | 6 +++- src/gConsent/index.ts | 3 +- src/gConsent/query/query.test.ts | 56 ++++++++++++++++++++--------- src/gConsent/query/query.ts | 60 ++++++++++++++++++++++---------- src/index.ts | 3 +- 5 files changed, 89 insertions(+), 39 deletions(-) diff --git a/e2e/node/e2e.test.ts b/e2e/node/e2e.test.ts index a60c000c..b13797e8 100644 --- a/e2e/node/e2e.test.ts +++ b/e2e/node/e2e.test.ts @@ -1721,7 +1721,10 @@ describe(`End-to-end access grant tests for environment [${environment}] `, () = () => { it("can navigate the paginated results", async () => { const allCredentialsPageOne = await query( - { pageSize: 10 }, + { + pageSize: 10, + type: "SolidAccessGrant" + }, { fetch: addUserAgent(requestorSession.fetch, TEST_USER_AGENT), // FIXME add query endpoint discovery check. @@ -1775,6 +1778,7 @@ describe(`End-to-end access grant tests for environment [${environment}] `, () = const pages = paginatedQuery( { pageSize: 20, + type: "SolidAccessRequest", }, { fetch: addUserAgent(requestorSession.fetch, TEST_USER_AGENT), diff --git a/src/gConsent/index.ts b/src/gConsent/index.ts index 6045bc54..38437901 100644 --- a/src/gConsent/index.ts +++ b/src/gConsent/index.ts @@ -48,7 +48,8 @@ export { export { CredentialFilter, CredentialResult, - CredentialStatus, + AccessRequestStatus, + AccessGrantStatus, CredentialType, DURATION, query, diff --git a/src/gConsent/query/query.test.ts b/src/gConsent/query/query.test.ts index ec286798..e82a7e26 100644 --- a/src/gConsent/query/query.test.ts +++ b/src/gConsent/query/query.test.ts @@ -20,7 +20,7 @@ // import { jest, it, describe, expect } from "@jest/globals"; -import type { CredentialFilter, CredentialResult } from "./query"; +import type { AccessGrantFilter, CredentialResult } from "./query"; import { DURATION, paginatedQuery, query } from "./query"; import { mockAccessGrantVc } from "../util/access.mock"; @@ -28,7 +28,9 @@ describe("query", () => { it("throws on server errors", async () => { await expect(() => query( - {}, + { + type: "SolidAccessRequest", + }, { queryEndpoint: new URL("https://vc.example.org/query"), fetch: jest.fn().mockResolvedValue( @@ -55,13 +57,13 @@ describe("query", () => { }), ), ); - const filter: CredentialFilter = { - fromAgent: "https://example.org/from-some-agent", + const filter: AccessGrantFilter = { + fromAgent: new URL("https://example.org/from-some-agent"), issuedWithin: "P1D", - purpose: "https://example.org/some-purpose", + purpose: new URL("https://example.org/some-purpose"), revokedWithin: "P1D", - toAgent: "https://example.org/to-some-agent", - resource: "https://example.org/some-resource", + toAgent: new URL("https://example.org/to-some-agent"), + resource: new URL("https://example.org/some-resource"), type: "SolidAccessGrant", status: "Active", pageSize: 10, @@ -72,8 +74,16 @@ describe("query", () => { fetch: mockedFetch, }); const expectedQueryParams = new URLSearchParams({ - ...filter, - pageSize: `${filter.pageSize}`, + fromAgent: "https://example.org/from-some-agent", + issuedWithin: "P1D", + purpose: "https://example.org/some-purpose", + revokedWithin: "P1D", + toAgent: "https://example.org/to-some-agent", + resource: "https://example.org/some-resource", + type: "SolidAccessGrant", + status: "Active", + pageSize: "10", + page: "some-page", }); expect(mockedFetch.mock.calls[0][0].toString()).toBe( `https://vc.example.org/query?${expectedQueryParams}`, @@ -91,7 +101,7 @@ describe("query", () => { const filter = { unknownKey: "some value", }; - await query(filter as CredentialFilter, { + await query(filter as unknown as AccessGrantFilter, { queryEndpoint: new URL("https://vc.example.org/query"), fetch: mockedFetch, }); @@ -135,7 +145,9 @@ describe("query", () => { ), ); const result = await query( - {}, + { + type: "SolidAccessRequest", + }, { queryEndpoint: new URL("https://vc.example.org/query"), fetch: mockedFetch, @@ -156,14 +168,18 @@ describe("query", () => { ), ); await query( - { issuedWithin: DURATION.ONE_DAY, revokedWithin: DURATION.ONE_WEEK }, + { + issuedWithin: DURATION.ONE_DAY, + revokedWithin: DURATION.ONE_WEEK, + type: "SolidAccessRequest", + }, { queryEndpoint: new URL("https://vc.example.org/query"), fetch: mockedFetch, }, ); expect(mockedFetch.mock.calls[0][0].toString()).toBe( - `https://vc.example.org/query?issuedWithin=P1D&revokedWithin=P7D`, + `https://vc.example.org/query?issuedWithin=P1D&revokedWithin=P7D&type=SolidAccessRequest`, ); }); @@ -177,7 +193,9 @@ describe("query", () => { ); await expect( query( - {}, + { + type: "SolidAccessGrant", + }, { queryEndpoint: new URL("https://vc.example.org/query"), fetch: mockedFetch, @@ -196,7 +214,9 @@ describe("query", () => { ); await expect( query( - {}, + { + type: "SolidAccessRequest", + }, { queryEndpoint: new URL("https://vc.example.org/query"), fetch: mockedFetch, @@ -244,7 +264,9 @@ describe("paginatedQuery", () => { ); for await (const page of paginatedQuery( - {}, + { + type: "SolidAccessRequest", + }, { queryEndpoint: new URL("https://vc.example.org/query"), fetch: mockedFetch, @@ -268,7 +290,7 @@ describe("paginatedQuery", () => { ); for await (const page of paginatedQuery( - {}, + { type: "SolidAccessGrant" }, { queryEndpoint: new URL("https://vc.example.org/query"), fetch: mockedFetch, diff --git a/src/gConsent/query/query.ts b/src/gConsent/query/query.ts index 39eb2f8c..ab6e32b4 100644 --- a/src/gConsent/query/query.ts +++ b/src/gConsent/query/query.ts @@ -24,20 +24,22 @@ import LinkHeader from "http-link-header"; import { handleErrorResponse } from "@inrupt/solid-client-errors"; import type { DatasetWithId } from "@inrupt/solid-client-vc"; import { verifiableCredentialToDataset } from "@inrupt/solid-client-vc"; -import type { UrlString } from "@inrupt/solid-client"; import { AccessGrantError } from "../../common/errors/AccessGrantError"; /** - * Supported Access Credential statuses + * Supported Access Requests statuses */ -export type CredentialStatus = +export type AccessRequestStatus = | "Pending" | "Denied" | "Granted" | "Canceled" - | "Expired" - | "Active" - | "Revoked"; + | "Expired"; + +/** + * Supported Access Grant statuses + */ +export type AccessGrantStatus = "Expired" | "Active" | "Revoked"; /** * Supported Access Credential types @@ -65,23 +67,23 @@ export type CredentialFilter = { /** * The Access Credential status (e.g. Active, Revoked...). */ - status?: CredentialStatus; + status?: AccessRequestStatus | AccessGrantStatus; /** * WebID of the Agent who issued the Access Credential. */ - fromAgent?: UrlString; + fromAgent?: URL; /** * WebID of the Agent who is the Access Credential recipient. */ - toAgent?: UrlString; + toAgent?: URL; /** * URL of the resource featured in the Access Credential. */ - resource?: UrlString; + resource?: URL; /** * URL of the Access Credential purpose. */ - purpose?: UrlString; + purpose?: URL; /** * Period (expressed using ISO 8601) during which the Credential was issued. */ @@ -100,7 +102,23 @@ export type CredentialFilter = { page?: string; }; -const FILTER_ELEMENTS: Array = [ +export type AccessGrantFilter = CredentialFilter & { + type: "SolidAccessGrant"; + /** + * The Access Grant status (e.g. Active, Revoked...). + */ + status?: AccessGrantStatus; +}; + +export type AccessRequestFilter = CredentialFilter & { + type: "SolidAccessRequest"; + /** + * The Access Request status (e.g. Pending, Granted...). + */ + status?: AccessRequestStatus; +}; + +const FILTER_ELEMENTS: Array = [ "fromAgent", "issuedWithin", "page", @@ -132,19 +150,19 @@ export type CredentialResult = { /** * First page of query results. */ - first?: CredentialFilter; + first?: AccessRequestFilter | AccessGrantFilter; /** * Previous page of query results. */ - prev?: CredentialFilter; + prev?: AccessRequestFilter | AccessGrantFilter; /** * Next page of query results. */ - next?: CredentialFilter; + next?: AccessRequestFilter | AccessGrantFilter; /** * Last page of query results. */ - last?: CredentialFilter; + last?: AccessRequestFilter | AccessGrantFilter; }; function toCredentialFilter(url: URL): CredentialFilter { @@ -198,7 +216,11 @@ async function toCredentialResult( if (link.length === 0) { return; } - result[rel] = toCredentialFilter(new URL(link[0].uri)); + // The type assertion here relies on the consistency of the server + // response with the client request, which must be AccessRequestFilter | AccessGrantFilter. + result[rel] = toCredentialFilter(new URL(link[0].uri)) as + | AccessRequestFilter + | AccessGrantFilter; }); } result.items = await parseQueryResponse(await response.json()); @@ -249,7 +271,7 @@ function toQueryUrl(endpoint: URL, filter: CredentialFilter): URL { * ``` */ export async function query( - filter: CredentialFilter, + filter: AccessRequestFilter | AccessGrantFilter, options: { fetch: typeof fetch; queryEndpoint: URL; @@ -291,7 +313,7 @@ export async function query( * ``` */ export async function* paginatedQuery( - filter: CredentialFilter, + filter: AccessRequestFilter | AccessGrantFilter, options: { fetch: typeof fetch; queryEndpoint: URL; diff --git a/src/index.ts b/src/index.ts index 6e576cad..12fc5068 100644 --- a/src/index.ts +++ b/src/index.ts @@ -48,7 +48,8 @@ export { cancelAccessRequest, CredentialFilter, CredentialResult, - CredentialStatus, + AccessGrantStatus, + AccessRequestStatus, CredentialType, denyAccessRequest, DURATION,