Skip to content

Commit

Permalink
Add restrictions on Access Crendentials queries
Browse files Browse the repository at this point in the history
Queries must now be on Access Grants or Access Requests, rather than
generically on Acces Credentials. This is aligned with the JCL general
behavior.
  • Loading branch information
NSeydoux committed Dec 22, 2024
1 parent f074a59 commit f05989d
Show file tree
Hide file tree
Showing 5 changed files with 89 additions and 39 deletions.
6 changes: 5 additions & 1 deletion e2e/node/e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Check failure on line 1726 in e2e/node/e2e.test.ts

View workflow job for this annotation

GitHub Actions / lint / lint

Insert `,`
},
{
fetch: addUserAgent(requestorSession.fetch, TEST_USER_AGENT),
// FIXME add query endpoint discovery check.
Expand Down Expand Up @@ -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),
Expand Down
3 changes: 2 additions & 1 deletion src/gConsent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ export {
export {
CredentialFilter,
CredentialResult,
CredentialStatus,
AccessRequestStatus,
AccessGrantStatus,
CredentialType,
DURATION,
query,
Expand Down
56 changes: 39 additions & 17 deletions src/gConsent/query/query.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,17 @@
//

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";

describe("query", () => {
it("throws on server errors", async () => {
await expect(() =>
query(
{},
{
type: "SolidAccessRequest",
},
{
queryEndpoint: new URL("https://vc.example.org/query"),
fetch: jest.fn<typeof fetch>().mockResolvedValue(
Expand All @@ -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,
Expand All @@ -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}`,
Expand All @@ -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,
});
Expand Down Expand Up @@ -135,7 +145,9 @@ describe("query", () => {
),
);
const result = await query(
{},
{
type: "SolidAccessRequest",
},
{
queryEndpoint: new URL("https://vc.example.org/query"),
fetch: mockedFetch,
Expand All @@ -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`,
);
});

Expand All @@ -177,7 +193,9 @@ describe("query", () => {
);
await expect(
query(
{},
{
type: "SolidAccessGrant",
},
{
queryEndpoint: new URL("https://vc.example.org/query"),
fetch: mockedFetch,
Expand All @@ -196,7 +214,9 @@ describe("query", () => {
);
await expect(
query(
{},
{
type: "SolidAccessRequest",
},
{
queryEndpoint: new URL("https://vc.example.org/query"),
fetch: mockedFetch,
Expand Down Expand Up @@ -244,7 +264,9 @@ describe("paginatedQuery", () => {
);

for await (const page of paginatedQuery(
{},
{
type: "SolidAccessRequest",
},
{
queryEndpoint: new URL("https://vc.example.org/query"),
fetch: mockedFetch,
Expand All @@ -268,7 +290,7 @@ describe("paginatedQuery", () => {
);

for await (const page of paginatedQuery(
{},
{ type: "SolidAccessGrant" },
{
queryEndpoint: new URL("https://vc.example.org/query"),
fetch: mockedFetch,
Expand Down
60 changes: 41 additions & 19 deletions src/gConsent/query/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
*/
Expand All @@ -100,7 +102,23 @@ export type CredentialFilter = {
page?: string;
};

const FILTER_ELEMENTS: Array<keyof CredentialFilter> = [
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<keyof AccessGrantFilter | AccessRequestFilter> = [
"fromAgent",
"issuedWithin",
"page",
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -291,7 +313,7 @@ export async function query(
* ```
*/
export async function* paginatedQuery(
filter: CredentialFilter,
filter: AccessRequestFilter | AccessGrantFilter,
options: {
fetch: typeof fetch;
queryEndpoint: URL;
Expand Down
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ export {
cancelAccessRequest,
CredentialFilter,
CredentialResult,
CredentialStatus,
AccessGrantStatus,
AccessRequestStatus,
CredentialType,
denyAccessRequest,
DURATION,
Expand Down

0 comments on commit f05989d

Please sign in to comment.