From ecd6839c49e0cd5d55ac78894f50c58275ed78a1 Mon Sep 17 00:00:00 2001 From: lennertdr Date: Fri, 4 Jul 2025 11:45:56 +0200 Subject: [PATCH 01/62] Basic POST /policies created --- packages/uma/config/default.json | 1 + packages/uma/config/routes/policies.json | 17 ++++++- packages/uma/src/routes/Policy.ts | 4 +- .../routeSpecific/policies/CreatePolicies.ts | 39 +++++++++++++++- .../routeSpecific/policies/GetPolicies.ts | 13 ++++-- .../util/routeSpecific/policies/helpers.ts | 4 ++ scripts/test-uma-ODRL-policy.ts | 46 +++++++++++++++---- 7 files changed, 107 insertions(+), 17 deletions(-) diff --git a/packages/uma/config/default.json b/packages/uma/config/default.json index a264f9c3..228d648a 100644 --- a/packages/uma/config/default.json +++ b/packages/uma/config/default.json @@ -99,6 +99,7 @@ { "@id": "urn:uma:default:JwksRoute" }, { "@id": "urn:uma:default:TokenRoute" }, { "@id": "urn:uma:default:PolicyRoute" }, + { "@id": "urn:uma:default:GetOnePolicyRoute" }, { "@id": "urn:uma:default:PermissionRegistrationRoute" }, { "@id": "urn:uma:default:ResourceRegistrationRoute" }, { "@id": "urn:uma:default:ResourceRegistrationOpsRoute" }, diff --git a/packages/uma/config/routes/policies.json b/packages/uma/config/routes/policies.json index bb546dd0..544fdb99 100644 --- a/packages/uma/config/routes/policies.json +++ b/packages/uma/config/routes/policies.json @@ -7,7 +7,8 @@ "@id": "urn:uma:default:PolicyRoute", "@type": "HttpHandlerRoute", "methods": [ - "GET" + "GET", + "POST" ], "handler": { "@type": "PolicyRequestHandler", @@ -16,6 +17,20 @@ } }, "path": "/uma/policies" + }, + { + "@id": "urn:uma:default:GetOnePolicyRoute", + "@type": "HttpHandlerRoute", + "methods": [ + "GET" + ], + "handler": { + "@type": "PolicyRequestHandler", + "storage": { + "@id": "urn:uma:default:RulesStorage" + } + }, + "path": "/uma/policies/{id}" } ] } diff --git a/packages/uma/src/routes/Policy.ts b/packages/uma/src/routes/Policy.ts index 85ce09a7..a5aebf90 100644 --- a/packages/uma/src/routes/Policy.ts +++ b/packages/uma/src/routes/Policy.ts @@ -2,6 +2,7 @@ import { BadRequestHttpError, getLoggerFor, MethodNotAllowedHttpError } from "@s import { UCRulesStorage } from "@solidlab/ucp"; import { HttpHandlerContext, HttpHandlerResponse, HttpHandler, HttpHandlerRequest } from "../util/http/models/HttpHandler"; import { getPolicies as getPolicies } from "../util/routeSpecific/policies/"; +import { addPolicies } from "../util/routeSpecific/policies/CreatePolicies"; /** * Endpoint to handle policies, this implementation gives all policies that have the @@ -22,7 +23,7 @@ export class PolicyRequestHandler extends HttpHandler { * (To be altered with actual Solid-OIDC) * * @param request the request with the client 'id' as body - * @returns the client id + * @returns the client webID */ protected getCredentials(request: HttpHandlerRequest): string { const header = request.headers['authorization']; @@ -44,6 +45,7 @@ export class PolicyRequestHandler extends HttpHandler { switch (request.method) { case 'GET': return getPolicies(request, store, client); + case 'POST': return addPolicies(request, store, client); // TODO: add other endpoints default: throw new MethodNotAllowedHttpError(); } diff --git a/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts b/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts index ea641885..464e3897 100644 --- a/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts @@ -1 +1,38 @@ -//TODO \ No newline at end of file +import { Store } from "n3"; +import { HttpHandlerRequest, HttpHandlerResponse } from "../../http/models/HttpHandler"; +import { namedNode, odrlAssigner, PolicyBody } from "./helpers"; +import { BadRequestHttpError } from "@solid/community-server"; +import { parseStringAsN3Store } from "koreografeye"; + +export async function addPolicies(request: HttpHandlerRequest, store: Store, clientId: string): Promise> { + + // 1. Parse the requested policy + const requestedPolicy = (request as HttpHandlerRequest).body?.policy; + if (typeof requestedPolicy !== 'string') { + throw new BadRequestHttpError(`Invalid request body`); + } + const parsedPolicy: Store = await parseStringAsN3Store(requestedPolicy); + + // 2. Check if assigner is client + const matchingClient = parsedPolicy.getQuads(null, odrlAssigner, namedNode(clientId), null); + if (matchingClient.length === 0) { + throw new BadRequestHttpError(`Policy is not authorized correctly`); + } + + // This check works if the 'assigner' relation only applies to rules of a policy + const allAssigners = parsedPolicy.getQuads(null, odrlAssigner, null, null); + if (allAssigners.length !== matchingClient.length) { + throw new BadRequestHttpError(`Policy is incorrectly built`); + } + + // 3. Perform other validity checks + + // Check if assigner of the policy has access to the target + + // 4. Add the policy to the store + store.addQuads(parsedPolicy.getQuads(null, null, null, null)); + + return { + status: 201 + } +} \ No newline at end of file diff --git a/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts b/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts index 783e353c..15cb7462 100644 --- a/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts @@ -14,8 +14,8 @@ export async function getPolicies(request: HttpHandlerRequest, store: Store, cli // If asked for a policy, validate the policy ID const args = request.url.pathname.split('/'); - if (args.length === 4 && isPolicy(args[-1])) - return getOnePolicy(args[-1], store, clientId); + if (args.length === 4 && isPolicy(args[3])) + return getOnePolicy(args[3], store, clientId); throw new MethodNotAllowedHttpError(); } @@ -35,9 +35,12 @@ function isPolicy(policyId: string): boolean { * Function to implement the GET /uma/policies/ endpoint, it retrieves all information about a certain * policy if available. Yet to be implemented. */ -function getOnePolicy(policyId: string, store: Store, clientId: string): Promise> { +async function getOnePolicy(policyId: string, store: Store, clientId: string): Promise> { // TODO - return getAllPolicies(store, clientId); + return { + status: 202, + body: `GET ${policyId} for ${clientId} received properly` + } } @@ -48,7 +51,7 @@ function getOnePolicy(policyId: string, store: Store, clientId: string): Promise * @param param0 a request with the clients webID as authorization header. * @returns all policy information (depth 1) relevant to the client */ -function getAllPolicies(store: Store, clientId: string): Promise> { +async function getAllPolicies(store: Store, clientId: string): Promise> { // Query the quads that have the requested client as assigner const quads = store.getQuads(null, odrlAssigner, namedNode(clientId), null); diff --git a/packages/uma/src/util/routeSpecific/policies/helpers.ts b/packages/uma/src/util/routeSpecific/policies/helpers.ts index 366fb543..55234e85 100644 --- a/packages/uma/src/util/routeSpecific/policies/helpers.ts +++ b/packages/uma/src/util/routeSpecific/policies/helpers.ts @@ -11,3 +11,7 @@ export const relations = [ export const { namedNode } = DataFactory; +export interface PolicyBody { + policy: string; +} + diff --git a/scripts/test-uma-ODRL-policy.ts b/scripts/test-uma-ODRL-policy.ts index d92d8212..d22d3564 100644 --- a/scripts/test-uma-ODRL-policy.ts +++ b/scripts/test-uma-ODRL-policy.ts @@ -4,24 +4,52 @@ * The purpose of this file is to test the /policies endpoint. */ -const endpoint = 'http://localhost:4000/uma/policies' +const endpoint = (extra: string = '') => 'http://localhost:4000/uma/policies' + extra; const client1 = 'https://pod.woutslabbinck.com/profile/card#me'; const client2 = 'https://pod.example.com/profile/card#me'; +const policyId = 'ex:usagePolicy1'; +const badPolicyId = 'nonExistentPolicy'; +const policyBody = "@prefix ex: .\n@prefix odrl: .\nex:usagePolicy4 a odrl:Agreement .\nex:usagePolicy4 odrl:permission ex:permission4 .\nex:permission4 a odrl:Permission .\nex:permission4 odrl:action odrl:read .\nex:permission4 odrl:target .\nex:permission4 odrl:assignee .\nex:permission4 odrl:assigner ."; +async function getAllPolicies() { + console.log("Simple test for the GET All Policies endpoint\n"); -async function main() { - console.log(`Primitive unit test to check policy access based on the client\n`); + let response = await fetch(endpoint(), { headers: { 'Authorization': client1 } }) + console.log("expecting all five policies and their relations: \n", await response.text()) - let response = await fetch(endpoint, { headers: { 'Authorization': client1 } }) + response = await fetch(endpoint(), { headers: { 'Authorization': client2 } }) + console.log("expecting zero policies: ", await response.text()) - console.log("expecting all five policies and their relations: \n", await response.text()) + response = await fetch(endpoint(), {}); + console.log(`expecting 4xx error code (no authorization header provided): ${response.status}`) +} - response = await fetch(endpoint, { headers: { 'Authorization': client2 } }) +async function getOnePolicy() { + console.log("Simple test for the GET One Policy endpoint"); - console.log("expecting zero policies: ", await response.text()) + let response = await fetch(endpoint(`/${policyId}`), { headers: { 'Authorization': client1 } }); + console.log(`expecting to return relevent information about ${policyId}`, await response.text()); - response = await fetch(endpoint, {}); + response = await fetch(endpoint(`/${badPolicyId}`), { headers: { 'Authorization': client1 } }); + console.log(`expecting 4xx error code since the policy ID is not valid`) - console.log(`expecting 4xx error code (no authorization header provided): ${response.status}`) + response = await fetch(endpoint(`/${policyId}`), { headers: { 'Authorization': client2 } }); + console.log(`expecting 4xx error code since the client is not authorized to access the policy`) +} + +async function postPolicy() { + console.log("Simple test for the POST policy endpoint"); + + let response = await fetch(endpoint(), { method: 'POST', headers: { 'Authorization': client1, 'Content-Type': 'application/json' }, body: JSON.stringify({ policy: policyBody }) }); + console.log(`expecting a negative response since assigner != client: status code ${response.status}`); + + response = await fetch(endpoint(), { method: 'POST', headers: { 'Authorization': client2, 'Content-Type': 'application/json' }, body: JSON.stringify({ policy: policyBody }) }); + console.log(`expecting a positive response: status code ${response.status}`); +} + +async function main() { + await getAllPolicies(); + //await getOnePolicy(); awaiting implementation + await postPolicy(); } main() From 846fcace9e9b2a054ee4e0cbfeee02f3bf23720c Mon Sep 17 00:00:00 2001 From: lennertdr Date: Fri, 4 Jul 2025 14:15:28 +0200 Subject: [PATCH 02/62] Practical addRule implementation to test the POST endpoint --- .../src/storage/DirectoryUCRulesStorage.ts | 31 +++++++++++++++++-- packages/uma/src/routes/Policy.ts | 2 +- .../routeSpecific/policies/CreatePolicies.ts | 21 ++++++++++--- 3 files changed, 46 insertions(+), 8 deletions(-) diff --git a/packages/ucp/src/storage/DirectoryUCRulesStorage.ts b/packages/ucp/src/storage/DirectoryUCRulesStorage.ts index 6d8f231b..4415a254 100644 --- a/packages/ucp/src/storage/DirectoryUCRulesStorage.ts +++ b/packages/ucp/src/storage/DirectoryUCRulesStorage.ts @@ -1,11 +1,13 @@ import { UCRulesStorage } from "./UCRulesStorage"; import * as path from 'path' import * as fs from 'fs' -import { Store } from "n3"; +import { Store, Writer } from "n3"; import { parseAsN3Store } from "koreografeye"; export class DirectoryUCRulesStorage implements UCRulesStorage { private directoryPath: string; + private addedRulesPath: string; + /** * * @param directoryPath The absolute path to a directory @@ -16,6 +18,8 @@ export class DirectoryUCRulesStorage implements UCRulesStorage { if (!fs.lstatSync(directoryPath).isDirectory()) { throw Error(`${directoryPath} does not resolve to a directory`) } + + this.addedRulesPath = path.join(this.directoryPath, 'addedRules.ttl'); } public async getStore(): Promise { @@ -30,8 +34,31 @@ export class DirectoryUCRulesStorage implements UCRulesStorage { } + /** + * TEST IMPLEMENTATION - This is just to test the POST :uma/policies endpoint + * + * @param rule The quads to be added + */ public async addRule(rule: Store): Promise { - throw Error('not implemented'); + const writer = new Writer({ format: 'Turtle' }); + + writer.addQuads(rule.getQuads(null, null, null, null)); + + const serializedTurtle: string = await new Promise((resolve, reject) => { + writer.end((error, result) => { + if (error) reject(error); + else resolve(result); + }); + }); + + // Append or create the file + if (!fs.existsSync(this.addedRulesPath)) { + fs.writeFileSync(this.addedRulesPath, serializedTurtle); + } else { + fs.appendFileSync(this.addedRulesPath, '\n' + serializedTurtle); + } + + console.log(`[${new Date().toISOString()}] - Added new rule to ${this.addedRulesPath}`); } public async getRule(identifier: string): Promise { throw Error('not implemented'); diff --git a/packages/uma/src/routes/Policy.ts b/packages/uma/src/routes/Policy.ts index a5aebf90..af6c241b 100644 --- a/packages/uma/src/routes/Policy.ts +++ b/packages/uma/src/routes/Policy.ts @@ -45,7 +45,7 @@ export class PolicyRequestHandler extends HttpHandler { switch (request.method) { case 'GET': return getPolicies(request, store, client); - case 'POST': return addPolicies(request, store, client); + case 'POST': return addPolicies(request, store, this.storage, client); // TODO: add other endpoints default: throw new MethodNotAllowedHttpError(); } diff --git a/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts b/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts index 464e3897..f273a53c 100644 --- a/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts @@ -1,17 +1,23 @@ import { Store } from "n3"; import { HttpHandlerRequest, HttpHandlerResponse } from "../../http/models/HttpHandler"; import { namedNode, odrlAssigner, PolicyBody } from "./helpers"; -import { BadRequestHttpError } from "@solid/community-server"; +import { BadRequestHttpError, InternalServerError } from "@solid/community-server"; import { parseStringAsN3Store } from "koreografeye"; +import { UCRulesStorage } from "@solidlab/ucp"; -export async function addPolicies(request: HttpHandlerRequest, store: Store, clientId: string): Promise> { +export async function addPolicies(request: HttpHandlerRequest, store: Store, storage: UCRulesStorage, clientId: string): Promise> { // 1. Parse the requested policy const requestedPolicy = (request as HttpHandlerRequest).body?.policy; if (typeof requestedPolicy !== 'string') { throw new BadRequestHttpError(`Invalid request body`); } - const parsedPolicy: Store = await parseStringAsN3Store(requestedPolicy); + let parsedPolicy: Store; + try { + parsedPolicy = await parseStringAsN3Store(requestedPolicy); + } catch (error) { + throw new BadRequestHttpError(`Policy string can not be parsed: ${error}`) + } // 2. Check if assigner is client const matchingClient = parsedPolicy.getQuads(null, odrlAssigner, namedNode(clientId), null); @@ -29,8 +35,13 @@ export async function addPolicies(request: HttpHandlerRequest, store: Store, cli // Check if assigner of the policy has access to the target - // 4. Add the policy to the store - store.addQuads(parsedPolicy.getQuads(null, null, null, null)); + // 4. Add the policy to the rule storage + try { + await storage.addRule(parsedPolicy); + } catch (error) { + throw new InternalServerError("Failed to add policy"); + } + return { status: 201 From 1c377a760c59632b078eff75f4094dfaf56386a3 Mon Sep 17 00:00:00 2001 From: lennertdr Date: Fri, 4 Jul 2025 15:09:37 +0200 Subject: [PATCH 03/62] added Get One Policy endpoint, need a way to fix the ID (encoding?) --- .../routeSpecific/policies/CreatePolicies.ts | 8 ++- .../routeSpecific/policies/GetPolicies.ts | 53 ++++++++++--------- .../util/routeSpecific/policies/helpers.ts | 22 +++++++- scripts/test-uma-ODRL-policy.ts | 2 +- 4 files changed, 57 insertions(+), 28 deletions(-) diff --git a/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts b/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts index f273a53c..decf31b7 100644 --- a/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts @@ -31,9 +31,12 @@ export async function addPolicies(request: HttpHandlerRequest, store: Store, sto throw new BadRequestHttpError(`Policy is incorrectly built`); } - // 3. Perform other validity checks + // TODO: 3. Perform other validity checks // Check if assigner of the policy has access to the target + // Check if there is at least one permission/prohibition/duty + // Check if every rule has a target + // ... // 4. Add the policy to the rule storage try { @@ -44,6 +47,7 @@ export async function addPolicies(request: HttpHandlerRequest, store: Store, sto return { - status: 201 + status: 201, + body: { message: "Policy stored successfully" } } } \ No newline at end of file diff --git a/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts b/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts index 15cb7462..b3912ce2 100644 --- a/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts @@ -1,7 +1,8 @@ import { HttpHandlerRequest, HttpHandlerResponse } from "../../http/models/HttpHandler"; import { Quad, Store, Writer } from "n3"; -import { odrlAssigner, relations, namedNode } from "./helpers"; -import { MethodNotAllowedHttpError } from "@solid/community-server"; +import { odrlAssigner, relations, namedNode, quadsToText } from "./helpers"; +import { BadRequestHttpError, InternalServerError, MethodNotAllowedHttpError } from "@solid/community-server"; +import { NotImplementedError } from "@inrupt/solid-client-authn-core"; /** * Handling of the GET /uma/policies endpoint @@ -33,14 +34,34 @@ function isPolicy(policyId: string): boolean { /** * Function to implement the GET /uma/policies/ endpoint, it retrieves all information about a certain - * policy if available. Yet to be implemented. + * policy if available. */ async function getOnePolicy(policyId: string, store: Store, clientId: string): Promise> { - // TODO - return { - status: 202, - body: `GET ${policyId} for ${clientId} received properly` + + // 1. Search the policy by ID + const policyMatches = store.getQuads(namedNode(policyId), null, null, null); + + // 2. Find the rules in the policy assigned by the client (first find the clients rules, then filter the ones based on the policy) + const ownedRules = store.getQuads(null, odrlAssigner, namedNode(clientId), null); + const filteredRules = ownedRules.filter(rule => + policyMatches.some(quad => quad.object.id === rule.subject.id) + ); + + // 3. Check if there are no other assigners in this policy (this is against ODRL definition of a policy) + const otherAssigners = store.getQuads(null, odrlAssigner, null, null); + if (ownedRules.length < otherAssigners.length) { + // TODO: We might expose information, handle here + throw new NotImplementedError("Handling of policies with multiple assigners is yet to be implemented"); } + + // 4. Search all info about the policy AND the rules, for now with depth 1 but a recursive variant needs to be implemented. + let details: Set = new Set(policyMatches); + filteredRules.forEach((rule) => { + details.add(rule); + store.getQuads(rule.subject, null, null, null).forEach(q => details.add(q)) + }); + + return quadsToText(Array.from(details)); } @@ -87,21 +108,5 @@ async function getAllPolicies(store: Store, clientId: string): Promise>((resolve, reject) => { - writer.end((error, result) => { - if (error) { - reject(error); - } else { - resolve({ - status: 200, - headers: { 'content-type': 'text/turtle' }, - body: result - }); - } - }); - }); + return quadsToText(policyDetails) } \ No newline at end of file diff --git a/packages/uma/src/util/routeSpecific/policies/helpers.ts b/packages/uma/src/util/routeSpecific/policies/helpers.ts index 55234e85..13395e72 100644 --- a/packages/uma/src/util/routeSpecific/policies/helpers.ts +++ b/packages/uma/src/util/routeSpecific/policies/helpers.ts @@ -1,5 +1,6 @@ import { ODRL } from "@solidlab/ucp"; -import { DataFactory } from "n3"; +import { DataFactory, Quad, Writer } from "n3"; +import { HttpHandlerResponse } from "../../http/models/HttpHandler"; // relevant ODRL implementations export const odrlAssigner = ODRL.terms.assigner; @@ -15,3 +16,22 @@ export interface PolicyBody { policy: string; } +export async function quadsToText(quads: Quad[]): Promise> { + // Serialize as Turtle + const writer = new Writer({ format: 'Turtle' }); + writer.addQuads(quads); + + return new Promise>((resolve, reject) => { + writer.end((error, result) => { + if (error) { + reject(error); + } else { + resolve({ + status: 200, + headers: { 'content-type': 'text/turtle' }, + body: result + }); + } + }); + }); +} \ No newline at end of file diff --git a/scripts/test-uma-ODRL-policy.ts b/scripts/test-uma-ODRL-policy.ts index d22d3564..a70ffd1a 100644 --- a/scripts/test-uma-ODRL-policy.ts +++ b/scripts/test-uma-ODRL-policy.ts @@ -49,7 +49,7 @@ async function postPolicy() { async function main() { await getAllPolicies(); - //await getOnePolicy(); awaiting implementation + await getOnePolicy(); await postPolicy(); } main() From e60eb75b677b90f07ce94cd2b52f46e5f45cf065 Mon Sep 17 00:00:00 2001 From: lennertdr Date: Fri, 4 Jul 2025 15:32:29 +0200 Subject: [PATCH 04/62] getOnePolicy works if a good encoding of ID's is implemented --- .../util/routeSpecific/policies/GetPolicies.ts | 1 + scripts/test-uma-ODRL-policy.ts | 16 +++++++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts b/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts index b3912ce2..b424932c 100644 --- a/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts @@ -37,6 +37,7 @@ function isPolicy(policyId: string): boolean { * policy if available. */ async function getOnePolicy(policyId: string, store: Store, clientId: string): Promise> { + policyId = decodeURI(policyId); // 1. Search the policy by ID const policyMatches = store.getQuads(namedNode(policyId), null, null, null); diff --git a/scripts/test-uma-ODRL-policy.ts b/scripts/test-uma-ODRL-policy.ts index a70ffd1a..015b4266 100644 --- a/scripts/test-uma-ODRL-policy.ts +++ b/scripts/test-uma-ODRL-policy.ts @@ -7,7 +7,7 @@ const endpoint = (extra: string = '') => 'http://localhost:4000/uma/policies' + extra; const client1 = 'https://pod.woutslabbinck.com/profile/card#me'; const client2 = 'https://pod.example.com/profile/card#me'; -const policyId = 'ex:usagePolicy1'; +const policyId = 'http://example.org/usagePolicy1'; const badPolicyId = 'nonExistentPolicy'; const policyBody = "@prefix ex: .\n@prefix odrl: .\nex:usagePolicy4 a odrl:Agreement .\nex:usagePolicy4 odrl:permission ex:permission4 .\nex:permission4 a odrl:Permission .\nex:permission4 odrl:action odrl:read .\nex:permission4 odrl:target .\nex:permission4 odrl:assignee .\nex:permission4 odrl:assigner ."; @@ -27,14 +27,16 @@ async function getAllPolicies() { async function getOnePolicy() { console.log("Simple test for the GET One Policy endpoint"); - let response = await fetch(endpoint(`/${policyId}`), { headers: { 'Authorization': client1 } }); + const encoded = encodeURI(policyId); + + let response = await fetch(endpoint(`/${encoded}`), { headers: { 'Authorization': client1 } }); console.log(`expecting to return relevent information about ${policyId}`, await response.text()); response = await fetch(endpoint(`/${badPolicyId}`), { headers: { 'Authorization': client1 } }); - console.log(`expecting 4xx error code since the policy ID is not valid`) + console.log(`expecting 4xx error code since the policy ID is not valid: ${response.status}`) - response = await fetch(endpoint(`/${policyId}`), { headers: { 'Authorization': client2 } }); - console.log(`expecting 4xx error code since the client is not authorized to access the policy`) + response = await fetch(endpoint(`/${encoded}`), { headers: { 'Authorization': client2 } }); + console.log(`expecting 4xx error code since the client is not authorized to access the policy: ${response.status}`) } async function postPolicy() { @@ -48,8 +50,12 @@ async function postPolicy() { } async function main() { + console.log("Testing all implemented Policy Endpoints:\n\n\n") await getAllPolicies(); + console.log("\n\n\n"); await getOnePolicy(); + console.log("\n\n\n"); await postPolicy(); + console.log("\n\n\n"); } main() From 6f0916f5e84c6f644f3800d131c0ca2ea0258e29 Mon Sep 17 00:00:00 2001 From: lennertdr Date: Fri, 4 Jul 2025 16:06:25 +0200 Subject: [PATCH 05/62] follow implementation from Main --- .../routeSpecific/policies/GetPolicies.ts | 45 +++++++------------ 1 file changed, 17 insertions(+), 28 deletions(-) diff --git a/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts b/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts index b424932c..a5b81e21 100644 --- a/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts @@ -75,39 +75,28 @@ async function getOnePolicy(policyId: string, store: Store, clientId: string): P */ async function getAllPolicies(store: Store, clientId: string): Promise> { - // Query the quads that have the requested client as assigner - const quads = store.getQuads(null, odrlAssigner, namedNode(clientId), null); + // Keep track of all the matching policies + const policyDetails: Set = new Set(); - // For every rule that has `client` as `assigner`, get its policy - const policies = new Set(); - - const rules = quads.map(quad => quad.subject); for (const relation of relations) { - for (const rule of rules) { - const foundPolicies = store.getQuads(null, relation, rule, null); - for (const quad of foundPolicies) { - policies.add(quad.subject.value); - } - } - } + // Collect every quad that matches with the relation (one of Permission, Prohibition or Duty) + const matchingRules = store.getQuads(null, relation, null, null); + + // Every quad will represent a policy in relation with a rule + for (const quad of matchingRules) { + // Extract the policy and rule out the quad + const policy = quad.subject; + const rule = quad.object; + + // Only go on if the rule is assigned by the client + if (store.getQuads(rule, odrlAssigner, namedNode(clientId), null).length > 0) { - // We use these policies to search everything about them, this will be changed to a recursive variant - let policyDetails: Quad[] = []; - - for (const policy of policies) { - const directQuads: Quad[] = store.getQuads(policy, null, null, null); - const relatedQuads: Quad[] = []; - for (const relation of relations) { - const relatedNodes = store.getQuads(policy, relation, null, null); - for (const q of relatedNodes) { - // Look at the rule in relation to the policy - const targetNode = q.object; - // Now find every quad over that rule, without check if the rule is assigned by our client - relatedQuads.push(...store.getQuads(targetNode, null, null, null)); + // Because an ODRL policy may only have one assigner, we can now add all policy and rule information + store.getQuads(policy, null, null, null).forEach(quad => policyDetails.add(quad)); + store.getQuads(rule, null, null, null).forEach(quad => policyDetails.add(quad)); } } - policyDetails = policyDetails.concat([...directQuads, ...relatedQuads]); } - return quadsToText(policyDetails) + return quadsToText(Array.from(policyDetails)); } \ No newline at end of file From a1831075dfdcfe5c936a67347e151064263f7437 Mon Sep 17 00:00:00 2001 From: lennertdr Date: Mon, 7 Jul 2025 11:12:21 +0200 Subject: [PATCH 06/62] test content type --- packages/uma/config/rules/odrl/addedRules.ttl | 7 ++++++ .../routeSpecific/policies/CreatePolicies.ts | 25 ++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 packages/uma/config/rules/odrl/addedRules.ttl diff --git a/packages/uma/config/rules/odrl/addedRules.ttl b/packages/uma/config/rules/odrl/addedRules.ttl new file mode 100644 index 00000000..9408efef --- /dev/null +++ b/packages/uma/config/rules/odrl/addedRules.ttl @@ -0,0 +1,7 @@ + a ; + . + a ; + ; + ; + ; + . diff --git a/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts b/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts index decf31b7..dab34bbc 100644 --- a/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts @@ -8,13 +8,36 @@ import { UCRulesStorage } from "@solidlab/ucp"; export async function addPolicies(request: HttpHandlerRequest, store: Store, storage: UCRulesStorage, clientId: string): Promise> { // 1. Parse the requested policy + const contentType = request.headers['content-type'] ?? ''; + + let parseFormat = 'Turtle'; + switch (true) { + case contentType.includes('application/n-triples'): + parseFormat = 'N-Triples'; + break; + case contentType.includes('application/n-quads'): + parseFormat = 'N-Quads'; + break; + case contentType.includes('application/trig'): + parseFormat = 'TriG'; + break; + case contentType.includes('text/n3'): + parseFormat = 'N3'; + break; + case contentType.includes('text/turtle'): + parseFormat = 'Turtle'; + break; + default: + throw new BadRequestHttpError(`Unsupported Content-Type: ${contentType}`); + } + const requestedPolicy = (request as HttpHandlerRequest).body?.policy; if (typeof requestedPolicy !== 'string') { throw new BadRequestHttpError(`Invalid request body`); } let parsedPolicy: Store; try { - parsedPolicy = await parseStringAsN3Store(requestedPolicy); + parsedPolicy = await parseStringAsN3Store(requestedPolicy, { format: parseFormat }); } catch (error) { throw new BadRequestHttpError(`Policy string can not be parsed: ${error}`) } From 89eb90007ac0173e982ef87d58c837b61000c1b6 Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Mon, 7 Jul 2025 13:52:32 +0200 Subject: [PATCH 07/62] feat: Allow APIs with raw input Some APIs might need non-JSON/form input, this allows this possibility. In the future we probably want a more generic solution, but for now this at least allows the possibility. --- packages/uma/config/default.json | 54 ++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/packages/uma/config/default.json b/packages/uma/config/default.json index c9a41ab8..526d739b 100644 --- a/packages/uma/config/default.json +++ b/packages/uma/config/default.json @@ -88,27 +88,41 @@ "@id": "urm:uma:default:JsonHttpErrorHandler", "@type": "JsonHttpErrorHandler", "handler": { - "@id": "urm:uma:default:JsonFormHttpHandler", - "@type": "JsonFormHttpHandler", - "handler": { - "@id": "urn:uma:default:RoutedHttpRequestHandler", - "@type": "RoutedHttpRequestHandler", - "routes": [ - { "@id": "urn:uma:default:UmaConfigRoute" }, - { "@id": "urn:uma:default:JwksRoute" }, - { "@id": "urn:uma:default:TokenRoute" }, - { "@id": "urn:uma:default:PermissionRegistrationRoute" }, - { "@id": "urn:uma:default:ResourceRegistrationRoute" }, - { "@id": "urn:uma:default:ResourceRegistrationOpsRoute" }, - { "@id": "urn:uma:default:IntrospectionRoute" }, - { "@id": "urn:uma:default:LogRoute" }, - { "@id": "urn:uma:default:VCRoute" }, - { "@id": "urn:uma:default:ContractRoute" } - ], - "defaultHandler": { - "@type": "DefaultRequestHandler" + "@id": "urm:uma:default:RouteHandler", + "@type": "WaterfallHandler", + "handlers": [ + { + "comment": "Handles all JSON and form encoded input/output.", + "@id": "urm:uma:default:JsonFormHttpHandler", + "@type": "JsonFormHttpHandler", + "handler": { + "@id": "urn:uma:default:JsonRoutedHttpRequestHandler", + "@type": "RoutedHttpRequestHandler", + "routes": [ + { "@id": "urn:uma:default:UmaConfigRoute" }, + { "@id": "urn:uma:default:JwksRoute" }, + { "@id": "urn:uma:default:TokenRoute" }, + { "@id": "urn:uma:default:PermissionRegistrationRoute" }, + { "@id": "urn:uma:default:ResourceRegistrationRoute" }, + { "@id": "urn:uma:default:ResourceRegistrationOpsRoute" }, + { "@id": "urn:uma:default:IntrospectionRoute" } + ], + "defaultHandler": { + "@type": "DefaultRequestHandler" + } + } + }, + { + "comment": "Handles all remaining output. These handlers have to handle input/output parsing themselves. TODO: At some point we want more generic conneg.", + "@id": "urn:uma:default:RawRoutedHttpRequestHandler", + "@type": "RoutedHttpRequestHandler", + "routes": [ + { "@id": "urn:uma:default:ContractRoute" }, + { "@id": "urn:uma:default:LogRoute" }, + { "@id": "urn:uma:default:VCRoute" } + ] } - } + ] } } }, From f2de8ad258dae6945d5cbb1ba54c32cce38d7515 Mon Sep 17 00:00:00 2001 From: lennertdr Date: Mon, 7 Jul 2025 14:05:52 +0200 Subject: [PATCH 08/62] Format checks removed, they are already in N3 Parser --- .../routeSpecific/policies/CreatePolicies.ts | 28 ++++--------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts b/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts index dab34bbc..6b940926 100644 --- a/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts @@ -8,36 +8,20 @@ import { UCRulesStorage } from "@solidlab/ucp"; export async function addPolicies(request: HttpHandlerRequest, store: Store, storage: UCRulesStorage, clientId: string): Promise> { // 1. Parse the requested policy - const contentType = request.headers['content-type'] ?? ''; + const contentType = request.headers['content-type'] ?? 'turtle'; - let parseFormat = 'Turtle'; - switch (true) { - case contentType.includes('application/n-triples'): - parseFormat = 'N-Triples'; - break; - case contentType.includes('application/n-quads'): - parseFormat = 'N-Quads'; - break; - case contentType.includes('application/trig'): - parseFormat = 'TriG'; - break; - case contentType.includes('text/n3'): - parseFormat = 'N3'; - break; - case contentType.includes('text/turtle'): - parseFormat = 'Turtle'; - break; - default: - throw new BadRequestHttpError(`Unsupported Content-Type: ${contentType}`); + // Regex check for content type + if (/(?:n3|trig|turtle|nquads?|ntriples?)$/i.test(contentType)) { + throw new BadRequestHttpError(`Content-Type ${contentType} is not supported.`); } - const requestedPolicy = (request as HttpHandlerRequest).body?.policy; + const requestedPolicy = request.body; if (typeof requestedPolicy !== 'string') { throw new BadRequestHttpError(`Invalid request body`); } let parsedPolicy: Store; try { - parsedPolicy = await parseStringAsN3Store(requestedPolicy, { format: parseFormat }); + parsedPolicy = await parseStringAsN3Store(requestedPolicy, { format: contentType }); } catch (error) { throw new BadRequestHttpError(`Policy string can not be parsed: ${error}`) } From f6dc97784b0e106c6596877171f044772b64f585 Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Mon, 7 Jul 2025 15:29:59 +0200 Subject: [PATCH 09/62] fix: Correctly handle multiple routing classes Due to the RoutedHttpRequestHandler catching unsupported requests with a default handler, it was impossible to combine multiple with a WaterfallHandler. This is now solved by moving most of the behaviour to the canHandle, and having a static throw handler for the 404. --- packages/uma/config/default.json | 9 ++-- .../http/server/RoutedHttpRequestHandler.ts | 41 +++++++++++-------- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/packages/uma/config/default.json b/packages/uma/config/default.json index 526d739b..ca9940ee 100644 --- a/packages/uma/config/default.json +++ b/packages/uma/config/default.json @@ -106,10 +106,7 @@ { "@id": "urn:uma:default:ResourceRegistrationRoute" }, { "@id": "urn:uma:default:ResourceRegistrationOpsRoute" }, { "@id": "urn:uma:default:IntrospectionRoute" } - ], - "defaultHandler": { - "@type": "DefaultRequestHandler" - } + ] } }, { @@ -121,6 +118,10 @@ { "@id": "urn:uma:default:LogRoute" }, { "@id": "urn:uma:default:VCRoute" } ] + }, + { + "@type": "StaticThrowHandler", + "error": { "@type": "NotFoundHttpError" } } ] } diff --git a/packages/uma/src/util/http/server/RoutedHttpRequestHandler.ts b/packages/uma/src/util/http/server/RoutedHttpRequestHandler.ts index 0c627b37..4840c687 100644 --- a/packages/uma/src/util/http/server/RoutedHttpRequestHandler.ts +++ b/packages/uma/src/util/http/server/RoutedHttpRequestHandler.ts @@ -1,8 +1,15 @@ -import { getLoggerFor } from '@solid/community-server'; +import { + getLoggerFor, + InternalServerError, + MethodNotAllowedHttpError, + NotImplementedHttpError +} from '@solid/community-server'; import Template from 'uri-template-lite'; import { HttpHandler, HttpHandlerContext, HttpHandlerResponse } from '../models/HttpHandler'; import { HttpHandlerRoute } from '../models/HttpHandlerRoute'; +type RouteMatch = { parameters: Record, route: HttpHandlerRoute }; + /** * A {@link HttpHandler} handling requests based on routes in a given list of {@link HttpHandlerRoute}s. * Route paths can contain variables as described in RFC 6570. @@ -18,16 +25,17 @@ import { HttpHandlerRoute } from '../models/HttpHandlerRoute'; * E.g., `http://example.com/foo/bar` will match the route template `/bar`. */ export class RoutedHttpRequestHandler extends HttpHandler { + protected readonly logger = getLoggerFor(this); protected readonly routeMap: Map; - protected readonly logger = getLoggerFor(this); + protected readonly handledRequests: WeakMap; + /** * Creates a RoutedHttpRequestHandler, super calls the HttpHandler class and expects a list of HttpHandlerControllers. */ constructor( routes: HttpHandlerRoute[], - protected readonly defaultHandler?: HttpHandler, onlyMatchTail = false, ) { super(); @@ -37,9 +45,10 @@ export class RoutedHttpRequestHandler extends HttpHandler { // Add a catchall variable to the front if only the URL tail needs to be matched. this.routeMap.set(new Template(onlyMatchTail ? `{_prefix}${route.path}` : route.path), route); } + this.handledRequests = new WeakMap(); } - async handle(context: HttpHandlerContext): Promise { + public async canHandle(context: HttpHandlerContext): Promise { const request = context.request; const path = request.url.pathname; @@ -55,26 +64,24 @@ export class RoutedHttpRequestHandler extends HttpHandler { } if (!match) { - if (this.defaultHandler) { - this.logger.info(`No matching route found, calling default handler. ${path}`); - return this.defaultHandler.handleSafe(context); - } else { - this.logger.error(`No matching route found. ${path}`); - return { body: '', headers: {}, status: 404 }; - } + throw new NotImplementedHttpError(); } this.logger.debug(`Route matched: ${JSON.stringify({ path, parameters: match.parameters })}`); if (match.route.methods && !match.route.methods.includes(request.method)) { this.logger.info(`Operation not supported. Supported operations: ${ match.route.methods }`); - return { - status: 405, - headers: { 'allow': match.route.methods.join(', ') }, - body: '', - }; + throw new MethodNotAllowedHttpError([ request.method ]); + } + this.handledRequests.set(context, match); + } + + public async handle(context: HttpHandlerContext): Promise { + const match = this.handledRequests.get(context); + if (!match) { + throw new InternalServerError('Calling handle without successful canHandle'); } - request.parameters = { ...request.parameters, ...match.parameters }; + context.request.parameters = { ...context.request.parameters, ...match.parameters }; return match.route.handler.handleSafe(context); } From 8295dbf438b681661ea7a61d2953dcf38ef5d413 Mon Sep 17 00:00:00 2001 From: lennertdr Date: Mon, 7 Jul 2025 15:34:43 +0200 Subject: [PATCH 10/62] Change to Memory structure for better testing on this branch --- packages/uma/config/default.json | 6 +++-- .../config/policies/authorizers/default.json | 5 +--- .../routeSpecific/policies/CreatePolicies.ts | 24 +++++++++---------- scripts/test-uma-ODRL-policy.ts | 13 +++++----- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/packages/uma/config/default.json b/packages/uma/config/default.json index d663c2bb..6d9cea55 100644 --- a/packages/uma/config/default.json +++ b/packages/uma/config/default.json @@ -106,7 +106,8 @@ { "@id": "urn:uma:default:PermissionRegistrationRoute" }, { "@id": "urn:uma:default:ResourceRegistrationRoute" }, { "@id": "urn:uma:default:ResourceRegistrationOpsRoute" }, - { "@id": "urn:uma:default:IntrospectionRoute" } + { "@id": "urn:uma:default:IntrospectionRoute" }, + { "@id": "urn:uma:default:GetOnePolicyRoute" } ], "defaultHandler": { "@type": "DefaultRequestHandler" @@ -120,7 +121,8 @@ "routes": [ { "@id": "urn:uma:default:ContractRoute" }, { "@id": "urn:uma:default:LogRoute" }, - { "@id": "urn:uma:default:VCRoute" } + { "@id": "urn:uma:default:VCRoute" }, + { "@id": "urn:uma:default:PolicyRoute" } ] } ] diff --git a/packages/uma/config/policies/authorizers/default.json b/packages/uma/config/policies/authorizers/default.json index c6daccc1..1b7a8ff5 100644 --- a/packages/uma/config/policies/authorizers/default.json +++ b/packages/uma/config/policies/authorizers/default.json @@ -54,10 +54,7 @@ "eyePath": { "@id": "urn:uma:variables:eyePath" }, "policies": { "@id": "urn:uma:default:RulesStorage", - "@type": "DirectoryUCRulesStorage", - "directoryPath": { - "@id": "urn:uma:variables:policyDir" - } + "@type": "MemoryUCRulesStorage" } } } diff --git a/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts b/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts index 6b940926..ededf2db 100644 --- a/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts @@ -8,20 +8,20 @@ import { UCRulesStorage } from "@solidlab/ucp"; export async function addPolicies(request: HttpHandlerRequest, store: Store, storage: UCRulesStorage, clientId: string): Promise> { // 1. Parse the requested policy - const contentType = request.headers['content-type'] ?? 'turtle'; - // Regex check for content type - if (/(?:n3|trig|turtle|nquads?|ntriples?)$/i.test(contentType)) { - throw new BadRequestHttpError(`Content-Type ${contentType} is not supported.`); - } + // const contentType = request.headers['content-type'] ?? 'turtle'; + // // Regex check for content type (awaiting server implementation) + // if (/(?:n3|trig|turtle|nquads?|ntriples?)$/i.test(contentType)) { + // throw new BadRequestHttpError(`Content-Type ${contentType} is not supported.`); + // } - const requestedPolicy = request.body; + const requestedPolicy = (request as HttpHandlerRequest).body?.policy; if (typeof requestedPolicy !== 'string') { throw new BadRequestHttpError(`Invalid request body`); } let parsedPolicy: Store; try { - parsedPolicy = await parseStringAsN3Store(requestedPolicy, { format: contentType }); + parsedPolicy = await parseStringAsN3Store(requestedPolicy/*, { format: contentType }*/); } catch (error) { throw new BadRequestHttpError(`Policy string can not be parsed: ${error}`) } @@ -32,11 +32,11 @@ export async function addPolicies(request: HttpHandlerRequest, store: Store, sto throw new BadRequestHttpError(`Policy is not authorized correctly`); } - // This check works if the 'assigner' relation only applies to rules of a policy - const allAssigners = parsedPolicy.getQuads(null, odrlAssigner, null, null); - if (allAssigners.length !== matchingClient.length) { - throw new BadRequestHttpError(`Policy is incorrectly built`); - } + // // This check works if the 'assigner' relation only applies to rules of a policy + // const allAssigners = parsedPolicy.getQuads(null, odrlAssigner, null, null); + // if (allAssigners.length !== matchingClient.length) { + // throw new BadRequestHttpError(`Policy is incorrectly built`); + // } // TODO: 3. Perform other validity checks diff --git a/scripts/test-uma-ODRL-policy.ts b/scripts/test-uma-ODRL-policy.ts index 015b4266..ae2252a8 100644 --- a/scripts/test-uma-ODRL-policy.ts +++ b/scripts/test-uma-ODRL-policy.ts @@ -10,6 +10,7 @@ const client2 = 'https://pod.example.com/profile/card#me'; const policyId = 'http://example.org/usagePolicy1'; const badPolicyId = 'nonExistentPolicy'; const policyBody = "@prefix ex: .\n@prefix odrl: .\nex:usagePolicy4 a odrl:Agreement .\nex:usagePolicy4 odrl:permission ex:permission4 .\nex:permission4 a odrl:Permission .\nex:permission4 odrl:action odrl:read .\nex:permission4 odrl:target .\nex:permission4 odrl:assignee .\nex:permission4 odrl:assigner ."; +const seed = "@prefix ex: .\r\n@prefix odrl: .\r\n@prefix dct: .\r\n\r\nex:usagePolicy1 a odrl:Agreement .\r\nex:usagePolicy1 odrl:permission ex:permission1 .\r\nex:permission1 a odrl:Permission .\r\nex:permission1 odrl:action odrl:modify .\r\nex:permission1 odrl:target .\r\nex:permission1 odrl:assignee .\r\nex:permission1 odrl:assigner .\r\n\r\nex:usagePolicy1a a odrl:Agreement .\r\nex:usagePolicy1a odrl:permission ex:permission1a .\r\nex:permission1a a odrl:Permission .\r\nex:permission1a odrl:action odrl:create .\r\nex:permission1a odrl:target .\r\nex:permission1a odrl:assignee .\r\nex:permission1a odrl:assigner .\r\n\r\nex:usagePolicy2 a odrl:Agreement .\r\nex:usagePolicy2 odrl:permission ex:permission2a .\r\nex:permission2 a odrl:Permission .\r\nex:permission2 odrl:action odrl:modify .\r\nex:permission2 odrl:target .\r\nex:permission2 odrl:assignee .\r\nex:permission2 odrl:assigner .\r\n\r\nex:usagePolicy2a a odrl:Agreement .\r\nex:usagePolicy2a odrl:permission ex:permission2 .\r\nex:permission2a a odrl:Permission .\r\nex:permission2a odrl:action odrl:create .\r\nex:permission2a odrl:target .\r\nex:permission2a odrl:assignee .\r\nex:permission2a odrl:assigner .\r\n\r\n\r\nex:usagePolicy3 a odrl:Agreement .\r\nex:usagePolicy3 odrl:permission ex:permission3 .\r\nex:permission3 a odrl:Permission .\r\nex:permission3 odrl:action odrl:read .\r\nex:permission3 odrl:target .\r\nex:permission3 odrl:assignee .\r\nex:permission3 odrl:assigner .\r\n\r\n a odrl:Set;\r\n dct:description \"ZENO is data owner of resource X. ALICE may READ resource X.\";\r\n odrl:permission , .\r\n a odrl:Permission;\r\n odrl:action odrl:read;\r\n odrl:target ex:x;\r\n odrl:assignee ex:alice;\r\n odrl:assigner ex:zeno.\r\n \r\n a odrl:Permission ;\r\n odrl:assignee ex:bob ;\r\n odrl:assigner ex:wout;\r\n odrl:action odrl:read ;\r\n odrl:target ex:x ." async function getAllPolicies() { console.log("Simple test for the GET All Policies endpoint\n"); @@ -43,19 +44,19 @@ async function postPolicy() { console.log("Simple test for the POST policy endpoint"); let response = await fetch(endpoint(), { method: 'POST', headers: { 'Authorization': client1, 'Content-Type': 'application/json' }, body: JSON.stringify({ policy: policyBody }) }); - console.log(`expecting a negative response since assigner != client: status code ${response.status}`); + console.log(`expecting a negative response since assigner != client: status code ${response.status}\nmessage: ${await response.text()}`); - response = await fetch(endpoint(), { method: 'POST', headers: { 'Authorization': client2, 'Content-Type': 'application/json' }, body: JSON.stringify({ policy: policyBody }) }); - console.log(`expecting a positive response: status code ${response.status}`); + response = await fetch(endpoint(), { method: 'POST', headers: { 'Authorization': client2, 'Content-Type': 'application/json' }, body: JSON.stringify({ policy: seed }) }); + console.log(`expecting a positive response: status code ${response.status}, ${await response.text()}`); } async function main() { - console.log("Testing all implemented Policy Endpoints:\n\n\n") + console.log("Testing all implemented Policy Endpoints:\n\n\n"); + await postPolicy(); + console.log("\n\n\n"); await getAllPolicies(); console.log("\n\n\n"); await getOnePolicy(); console.log("\n\n\n"); - await postPolicy(); - console.log("\n\n\n"); } main() From 3ee80a64b9a1f109f4b8d1915cc2a938be043303 Mon Sep 17 00:00:00 2001 From: lennertdr Date: Mon, 7 Jul 2025 16:39:28 +0200 Subject: [PATCH 11/62] POST with proper content types --- packages/uma/config/default.json | 3 +- .../routeSpecific/policies/CreatePolicies.ts | 37 ++++++++++--------- scripts/test-uma-ODRL-policy.ts | 19 ++++++---- 3 files changed, 34 insertions(+), 25 deletions(-) diff --git a/packages/uma/config/default.json b/packages/uma/config/default.json index d8371aff..acf5f3ba 100644 --- a/packages/uma/config/default.json +++ b/packages/uma/config/default.json @@ -106,7 +106,8 @@ { "@id": "urn:uma:default:PermissionRegistrationRoute" }, { "@id": "urn:uma:default:ResourceRegistrationRoute" }, { "@id": "urn:uma:default:ResourceRegistrationOpsRoute" }, - { "@id": "urn:uma:default:IntrospectionRoute" } + { "@id": "urn:uma:default:IntrospectionRoute" }, + { "@id": "urn:uma:default:GetOnePolicyRoute"} ] } }, diff --git a/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts b/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts index ededf2db..59cdc2ec 100644 --- a/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts @@ -9,19 +9,23 @@ export async function addPolicies(request: HttpHandlerRequest, store: Store, sto // 1. Parse the requested policy - // const contentType = request.headers['content-type'] ?? 'turtle'; - // // Regex check for content type (awaiting server implementation) - // if (/(?:n3|trig|turtle|nquads?|ntriples?)$/i.test(contentType)) { - // throw new BadRequestHttpError(`Content-Type ${contentType} is not supported.`); - // } - - const requestedPolicy = (request as HttpHandlerRequest).body?.policy; - if (typeof requestedPolicy !== 'string') { - throw new BadRequestHttpError(`Invalid request body`); + const contentType = request.headers['content-type'] ?? 'turtle'; + // Regex check for content type (awaiting server implementation) + if (!/(?:n3|trig|turtle|nquads?|ntriples?)$/i.test(contentType)) { + throw new BadRequestHttpError(`Content-Type ${contentType} is not supported.`); + } + + console.log("Requested Policy:", request.body) + let requestedPolicy; + if (Buffer.isBuffer(request.body)) { + requestedPolicy = request.body.toString('utf-8'); + console.log('RDF body:', requestedPolicy); + } else { + throw new Error("Expected Buffer body"); } let parsedPolicy: Store; try { - parsedPolicy = await parseStringAsN3Store(requestedPolicy/*, { format: contentType }*/); + parsedPolicy = await parseStringAsN3Store(requestedPolicy, { format: contentType }); } catch (error) { throw new BadRequestHttpError(`Policy string can not be parsed: ${error}`) } @@ -32,11 +36,11 @@ export async function addPolicies(request: HttpHandlerRequest, store: Store, sto throw new BadRequestHttpError(`Policy is not authorized correctly`); } - // // This check works if the 'assigner' relation only applies to rules of a policy - // const allAssigners = parsedPolicy.getQuads(null, odrlAssigner, null, null); - // if (allAssigners.length !== matchingClient.length) { - // throw new BadRequestHttpError(`Policy is incorrectly built`); - // } + // Making sure there are no rules added with other assigners then yourself + const allAssigners = parsedPolicy.getQuads(null, odrlAssigner, null, null); + if (allAssigners.length !== matchingClient.length) { + throw new BadRequestHttpError(`Policy is incorrectly built`); + } // TODO: 3. Perform other validity checks @@ -54,7 +58,6 @@ export async function addPolicies(request: HttpHandlerRequest, store: Store, sto return { - status: 201, - body: { message: "Policy stored successfully" } + status: 201 } } \ No newline at end of file diff --git a/scripts/test-uma-ODRL-policy.ts b/scripts/test-uma-ODRL-policy.ts index ae2252a8..4081587a 100644 --- a/scripts/test-uma-ODRL-policy.ts +++ b/scripts/test-uma-ODRL-policy.ts @@ -50,13 +50,18 @@ async function postPolicy() { console.log(`expecting a positive response: status code ${response.status}, ${await response.text()}`); } +const typeCheck = ((contentType: string): boolean => { + return (/(?:n3|trig|turtle|nquads?|ntriples?)$/i.test(contentType)) +}) + async function main() { - console.log("Testing all implemented Policy Endpoints:\n\n\n"); - await postPolicy(); - console.log("\n\n\n"); - await getAllPolicies(); - console.log("\n\n\n"); - await getOnePolicy(); - console.log("\n\n\n"); + console.log(typeCheck('text/turtle')) + // console.log("Testing all implemented Policy Endpoints:\n\n\n"); + // await postPolicy(); + // console.log("\n\n\n"); + // await getAllPolicies(); + // console.log("\n\n\n"); + // await getOnePolicy(); + // console.log("\n\n\n"); } main() From 90ff9502587e2a06fb0b9f6085c2e21be2f01838 Mon Sep 17 00:00:00 2001 From: lennertdr Date: Mon, 7 Jul 2025 17:27:44 +0200 Subject: [PATCH 12/62] Memory based tests --- .../routeSpecific/policies/GetPolicies.ts | 2 +- scripts/test-uma-ODRL-policy.ts | 122 ++++++++++++++---- 2 files changed, 98 insertions(+), 26 deletions(-) diff --git a/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts b/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts index a5b81e21..7b42f719 100644 --- a/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts @@ -37,7 +37,7 @@ function isPolicy(policyId: string): boolean { * policy if available. */ async function getOnePolicy(policyId: string, store: Store, clientId: string): Promise> { - policyId = decodeURI(policyId); + policyId = decodeURIComponent(policyId); // 1. Search the policy by ID const policyMatches = store.getQuads(namedNode(policyId), null, null, null); diff --git a/scripts/test-uma-ODRL-policy.ts b/scripts/test-uma-ODRL-policy.ts index 4081587a..8d2e16ca 100644 --- a/scripts/test-uma-ODRL-policy.ts +++ b/scripts/test-uma-ODRL-policy.ts @@ -5,21 +5,91 @@ */ const endpoint = (extra: string = '') => 'http://localhost:4000/uma/policies' + extra; -const client1 = 'https://pod.woutslabbinck.com/profile/card#me'; -const client2 = 'https://pod.example.com/profile/card#me'; +const client = (client: string = 'a') => `https://pod.${client}.com/profile/card#me`; const policyId = 'http://example.org/usagePolicy1'; const badPolicyId = 'nonExistentPolicy'; -const policyBody = "@prefix ex: .\n@prefix odrl: .\nex:usagePolicy4 a odrl:Agreement .\nex:usagePolicy4 odrl:permission ex:permission4 .\nex:permission4 a odrl:Permission .\nex:permission4 odrl:action odrl:read .\nex:permission4 odrl:target .\nex:permission4 odrl:assignee .\nex:permission4 odrl:assigner ."; -const seed = "@prefix ex: .\r\n@prefix odrl: .\r\n@prefix dct: .\r\n\r\nex:usagePolicy1 a odrl:Agreement .\r\nex:usagePolicy1 odrl:permission ex:permission1 .\r\nex:permission1 a odrl:Permission .\r\nex:permission1 odrl:action odrl:modify .\r\nex:permission1 odrl:target .\r\nex:permission1 odrl:assignee .\r\nex:permission1 odrl:assigner .\r\n\r\nex:usagePolicy1a a odrl:Agreement .\r\nex:usagePolicy1a odrl:permission ex:permission1a .\r\nex:permission1a a odrl:Permission .\r\nex:permission1a odrl:action odrl:create .\r\nex:permission1a odrl:target .\r\nex:permission1a odrl:assignee .\r\nex:permission1a odrl:assigner .\r\n\r\nex:usagePolicy2 a odrl:Agreement .\r\nex:usagePolicy2 odrl:permission ex:permission2a .\r\nex:permission2 a odrl:Permission .\r\nex:permission2 odrl:action odrl:modify .\r\nex:permission2 odrl:target .\r\nex:permission2 odrl:assignee .\r\nex:permission2 odrl:assigner .\r\n\r\nex:usagePolicy2a a odrl:Agreement .\r\nex:usagePolicy2a odrl:permission ex:permission2 .\r\nex:permission2a a odrl:Permission .\r\nex:permission2a odrl:action odrl:create .\r\nex:permission2a odrl:target .\r\nex:permission2a odrl:assignee .\r\nex:permission2a odrl:assigner .\r\n\r\n\r\nex:usagePolicy3 a odrl:Agreement .\r\nex:usagePolicy3 odrl:permission ex:permission3 .\r\nex:permission3 a odrl:Permission .\r\nex:permission3 odrl:action odrl:read .\r\nex:permission3 odrl:target .\r\nex:permission3 odrl:assignee .\r\nex:permission3 odrl:assigner .\r\n\r\n a odrl:Set;\r\n dct:description \"ZENO is data owner of resource X. ALICE may READ resource X.\";\r\n odrl:permission , .\r\n a odrl:Permission;\r\n odrl:action odrl:read;\r\n odrl:target ex:x;\r\n odrl:assignee ex:alice;\r\n odrl:assigner ex:zeno.\r\n \r\n a odrl:Permission ;\r\n odrl:assignee ex:bob ;\r\n odrl:assigner ex:wout;\r\n odrl:action odrl:read ;\r\n odrl:target ex:x ." +const policyA = ` +@prefix ex: . +@prefix odrl: . +@prefix dct: . + +ex:usagePolicy1 a odrl:Agreement . +ex:usagePolicy1 odrl:permission ex:permission1 . +ex:permission1 a odrl:Permission . +ex:permission1 odrl:action odrl:modify . +ex:permission1 odrl:target . +ex:permission1 odrl:assignee . +ex:permission1 odrl:assigner . + +ex:usagePolicy1a a odrl:Agreement . +ex:usagePolicy1a odrl:permission ex:permission1a . +ex:permission1a a odrl:Permission . +ex:permission1a odrl:action odrl:create . +ex:permission1a odrl:target . +ex:permission1a odrl:assignee . +ex:permission1a odrl:assigner . + + a odrl:Set; + dct:description "A is data owner of resource X. ALICE may READ resource X."; + odrl:permission . + a odrl:Permission; + odrl:action odrl:read; + odrl:target ex:x; + odrl:assignee ex:alice; + odrl:assigner . +`; +const policyB = `@prefix ex: . +@prefix odrl: . +@prefix dct: . + +ex:usagePolicy2 a odrl:Agreement . +ex:usagePolicy2 odrl:permission ex:permission2a . +ex:permission2 a odrl:Permission . +ex:permission2 odrl:action odrl:modify . +ex:permission2 odrl:target . +ex:permission2 odrl:assignee . +ex:permission2 odrl:assigner . + +ex:usagePolicy2a a odrl:Agreement . +ex:usagePolicy2a odrl:permission ex:permission2 . +ex:permission2a a odrl:Permission . +ex:permission2a odrl:action odrl:create . +ex:permission2a odrl:target . +ex:permission2a odrl:assignee . +ex:permission2a odrl:assigner . + + a odrl:Set; + dct:description "ZENO is data owner of resource X. ALICE may READ resource X."; + odrl:permission . + + a odrl:Permission ; + odrl:assignee ex:bob ; + odrl:assigner ; + odrl:action odrl:read ; + odrl:target ex:x . +`; + +const policyC = `@prefix ex: . +@prefix odrl: . +@prefix dct: . + +ex:usagePolicy3 a odrl:Agreement . +ex:usagePolicy3 odrl:permission ex:permission3 . +ex:permission3 a odrl:Permission . +ex:permission3 odrl:action odrl:read . +ex:permission3 odrl:target . +ex:permission3 odrl:assignee . +ex:permission3 odrl:assigner .`; + async function getAllPolicies() { console.log("Simple test for the GET All Policies endpoint\n"); - let response = await fetch(endpoint(), { headers: { 'Authorization': client1 } }) - console.log("expecting all five policies and their relations: \n", await response.text()) + let response = await fetch(endpoint(), { headers: { 'Authorization': client('a') } }) + console.log("expecting policy 1, 1a and : \n", await response.text()) - response = await fetch(endpoint(), { headers: { 'Authorization': client2 } }) - console.log("expecting zero policies: ", await response.text()) + response = await fetch(endpoint(), { headers: { 'Authorization': client('b') } }) + console.log("expecting policy 2, 2a and : \n", await response.text()) response = await fetch(endpoint(), {}); console.log(`expecting 4xx error code (no authorization header provided): ${response.status}`) @@ -28,40 +98,42 @@ async function getAllPolicies() { async function getOnePolicy() { console.log("Simple test for the GET One Policy endpoint"); - const encoded = encodeURI(policyId); + const encoded = encodeURIComponent(policyId); - let response = await fetch(endpoint(`/${encoded}`), { headers: { 'Authorization': client1 } }); + let response = await fetch(endpoint(`/${encoded}`), { headers: { 'Authorization': client('a') } }); console.log(`expecting to return relevent information about ${policyId}`, await response.text()); - response = await fetch(endpoint(`/${badPolicyId}`), { headers: { 'Authorization': client1 } }); + response = await fetch(endpoint(`/${badPolicyId}`), { headers: { 'Authorization': client('b') } }); console.log(`expecting 4xx error code since the policy ID is not valid: ${response.status}`) - response = await fetch(endpoint(`/${encoded}`), { headers: { 'Authorization': client2 } }); + response = await fetch(endpoint(`/${encoded}`), { headers: { 'Authorization': client('c') } }); console.log(`expecting 4xx error code since the client is not authorized to access the policy: ${response.status}`) } async function postPolicy() { console.log("Simple test for the POST policy endpoint"); - let response = await fetch(endpoint(), { method: 'POST', headers: { 'Authorization': client1, 'Content-Type': 'application/json' }, body: JSON.stringify({ policy: policyBody }) }); + let response = await fetch(endpoint(), { method: 'POST', headers: { 'Authorization': client('a'), 'Content-Type': 'text/turtle' }, body: Buffer.from(policyB, 'utf-8') }); console.log(`expecting a negative response since assigner != client: status code ${response.status}\nmessage: ${await response.text()}`); - response = await fetch(endpoint(), { method: 'POST', headers: { 'Authorization': client2, 'Content-Type': 'application/json' }, body: JSON.stringify({ policy: seed }) }); + response = await fetch(endpoint(), { method: 'POST', headers: { 'Authorization': client('a'), 'Content-Type': 'text/turtle' }, body: Buffer.from(policyA, 'utf-8') }); + console.log(`expecting a positive response: status code ${response.status}, ${await response.text()}`); + + response = await fetch(endpoint(), { method: 'POST', headers: { 'Authorization': client('b'), 'Content-Type': 'text/turtle' }, body: Buffer.from(policyB, 'utf-8') }); + console.log(`expecting a positive response: status code ${response.status}, ${await response.text()}`); + + response = await fetch(endpoint(), { method: 'POST', headers: { 'Authorization': client('c'), 'Content-Type': 'text/turtle' }, body: Buffer.from(policyC, 'utf-8') }); console.log(`expecting a positive response: status code ${response.status}, ${await response.text()}`); } -const typeCheck = ((contentType: string): boolean => { - return (/(?:n3|trig|turtle|nquads?|ntriples?)$/i.test(contentType)) -}) async function main() { - console.log(typeCheck('text/turtle')) - // console.log("Testing all implemented Policy Endpoints:\n\n\n"); - // await postPolicy(); - // console.log("\n\n\n"); - // await getAllPolicies(); - // console.log("\n\n\n"); - // await getOnePolicy(); - // console.log("\n\n\n"); + console.log("Testing all implemented Policy Endpoints:\n\n\n"); + await postPolicy(); + console.log("\n\n\n"); + await getAllPolicies(); + console.log("\n\n\n"); + await getOnePolicy(); + console.log("\n\n\n"); } main() From b5dab70a5c3a75f9c3478f7bab381bc16941bb26 Mon Sep 17 00:00:00 2001 From: lennertdr Date: Tue, 8 Jul 2025 09:13:58 +0200 Subject: [PATCH 13/62] import fix --- packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts b/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts index 59cdc2ec..e3ba533f 100644 --- a/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts @@ -1,6 +1,6 @@ import { Store } from "n3"; import { HttpHandlerRequest, HttpHandlerResponse } from "../../http/models/HttpHandler"; -import { namedNode, odrlAssigner, PolicyBody } from "./helpers"; +import { namedNode, odrlAssigner } from "./PolicyUtil"; import { BadRequestHttpError, InternalServerError } from "@solid/community-server"; import { parseStringAsN3Store } from "koreografeye"; import { UCRulesStorage } from "@solidlab/ucp"; From 2dad0f7c0abcca8f3cdb0169a2598f7ddaac5fae Mon Sep 17 00:00:00 2001 From: lennertdr Date: Tue, 8 Jul 2025 11:13:23 +0200 Subject: [PATCH 14/62] GET /uma/policies/ first finished implementation --- .../routeSpecific/policies/GetPolicies.ts | 41 ++++++++++++------- scripts/test-uma-ODRL-policy.ts | 4 +- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts b/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts index 65e7d9ba..41b7c513 100644 --- a/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts @@ -35,6 +35,11 @@ function isPolicy(policyId: string): boolean { /** * Function to implement the GET /uma/policies/ endpoint, it retrieves all information about a certain * policy if available. + * + * @param policyId the policy id, which is ENCODED + * @param store + * @param clientId the clients webID + * @returns aynchronous HTTP response: */ async function getOnePolicy(policyId: string, store: Store, clientId: string): Promise> { policyId = decodeURIComponent(policyId); @@ -42,24 +47,30 @@ async function getOnePolicy(policyId: string, store: Store, clientId: string): P // 1. Search the policy by ID const policyMatches = store.getQuads(namedNode(policyId), null, null, null); - // 2. Find the rules in the policy assigned by the client (first find the clients rules, then filter the ones based on the policy) - const ownedRules = store.getQuads(null, odrlAssigner, namedNode(clientId), null); - const filteredRules = ownedRules.filter(rule => - policyMatches.some(quad => quad.object.id === rule.subject.id) - ); - - // 3. Check if there are no other assigners in this policy (this is against ODRL definition of a policy) - const otherAssigners = store.getQuads(null, odrlAssigner, null, null); - if (ownedRules.length < otherAssigners.length) { - // TODO: We might expose information, handle here - throw new NotImplementedError("Handling of policies with multiple assigners is yet to be implemented"); + // 2. Find the rules that this policy defines + let policyRules: Quad[] = [] + for (const relation of relations) { + policyRules = [...policyRules, ...store.getQuads(namedNode(policyId), relation, null, null)] + } + + // 3. Only keep the rules assigned by the client + const ownedRules = policyRules.filter(quad => store.getQuads(quad.object, odrlAssigner, namedNode(clientId), null).length > 0); + + // Return an empty body when no rules are found + if (ownedRules.length === 0) { + return { + status: 200, + headers: { + 'content-type': 'text/turtle', + }, + body: '', + } } - // 4. Search all info about the policy AND the rules, for now with depth 1 but a recursive variant needs to be implemented. + // 4. Search all info about the policy AND the rules, for now with depth 1 but a recursive variant needs to be implemented here. let details: Set = new Set(policyMatches); - filteredRules.forEach((rule) => { - details.add(rule); - store.getQuads(rule.subject, null, null, null).forEach(q => details.add(q)) + ownedRules.forEach((rule) => { + store.getQuads(rule.object, null, null, null).forEach(q => details.add(q)); }); return quadsToText(Array.from(details)); diff --git a/scripts/test-uma-ODRL-policy.ts b/scripts/test-uma-ODRL-policy.ts index d83cce6c..eeea4b2d 100644 --- a/scripts/test-uma-ODRL-policy.ts +++ b/scripts/test-uma-ODRL-policy.ts @@ -104,10 +104,10 @@ async function getOnePolicy() { console.log(`expecting to return relevent information about ${policyId}`, await response.text()); response = await fetch(endpoint(`/${badPolicyId}`), { headers: { 'Authorization': client('b') } }); - console.log(`expecting 4xx error code since the policy ID is not valid: ${response.status}`) + console.log(`expecting an empty body, the policy ID does not exist: ${await response.text()}`); response = await fetch(endpoint(`/${encoded}`), { headers: { 'Authorization': client('c') } }); - console.log(`expecting 4xx error code since the client is not authorized to access the policy: ${response.status}`) + console.log(`expecting an empty body, the client is not authorized: ${await response.text()}`); } async function postPolicy() { From 2f43a304ca0f970f379c3472aa200c74d9747dc4 Mon Sep 17 00:00:00 2001 From: lennertdr Date: Tue, 8 Jul 2025 12:43:58 +0200 Subject: [PATCH 15/62] Add extra checks to POST --- .../routeSpecific/policies/CreatePolicies.ts | 43 ++++++---- scripts/test-uma-ODRL-policy.ts | 77 ++--------------- scripts/util/policyExampels.ts | 85 +++++++++++++++++++ 3 files changed, 117 insertions(+), 88 deletions(-) create mode 100644 scripts/util/policyExampels.ts diff --git a/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts b/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts index e3ba533f..01df33c7 100644 --- a/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts @@ -1,6 +1,6 @@ -import { Store } from "n3"; +import { Quad, Quad_Subject, Store } from "n3"; import { HttpHandlerRequest, HttpHandlerResponse } from "../../http/models/HttpHandler"; -import { namedNode, odrlAssigner } from "./PolicyUtil"; +import { namedNode, odrlAssigner, relations } from "./PolicyUtil"; import { BadRequestHttpError, InternalServerError } from "@solid/community-server"; import { parseStringAsN3Store } from "koreografeye"; import { UCRulesStorage } from "@solidlab/ucp"; @@ -9,8 +9,8 @@ export async function addPolicies(request: HttpHandlerRequest, store: Store, sto // 1. Parse the requested policy - const contentType = request.headers['content-type'] ?? 'turtle'; - // Regex check for content type (awaiting server implementation) + // Regex check for content type + const contentType = request.headers['content-type']; if (!/(?:n3|trig|turtle|nquads?|ntriples?)$/i.test(contentType)) { throw new BadRequestHttpError(`Content-Type ${contentType} is not supported.`); } @@ -30,26 +30,37 @@ export async function addPolicies(request: HttpHandlerRequest, store: Store, sto throw new BadRequestHttpError(`Policy string can not be parsed: ${error}`) } - // 2. Check if assigner is client - const matchingClient = parsedPolicy.getQuads(null, odrlAssigner, namedNode(clientId), null); - if (matchingClient.length === 0) { - throw new BadRequestHttpError(`Policy is not authorized correctly`); - } + // 2. Sanitization checks - // Making sure there are no rules added with other assigners then yourself - const allAssigners = parsedPolicy.getQuads(null, odrlAssigner, null, null); - if (allAssigners.length !== matchingClient.length) { - throw new BadRequestHttpError(`Policy is incorrectly built`); - } + // Check that every rule defined by the policy has exactly one assigner, every rule is unique and every assigner is the client + const definedRules = new Set(); + for (const relation of relations) { + + // Every rule definition of every policy + const policyRelationRules = parsedPolicy.getQuads(null, relation, null, null); + + for (const quad of policyRelationRules) { + const rule = quad.object; - // TODO: 3. Perform other validity checks + // The policy should not define multiple rules with the same ID, this check also + // restricts the same rule to be defined twice (even if relation is equal) + if (definedRules.has(rule)) throw new BadRequestHttpError(`Rule ambiguity in rule ${rule.id}`); + definedRules.add(rule); + + // The rule should have exactly one assigner, and it should be the client + const ruleAssignerX = parsedPolicy.getQuads(rule, odrlAssigner, null, null); + + if (ruleAssignerX.length !== 1) throw new BadRequestHttpError(`Rule ${rule.id} should have exactly one assigner`); + if (ruleAssignerX[0].object.id !== clientId) throw new BadRequestHttpError(`Rule ${rule.id} needs an authorized assigner`); + } + } // Check if assigner of the policy has access to the target // Check if there is at least one permission/prohibition/duty // Check if every rule has a target // ... - // 4. Add the policy to the rule storage + // 3 Add the policy to the rule storage try { await storage.addRule(parsedPolicy); } catch (error) { diff --git a/scripts/test-uma-ODRL-policy.ts b/scripts/test-uma-ODRL-policy.ts index eeea4b2d..3a21f547 100644 --- a/scripts/test-uma-ODRL-policy.ts +++ b/scripts/test-uma-ODRL-policy.ts @@ -4,82 +4,12 @@ * The purpose of this file is to test the /policies endpoint. */ +import { policyA, policyB, policyC, badPolicy1 } from "./util/policyExampels"; + const endpoint = (extra: string = '') => 'http://localhost:4000/uma/policies' + extra; const client = (client: string = 'a') => `https://pod.${client}.com/profile/card#me`; const policyId = 'http://example.org/usagePolicy1'; const badPolicyId = 'nonExistentPolicy'; -const policyA = ` -@prefix ex: . -@prefix odrl: . -@prefix dct: . - -ex:usagePolicy1 a odrl:Agreement . -ex:usagePolicy1 odrl:permission ex:permission1 . -ex:permission1 a odrl:Permission . -ex:permission1 odrl:action odrl:modify . -ex:permission1 odrl:target . -ex:permission1 odrl:assignee . -ex:permission1 odrl:assigner . - -ex:usagePolicy1a a odrl:Agreement . -ex:usagePolicy1a odrl:permission ex:permission1a . -ex:permission1a a odrl:Permission . -ex:permission1a odrl:action odrl:create . -ex:permission1a odrl:target . -ex:permission1a odrl:assignee . -ex:permission1a odrl:assigner . - - a odrl:Set; - dct:description "A is data owner of resource X. ALICE may READ resource X."; - odrl:permission . - a odrl:Permission; - odrl:action odrl:read; - odrl:target ex:x; - odrl:assignee ex:alice; - odrl:assigner . -`; -const policyB = `@prefix ex: . -@prefix odrl: . -@prefix dct: . - -ex:usagePolicy2 a odrl:Agreement . -ex:usagePolicy2 odrl:permission ex:permission2a . -ex:permission2 a odrl:Permission . -ex:permission2 odrl:action odrl:modify . -ex:permission2 odrl:target . -ex:permission2 odrl:assignee . -ex:permission2 odrl:assigner . - -ex:usagePolicy2a a odrl:Agreement . -ex:usagePolicy2a odrl:permission ex:permission2 . -ex:permission2a a odrl:Permission . -ex:permission2a odrl:action odrl:create . -ex:permission2a odrl:target . -ex:permission2a odrl:assignee . -ex:permission2a odrl:assigner . - - a odrl:Set; - dct:description "ZENO is data owner of resource X. ALICE may READ resource X."; - odrl:permission . - - a odrl:Permission ; - odrl:assignee ex:bob ; - odrl:assigner ; - odrl:action odrl:read ; - odrl:target ex:x . -`; - -const policyC = `@prefix ex: . -@prefix odrl: . -@prefix dct: . - -ex:usagePolicy3 a odrl:Agreement . -ex:usagePolicy3 odrl:permission ex:permission3 . -ex:permission3 a odrl:Permission . -ex:permission3 odrl:action odrl:read . -ex:permission3 odrl:target . -ex:permission3 odrl:assignee . -ex:permission3 odrl:assigner .`; async function getAllPolicies() { @@ -116,6 +46,9 @@ async function postPolicy() { let response = await fetch(endpoint(), { method: 'POST', headers: { 'Authorization': client('a'), 'Content-Type': 'text/turtle' }, body: Buffer.from(policyB, 'utf-8') }); console.log(`expecting a negative response since assigner != client: status code ${response.status}\nmessage: ${await response.text()}`); + response = await fetch(endpoint(), { method: 'POST', headers: { 'Authorization': client('b'), 'Content-Type': 'text/turtle' }, body: Buffer.from(badPolicy1, 'utf-8') }); + console.log(`expecting a negative response since policy has a rule with no assigner ${response.status}\nmessage: ${await response.text()}`); + response = await fetch(endpoint(), { method: 'POST', headers: { 'Authorization': client('a'), 'Content-Type': 'text/turtle' }, body: Buffer.from(policyA, 'utf-8') }); console.log(`expecting a positive response: status code ${response.status}, ${await response.text()}`); diff --git a/scripts/util/policyExampels.ts b/scripts/util/policyExampels.ts new file mode 100644 index 00000000..732f2e2e --- /dev/null +++ b/scripts/util/policyExampels.ts @@ -0,0 +1,85 @@ +export const policyA = ` +@prefix ex: . +@prefix odrl: . +@prefix dct: . + +ex:usagePolicy1 a odrl:Agreement . +ex:usagePolicy1 odrl:permission ex:permission1 . +ex:permission1 a odrl:Permission . +ex:permission1 odrl:action odrl:modify . +ex:permission1 odrl:target . +ex:permission1 odrl:assignee . +ex:permission1 odrl:assigner . + +ex:usagePolicy1a a odrl:Agreement . +ex:usagePolicy1a odrl:permission ex:permission1a . +ex:permission1a a odrl:Permission . +ex:permission1a odrl:action odrl:create . +ex:permission1a odrl:target . +ex:permission1a odrl:assignee . +ex:permission1a odrl:assigner . + + a odrl:Set; + dct:description "A is data owner of resource X. ALICE may READ resource X."; + odrl:permission . + a odrl:Permission; + odrl:action odrl:read; + odrl:target ex:x; + odrl:assignee ex:alice; + odrl:assigner . +`; +export const policyB = `@prefix ex: . +@prefix odrl: . +@prefix dct: . + +ex:usagePolicy2 a odrl:Agreement . +ex:usagePolicy2 odrl:permission ex:permission2a . +ex:permission2 a odrl:Permission . +ex:permission2 odrl:action odrl:modify . +ex:permission2 odrl:target . +ex:permission2 odrl:assignee . +ex:permission2 odrl:assigner . + +ex:usagePolicy2a a odrl:Agreement . +ex:usagePolicy2a odrl:permission ex:permission2 . +ex:permission2a a odrl:Permission . +ex:permission2a odrl:action odrl:create . +ex:permission2a odrl:target . +ex:permission2a odrl:assignee . +ex:permission2a odrl:assigner . + + a odrl:Set; + dct:description "ZENO is data owner of resource X. ALICE may READ resource X."; + odrl:permission . + + a odrl:Permission ; + odrl:assignee ex:bob ; + odrl:assigner ; + odrl:action odrl:read ; + odrl:target ex:x . +`; + +export const policyC = `@prefix ex: . +@prefix odrl: . +@prefix dct: . + +ex:usagePolicy3 a odrl:Agreement . +ex:usagePolicy3 odrl:permission ex:permission3 . +ex:permission3 a odrl:Permission . +ex:permission3 odrl:action odrl:read . +ex:permission3 odrl:target . +ex:permission3 odrl:assignee . +ex:permission3 odrl:assigner .`; + +export const badPolicy1 = ` +@prefix ex: . +@prefix odrl: . +@prefix dct: . + +ex:usagePolicyBad a odrl:Agreement . +ex:usagePolicyBad odrl:permission ex:permissionBad . +ex:permissionBad a odrl:Permission . +ex:permissionBad odrl:action odrl:modify . +ex:permissionBad odrl:target . +ex:permissionBad odrl:assignee . +` \ No newline at end of file From 73d32d8311368aa1af8d019d4aea7d49bb2fcec5 Mon Sep 17 00:00:00 2001 From: lennertdr Date: Tue, 8 Jul 2025 13:24:38 +0200 Subject: [PATCH 16/62] More generic url handling and POST with sanitize function (to be completed) --- packages/uma/src/routes/Policy.ts | 7 ++- .../routeSpecific/policies/CreatePolicies.ts | 61 ++++++++++--------- .../routeSpecific/policies/GetPolicies.ts | 30 ++++----- 3 files changed, 48 insertions(+), 50 deletions(-) diff --git a/packages/uma/src/routes/Policy.ts b/packages/uma/src/routes/Policy.ts index 60db3ede..c30b7146 100644 --- a/packages/uma/src/routes/Policy.ts +++ b/packages/uma/src/routes/Policy.ts @@ -13,7 +13,8 @@ export class PolicyRequestHandler extends HttpHandler { protected readonly logger = getLoggerFor(this); constructor( - private readonly storage: UCRulesStorage + protected readonly storage: UCRulesStorage, + protected readonly baseUrl: string, ) { super(); } @@ -44,8 +45,8 @@ export class PolicyRequestHandler extends HttpHandler { const store = await this.storage.getStore(); switch (request.method) { - case 'GET': return getPolicies(request, store, client); - case 'POST': return addPolicies(request, store, this.storage, client); + case 'GET': return getPolicies(request, store, client, this.baseUrl); + case 'POST': return addPolicies(request, this.storage, client); // TODO: add other endpoints default: throw new MethodNotAllowedHttpError(); } diff --git a/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts b/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts index 01df33c7..f17619ef 100644 --- a/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts @@ -1,37 +1,11 @@ -import { Quad, Quad_Subject, Store } from "n3"; +import { Quad, Store } from "n3"; import { HttpHandlerRequest, HttpHandlerResponse } from "../../http/models/HttpHandler"; -import { namedNode, odrlAssigner, relations } from "./PolicyUtil"; +import { odrlAssigner, relations } from "./PolicyUtil"; import { BadRequestHttpError, InternalServerError } from "@solid/community-server"; import { parseStringAsN3Store } from "koreografeye"; import { UCRulesStorage } from "@solidlab/ucp"; -export async function addPolicies(request: HttpHandlerRequest, store: Store, storage: UCRulesStorage, clientId: string): Promise> { - - // 1. Parse the requested policy - - // Regex check for content type - const contentType = request.headers['content-type']; - if (!/(?:n3|trig|turtle|nquads?|ntriples?)$/i.test(contentType)) { - throw new BadRequestHttpError(`Content-Type ${contentType} is not supported.`); - } - - console.log("Requested Policy:", request.body) - let requestedPolicy; - if (Buffer.isBuffer(request.body)) { - requestedPolicy = request.body.toString('utf-8'); - console.log('RDF body:', requestedPolicy); - } else { - throw new Error("Expected Buffer body"); - } - let parsedPolicy: Store; - try { - parsedPolicy = await parseStringAsN3Store(requestedPolicy, { format: contentType }); - } catch (error) { - throw new BadRequestHttpError(`Policy string can not be parsed: ${error}`) - } - - // 2. Sanitization checks - +export function sanitizeRule(parsedPolicy: Store, clientId: string): void { // Check that every rule defined by the policy has exactly one assigner, every rule is unique and every assigner is the client const definedRules = new Set(); for (const relation of relations) { @@ -59,6 +33,35 @@ export async function addPolicies(request: HttpHandlerRequest, store: Store, sto // Check if there is at least one permission/prohibition/duty // Check if every rule has a target // ... +} + +export async function addPolicies(request: HttpHandlerRequest, storage: UCRulesStorage, clientId: string): Promise> { + + // 1. Parse the requested policy + + // Regex check for content type + const contentType = request.headers['content-type']; + if (!/(?:n3|trig|turtle|nquads?|ntriples?)$/i.test(contentType)) { + throw new BadRequestHttpError(`Content-Type ${contentType} is not supported.`); + } + + let requestedPolicy; + if (Buffer.isBuffer(request.body)) { + requestedPolicy = request.body.toString('utf-8'); + console.log('RDF body:', requestedPolicy); + } else { + throw new Error("Expected Buffer body"); + } + let parsedPolicy: Store; + try { + parsedPolicy = await parseStringAsN3Store(requestedPolicy, { format: contentType }); + } catch (error) { + throw new BadRequestHttpError(`Policy string can not be parsed: ${error}`) + } + + // 2. Sanitization checks (error is thrown when checks fail) + sanitizeRule(parsedPolicy, clientId); + // 3 Add the policy to the rule storage try { diff --git a/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts b/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts index 41b7c513..e8e06d49 100644 --- a/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts @@ -1,36 +1,30 @@ import { HttpHandlerRequest, HttpHandlerResponse } from "../../http/models/HttpHandler"; -import { Quad, Store, Writer } from "n3"; +import { Quad, Store } from "n3"; import { odrlAssigner, relations, namedNode, quadsToText } from "./PolicyUtil"; import { MethodNotAllowedHttpError } from "@solid/community-server"; -import { NotImplementedError } from "@inrupt/solid-client-authn-core"; /** * Handling of the GET /uma/policies endpoint * * @param request will give all policies when no is fixed in the URL, otherwise it will give the required policy (if allowed) */ -export async function getPolicies(request: HttpHandlerRequest, store: Store, clientId: string): Promise> { - if (request.url.pathname === '/uma/policies') +export async function getPolicies(request: HttpHandlerRequest, store: Store, clientId: string, baseUrl: string): Promise> { + console.log("PATHNAME VS BASE URL", request.url, baseUrl); + if (request.url.href.slice(0, baseUrl.length) !== baseUrl) throw new MethodNotAllowedHttpError(); + const pathname = request.url.href.slice(baseUrl.length); + console.log(pathname) + if (pathname === '/policies') return getAllPolicies(store, clientId); // If asked for a policy, validate the policy ID - const args = request.url.pathname.split('/'); - if (args.length === 4 && isPolicy(args[3])) - return getOnePolicy(args[3], store, clientId); + const args = pathname.split('/'); + console.log(args) + if (args.length === 3 && args[1] === 'policies') + return getOnePolicy(args[2], store, clientId); throw new MethodNotAllowedHttpError(); } -/** - * Function to determine the validity of the of the GET /uma/policies/ endpoint (not implemented yet) - * - * @param policyId - * @returns the validity of policyId - */ -function isPolicy(policyId: string): boolean { - // TODO - return true; -} /** * Function to implement the GET /uma/policies/ endpoint, it retrieves all information about a certain @@ -55,7 +49,7 @@ async function getOnePolicy(policyId: string, store: Store, clientId: string): P // 3. Only keep the rules assigned by the client const ownedRules = policyRules.filter(quad => store.getQuads(quad.object, odrlAssigner, namedNode(clientId), null).length > 0); - + console.log('owned rules', ownedRules) // Return an empty body when no rules are found if (ownedRules.length === 0) { return { From 9e1102b5c51c7789598d4a44457fead1bb947fdb Mon Sep 17 00:00:00 2001 From: lennertdr Date: Tue, 8 Jul 2025 14:55:12 +0200 Subject: [PATCH 17/62] excessive documentation --- docs/policy-management.md | 46 +++++++++++++++++++ .../routeSpecific/policies/GetPolicies.ts | 7 +-- 2 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 docs/policy-management.md diff --git a/docs/policy-management.md b/docs/policy-management.md new file mode 100644 index 00000000..88a8436d --- /dev/null +++ b/docs/policy-management.md @@ -0,0 +1,46 @@ +# Policy Management +In this document we describe the *policy adminstration endpoint*. +It contains the methods to describe how to create, read, update and delete policies + +## Supported endpoints +The current implementation supports `GET` and `POST` requests, which comply with some restrictions: +- The request has its `'Authorization'` header set to the clients webID. More on that [later](#authorizationauthentication-decisions) +- The requests as discussed in this document use the same base URL: `/uma/policies` + +### Creating policies +Create a policy/multiple policies through a POST request to `/uma/policies`. Apart from its Authorization header, the `'Content-Type'` must be set to the RDF serialization format in which the body is written. The accepted formats are those accepted by the [N3 Parser](https://github.com/rdfjs/N3.js/?tab=readme-ov-file#parsing). + +The body is expected to represent a proper ODRL policy, although some [sanitization](#sanitization-decisions) is applied to ensure minimal validity. + +### Reading policies +To read policies, two endpoints are implemented: +- GET `/uma/policies`: get policy information that you are authorized to see, for every policy +- GET `/uma/policies/`: get policy information that you are authorized to see, for the policy with the requested ID + +The current algorithm will retrieve the IDs of the policies and its rules that you are authorized to see. It will seek information about those properties with **depth 1**. This is not representative for a lot of policies, hence a recursive algorithm will be implemented in the future. + +### Updating policies +Yet to be implemented... + +### Deleting policies +Yet to be implemented... + +extra info + +#### Authorization/Authentication decisions +The current implementation has insufficient authentication restrictions. Currently, the only requirement is that the 'Authorization' header is to be set to the webID of the "logged on" client. Proper procedures to authenticate this client are still to be implemented. + +#### Sanitization decisions +Some endpoints allow new policies to be created, or existing policies to be modified. This introduces the possibility of invalid syntactic or semantic policies, hence a sanitization strategy is required. In the current implementation, only POST could introduce such problems. We provided the following basic checks: +- Every defined rule must have a unique ID +- Every rule must have exactly one assigner +- Every assigner must match the authenticated client + +Sanitization Limitations +- It is possible to `POST` a policy with an ID that already exists, or with rules that reuse already existing IDs +- There are currently no checks to verify whether a client is sufficiently authorized to create or modify a policy/rule for a specific target. + * A client should not be in able to alter rights about a target it does not have access to +- There are plenty of other sanitization checks to be considered. + +#### URI encodig decision +Some operations require the client to specify a policy ID in the URL. Since policy ID's might contain reserved characters (e.g. `/`, `:`, ...), we have chosen to encode them with the builtin [`encodeURIComponent()` function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent). Using this method, reserved characters will be converted to their respective UTF-8 encodings. \ No newline at end of file diff --git a/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts b/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts index e8e06d49..ac930047 100644 --- a/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts @@ -9,14 +9,15 @@ import { MethodNotAllowedHttpError } from "@solid/community-server"; * @param request will give all policies when no is fixed in the URL, otherwise it will give the required policy (if allowed) */ export async function getPolicies(request: HttpHandlerRequest, store: Store, clientId: string, baseUrl: string): Promise> { - console.log("PATHNAME VS BASE URL", request.url, baseUrl); + // This shouldn't happen if (request.url.href.slice(0, baseUrl.length) !== baseUrl) throw new MethodNotAllowedHttpError(); const pathname = request.url.href.slice(baseUrl.length); - console.log(pathname) + + // If no other argument(s), get all if (pathname === '/policies') return getAllPolicies(store, clientId); - // If asked for a policy, validate the policy ID + // If asked for a policy, get one const args = pathname.split('/'); console.log(args) if (args.length === 3 && args[1] === 'policies') From e11cc433d02919008215f259e03a6dacea62a5f9 Mon Sep 17 00:00:00 2001 From: lennertdr Date: Tue, 8 Jul 2025 15:46:11 +0200 Subject: [PATCH 18/62] test doc and very primitive way to detect fails --- docs/policy-management.md | 5 ++++- scripts/test-uma-ODRL-policy.ts | 30 ++++++++++++++++++++++++++++-- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/docs/policy-management.md b/docs/policy-management.md index 88a8436d..c07d33ac 100644 --- a/docs/policy-management.md +++ b/docs/policy-management.md @@ -43,4 +43,7 @@ Sanitization Limitations - There are plenty of other sanitization checks to be considered. #### URI encodig decision -Some operations require the client to specify a policy ID in the URL. Since policy ID's might contain reserved characters (e.g. `/`, `:`, ...), we have chosen to encode them with the builtin [`encodeURIComponent()` function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent). Using this method, reserved characters will be converted to their respective UTF-8 encodings. \ No newline at end of file +Some operations require the client to specify a policy ID in the URL. Since policy ID's might contain reserved characters (e.g. `/`, `:`, ...), we have chosen to encode them with the builtin [`encodeURIComponent()` function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent). Using this method, reserved characters will be converted to their respective UTF-8 encodings. + +## Testing +The current implementation is tested only by the script in `scripts\test-uma-ODRL-policy.ts`. This script tests every implemented endpoint with a designated flow. Since the script initiates with an empty storage, and there is no endpoint or other way to seed it, the first requests must test the POST endpoint. These tests are designed to ensure that the storage is filled. After the POST tests, the access endpoints can be tested. \ No newline at end of file diff --git a/scripts/test-uma-ODRL-policy.ts b/scripts/test-uma-ODRL-policy.ts index 3a21f547..7193758f 100644 --- a/scripts/test-uma-ODRL-policy.ts +++ b/scripts/test-uma-ODRL-policy.ts @@ -11,18 +11,27 @@ const client = (client: string = 'a') => `https://pod.${client}.com/profile/card const policyId = 'http://example.org/usagePolicy1'; const badPolicyId = 'nonExistentPolicy'; +let errorCounter = 0; + +// Test if the first digit of the status code equals the second arg, or match the entire code when specific is false +const testCode = (code: number, shouldbe: number = 2, specific: boolean = true) => { + if ((specific ? Math.trunc(Number(code) / 100) : code) !== shouldbe) errorCounter++; +} async function getAllPolicies() { console.log("Simple test for the GET All Policies endpoint\n"); let response = await fetch(endpoint(), { headers: { 'Authorization': client('a') } }) console.log("expecting policy 1, 1a and : \n", await response.text()) + testCode(response.status); response = await fetch(endpoint(), { headers: { 'Authorization': client('b') } }) console.log("expecting policy 2, 2a and : \n", await response.text()) + testCode(response.status); response = await fetch(endpoint(), {}); console.log(`expecting 4xx error code (no authorization header provided): ${response.status}`) + testCode(response.status, 4); } async function getOnePolicy() { @@ -34,10 +43,14 @@ async function getOnePolicy() { console.log(`expecting to return relevent information about ${policyId}`, await response.text()); response = await fetch(endpoint(`/${badPolicyId}`), { headers: { 'Authorization': client('b') } }); - console.log(`expecting an empty body, the policy ID does not exist: ${await response.text()}`); + let resText = await response.text(); + console.log(`expecting an empty body, the policy ID does not exist: ${resText}`); + testCode(resText.length, 0, false); response = await fetch(endpoint(`/${encoded}`), { headers: { 'Authorization': client('c') } }); - console.log(`expecting an empty body, the client is not authorized: ${await response.text()}`); + resText = await response.text(); + console.log(`expecting an empty body, the client is not authorized: ${resText}`); + testCode(resText.length, 0, false); } async function postPolicy() { @@ -45,20 +58,31 @@ async function postPolicy() { let response = await fetch(endpoint(), { method: 'POST', headers: { 'Authorization': client('a'), 'Content-Type': 'text/turtle' }, body: Buffer.from(policyB, 'utf-8') }); console.log(`expecting a negative response since assigner != client: status code ${response.status}\nmessage: ${await response.text()}`); + testCode(response.status, 4); response = await fetch(endpoint(), { method: 'POST', headers: { 'Authorization': client('b'), 'Content-Type': 'text/turtle' }, body: Buffer.from(badPolicy1, 'utf-8') }); console.log(`expecting a negative response since policy has a rule with no assigner ${response.status}\nmessage: ${await response.text()}`); + testCode(response.status, 4); response = await fetch(endpoint(), { method: 'POST', headers: { 'Authorization': client('a'), 'Content-Type': 'text/turtle' }, body: Buffer.from(policyA, 'utf-8') }); console.log(`expecting a positive response: status code ${response.status}, ${await response.text()}`); + testCode(response.status); response = await fetch(endpoint(), { method: 'POST', headers: { 'Authorization': client('b'), 'Content-Type': 'text/turtle' }, body: Buffer.from(policyB, 'utf-8') }); console.log(`expecting a positive response: status code ${response.status}, ${await response.text()}`); + testCode(response.status); response = await fetch(endpoint(), { method: 'POST', headers: { 'Authorization': client('c'), 'Content-Type': 'text/turtle' }, body: Buffer.from(policyC, 'utf-8') }); console.log(`expecting a positive response: status code ${response.status}, ${await response.text()}`); + testCode(response.status); } + +/** + * As explained in the docs, the order of execution is extremely important. + * The storage is filled with the POST requests, so this must precede the other tests! + */ async function main() { + errorCounter = 0; console.log("Testing all implemented Policy Endpoints:\n\n\n"); await postPolicy(); console.log("\n\n\n"); @@ -66,5 +90,7 @@ async function main() { console.log("\n\n\n"); await getOnePolicy(); console.log("\n\n\n"); + + console.log(errorCounter === 0 ? `No fails detected` : `${errorCounter} tests have failed`); } main() From 9f78328cee84e48d63c803b63480742a2f43b4d6 Mon Sep 17 00:00:00 2001 From: lennertdr Date: Tue, 8 Jul 2025 17:03:28 +0200 Subject: [PATCH 19/62] DELETE endpoint implemented, still needs tests --- packages/uma/config/routes/policies.json | 9 ++- packages/uma/src/routes/Policy.ts | 2 + .../routeSpecific/policies/DeletePolicies.ts | 58 +++++++++++++++++++ .../routeSpecific/policies/GetPolicies.ts | 17 ++---- .../util/routeSpecific/policies/PolicyUtil.ts | 27 ++++++++- 5 files changed, 98 insertions(+), 15 deletions(-) create mode 100644 packages/uma/src/util/routeSpecific/policies/DeletePolicies.ts diff --git a/packages/uma/config/routes/policies.json b/packages/uma/config/routes/policies.json index 544fdb99..afa22405 100644 --- a/packages/uma/config/routes/policies.json +++ b/packages/uma/config/routes/policies.json @@ -14,7 +14,8 @@ "@type": "PolicyRequestHandler", "storage": { "@id": "urn:uma:default:RulesStorage" - } + }, + "baseUrl": { "@id": "urn:uma:variables:baseUrl" } }, "path": "/uma/policies" }, @@ -22,13 +23,15 @@ "@id": "urn:uma:default:GetOnePolicyRoute", "@type": "HttpHandlerRoute", "methods": [ - "GET" + "GET", + "DELETE" ], "handler": { "@type": "PolicyRequestHandler", "storage": { "@id": "urn:uma:default:RulesStorage" - } + }, + "baseUrl": { "@id": "urn:uma:variables:baseUrl" } }, "path": "/uma/policies/{id}" } diff --git a/packages/uma/src/routes/Policy.ts b/packages/uma/src/routes/Policy.ts index c30b7146..b1f40637 100644 --- a/packages/uma/src/routes/Policy.ts +++ b/packages/uma/src/routes/Policy.ts @@ -3,6 +3,7 @@ import { UCRulesStorage } from "@solidlab/ucp"; import { HttpHandlerContext, HttpHandlerResponse, HttpHandler, HttpHandlerRequest } from "../util/http/models/HttpHandler"; import { getPolicies } from "../util/routeSpecific/policies/GetPolicies"; import { addPolicies } from "../util/routeSpecific/policies/CreatePolicies"; +import { deletePolicies } from "../util/routeSpecific/policies/DeletePolicies"; /** * Endpoint to handle policies, this implementation gives all policies that have the @@ -47,6 +48,7 @@ export class PolicyRequestHandler extends HttpHandler { switch (request.method) { case 'GET': return getPolicies(request, store, client, this.baseUrl); case 'POST': return addPolicies(request, this.storage, client); + case 'DELETE': return deletePolicies(request, store, this.storage, client, this.baseUrl); // TODO: add other endpoints default: throw new MethodNotAllowedHttpError(); } diff --git a/packages/uma/src/util/routeSpecific/policies/DeletePolicies.ts b/packages/uma/src/util/routeSpecific/policies/DeletePolicies.ts new file mode 100644 index 00000000..8813f554 --- /dev/null +++ b/packages/uma/src/util/routeSpecific/policies/DeletePolicies.ts @@ -0,0 +1,58 @@ +import { UCRulesStorage } from "@solidlab/ucp"; +import { HttpHandlerRequest, HttpHandlerResponse } from "../../http/models/HttpHandler"; +import { checkBaseURL, namedNode, odrlAssigner, quadsToText, relations, retrieveID } from "./PolicyUtil"; +import { Quad, Store } from "n3"; +import { InternalServerError } from "@solid/community-server"; + +/** + * TODO: documentation + * @param request + * @param store + * @param storage + * @param clientId + * @param baseUrl + * @returns + */ +export async function deletePolicies(request: HttpHandlerRequest, store: Store, storage: UCRulesStorage, clientId: string, baseUrl: string): Promise> { + // 1. Retrieve Policy ID + const policyId = decodeURIComponent(retrieveID(checkBaseURL(request, baseUrl))); + + // 2. Collect the IDs of the rules we want to delete + let policyRules: Quad[] = [] + for (const relation of relations) { + policyRules = [...policyRules, ...store.getQuads(namedNode(policyId), relation, null, null)] + } + + // Nothing to delete + // TODO: change status code, nothing to delete + if (policyRules.length === 0) { + return { + status: 200, + }; + } + + // Keep track of IDs that are not within the clients reach + const otherRules: string[] = []; + const ownedRules: string[] = []; + policyRules.forEach(quad => { + if (store.getQuads(quad.object, odrlAssigner, namedNode(clientId), null).length > 0) + ownedRules.push(quad.object.id); + else + otherRules.push(quad.object.id); + }); + + // 3. If the policy has rules that do not belong to the client, we cannot remove the policy quads + const rulesToDelete = otherRules.length === 0 ? ownedRules : ownedRules.concat(store.getQuads(namedNode(policyId), null, null, null).map(quad => quad.subject.id)); + + // 4. Remove the specified quads + try { + rulesToDelete.forEach(id => storage.deleteRule(id)); + } catch (error) { + throw new InternalServerError("Failed to delete rules"); + } + + // Delete succesful + return { + status: 200 + } +} \ No newline at end of file diff --git a/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts b/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts index ac930047..bfe6df02 100644 --- a/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts @@ -1,8 +1,9 @@ import { HttpHandlerRequest, HttpHandlerResponse } from "../../http/models/HttpHandler"; import { Quad, Store } from "n3"; -import { odrlAssigner, relations, namedNode, quadsToText } from "./PolicyUtil"; +import { odrlAssigner, relations, namedNode, quadsToText, checkBaseURL, retrieveID } from "./PolicyUtil"; import { MethodNotAllowedHttpError } from "@solid/community-server"; + /** * Handling of the GET /uma/policies endpoint * @@ -10,23 +11,18 @@ import { MethodNotAllowedHttpError } from "@solid/community-server"; */ export async function getPolicies(request: HttpHandlerRequest, store: Store, clientId: string, baseUrl: string): Promise> { // This shouldn't happen - if (request.url.href.slice(0, baseUrl.length) !== baseUrl) throw new MethodNotAllowedHttpError(); - const pathname = request.url.href.slice(baseUrl.length); + const pathname = checkBaseURL(request, baseUrl); // If no other argument(s), get all if (pathname === '/policies') return getAllPolicies(store, clientId); // If asked for a policy, get one - const args = pathname.split('/'); - console.log(args) - if (args.length === 3 && args[1] === 'policies') - return getOnePolicy(args[2], store, clientId); + const id = retrieveID(pathname); + return getOnePolicy(id, store, clientId); - throw new MethodNotAllowedHttpError(); } - /** * Function to implement the GET /uma/policies/ endpoint, it retrieves all information about a certain * policy if available. @@ -50,7 +46,6 @@ async function getOnePolicy(policyId: string, store: Store, clientId: string): P // 3. Only keep the rules assigned by the client const ownedRules = policyRules.filter(quad => store.getQuads(quad.object, odrlAssigner, namedNode(clientId), null).length > 0); - console.log('owned rules', ownedRules) // Return an empty body when no rules are found if (ownedRules.length === 0) { return { @@ -59,7 +54,7 @@ async function getOnePolicy(policyId: string, store: Store, clientId: string): P 'content-type': 'text/turtle', }, body: '', - } + }; } // 4. Search all info about the policy AND the rules, for now with depth 1 but a recursive variant needs to be implemented here. diff --git a/packages/uma/src/util/routeSpecific/policies/PolicyUtil.ts b/packages/uma/src/util/routeSpecific/policies/PolicyUtil.ts index 13395e72..29e29a2d 100644 --- a/packages/uma/src/util/routeSpecific/policies/PolicyUtil.ts +++ b/packages/uma/src/util/routeSpecific/policies/PolicyUtil.ts @@ -1,6 +1,7 @@ import { ODRL } from "@solidlab/ucp"; import { DataFactory, Quad, Writer } from "n3"; -import { HttpHandlerResponse } from "../../http/models/HttpHandler"; +import { HttpHandlerRequest, HttpHandlerResponse } from "../../http/models/HttpHandler"; +import { MethodNotAllowedHttpError } from "@solid/community-server"; // relevant ODRL implementations export const odrlAssigner = ODRL.terms.assigner; @@ -16,6 +17,30 @@ export interface PolicyBody { policy: string; } +/** + * Function to test if the baseUrl is a prefix of the full URL. It returns the part after the baseUrl + * + * @param request + * @param baseUrl + * @returns the part after the baseUrl + */ +export const checkBaseURL = (request: HttpHandlerRequest, baseUrl: string) => { + if (request.url.href.slice(0, baseUrl.length) !== baseUrl) throw new MethodNotAllowedHttpError(); + return request.url.href.slice(baseUrl.length); +} + +/** + * Function to extract the ID for endpoints with URL baseUrl/policies/ + * + * @param pathname the part of the URL after the baseUrl + * @returns the ID + */ +export const retrieveID = (pathname: string): string => { + const args = pathname.split('/'); + if (args.length !== 3 || args[1] !== 'policies') throw new MethodNotAllowedHttpError(); + return args[2]; +} + export async function quadsToText(quads: Quad[]): Promise> { // Serialize as Turtle const writer = new Writer({ format: 'Turtle' }); From 944b52e1e69a46f149d7453628c72e34b8f95fef Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Tue, 1 Jul 2025 14:12:20 +0200 Subject: [PATCH 20/62] fix: Export OperationLogger This prevents Components.js build errors --- packages/uma/src/logging/OperationLogger.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/uma/src/logging/OperationLogger.ts b/packages/uma/src/logging/OperationLogger.ts index 12be796a..51f6cb59 100644 --- a/packages/uma/src/logging/OperationLogger.ts +++ b/packages/uma/src/logging/OperationLogger.ts @@ -1,7 +1,6 @@ import { Store, Triple, DataFactory, Quad_Graph, Quad } from 'n3' - -class OperationLogger { +export class OperationLogger { store: Store = new Store(); @@ -11,7 +10,7 @@ class OperationLogger { * @returns {Quad_Graph} name of the graph in which the triples are stored */ addLogEntry(triples: Triple[], graphName?: string): Quad_Graph { - let graphTerm: Quad_Graph = graphName + let graphTerm: Quad_Graph = graphName ? DataFactory.namedNode(graphName) : DataFactory.blankNode() this.store.addQuads( @@ -44,4 +43,4 @@ const logger = new OperationLogger(); export function getOperationLogger() { return logger; -} \ No newline at end of file +} From 4c052311c01130c4ceaf37a681f6d1dcbfff786c Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Tue, 1 Jul 2025 14:13:32 +0200 Subject: [PATCH 21/62] fix: Prevent contract creation errors from stopping the request When looking into contracts in the future, this should be handled in a more robust way. --- packages/uma/src/dialog/ContractNegotiator.ts | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/packages/uma/src/dialog/ContractNegotiator.ts b/packages/uma/src/dialog/ContractNegotiator.ts index ab30d720..3bad32f4 100644 --- a/packages/uma/src/dialog/ContractNegotiator.ts +++ b/packages/uma/src/dialog/ContractNegotiator.ts @@ -165,14 +165,21 @@ export class ContractNegotiator extends BaseNegotiator { // todo: fix instantiated from url // contract['http://www.w3.org/ns/prov#wasDerivedFrom'] = [ 'urn:ucp:be-gov:policy:d81b8118-af99-4ab3-b2a7-63f8477b6386 '] // TODO: test-private error: this container does not exist and unauth does not have append perms - const instantiatedPolicyContainer = 'http://localhost:3000/ruben/settings/policies/instantiated/'; - const policyCreationResponse = await fetch(instantiatedPolicyContainer, { - method: 'POST', - headers: { 'content-type': 'application/ld+json' }, - body: JSON.stringify(contract, null, 2) - }); - - if (policyCreationResponse.status !== 201) { this.logger.warn('Adding a policy did not succeed...') } + try { + const instantiatedPolicyContainer = 'http://localhost:3000/ruben/settings/policies/instantiated/'; + const policyCreationResponse = await fetch(instantiatedPolicyContainer, { + method: 'POST', + headers: { 'content-type': 'application/ld+json' }, + body: JSON.stringify(contract, null, 2) + }); + + if (policyCreationResponse.status !== 201) { + this.logger.warn(`Adding the contract to the instantiated policies failed: ${ + policyCreationResponse.status} - ${await policyCreationResponse.text()}`); + } + } catch (error: unknown) { + this.logger.warn(`Adding the contract to the instantiated policies failed: ${createErrorMessage(error)}`); + } // TODO:: dynamic contract link to stored signed contract. // If needed we can always embed here directly into the return JSON From 1b08acb0380085ab3d18a3e0f54ac65e28dca0fe Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Tue, 1 Jul 2025 14:15:48 +0200 Subject: [PATCH 22/62] fix: Request subject resource permissions for auxiliaries See the comment in AuxiliaryModesExtractor.ts for more details. This allows policies on resources to also apply to their auxiliary resources, such as .meta resources. --- packages/css/config/default.json | 2 +- .../config/ldp/authorization/readers/uma.json | 40 +++++++--------- packages/css/config/uma/default.json | 1 + packages/css/config/uma/overrides/modes.json | 31 ++++++++++++ .../authorization/AuxiliaryModesExtractor.ts | 48 +++++++++++++++++++ packages/css/src/index.ts | 1 + 6 files changed, 99 insertions(+), 24 deletions(-) create mode 100644 packages/css/config/uma/overrides/modes.json create mode 100644 packages/css/src/authorization/AuxiliaryModesExtractor.ts diff --git a/packages/css/config/default.json b/packages/css/config/default.json index 4b6d6bec..d137948d 100644 --- a/packages/css/config/default.json +++ b/packages/css/config/default.json @@ -28,7 +28,7 @@ "css:config/storage/key-value/resource-store.json", "css:config/storage/location/pod.json", "css:config/storage/middleware/default.json", - "css:config/util/auxiliary/acl.json", + "css:config/util/auxiliary/empty.json", "css:config/util/identifiers/suffix.json", "css:config/util/index/default.json", "css:config/util/logging/winston.json", diff --git a/packages/css/config/ldp/authorization/readers/uma.json b/packages/css/config/ldp/authorization/readers/uma.json index d22f1f95..ff1b6cd0 100644 --- a/packages/css/config/ldp/authorization/readers/uma.json +++ b/packages/css/config/ldp/authorization/readers/uma.json @@ -13,30 +13,24 @@ "requestedModes" ], "source": { - "comment": "Requests permissions on subject resources for auxiliary resources.", - "@type": "AuxiliaryReader", - "auxiliaryStrategy": { - "@id": "urn:solid-server:default:AuxiliaryStrategy" - }, - "reader": { - "@type": "UnionPermissionReader", - "readers": [ - { - "comment": "This PermissionReader will be used to prevent external access to containers used for internal storage.", - "@id": "urn:solid-server:default:PathBasedReader", - "@type": "PathBasedReader", - "baseUrl": { - "@id": "urn:solid-server:default:variable:baseUrl" - } - }, - { - "comment": "The main reader, checks permissions from UMA token.", - "@id": "urn:solid-server:default:UmaPermissionReader", - "@type": "UmaPermissionReader" + "@id": "urn:uma:default:UnionPermissionReader", + "@type": "UnionPermissionReader", + "readers": [ + { + "comment": "This PermissionReader will be used to prevent external access to containers used for internal storage.", + "@id": "urn:solid-server:default:PathBasedReader", + "@type": "PathBasedReader", + "baseUrl": { + "@id": "urn:solid-server:default:variable:baseUrl" } - ] - } + }, + { + "comment": "The main reader, checks permissions from UMA token.", + "@id": "urn:solid-server:default:UmaPermissionReader", + "@type": "UmaPermissionReader" + } + ] } } ] -} \ No newline at end of file +} diff --git a/packages/css/config/uma/default.json b/packages/css/config/uma/default.json index 61ddfd5f..94050854 100644 --- a/packages/css/config/uma/default.json +++ b/packages/css/config/uma/default.json @@ -8,6 +8,7 @@ "uma-css:config/uma/overrides/authorization-handler.json", "uma-css:config/uma/overrides/authorizer.json", "uma-css:config/uma/overrides/jwks.json", + "uma-css:config/uma/overrides/modes.json", "uma-css:config/uma/overrides/token-extractor.json", "uma-css:config/uma/overrides/www-auth.json", diff --git a/packages/css/config/uma/overrides/modes.json b/packages/css/config/uma/overrides/modes.json new file mode 100644 index 00000000..d85e4df8 --- /dev/null +++ b/packages/css/config/uma/overrides/modes.json @@ -0,0 +1,31 @@ +{ + "@context": [ + "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^7.0.0/components/context.jsonld", + "https://linkedsoftwaredependencies.org/bundles/npm/@solidlab/uma-css/^0.0.0/components/context.jsonld" + ], + "@graph": [ + { + "comment": "Replace the account seeder with the UMA version so the AS is taken into account.", + "@id": "urn:solid-server:override:ModesExtractor", + "@type": "Override", + "overrideInstance": { + "@id": "urn:solid-server:default:ModesExtractor" + }, + "overrideParameters": { + "@type": "CachedHandler", + "source": { + "@id": "urn:uma:default:AuxiliaryModesExtractor", + "@type": "AuxiliaryModesExtractor", + "source": { + "comment": "Checks if an operation on a resource requires permissions on intermediate resources (such as newly created parent containers).", + "@type": "IntermediateCreateExtractor", + "resourceSet": { "@id": "urn:solid-server:default:CachedResourceSet" }, + "strategy": { "@id": "urn:solid-server:default:IdentifierStrategy" }, + "source": { "@id": "urn:solid-server:default:HttpModesExtractor" } + }, + "auxiliaryStrategy": { "@id": "urn:solid-server:default:AuxiliaryStrategy" } + } + } + } + ] +} diff --git a/packages/css/src/authorization/AuxiliaryModesExtractor.ts b/packages/css/src/authorization/AuxiliaryModesExtractor.ts new file mode 100644 index 00000000..57ebadfc --- /dev/null +++ b/packages/css/src/authorization/AuxiliaryModesExtractor.ts @@ -0,0 +1,48 @@ +import { AccessMap, AuxiliaryStrategy, ModesExtractor, Operation } from '@solid/community-server'; + +/** + * When targeting an auxiliary resource, + * this class will set the subject resource as the target instead. + * + * Below is some context on why this class is needed, compared to how permissions are done in the CSS. + * There are 2 major components for permissions: a `ModesExtractor`, + * which determines which permissions are needed to perform a certain operation on a resource, + * and a `PermissionReader`, which determines which permissions are allowed (based on credentials) on that resource. + * The `Authorizer` then checks if these match. + * The Solid protocol defines that if you have access to a resource, you have access to its auxiliary resources. + * The default CSS implementation handles this by extracting modes with the auxiliary resource as target, + * and having a permission reader that is smart enough + * to know it has to check the subject resource for the actual permissions. + * + * The above cannot work with the UMA setup, or at least not with how the other related components are implemented. + * With UMA, the Resource Server extracts the modes, and then sends the result to the Authorization Server. + * In the case of auxiliary resources, this means it sends the identifier of the auxiliary resource. + * The Authorization server does not know that the permissions of the subject resource should be used, + * so this would require having policies for auxiliary resources as well, + * which is not ideal. + * In case you do want to have separate policies for those, this class should be removed from the configuration. + */ +export class AuxiliaryModesExtractor extends ModesExtractor { + public constructor( + protected readonly source: ModesExtractor, + protected readonly auxiliaryStrategy: AuxiliaryStrategy, + ) { + super(); + } + + public async canHandle(input: Operation): Promise { + return this.source.canHandle(input); + } + + public async handle(input: Operation): Promise { + const result = await this.source.handle(input); + const keys = [ ...result.distinctKeys() ]; + for (const key of keys) { + if (this.auxiliaryStrategy.isAuxiliaryIdentifier(key) && !this.auxiliaryStrategy.usesOwnAuthorization(key)) { + result.set(this.auxiliaryStrategy.getSubjectIdentifier(key), result.get(key)!); + result.delete(key); + } + } + return result; + } +} diff --git a/packages/css/src/index.ts b/packages/css/src/index.ts index 22113daf..86784098 100644 --- a/packages/css/src/index.ts +++ b/packages/css/src/index.ts @@ -1,5 +1,6 @@ export * from './authentication/UmaTokenExtractor'; +export * from './authorization/AuxiliaryModesExtractor'; export * from './authorization/UmaAuthorizer'; export * from './authorization/UmaPermissionReader'; From 0f0e025e243ec770155cc4cb7749cf7c94b9598f Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Tue, 1 Jul 2025 14:18:55 +0200 Subject: [PATCH 23/62] feat: Allow relative URIs when using DirectoryUCRulesStorage --- .../src/storage/DirectoryUCRulesStorage.ts | 20 ++++++++++--------- packages/uma/bin/main.ts | 1 + packages/uma/bin/odrl.ts | 1 + .../config/policies/authorizers/default.json | 3 +++ packages/uma/config/rules/odrl/policy0.ttl | 10 +++++----- packages/uma/config/rules/policy/policy0.ttl | 4 ++-- packages/uma/config/variables/default.json | 5 +++++ 7 files changed, 28 insertions(+), 16 deletions(-) diff --git a/packages/ucp/src/storage/DirectoryUCRulesStorage.ts b/packages/ucp/src/storage/DirectoryUCRulesStorage.ts index 6d8f231b..05e7ca53 100644 --- a/packages/ucp/src/storage/DirectoryUCRulesStorage.ts +++ b/packages/ucp/src/storage/DirectoryUCRulesStorage.ts @@ -1,30 +1,33 @@ import { UCRulesStorage } from "./UCRulesStorage"; import * as path from 'path' import * as fs from 'fs' -import { Store } from "n3"; -import { parseAsN3Store } from "koreografeye"; +import { Parser, Store } from 'n3'; export class DirectoryUCRulesStorage implements UCRulesStorage { - private directoryPath: string; + protected directoryPath: string; + protected readonly baseIRI: string; + /** - * + * * @param directoryPath The absolute path to a directory + * @param baseIRI The base to use when parsing RDF documents. */ - public constructor(directoryPath: string) { + public constructor(directoryPath: string, baseIRI: string) { this.directoryPath = path.resolve(directoryPath); - console.log(`[${new Date().toISOString()}] - DirectoryUCRulesStore: Path that will be used as source directory for the Usage Control Rules`, this.directoryPath); if (!fs.lstatSync(directoryPath).isDirectory()) { throw Error(`${directoryPath} does not resolve to a directory`) } + this.baseIRI = baseIRI; } public async getStore(): Promise { const store = new Store() + const parser = new Parser({ baseIRI: this.baseIRI }); const files = fs.readdirSync(this.directoryPath).map(file => path.join(this.directoryPath, file)) for (const file of files) { - const fileStore = await parseAsN3Store(file) - store.addQuads(fileStore.getQuads(null, null, null, null)) + const quads = parser.parse((await fs.promises.readFile(file)).toString()); + store.addQuads(quads); } return store; } @@ -40,4 +43,3 @@ export class DirectoryUCRulesStorage implements UCRulesStorage { throw Error('not implemented'); } } - diff --git a/packages/uma/bin/main.ts b/packages/uma/bin/main.ts index 37d7eebc..6fcb2b64 100644 --- a/packages/uma/bin/main.ts +++ b/packages/uma/bin/main.ts @@ -15,6 +15,7 @@ export const launch: () => Promise = async () => { variables['urn:uma:variables:port'] = port; variables['urn:uma:variables:baseUrl'] = baseUrl; + variables['urn:uma:variables:policyBaseIRI'] = 'http://localhost:3000/'; variables['urn:uma:variables:policyDir'] = path.join(rootDir, './config/rules/policy'); variables['urn:uma:variables:eyePath'] = 'eye'; diff --git a/packages/uma/bin/odrl.ts b/packages/uma/bin/odrl.ts index c7a39a6a..27aea6fd 100644 --- a/packages/uma/bin/odrl.ts +++ b/packages/uma/bin/odrl.ts @@ -15,6 +15,7 @@ export const launch: () => Promise = async () => { variables['urn:uma:variables:port'] = port; variables['urn:uma:variables:baseUrl'] = baseUrl; + variables['urn:uma:variables:policyBaseIRI'] = 'http://localhost:3000/'; variables['urn:uma:variables:policyDir'] = path.join(rootDir, './config/rules/odrl'); variables['urn:uma:variables:eyePath'] = 'eye'; diff --git a/packages/uma/config/policies/authorizers/default.json b/packages/uma/config/policies/authorizers/default.json index c6daccc1..34fd3b48 100644 --- a/packages/uma/config/policies/authorizers/default.json +++ b/packages/uma/config/policies/authorizers/default.json @@ -57,6 +57,9 @@ "@type": "DirectoryUCRulesStorage", "directoryPath": { "@id": "urn:uma:variables:policyDir" + }, + "baseIRI": { + "@id": "urn:uma:variables:policyBaseIRI" } } } diff --git a/packages/uma/config/rules/odrl/policy0.ttl b/packages/uma/config/rules/odrl/policy0.ttl index 07b9f138..6d103668 100644 --- a/packages/uma/config/rules/odrl/policy0.ttl +++ b/packages/uma/config/rules/odrl/policy0.ttl @@ -5,7 +5,7 @@ ex:usagePolicy1 a odrl:Agreement . ex:usagePolicy1 odrl:permission ex:permission1 . ex:permission1 a odrl:Permission . ex:permission1 odrl:action odrl:modify . -ex:permission1 odrl:target . +ex:permission1 odrl:target . ex:permission1 odrl:assignee . ex:permission1 odrl:assigner . @@ -13,7 +13,7 @@ ex:usagePolicy1a a odrl:Agreement . ex:usagePolicy1a odrl:permission ex:permission1a . ex:permission1a a odrl:Permission . ex:permission1a odrl:action odrl:create . -ex:permission1a odrl:target . +ex:permission1a odrl:target . ex:permission1a odrl:assignee . ex:permission1a odrl:assigner . @@ -21,7 +21,7 @@ ex:usagePolicy2 a odrl:Agreement . ex:usagePolicy2 odrl:permission ex:permission2a . ex:permission2 a odrl:Permission . ex:permission2 odrl:action odrl:modify . -ex:permission2 odrl:target . +ex:permission2 odrl:target . ex:permission2 odrl:assignee . ex:permission2 odrl:assigner . @@ -29,7 +29,7 @@ ex:usagePolicy2a a odrl:Agreement . ex:usagePolicy2a odrl:permission ex:permission2 . ex:permission2a a odrl:Permission . ex:permission2a odrl:action odrl:create . -ex:permission2a odrl:target . +ex:permission2a odrl:target . ex:permission2a odrl:assignee . ex:permission2a odrl:assigner . @@ -38,6 +38,6 @@ ex:usagePolicy3 a odrl:Agreement . ex:usagePolicy3 odrl:permission ex:permission3 . ex:permission3 a odrl:Permission . ex:permission3 odrl:action odrl:read . -ex:permission3 odrl:target . +ex:permission3 odrl:target . ex:permission3 odrl:assignee . ex:permission3 odrl:assigner . diff --git a/packages/uma/config/rules/policy/policy0.ttl b/packages/uma/config/rules/policy/policy0.ttl index 8a267103..971ed60f 100644 --- a/packages/uma/config/rules/policy/policy0.ttl +++ b/packages/uma/config/rules/policy/policy0.ttl @@ -5,7 +5,7 @@ ex:usagePolicy a odrl:Agreement . ex:usagePolicy odrl:permission ex:permission . ex:permission a odrl:Permission . ex:permission odrl:action odrl:read , odrl:create , odrl:modify . -ex:permission odrl:target , . +ex:permission odrl:target , . ex:permission odrl:assignee . ex:permission odrl:assigner . @@ -13,6 +13,6 @@ ex:usagePolicy2 a odrl:Agreement . ex:usagePolicy2 odrl:permission ex:permission2 . ex:permission2 a odrl:Permission . ex:permission2 odrl:action odrl:create , odrl:modify . -ex:permission2 odrl:target , . +ex:permission2 odrl:target , . ex:permission2 odrl:assignee . ex:permission2 odrl:assigner . diff --git a/packages/uma/config/variables/default.json b/packages/uma/config/variables/default.json index 43b003c3..3e31b1af 100644 --- a/packages/uma/config/variables/default.json +++ b/packages/uma/config/variables/default.json @@ -17,6 +17,11 @@ "@id": "urn:uma:variables:policyDir", "@type": "Variable" }, + { + "comment": "Base IRI to use when parsing policies.", + "@id": "urn:uma:variables:policyBaseIRI", + "@type": "Variable" + }, { "comment": "Path of the local eye reasoner.", "@id": "urn:uma:variables:eyePath", From 0d918228a0be4f4db86e857e7473a44241bc10ae Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Tue, 1 Jul 2025 14:21:34 +0200 Subject: [PATCH 24/62] refactor: Remove unused seeding fields --- packages/css/config/seed.json | 29 ++++------------------------- packages/css/src/util/OwnerUtil.ts | 3 --- 2 files changed, 4 insertions(+), 28 deletions(-) diff --git a/packages/css/config/seed.json b/packages/css/config/seed.json index 515cc295..efda127c 100644 --- a/packages/css/config/seed.json +++ b/packages/css/config/seed.json @@ -2,46 +2,25 @@ { "email": "alice@example.org", "password": "abc123", - "authz": { - "server": "http://localhost:4000/uma" - }, - "pods": [{ - "name": "alice", - "settings": { - "name": "Alice", - "umaServer": "http://localhost:4000/uma" - } + "pods": [{ + "name": "alice" }] }, { "email": "bob@example.org", "password": "abc123", - "authz": { - "server": "http://localhost:4000/uma" - }, "pods": [ { - "name": "bob", - "settings": { - "name": "Bob", - "umaServer": "http://localhost:4000/uma" - } + "name": "bob" } ] }, { "email": "demo@example.org", "password": "abc123", - "authz": { - "server": "http://localhost:4000/uma" - }, "pods": [ { - "name": "demo", - "settings": { - "name": "Demo", - "umaServer": "http://localhost:4000/uma" - } + "name": "demo" } ] } diff --git a/packages/css/src/util/OwnerUtil.ts b/packages/css/src/util/OwnerUtil.ts index d192c59d..baa0d943 100644 --- a/packages/css/src/util/OwnerUtil.ts +++ b/packages/css/src/util/OwnerUtil.ts @@ -51,9 +51,6 @@ export class OwnerUtil { this.logger.debug(`Looking up owners of pod ${pod.id}`); - const as = await this.accountStore.getSetting(pod.accountId, ACCOUNT_SETTINGS_AUTHZ_SERVER); - this.logger.warn(`REAL AS is ${JSON.stringify(as)}`); - const owners = await this.podStore.getOwners(pod.id); if (!owners) throw new Error(`Unable to find owners for pod ${storage.path}`); From 1e3c9e18f63975e3f2eeed6eb21debf7430bba28 Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Tue, 1 Jul 2025 14:24:15 +0200 Subject: [PATCH 25/62] feat: Make containerURL of ContainerUCRulesStorage configurable --- packages/uma/bin/demo.ts | 1 + packages/uma/config/demo.json | 4 +++- packages/uma/config/variables/default.json | 5 +++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/uma/bin/demo.ts b/packages/uma/bin/demo.ts index e704569b..1a68c975 100644 --- a/packages/uma/bin/demo.ts +++ b/packages/uma/bin/demo.ts @@ -16,6 +16,7 @@ export const launch: () => Promise = async () => { variables['urn:uma:variables:baseUrl'] = baseUrl; // variables['urn:uma:variables:policyDir'] = path.join(rootDir, './config/rules/policy'); + variables['urn:uma:variables:policyContainer'] = 'http://localhost:3000/ruben/settings/policies/'; variables['urn:uma:variables:eyePath'] = 'eye'; const configPath = path.join(rootDir, './config/demo.json'); diff --git a/packages/uma/config/demo.json b/packages/uma/config/demo.json index 5ba92ebd..07ae53a4 100644 --- a/packages/uma/config/demo.json +++ b/packages/uma/config/demo.json @@ -44,7 +44,9 @@ }, "overrideParameters": { "@type": "ContainerUCRulesStorage", - "containerURL": "http://localhost:3000/ruben/settings/policies/" + "containerURL": { + "@id": "urn:uma:variables:policyContainer" + } } } ] diff --git a/packages/uma/config/variables/default.json b/packages/uma/config/variables/default.json index 3e31b1af..ed9164dd 100644 --- a/packages/uma/config/variables/default.json +++ b/packages/uma/config/variables/default.json @@ -26,6 +26,11 @@ "comment": "Path of the local eye reasoner.", "@id": "urn:uma:variables:eyePath", "@type": "Variable" + }, + { + "comment": "URL of container where policies are stored.", + "@id": "urn:uma:variables:policyContainer", + "@type": "Variable" } ] } From 3e7d7b622c30ff2e3eab2e0138fe51a54360da08 Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Tue, 1 Jul 2025 14:25:44 +0200 Subject: [PATCH 26/62] feat: Make App the root configured component --- packages/uma/bin/demo.ts | 6 +++--- packages/uma/bin/main.ts | 6 +++--- packages/uma/bin/odrl.ts | 6 +++--- packages/uma/config/default.json | 17 +++++++++++++++++ 4 files changed, 26 insertions(+), 9 deletions(-) diff --git a/packages/uma/bin/demo.ts b/packages/uma/bin/demo.ts index 1a68c975..eba751b7 100644 --- a/packages/uma/bin/demo.ts +++ b/packages/uma/bin/demo.ts @@ -1,6 +1,6 @@ import * as path from 'path'; import { ComponentsManager } from 'componentsjs'; -import { ServerInitializer, setGlobalLoggerFactory, WinstonLoggerFactory } from '@solid/community-server'; +import { App, setGlobalLoggerFactory, WinstonLoggerFactory } from '@solid/community-server'; const protocol = 'http'; const host = 'localhost'; @@ -31,8 +31,8 @@ export const launch: () => Promise = async () => { await manager.configRegistry.register(configPath); - const umaServer: ServerInitializer = await manager.instantiate('urn:uma:default:NodeHttpServer',{variables}); - await umaServer.handleSafe(); + const umaServer: App = await manager.instantiate('urn:uma:default:App',{variables}); + await umaServer.start(); }; launch(); diff --git a/packages/uma/bin/main.ts b/packages/uma/bin/main.ts index 6fcb2b64..ef3d34a2 100644 --- a/packages/uma/bin/main.ts +++ b/packages/uma/bin/main.ts @@ -1,6 +1,6 @@ import * as path from 'path'; import { ComponentsManager } from 'componentsjs'; -import { ServerInitializer, setGlobalLoggerFactory, WinstonLoggerFactory } from '@solid/community-server'; +import { App, setGlobalLoggerFactory, WinstonLoggerFactory } from '@solid/community-server'; const protocol = 'http'; const host = 'localhost'; @@ -31,8 +31,8 @@ export const launch: () => Promise = async () => { await manager.configRegistry.register(configPath); - const umaServer: ServerInitializer = await manager.instantiate('urn:uma:default:NodeHttpServer',{variables}); - await umaServer.handleSafe(); + const umaServer: App = await manager.instantiate('urn:uma:default:App',{variables}); + await umaServer.start(); }; launch(); diff --git a/packages/uma/bin/odrl.ts b/packages/uma/bin/odrl.ts index 27aea6fd..3845767c 100644 --- a/packages/uma/bin/odrl.ts +++ b/packages/uma/bin/odrl.ts @@ -1,4 +1,4 @@ -import { ServerInitializer, setGlobalLoggerFactory, WinstonLoggerFactory } from '@solid/community-server'; +import { App, setGlobalLoggerFactory, WinstonLoggerFactory } from '@solid/community-server'; import * as path from 'path'; import { ComponentsManager } from 'componentsjs'; @@ -31,8 +31,8 @@ export const launch: () => Promise = async () => { await manager.configRegistry.register(configPath); - const umaServer: ServerInitializer = await manager.instantiate('urn:uma:default:NodeHttpServer',{variables}); - await umaServer.handleSafe(); + const umaServer: App = await manager.instantiate('urn:uma:default:App',{variables}); + await umaServer.start(); }; launch(); diff --git a/packages/uma/config/default.json b/packages/uma/config/default.json index ca9940ee..36d5f8ff 100644 --- a/packages/uma/config/default.json +++ b/packages/uma/config/default.json @@ -24,6 +24,23 @@ "sai-uma:config/variables/default.json" ], "@graph": [ + { + "comment": "This is the entry point to the application. It can be used to both start and stop the server.", + "@id": "urn:uma:default:App", + "@type": "App", + "initializer": { "@id": "urn:uma:default:NodeHttpServer" }, + "finalizer": { + "@id": "urn:uma:default:Finalizer", + "@type": "SequenceHandler", + "handlers": [ + { + "comment": "Provides handler to shut down the server", + "@type": "FinalizableHandler", + "finalizable": { "@id": "urn:uma:default:NodeHttpServer" } + } + ] + } + }, { "@id": "urn:uma:default:NodeHttpServer", "@type": "ServerInitializer", From b7588c73a9ae65c25f1a622a0ff983b7bd90581a Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Tue, 1 Jul 2025 14:28:16 +0200 Subject: [PATCH 27/62] chore: Remove unused dependencies This includes derived resources, which is referenced in the demo setup, but not used in the example script. This can be re-added in the future should it be required. --- demo/flow-test.ts | 11 - demo/flow.ts | 9 - package.json | 18 +- packages/css/config/demo.json | 1 - packages/css/config/derived.json | 72 --- packages/css/package.json | 1 - packages/uma/package.json | 1 - yarn.lock | 862 +------------------------------ 8 files changed, 21 insertions(+), 954 deletions(-) delete mode 100644 packages/css/config/derived.json diff --git a/demo/flow-test.ts b/demo/flow-test.ts index 53ed6e5c..c33689d5 100644 --- a/demo/flow-test.ts +++ b/demo/flow-test.ts @@ -5,17 +5,6 @@ import { Parser, Writer, Store } from 'n3'; import { randomUUID } from 'crypto'; import chalk from 'chalk' -// import * as jsonld from 'jsonld'; - -// import vc from '@digitalcredentials/vc'; - -// // Required to set up a suite instance with private key -// import {Ed25519VerificationKey2020} from -// '@digitalcredentials/ed25519-verification-key-2020'; -// import {Ed25519Signature2020} from '@digitalcredentials/ed25519-signature-2020'; - - - const parser = new Parser(); const writer = new Writer(); diff --git a/demo/flow.ts b/demo/flow.ts index b88bca26..da379a4c 100644 --- a/demo/flow.ts +++ b/demo/flow.ts @@ -5,15 +5,6 @@ import { Parser, Writer, Store } from 'n3'; import { randomUUID } from 'crypto'; import chalk from 'chalk' -import * as jsonld from 'jsonld'; - -import vc from '@digitalcredentials/vc'; - -// Required to set up a suite instance with private key -import {Ed25519VerificationKey2020} from - '@digitalcredentials/ed25519-verification-key-2020'; -import {Ed25519Signature2020} from '@digitalcredentials/ed25519-signature-2020'; - const parser = new Parser(); diff --git a/package.json b/package.json index 7fd60dee..5693dbe5 100644 --- a/package.json +++ b/package.json @@ -72,17 +72,15 @@ "devDependencies": { "@commitlint/cli": "^16.1.0", "@commitlint/config-conventional": "^16.0.0", - "@solidlab/ucp": "workspace:^", "@types/jest": "^29.5.12", "@types/node": "^20.11.25", "@typescript-eslint/eslint-plugin": "^5.12.1", "@typescript-eslint/parser": "^5.12.1", + "chalk": "^5.4.1", "componentsjs-generator": "^3.1.2", - "concurrently": "^8.2.2", "eslint": "^8.10.0", "jest": "^29.7.0", "jest-rdf": "^1.8.1", - "koreografeye": "^0.4.8", "shx": "^0.3.4", "syncpack": "^13.0.2", "ts-jest": "^29.1.2", @@ -154,19 +152,5 @@ ] ] } - }, - "dependencies": { - "@digitalbazaar/ed25519-signature-2020": "^5.4.0", - "@digitalbazaar/ed25519-verification-key-2020": "^4.2.0", - "@digitalbazaar/vc": "^7.1.0", - "@digitalcredentials/ed25519-signature-2020": "^6.0.0", - "@digitalcredentials/ed25519-verification-key-2020": "^4.0.0", - "@digitalcredentials/vc": "^9.0.1", - "@digitalcredentials/vc-data-model": "^2.0.0", - "@inrupt/solid-client": "^2.0.1", - "@inrupt/solid-client-authn-core": "^2.1.0", - "chalk": "^5.4.1", - "jsonld": "^8.3.3", - "tsx": "^4.19.2" } } diff --git a/packages/css/config/demo.json b/packages/css/config/demo.json index 55a30e82..711a8cf8 100644 --- a/packages/css/config/demo.json +++ b/packages/css/config/demo.json @@ -5,7 +5,6 @@ ], "import": [ "uma-css:config/default.json", - "uma-css:config/derived.json", "css:config/storage/backend/data-accessors/file.json" ], "@graph": [ diff --git a/packages/css/config/derived.json b/packages/css/config/derived.json deleted file mode 100644 index 5ca653eb..00000000 --- a/packages/css/config/derived.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "@context": [ - "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^7.0.0/components/context.jsonld", - "https://linkedsoftwaredependencies.org/bundles/npm/@solidlab/derived-resources-component/^1.0.0/components/context.jsonld" - ], - "@graph": [ - { - "comment": "We need to disable the index store as it might accidentally return derived template resources.", - "@id": "urn:solid-server:default:ResourceStore_Index", - "@type": "IndexRepresentationStore", - "mediaRange": "" - }, - { - "comment": "A second converting store", - "@id": "urn:solid-server:derived:RepresentationConvertingStore", - "@type": "RepresentationConvertingStore", - "metadataStrategy": { "@id": "urn:solid-server:default:MetadataStrategy" }, - "options_inConverter": { "@id": "urn:solid-server:default:RepresentationConverter" }, - "options_outConverter": { "@id": "urn:solid-server:default:UiEnabledConverter" }, - "source": { "@id": "urn:solid-server:derived:DerivedResourceStore" } - }, - { - "comment": "The store responsible for generating derived resources.", - "@id": "urn:solid-server:derived:DerivedResourceStore", - "@type": "DerivedResourceStore", - "manager": { - "@id": "urn:solid-server:derived:DerivationManager", - "@type": "MetadataDerivationManager", - "identifierStrategy": { "@id": "urn:solid-server:default:IdentifierStrategy" }, - "store": { "@id": "urn:solid-server:default:ResourceStore_Patching" }, - "derivationMatcher": { - "@type": "PresetDerivationMatcher", - "source": { "@type": "TemplateDerivationMatcher" } - }, - "selectorHandler": { - "@id": "urn:solid-server:default:SelectorHandler", - "@type": "GlobSelectorHandler", - "store": { "@id": "urn:solid-server:default:ResourceStore_Patching" } - }, - "filterHandler": { - "@id": "urn:solid-server:default:FilterHandler", - "@type": "SparqlFilterHandler", - "store": { "@id": "urn:solid-server:default:ResourceStore_Patching" } - } - }, - "source": { "@id": "urn:solid-server:default:ResourceStore_Patching" } - }, - { - "comment": "Insert our new stores in front of the original converting store.", - "@id": "urn:solid-server:derived:PatchOverride", - "@type": "Override", - "overrideInstance": { "@id": "urn:solid-server:default:ResourceStore_Locking" }, - "overrideParameters": { - "@type": "PatchingStore", - "source": { "@id": "urn:solid-server:derived:RepresentationConvertingStore" } - } - }, - { - "comment": [ - "Keep query parameters in identifiers.", - "This makes it so that any query parameters can cause issues with normal resources so we might want a more robust solution." - ], - "@id": "urn:solid-server:derived:TargetExtractorOverride", - "@type": "Override", - "overrideInstance": { "@id": "urn:solid-server:default:TargetExtractor" }, - "overrideParameters": { - "@type": "OriginalUrlExtractor", - "includeQueryString": true - } - } - ] -} diff --git a/packages/css/package.json b/packages/css/package.json index 77fbab42..a66a06c1 100644 --- a/packages/css/package.json +++ b/packages/css/package.json @@ -68,7 +68,6 @@ }, "dependencies": { "@solid/community-server": "^7.1.7", - "@solidlab/derived-resources-component": "^1.0.2", "@solidlab/uma": "workspace:^", "@types/n3": "^1.16.4", "componentsjs": "^5.5.1", diff --git a/packages/uma/package.json b/packages/uma/package.json index 59a154c7..c057a164 100644 --- a/packages/uma/package.json +++ b/packages/uma/package.json @@ -70,7 +70,6 @@ "get-jwks": "^9.0.1", "http-message-signatures": "^1.0.4", "jose": "^5.2.2", - "koreografeye": "^0.4.8", "logform": "^2.6.0", "n3": "^1.17.2", "odrl-evaluator": "^0.3.0", diff --git a/yarn.lock b/yarn.lock index e0b71c5b..baee7659 100644 --- a/yarn.lock +++ b/yarn.lock @@ -368,15 +368,6 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.21.0": - version: 7.24.1 - resolution: "@babel/runtime@npm:7.24.1" - dependencies: - regenerator-runtime: "npm:^0.14.0" - checksum: 10c0/500c6a99ddd84f37c7bc5dbc84777af47b1372b20e879941670451d55484faf18a673c5ebee9ca2b0f36208a729417873b35b1b92e76f811620f6adf7b8cb0f1 - languageName: node - linkType: hard - "@babel/template@npm:^7.22.15, @babel/template@npm:^7.3.3": version: 7.22.15 resolution: "@babel/template@npm:7.22.15" @@ -2878,7 +2869,7 @@ __metadata: languageName: node linkType: hard -"@comunica/query-sparql@npm:^2.10.2, @comunica/query-sparql@npm:^2.9.0": +"@comunica/query-sparql@npm:^2.9.0": version: 2.10.2 resolution: "@comunica/query-sparql@npm:2.10.2" dependencies: @@ -3082,49 +3073,6 @@ __metadata: languageName: node linkType: hard -"@digitalbazaar/credentials-context@npm:^3.1.0": - version: 3.1.0 - resolution: "@digitalbazaar/credentials-context@npm:3.1.0" - checksum: 10c0/0fa22d00542450b3e0567511779dafcd943bcba75a2be83fa80b95c1f255f1b2f701ea64a0ab3959c8503a9f1682f724de0ffe8f5eea18b065d5074d61e52cb7 - languageName: node - linkType: hard - -"@digitalbazaar/ed25519-multikey@npm:^1.1.0": - version: 1.3.0 - resolution: "@digitalbazaar/ed25519-multikey@npm:1.3.0" - dependencies: - "@noble/ed25519": "npm:^1.6.0" - base58-universal: "npm:^2.0.0" - base64url-universal: "npm:^2.0.0" - checksum: 10c0/07cf6ea5591a269fca5b9709481e249b20cf83145ab56c25c8b75f668739163af95eae6742b50b8575522075e0933242343564d14d661f5bcae70fade13726b5 - languageName: node - linkType: hard - -"@digitalbazaar/ed25519-signature-2020@npm:^5.4.0": - version: 5.4.0 - resolution: "@digitalbazaar/ed25519-signature-2020@npm:5.4.0" - dependencies: - "@digitalbazaar/ed25519-multikey": "npm:^1.1.0" - "@digitalbazaar/ed25519-verification-key-2020": "npm:^4.1.0" - base58-universal: "npm:^2.0.0" - ed25519-signature-2020-context: "npm:^1.1.0" - jsonld-signatures: "npm:^11.3.0" - checksum: 10c0/7fcdd54c18d85dbbf937464ff8f330876de336d2607c944981fa8e69bcc2df59a5181441d364c27e269d1a6d2028217ed3f662028634c57ab55e739c3af42c16 - languageName: node - linkType: hard - -"@digitalbazaar/ed25519-verification-key-2020@npm:^4.1.0, @digitalbazaar/ed25519-verification-key-2020@npm:^4.2.0": - version: 4.2.0 - resolution: "@digitalbazaar/ed25519-verification-key-2020@npm:4.2.0" - dependencies: - "@noble/ed25519": "npm:^1.6.0" - base58-universal: "npm:^2.0.0" - base64url-universal: "npm:^2.0.0" - crypto-ld: "npm:^7.0.0" - checksum: 10c0/386b4be965e10b0253c5fd620c4e386c2f57587937c9215ed0127c3f42016d32e99844cc5eabbddc41642ade31760539e67e97d69cfdb0df6b3beb58737b9a01 - languageName: node - linkType: hard - "@digitalbazaar/http-client@npm:^3.4.1": version: 3.4.1 resolution: "@digitalbazaar/http-client@npm:3.4.1" @@ -3136,144 +3084,6 @@ __metadata: languageName: node linkType: hard -"@digitalbazaar/security-context@npm:^1.0.0": - version: 1.0.1 - resolution: "@digitalbazaar/security-context@npm:1.0.1" - checksum: 10c0/6d1444002958454be5dd70cc6ba4d0df952f90162756d6320d88501ff44fc7d48dcb2aa3452b77e8941a9da4e4c7d050d3510827db93637ff469b92391c63324 - languageName: node - linkType: hard - -"@digitalbazaar/vc@npm:^7.1.0": - version: 7.1.0 - resolution: "@digitalbazaar/vc@npm:7.1.0" - dependencies: - "@digitalbazaar/credentials-context": "npm:^3.1.0" - ed25519-signature-2018-context: "npm:^1.1.0" - jsonld: "npm:^8.3.1" - jsonld-signatures: "npm:^11.2.1" - checksum: 10c0/4914cd7bfc2556512b7b1b78b7032180fcd8a966d365ecd62f0534da2347fa43b9023f2460f15fd9c8029088af8ecc81481603fe9cf953e40393a956338356bb - languageName: node - linkType: hard - -"@digitalcredentials/base58-universal@npm:^1.0.1": - version: 1.0.1 - resolution: "@digitalcredentials/base58-universal@npm:1.0.1" - checksum: 10c0/2f92d8e49fa56bf40d297c668d2da1581c8f8d10d0907565d42b5414c829f1b7506d7e68b3dca74497d8113011685fdb916924c2dbffcbf77d32a06b1eab43a1 - languageName: node - linkType: hard - -"@digitalcredentials/credentials-v2-context@npm:~0.0.1-beta.0": - version: 0.0.1-beta.0 - resolution: "@digitalcredentials/credentials-v2-context@npm:0.0.1-beta.0" - checksum: 10c0/4e51299fda24c8433994a8fba2c8ce707fc96d57469e4e6a0577130c7ce39bca975b9c3dba4c9aecefbcc9d42c2b754ca6b4e5b46274d3c4d9e16d3c0e1fddad - languageName: node - linkType: hard - -"@digitalcredentials/ed25519-signature-2020@npm:^6.0.0": - version: 6.0.0 - resolution: "@digitalcredentials/ed25519-signature-2020@npm:6.0.0" - dependencies: - "@digitalcredentials/base58-universal": "npm:^1.0.1" - "@digitalcredentials/ed25519-verification-key-2020": "npm:^3.1.1" - "@digitalcredentials/jsonld-signatures": "npm:^12.0.0" - ed25519-signature-2018-context: "npm:^1.1.0" - ed25519-signature-2020-context: "npm:^1.0.1" - checksum: 10c0/9a81cbfb7e222720b223df5cff50cf291e1cbc015fb39d2ce299cc4efac51eb6b0958d2ace52e5dd14e3398f9c9f49a1005cb177aca95d123836bafb43823094 - languageName: node - linkType: hard - -"@digitalcredentials/ed25519-verification-key-2020@npm:^3.1.1": - version: 3.2.2 - resolution: "@digitalcredentials/ed25519-verification-key-2020@npm:3.2.2" - dependencies: - "@digitalcredentials/base58-universal": "npm:^1.0.1" - "@stablelib/ed25519": "npm:^1.0.1" - base64url-universal: "npm:^1.1.0" - crypto-ld: "npm:^6.0.0" - checksum: 10c0/a421b2097025ce70da131ae41323d31e18049c71daae8f6c922b34d3fd4ee5c1a2a249c0cbd4c16ed37de7054f617eccecfeb94a8126fe264e72b35611c54d00 - languageName: node - linkType: hard - -"@digitalcredentials/ed25519-verification-key-2020@npm:^4.0.0": - version: 4.0.0 - resolution: "@digitalcredentials/ed25519-verification-key-2020@npm:4.0.0" - dependencies: - "@digitalcredentials/keypair": "npm:^1.0.5" - "@noble/ed25519": "npm:^1.7.1" - base-x: "npm:^4.0.0" - checksum: 10c0/43e73b4dd928e32edbd901d8efbf0d4e1c456fd9a6df5e2239a73f4d1ff9abdf9ab706860e2822b9bd9c0de5421d781124b25198473cffd44d499cbb67b5fc44 - languageName: node - linkType: hard - -"@digitalcredentials/http-client@npm:^5.0.1": - version: 5.0.4 - resolution: "@digitalcredentials/http-client@npm:5.0.4" - dependencies: - ky: "npm:^1.0.1" - undici: "npm:^6.6.2" - checksum: 10c0/5d0edf0207844ef913a0841ac3715813488c336f282c9c11ba3be0061093d1ce04299886b64a10936b95a6faa5a5ec87cc1b1a9694460c99ced859471eb410ce - languageName: node - linkType: hard - -"@digitalcredentials/jsonld-signatures@npm:^12.0.0": - version: 12.0.1 - resolution: "@digitalcredentials/jsonld-signatures@npm:12.0.1" - dependencies: - "@digitalbazaar/security-context": "npm:^1.0.0" - "@digitalcredentials/jsonld": "npm:^9.0.0" - fast-text-encoding: "npm:^1.0.3" - serialize-error: "npm:^8.0.1" - checksum: 10c0/436317dfd0628d6aa9c63df13935f73c2e835d21d6444ff12f5e139b1256949607b9aa88cedc7a8d69522bb83b4d2b1a4f5341046fa1d95d1b1a5a75ee12b21c - languageName: node - linkType: hard - -"@digitalcredentials/jsonld@npm:^9.0.0": - version: 9.0.0 - resolution: "@digitalcredentials/jsonld@npm:9.0.0" - dependencies: - "@digitalcredentials/http-client": "npm:^5.0.1" - canonicalize: "npm:^1.0.1" - lru-cache: "npm:^6.0.0" - rdf-canonize: "npm:^3.4.0" - checksum: 10c0/3d3929ff7cdffd249746052ab1bdf53047b59c9fb3d316f642461bd77183d443f48ebe49ceaaf7164bc175c11b5bbeee0c33b37e951311b83fe9abf8735c5ed5 - languageName: node - linkType: hard - -"@digitalcredentials/keypair@npm:^1.0.5": - version: 1.0.5 - resolution: "@digitalcredentials/keypair@npm:1.0.5" - checksum: 10c0/693fb83f5043c7c61c0be0317aa633d49fc82faee791b3354a9dc033e2f63b4fd1890e35958b3d3e6fa767ad4f6315e7a82d01c4c1f5c076ea18663a7e910318 - languageName: node - linkType: hard - -"@digitalcredentials/open-badges-context@npm:^2.1.0": - version: 2.1.0 - resolution: "@digitalcredentials/open-badges-context@npm:2.1.0" - checksum: 10c0/faa298b29f8dc4cc157078fea2131b778eeb7d5a622fbce05624040a1ebbb3fa8a1c992e6044c61e39723dcdf399d1b4893b82e005bb47fce326ce8c8f9c298c - languageName: node - linkType: hard - -"@digitalcredentials/vc-data-model@npm:^2.0.0": - version: 2.0.0 - resolution: "@digitalcredentials/vc-data-model@npm:2.0.0" - checksum: 10c0/a1f23b9517615bb57d88e406606361f90f8ee2ce7303ed45f77d10043719f8949f72d3fc5dce8aafdd01ed7bc35fb3e0067145c720d6ffe967a9a7986d7b0cad - languageName: node - linkType: hard - -"@digitalcredentials/vc@npm:^9.0.1": - version: 9.0.1 - resolution: "@digitalcredentials/vc@npm:9.0.1" - dependencies: - "@digitalcredentials/credentials-v2-context": "npm:~0.0.1-beta.0" - "@digitalcredentials/jsonld": "npm:^9.0.0" - "@digitalcredentials/jsonld-signatures": "npm:^12.0.0" - "@digitalcredentials/open-badges-context": "npm:^2.1.0" - credentials-context: "npm:^2.0.0" - ed25519-signature-2018-context: "npm:^1.1.0" - checksum: 10c0/0624f8bba05750b18ab4ddc5185145753c7bc6ecb4c6d20ce101057982b34db1c68be390bb16948cfc364efbf25b160cb48239c5cb6883262ef3e575360bef3e - languageName: node - linkType: hard - "@effect/schema@npm:0.75.5": version: 0.75.5 resolution: "@effect/schema@npm:0.75.5" @@ -3285,174 +3095,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/aix-ppc64@npm:0.23.1": - version: 0.23.1 - resolution: "@esbuild/aix-ppc64@npm:0.23.1" - conditions: os=aix & cpu=ppc64 - languageName: node - linkType: hard - -"@esbuild/android-arm64@npm:0.23.1": - version: 0.23.1 - resolution: "@esbuild/android-arm64@npm:0.23.1" - conditions: os=android & cpu=arm64 - languageName: node - linkType: hard - -"@esbuild/android-arm@npm:0.23.1": - version: 0.23.1 - resolution: "@esbuild/android-arm@npm:0.23.1" - conditions: os=android & cpu=arm - languageName: node - linkType: hard - -"@esbuild/android-x64@npm:0.23.1": - version: 0.23.1 - resolution: "@esbuild/android-x64@npm:0.23.1" - conditions: os=android & cpu=x64 - languageName: node - linkType: hard - -"@esbuild/darwin-arm64@npm:0.23.1": - version: 0.23.1 - resolution: "@esbuild/darwin-arm64@npm:0.23.1" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - -"@esbuild/darwin-x64@npm:0.23.1": - version: 0.23.1 - resolution: "@esbuild/darwin-x64@npm:0.23.1" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - -"@esbuild/freebsd-arm64@npm:0.23.1": - version: 0.23.1 - resolution: "@esbuild/freebsd-arm64@npm:0.23.1" - conditions: os=freebsd & cpu=arm64 - languageName: node - linkType: hard - -"@esbuild/freebsd-x64@npm:0.23.1": - version: 0.23.1 - resolution: "@esbuild/freebsd-x64@npm:0.23.1" - conditions: os=freebsd & cpu=x64 - languageName: node - linkType: hard - -"@esbuild/linux-arm64@npm:0.23.1": - version: 0.23.1 - resolution: "@esbuild/linux-arm64@npm:0.23.1" - conditions: os=linux & cpu=arm64 - languageName: node - linkType: hard - -"@esbuild/linux-arm@npm:0.23.1": - version: 0.23.1 - resolution: "@esbuild/linux-arm@npm:0.23.1" - conditions: os=linux & cpu=arm - languageName: node - linkType: hard - -"@esbuild/linux-ia32@npm:0.23.1": - version: 0.23.1 - resolution: "@esbuild/linux-ia32@npm:0.23.1" - conditions: os=linux & cpu=ia32 - languageName: node - linkType: hard - -"@esbuild/linux-loong64@npm:0.23.1": - version: 0.23.1 - resolution: "@esbuild/linux-loong64@npm:0.23.1" - conditions: os=linux & cpu=loong64 - languageName: node - linkType: hard - -"@esbuild/linux-mips64el@npm:0.23.1": - version: 0.23.1 - resolution: "@esbuild/linux-mips64el@npm:0.23.1" - conditions: os=linux & cpu=mips64el - languageName: node - linkType: hard - -"@esbuild/linux-ppc64@npm:0.23.1": - version: 0.23.1 - resolution: "@esbuild/linux-ppc64@npm:0.23.1" - conditions: os=linux & cpu=ppc64 - languageName: node - linkType: hard - -"@esbuild/linux-riscv64@npm:0.23.1": - version: 0.23.1 - resolution: "@esbuild/linux-riscv64@npm:0.23.1" - conditions: os=linux & cpu=riscv64 - languageName: node - linkType: hard - -"@esbuild/linux-s390x@npm:0.23.1": - version: 0.23.1 - resolution: "@esbuild/linux-s390x@npm:0.23.1" - conditions: os=linux & cpu=s390x - languageName: node - linkType: hard - -"@esbuild/linux-x64@npm:0.23.1": - version: 0.23.1 - resolution: "@esbuild/linux-x64@npm:0.23.1" - conditions: os=linux & cpu=x64 - languageName: node - linkType: hard - -"@esbuild/netbsd-x64@npm:0.23.1": - version: 0.23.1 - resolution: "@esbuild/netbsd-x64@npm:0.23.1" - conditions: os=netbsd & cpu=x64 - languageName: node - linkType: hard - -"@esbuild/openbsd-arm64@npm:0.23.1": - version: 0.23.1 - resolution: "@esbuild/openbsd-arm64@npm:0.23.1" - conditions: os=openbsd & cpu=arm64 - languageName: node - linkType: hard - -"@esbuild/openbsd-x64@npm:0.23.1": - version: 0.23.1 - resolution: "@esbuild/openbsd-x64@npm:0.23.1" - conditions: os=openbsd & cpu=x64 - languageName: node - linkType: hard - -"@esbuild/sunos-x64@npm:0.23.1": - version: 0.23.1 - resolution: "@esbuild/sunos-x64@npm:0.23.1" - conditions: os=sunos & cpu=x64 - languageName: node - linkType: hard - -"@esbuild/win32-arm64@npm:0.23.1": - version: 0.23.1 - resolution: "@esbuild/win32-arm64@npm:0.23.1" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - -"@esbuild/win32-ia32@npm:0.23.1": - version: 0.23.1 - resolution: "@esbuild/win32-ia32@npm:0.23.1" - conditions: os=win32 & cpu=ia32 - languageName: node - linkType: hard - -"@esbuild/win32-x64@npm:0.23.1": - version: 0.23.1 - resolution: "@esbuild/win32-x64@npm:0.23.1" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - "@eslint-community/eslint-utils@npm:^4.2.0": version: 4.4.0 resolution: "@eslint-community/eslint-utils@npm:4.4.0" @@ -3549,36 +3191,6 @@ __metadata: languageName: node linkType: hard -"@inrupt/solid-client-authn-core@npm:^2.1.0": - version: 2.1.0 - resolution: "@inrupt/solid-client-authn-core@npm:2.1.0" - dependencies: - events: "npm:^3.3.0" - jose: "npm:^5.1.3" - uuid: "npm:^9.0.1" - checksum: 10c0/44290be694cdc076d39bd232d3d6a3d06cd9d3f963fb582f6e5a919befa4030597f1335f372d402a5a073ff788c1db08346bbd44af877d172f0d614eb41de2dc - languageName: node - linkType: hard - -"@inrupt/solid-client@npm:^2.0.1": - version: 2.0.1 - resolution: "@inrupt/solid-client@npm:2.0.1" - dependencies: - "@rdfjs/dataset": "npm:^1.1.1" - buffer: "npm:^6.0.3" - fsevents: "npm:^2.3.3" - http-link-header: "npm:^1.1.1" - jsonld-context-parser: "npm:^2.4.0" - jsonld-streaming-parser: "npm:^3.3.0" - n3: "npm:^1.17.2" - uuid: "npm:^9.0.1" - dependenciesMeta: - fsevents: - optional: true - checksum: 10c0/53ff9eaa1eae126dcef52562dbbcbf8d15db23b23d86575d203deaa071da89d2799507036a13f3e624e39a15e07f16c8415832042cc8a3136d31afbde6a247c0 - languageName: node - linkType: hard - "@inrupt/universal-fetch@npm:^1.0.3": version: 1.0.3 resolution: "@inrupt/universal-fetch@npm:1.0.3" @@ -3977,13 +3589,6 @@ __metadata: languageName: node linkType: hard -"@noble/ed25519@npm:^1.6.0, @noble/ed25519@npm:^1.7.1": - version: 1.7.3 - resolution: "@noble/ed25519@npm:1.7.3" - checksum: 10c0/dc162c3be5ae5a3cc0e6aff8209c8d175f24bba22f2b473aa849e102471193c83664b06f0ba2b5e01e9aa1a67a44daf313f478adb9f38768408a8bcad6145a48 - languageName: node - linkType: hard - "@nodelib/fs.scandir@npm:2.1.5": version: 2.1.5 resolution: "@nodelib/fs.scandir@npm:2.1.5" @@ -4087,21 +3692,21 @@ __metadata: languageName: node linkType: hard -"@rdfjs/types@npm:*, @rdfjs/types@npm:>=1.0.1, @rdfjs/types@npm:^1.1.0": - version: 1.1.0 - resolution: "@rdfjs/types@npm:1.1.0" +"@rdfjs/types@npm:*, @rdfjs/types@npm:>=1.0.1, @rdfjs/types@npm:^2.0.0, @rdfjs/types@npm:^2.0.1": + version: 2.0.1 + resolution: "@rdfjs/types@npm:2.0.1" dependencies: "@types/node": "npm:*" - checksum: 10c0/ed18e5eded11fba0b297b19ecc51422e9d7d9ef121759a7023a949794092738075cb834f9d6da906a39aa75eb1ba18eb5256a1871f67754d654540892a55ea42 + checksum: 10c0/81012b02e28737e15dfc27068664bbab7b989cc2ff82e0a6a341df492d9d97210b5e462d85bfb7882aa0ef5bf84e2c5e95647fb779b67b7b1910b1837d79c500 languageName: node linkType: hard -"@rdfjs/types@npm:^2.0.0, @rdfjs/types@npm:^2.0.1": - version: 2.0.1 - resolution: "@rdfjs/types@npm:2.0.1" +"@rdfjs/types@npm:^1.1.0": + version: 1.1.0 + resolution: "@rdfjs/types@npm:1.1.0" dependencies: "@types/node": "npm:*" - checksum: 10c0/81012b02e28737e15dfc27068664bbab7b989cc2ff82e0a6a341df492d9d97210b5e462d85bfb7882aa0ef5bf84e2c5e95647fb779b67b7b1910b1837d79c500 + checksum: 10c0/ed18e5eded11fba0b297b19ecc51422e9d7d9ef121759a7023a949794092738075cb834f9d6da906a39aa75eb1ba18eb5256a1871f67754d654540892a55ea42 languageName: node linkType: hard @@ -4196,7 +3801,7 @@ __metadata: languageName: node linkType: hard -"@solid/community-server@npm:^7.0.3, @solid/community-server@npm:^7.1.7": +"@solid/community-server@npm:^7.1.7": version: 7.1.7 resolution: "@solid/community-server@npm:7.1.7" dependencies: @@ -4275,20 +3880,6 @@ __metadata: languageName: node linkType: hard -"@solidlab/derived-resources-component@npm:^1.0.2": - version: 1.0.2 - resolution: "@solidlab/derived-resources-component@npm:1.0.2" - dependencies: - "@comunica/query-sparql": "npm:^2.10.2" - "@rdfjs/types": "npm:^1.1.0" - "@solid/community-server": "npm:^7.0.3" - asynciterator: "npm:^3.8.1" - n3: "npm:^1.17.2" - uri-template-lite: "npm:^23.4.0" - checksum: 10c0/f57f57253cf9d8099ad3651eae09c7291b37911d08b28e76e8a011f0793619eaca46fa315ace36a0dc012da936ce528b130e7a55a6f31ddaa1d4e37c5cf4953f - languageName: node - linkType: hard - "@solidlab/ucp@workspace:^, @solidlab/ucp@workspace:packages/ucp": version: 0.0.0-use.local resolution: "@solidlab/ucp@workspace:packages/ucp" @@ -4307,7 +3898,6 @@ __metadata: resolution: "@solidlab/uma-css@workspace:packages/css" dependencies: "@solid/community-server": "npm:^7.1.7" - "@solidlab/derived-resources-component": "npm:^1.0.2" "@solidlab/uma": "workspace:^" "@types/n3": "npm:^1.16.4" componentsjs: "npm:^5.5.1" @@ -4331,7 +3921,6 @@ __metadata: get-jwks: "npm:^9.0.1" http-message-signatures: "npm:^1.0.4" jose: "npm:^5.2.2" - koreografeye: "npm:^0.4.8" logform: "npm:^2.6.0" n3: "npm:^1.17.2" odrl-evaluator: "npm:^0.3.0" @@ -4347,99 +3936,23 @@ __metadata: dependencies: "@commitlint/cli": "npm:^16.1.0" "@commitlint/config-conventional": "npm:^16.0.0" - "@digitalbazaar/ed25519-signature-2020": "npm:^5.4.0" - "@digitalbazaar/ed25519-verification-key-2020": "npm:^4.2.0" - "@digitalbazaar/vc": "npm:^7.1.0" - "@digitalcredentials/ed25519-signature-2020": "npm:^6.0.0" - "@digitalcredentials/ed25519-verification-key-2020": "npm:^4.0.0" - "@digitalcredentials/vc": "npm:^9.0.1" - "@digitalcredentials/vc-data-model": "npm:^2.0.0" - "@inrupt/solid-client": "npm:^2.0.1" - "@inrupt/solid-client-authn-core": "npm:^2.1.0" - "@solidlab/ucp": "workspace:^" "@types/jest": "npm:^29.5.12" "@types/node": "npm:^20.11.25" "@typescript-eslint/eslint-plugin": "npm:^5.12.1" "@typescript-eslint/parser": "npm:^5.12.1" chalk: "npm:^5.4.1" componentsjs-generator: "npm:^3.1.2" - concurrently: "npm:^8.2.2" eslint: "npm:^8.10.0" jest: "npm:^29.7.0" jest-rdf: "npm:^1.8.1" - jsonld: "npm:^8.3.3" - koreografeye: "npm:^0.4.8" shx: "npm:^0.3.4" syncpack: "npm:^13.0.2" ts-jest: "npm:^29.1.2" ts-node: "npm:^10.9.2" - tsx: "npm:^4.19.2" typescript: "npm:^5.3.3" languageName: unknown linkType: soft -"@stablelib/binary@npm:^1.0.1": - version: 1.0.1 - resolution: "@stablelib/binary@npm:1.0.1" - dependencies: - "@stablelib/int": "npm:^1.0.1" - checksum: 10c0/154cb558d8b7c20ca5dc2e38abca2a3716ce36429bf1b9c298939cea0929766ed954feb8a9c59245ac64c923d5d3466bb7d99f281debd3a9d561e1279b11cd35 - languageName: node - linkType: hard - -"@stablelib/ed25519@npm:^1.0.1": - version: 1.0.3 - resolution: "@stablelib/ed25519@npm:1.0.3" - dependencies: - "@stablelib/random": "npm:^1.0.2" - "@stablelib/sha512": "npm:^1.0.1" - "@stablelib/wipe": "npm:^1.0.1" - checksum: 10c0/b4a05e3c24dabd8a9e0b5bd72dea761bfb4b5c66404308e9f0529ef898e75d6f588234920762d5372cb920d9d47811250160109f02d04b6eed53835fb6916eb9 - languageName: node - linkType: hard - -"@stablelib/hash@npm:^1.0.1": - version: 1.0.1 - resolution: "@stablelib/hash@npm:1.0.1" - checksum: 10c0/58b5572a4067820b77a1606ed2d4a6dc4068c5475f68ba0918860a5f45adf60b33024a0cea9532dcd8b7345c53b3c9636a23723f5f8ae83e0c3648f91fb5b5cc - languageName: node - linkType: hard - -"@stablelib/int@npm:^1.0.1": - version: 1.0.1 - resolution: "@stablelib/int@npm:1.0.1" - checksum: 10c0/e1a6a7792fc2146d65de56e4ef42e8bc385dd5157eff27019b84476f564a1a6c43413235ed0e9f7c9bb8907dbdab24679467aeb10f44c92e6b944bcd864a7ee0 - languageName: node - linkType: hard - -"@stablelib/random@npm:^1.0.2": - version: 1.0.2 - resolution: "@stablelib/random@npm:1.0.2" - dependencies: - "@stablelib/binary": "npm:^1.0.1" - "@stablelib/wipe": "npm:^1.0.1" - checksum: 10c0/ebb217cfb76db97d98ec07bd7ce03a650fa194b91f0cb12382738161adff1830f405de0e9bad22bbc352422339ff85f531873b6a874c26ea9b59cfcc7ea787e0 - languageName: node - linkType: hard - -"@stablelib/sha512@npm:^1.0.1": - version: 1.0.1 - resolution: "@stablelib/sha512@npm:1.0.1" - dependencies: - "@stablelib/binary": "npm:^1.0.1" - "@stablelib/hash": "npm:^1.0.1" - "@stablelib/wipe": "npm:^1.0.1" - checksum: 10c0/84549070a383f4daf23d9065230eb81bc8f590c68bf5f7968f1b78901236b3bb387c14f63773dc6c3dc78e823b1c15470d2a04d398a2506391f466c16ba29b58 - languageName: node - linkType: hard - -"@stablelib/wipe@npm:^1.0.1": - version: 1.0.1 - resolution: "@stablelib/wipe@npm:1.0.1" - checksum: 10c0/c5a54f769c286a5b3ecff979471dfccd4311f2e84a959908e8c0e3aa4eed1364bd9707f7b69d1384b757e62cc295c221fa27286c7f782410eb8a690f30cfd796 - languageName: node - linkType: hard - "@szmarczak/http-timer@npm:^5.0.1": version: 5.0.1 resolution: "@szmarczak/http-timer@npm:5.0.1" @@ -4853,17 +4366,7 @@ __metadata: languageName: node linkType: hard -"@types/n3@npm:^1.10.4, @types/n3@npm:^1.16.3, @types/n3@npm:^1.16.4": - version: 1.16.4 - resolution: "@types/n3@npm:1.16.4" - dependencies: - "@rdfjs/types": "npm:^1.1.0" - "@types/node": "npm:*" - checksum: 10c0/34486916aca38ea90a40da5c2dba9326ed8e7784bf986fc3231976334a3fb2b7080e05a0f4f29780b33d81f7349b472c24561e8fbe0630e299dfda116bdb30c6 - languageName: node - linkType: hard - -"@types/n3@npm:^1.21.1": +"@types/n3@npm:^1.10.4, @types/n3@npm:^1.16.3, @types/n3@npm:^1.16.4, @types/n3@npm:^1.21.1": version: 1.24.2 resolution: "@types/n3@npm:1.24.2" dependencies: @@ -5563,20 +5066,6 @@ __metadata: languageName: node linkType: hard -"base-x@npm:^4.0.0": - version: 4.0.0 - resolution: "base-x@npm:4.0.0" - checksum: 10c0/0cb47c94535144ab138f70bb5aa7e6e03049ead88615316b62457f110fc204f2c3baff5c64a1c1b33aeb068d79a68092c08a765c7ccfa133eee1e70e4c6eb903 - languageName: node - linkType: hard - -"base58-universal@npm:^2.0.0": - version: 2.0.0 - resolution: "base58-universal@npm:2.0.0" - checksum: 10c0/f21fd50904e1f2d083832e39269058ffbd95592ad27fc2acc53ad3d9d7eacfa82c4e655181e0fd8a817619f4c925a42baca877276dd73e755d30f52def748ab6 - languageName: node - linkType: hard - "base64-js@npm:^1.3.1": version: 1.5.1 resolution: "base64-js@npm:1.5.1" @@ -5584,31 +5073,6 @@ __metadata: languageName: node linkType: hard -"base64url-universal@npm:^1.1.0": - version: 1.1.0 - resolution: "base64url-universal@npm:1.1.0" - dependencies: - base64url: "npm:^3.0.0" - checksum: 10c0/d1408b3062c7a73cca75c6cc47eaefc47089ca20e019f5b6c3ab4641d7a87caec3e8e1fb17165b63380bef924f62f59dc7f257586490d5718c8c352f1dfc98e2 - languageName: node - linkType: hard - -"base64url-universal@npm:^2.0.0": - version: 2.0.0 - resolution: "base64url-universal@npm:2.0.0" - dependencies: - base64url: "npm:^3.0.1" - checksum: 10c0/6143326c1b5011d987d410a7451fcacad33af9282f9e7d06a21e1d4d7ed9d884e6c09a8f0d178f6c9ef3b11f8cda10ece4c84768e43dc9cb4dc47d1b5056de40 - languageName: node - linkType: hard - -"base64url@npm:^3.0.0, base64url@npm:^3.0.1": - version: 3.0.1 - resolution: "base64url@npm:3.0.1" - checksum: 10c0/5ca9d6064e9440a2a45749558dddd2549ca439a305793d4f14a900b7256b5f4438ef1b7a494e1addc66ced5d20f5c010716d353ed267e4b769e6c78074991241 - languageName: node - linkType: hard - "bcryptjs@npm:^2.4.3": version: 2.4.3 resolution: "bcryptjs@npm:2.4.3" @@ -5853,7 +5317,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^4.0.0, chalk@npm:^4.0.2, chalk@npm:^4.1.2": +"chalk@npm:^4.0.0, chalk@npm:^4.0.2": version: 4.1.2 resolution: "chalk@npm:4.1.2" dependencies: @@ -6100,26 +5564,6 @@ __metadata: languageName: node linkType: hard -"concurrently@npm:^8.2.2": - version: 8.2.2 - resolution: "concurrently@npm:8.2.2" - dependencies: - chalk: "npm:^4.1.2" - date-fns: "npm:^2.30.0" - lodash: "npm:^4.17.21" - rxjs: "npm:^7.8.1" - shell-quote: "npm:^1.8.1" - spawn-command: "npm:0.0.2" - supports-color: "npm:^8.1.1" - tree-kill: "npm:^1.2.2" - yargs: "npm:^17.7.2" - bin: - conc: dist/bin/concurrently.js - concurrently: dist/bin/concurrently.js - checksum: 10c0/0e9683196fe9c071d944345d21d8f34aa6c0cc50c0dd897e95619f2f1c9eb4871dca851b2569da17888235b7335b4c821ca19deed35bebcd9a131ee5d247f34c - languageName: node - linkType: hard - "content-disposition@npm:~0.5.2": version: 0.5.4 resolution: "content-disposition@npm:0.5.4" @@ -6275,13 +5719,6 @@ __metadata: languageName: node linkType: hard -"credentials-context@npm:^2.0.0": - version: 2.0.0 - resolution: "credentials-context@npm:2.0.0" - checksum: 10c0/bdafba13fdbef62bb48402af6637e81b8f18ee380ea246ee2153a00842d8876e3b6aea0bf4526fd80346e6403d1b8cbc0bfe3c80e5813a29e5dc5bf4091374f7 - languageName: node - linkType: hard - "cross-fetch@npm:^3.0.6, cross-fetch@npm:^3.1.5": version: 3.1.8 resolution: "cross-fetch@npm:3.1.8" @@ -6311,20 +5748,6 @@ __metadata: languageName: node linkType: hard -"crypto-ld@npm:^6.0.0": - version: 6.0.0 - resolution: "crypto-ld@npm:6.0.0" - checksum: 10c0/af8bd8483bdf845d0b57e8e7ea30c7ba22f1b3a927dc683b267af663d9ec204ebc329525fd99ff212807e82140e89800c63e28ce56ed0e817a372507005af0a7 - languageName: node - linkType: hard - -"crypto-ld@npm:^7.0.0": - version: 7.0.0 - resolution: "crypto-ld@npm:7.0.0" - checksum: 10c0/67fac3bf5c52d71e02ae436a305f223376746cb43d2aa6550764d2885e15e8802267c8fd6a451c4255b90b58eeab1c036b6ee6e88f21425620a3d5ecc8b36d92 - languageName: node - linkType: hard - "dargs@npm:^7.0.0": version: 7.0.0 resolution: "dargs@npm:7.0.0" @@ -6339,15 +5762,6 @@ __metadata: languageName: node linkType: hard -"date-fns@npm:^2.30.0": - version: 2.30.0 - resolution: "date-fns@npm:2.30.0" - dependencies: - "@babel/runtime": "npm:^7.21.0" - checksum: 10c0/e4b521fbf22bc8c3db332bbfb7b094fd3e7627de0259a9d17c7551e2d2702608a7307a449206065916538e384f37b181565447ce2637ae09828427aed9cb5581 - languageName: node - linkType: hard - "date-format@npm:^4.0.14": version: 4.0.14 resolution: "date-format@npm:4.0.14" @@ -6561,20 +5975,6 @@ __metadata: languageName: node linkType: hard -"ed25519-signature-2018-context@npm:^1.1.0": - version: 1.1.0 - resolution: "ed25519-signature-2018-context@npm:1.1.0" - checksum: 10c0/c0fb62dd448de59f9cb45ac55f026b793b8da9c0dbe83014d0ddf73d50ea78e37e6bf88b9445f2301a6ead3a42031ef5839f625e3186169d07897e19a056de7c - languageName: node - linkType: hard - -"ed25519-signature-2020-context@npm:^1.0.1, ed25519-signature-2020-context@npm:^1.1.0": - version: 1.1.0 - resolution: "ed25519-signature-2020-context@npm:1.1.0" - checksum: 10c0/7e5fba0ad8c1979412c45439654c0e1ac05372305760ad7c53e6b8d9a8106ed2f109e57ef19a7f0b23ea0efd69fee9c045d38280d18be11ef3dd68a433e55c46 - languageName: node - linkType: hard - "ee-first@npm:1.1.1": version: 1.1.1 resolution: "ee-first@npm:1.1.1" @@ -6724,89 +6124,6 @@ __metadata: languageName: node linkType: hard -"esbuild@npm:~0.23.0": - version: 0.23.1 - resolution: "esbuild@npm:0.23.1" - dependencies: - "@esbuild/aix-ppc64": "npm:0.23.1" - "@esbuild/android-arm": "npm:0.23.1" - "@esbuild/android-arm64": "npm:0.23.1" - "@esbuild/android-x64": "npm:0.23.1" - "@esbuild/darwin-arm64": "npm:0.23.1" - "@esbuild/darwin-x64": "npm:0.23.1" - "@esbuild/freebsd-arm64": "npm:0.23.1" - "@esbuild/freebsd-x64": "npm:0.23.1" - "@esbuild/linux-arm": "npm:0.23.1" - "@esbuild/linux-arm64": "npm:0.23.1" - "@esbuild/linux-ia32": "npm:0.23.1" - "@esbuild/linux-loong64": "npm:0.23.1" - "@esbuild/linux-mips64el": "npm:0.23.1" - "@esbuild/linux-ppc64": "npm:0.23.1" - "@esbuild/linux-riscv64": "npm:0.23.1" - "@esbuild/linux-s390x": "npm:0.23.1" - "@esbuild/linux-x64": "npm:0.23.1" - "@esbuild/netbsd-x64": "npm:0.23.1" - "@esbuild/openbsd-arm64": "npm:0.23.1" - "@esbuild/openbsd-x64": "npm:0.23.1" - "@esbuild/sunos-x64": "npm:0.23.1" - "@esbuild/win32-arm64": "npm:0.23.1" - "@esbuild/win32-ia32": "npm:0.23.1" - "@esbuild/win32-x64": "npm:0.23.1" - dependenciesMeta: - "@esbuild/aix-ppc64": - optional: true - "@esbuild/android-arm": - optional: true - "@esbuild/android-arm64": - optional: true - "@esbuild/android-x64": - optional: true - "@esbuild/darwin-arm64": - optional: true - "@esbuild/darwin-x64": - optional: true - "@esbuild/freebsd-arm64": - optional: true - "@esbuild/freebsd-x64": - optional: true - "@esbuild/linux-arm": - optional: true - "@esbuild/linux-arm64": - optional: true - "@esbuild/linux-ia32": - optional: true - "@esbuild/linux-loong64": - optional: true - "@esbuild/linux-mips64el": - optional: true - "@esbuild/linux-ppc64": - optional: true - "@esbuild/linux-riscv64": - optional: true - "@esbuild/linux-s390x": - optional: true - "@esbuild/linux-x64": - optional: true - "@esbuild/netbsd-x64": - optional: true - "@esbuild/openbsd-arm64": - optional: true - "@esbuild/openbsd-x64": - optional: true - "@esbuild/sunos-x64": - optional: true - "@esbuild/win32-arm64": - optional: true - "@esbuild/win32-ia32": - optional: true - "@esbuild/win32-x64": - optional: true - bin: - esbuild: bin/esbuild - checksum: 10c0/08c2ed1105cc3c5e3a24a771e35532fe6089dd24a39c10097899072cef4a99f20860e41e9294e000d86380f353b04d8c50af482483d7f69f5208481cce61eec7 - languageName: node - linkType: hard - "escalade@npm:^3.1.1": version: 3.1.1 resolution: "escalade@npm:3.1.1" @@ -7120,13 +6437,6 @@ __metadata: languageName: node linkType: hard -"fast-text-encoding@npm:^1.0.3": - version: 1.0.6 - resolution: "fast-text-encoding@npm:1.0.6" - checksum: 10c0/e1d0381bda229c92c7906f63308f3b9caca8c78b732768b1ee16f560089ed21bc159bbe1434138ccd3815931ec8d4785bdade1ad1c45accfdf27ac6606ac67d2 - languageName: node - linkType: hard - "fastq@npm:^1.6.0": version: 1.15.0 resolution: "fastq@npm:1.15.0" @@ -7356,7 +6666,7 @@ __metadata: languageName: node linkType: hard -"fsevents@npm:^2.3.2, fsevents@npm:^2.3.3, fsevents@npm:~2.3.3": +"fsevents@npm:^2.3.2": version: 2.3.3 resolution: "fsevents@npm:2.3.3" dependencies: @@ -7366,7 +6676,7 @@ __metadata: languageName: node linkType: hard -"fsevents@patch:fsevents@npm%3A^2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A^2.3.3#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin": +"fsevents@patch:fsevents@npm%3A^2.3.2#optional!builtin": version: 2.3.3 resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1" dependencies: @@ -7428,15 +6738,6 @@ __metadata: languageName: node linkType: hard -"get-tsconfig@npm:^4.7.5": - version: 4.10.0 - resolution: "get-tsconfig@npm:4.10.0" - dependencies: - resolve-pkg-maps: "npm:^1.0.0" - checksum: 10c0/c9b5572c5118923c491c04285c73bd55b19e214992af957c502a3be0fc0043bb421386ffd45ca3433c0a7fba81221ca300479e8393960acf15d0ed4563f38a86 - languageName: node - linkType: hard - "git-raw-commits@npm:^2.0.0": version: 2.0.11 resolution: "git-raw-commits@npm:2.0.11" @@ -7792,7 +7093,7 @@ __metadata: languageName: node linkType: hard -"http-link-header@npm:^1.0.2, http-link-header@npm:^1.1.1": +"http-link-header@npm:^1.0.2": version: 1.1.3 resolution: "http-link-header@npm:1.1.3" checksum: 10c0/56698a9d3aee4d5319d1cdfe62ef5d7179f179ec1e6432d23c9e6a0c896be642ba47a4985a45419cff91008032aef920aca9df94ff9e763e646c83bf54b7243d @@ -8830,19 +8131,7 @@ __metadata: languageName: node linkType: hard -"jsonld-signatures@npm:^11.2.1, jsonld-signatures@npm:^11.3.0": - version: 11.5.0 - resolution: "jsonld-signatures@npm:11.5.0" - dependencies: - "@digitalbazaar/security-context": "npm:^1.0.0" - jsonld: "npm:^8.0.0" - rdf-canonize: "npm:^4.0.1" - serialize-error: "npm:^8.1.0" - checksum: 10c0/a624ef4706c91064ba19dd3f1bb2a43f40cabd68f38e1c3e57fb0cea830b5ff4763364a00197313ccf704f571295803220d5c20d40df371562d84de98c5df9a0 - languageName: node - linkType: hard - -"jsonld-streaming-parser@npm:^3.0.1, jsonld-streaming-parser@npm:^3.3.0": +"jsonld-streaming-parser@npm:^3.0.1": version: 3.4.0 resolution: "jsonld-streaming-parser@npm:3.4.0" dependencies: @@ -8873,7 +8162,7 @@ __metadata: languageName: node linkType: hard -"jsonld@npm:^8.0.0, jsonld@npm:^8.3.1, jsonld@npm:^8.3.3, jsonld@npm:^8.x.x": +"jsonld@npm:^8.x.x": version: 8.3.3 resolution: "jsonld@npm:8.3.3" dependencies: @@ -9042,13 +8331,6 @@ __metadata: languageName: node linkType: hard -"ky@npm:^1.0.1": - version: 1.7.4 - resolution: "ky@npm:1.7.4" - checksum: 10c0/8b28b85cbee6d3e073ff796b92661f4bf155ec9b9a131411de1c34fb2f89f8507e67ff3df369e3c6d18714134774e8735e88cba72b19d005a09112b800d14474 - languageName: node - linkType: hard - "leven@npm:^3.1.0": version: 3.1.0 resolution: "leven@npm:3.1.0" @@ -9126,7 +8408,7 @@ __metadata: languageName: node linkType: hard -"lodash@npm:^4.17.15, lodash@npm:^4.17.19, lodash@npm:^4.17.21": +"lodash@npm:^4.17.15, lodash@npm:^4.17.19": version: 4.17.21 resolution: "lodash@npm:4.17.21" checksum: 10c0/d8cbea072bb08655bb4c989da418994b073a608dffa608b09ac04b43a791b12aeae7cd7ad919aa4c925f33b48490b5cfe6c1f71d827956071dae2e7bb3a6b74c @@ -9570,17 +8852,7 @@ __metadata: languageName: node linkType: hard -"n3@npm:^1.16.1, n3@npm:^1.16.3, n3@npm:^1.16.4, n3@npm:^1.17.0, n3@npm:^1.17.1, n3@npm:^1.17.2, n3@npm:^1.20.4, n3@npm:^1.6.3": - version: 1.25.1 - resolution: "n3@npm:1.25.1" - dependencies: - buffer: "npm:^6.0.3" - readable-stream: "npm:^4.0.0" - checksum: 10c0/0a4efc3151d79c7200b57f78bb5d4411f6746d7f2d63ccc7fe068fa9a4d20afc03001957d9216ce2da62f2de266452713f9867112e9b3170307eb4ac059d5d1d - languageName: node - linkType: hard - -"n3@npm:^1.23.1": +"n3@npm:^1.16.1, n3@npm:^1.16.3, n3@npm:^1.16.4, n3@npm:^1.17.0, n3@npm:^1.17.1, n3@npm:^1.17.2, n3@npm:^1.20.4, n3@npm:^1.23.1, n3@npm:^1.6.3": version: 1.25.2 resolution: "n3@npm:1.25.2" dependencies: @@ -10269,15 +9541,6 @@ __metadata: languageName: node linkType: hard -"rdf-canonize@npm:^4.0.1": - version: 4.0.1 - resolution: "rdf-canonize@npm:4.0.1" - dependencies: - setimmediate: "npm:^1.0.5" - checksum: 10c0/8f6040847c5731817cc5ad95ca16caa588fba76c94ee3b24f282c9a3054f36226560bcb33bed8a173903960af1b9937e21db6e0dc2e90caa749682fff8228250 - languageName: node - linkType: hard - "rdf-data-factory@npm:^1.0.1, rdf-data-factory@npm:^1.1.0, rdf-data-factory@npm:^1.1.1, rdf-data-factory@npm:^1.1.2": version: 1.1.2 resolution: "rdf-data-factory@npm:1.1.2" @@ -10738,13 +10001,6 @@ __metadata: languageName: node linkType: hard -"regenerator-runtime@npm:^0.14.0": - version: 0.14.1 - resolution: "regenerator-runtime@npm:0.14.1" - checksum: 10c0/1b16eb2c4bceb1665c89de70dcb64126a22bc8eb958feef3cd68fe11ac6d2a4899b5cd1b80b0774c7c03591dc57d16631a7f69d2daa2ec98100e2f29f7ec4cc4 - languageName: node - linkType: hard - "relative-to-absolute-iri@npm:^1.0.0, relative-to-absolute-iri@npm:^1.0.2, relative-to-absolute-iri@npm:^1.0.5, relative-to-absolute-iri@npm:^1.0.6, relative-to-absolute-iri@npm:^1.0.7": version: 1.0.7 resolution: "relative-to-absolute-iri@npm:1.0.7" @@ -10798,13 +10054,6 @@ __metadata: languageName: node linkType: hard -"resolve-pkg-maps@npm:^1.0.0": - version: 1.0.0 - resolution: "resolve-pkg-maps@npm:1.0.0" - checksum: 10c0/fb8f7bbe2ca281a73b7ef423a1cbc786fb244bd7a95cbe5c3fba25b27d327150beca8ba02f622baea65919a57e061eb5005204daa5f93ed590d9b77463a567ab - languageName: node - linkType: hard - "resolve.exports@npm:^2.0.0": version: 2.0.2 resolution: "resolve.exports@npm:2.0.2" @@ -10898,15 +10147,6 @@ __metadata: languageName: node linkType: hard -"rxjs@npm:^7.8.1": - version: 7.8.1 - resolution: "rxjs@npm:7.8.1" - dependencies: - tslib: "npm:^2.1.0" - checksum: 10c0/3c49c1ecd66170b175c9cacf5cef67f8914dcbc7cd0162855538d365c83fea631167cacb644b3ce533b2ea0e9a4d0b12175186985f89d75abe73dbd8f7f06f68 - languageName: node - linkType: hard - "safe-buffer@npm:5.2.1, safe-buffer@npm:^5.0.1, safe-buffer@npm:~5.2.0": version: 5.2.1 resolution: "safe-buffer@npm:5.2.1" @@ -10973,15 +10213,6 @@ __metadata: languageName: node linkType: hard -"serialize-error@npm:^8.0.1, serialize-error@npm:^8.1.0": - version: 8.1.0 - resolution: "serialize-error@npm:8.1.0" - dependencies: - type-fest: "npm:^0.20.2" - checksum: 10c0/8cfd89f43ca93e283c5f1d16178a536bdfac9bc6029f4a9df988610cc399bc4f2478d1f10ce40b9dff66b863a5158a19b438fbec929045c96d92174f6bca1e88 - languageName: node - linkType: hard - "setimmediate@npm:^1.0.5": version: 1.0.5 resolution: "setimmediate@npm:1.0.5" @@ -11033,13 +10264,6 @@ __metadata: languageName: node linkType: hard -"shell-quote@npm:^1.8.1": - version: 1.8.1 - resolution: "shell-quote@npm:1.8.1" - checksum: 10c0/8cec6fd827bad74d0a49347057d40dfea1e01f12a6123bf82c4649f3ef152fc2bc6d6176e6376bffcd205d9d0ccb4f1f9acae889384d20baff92186f01ea455a - languageName: node - linkType: hard - "shelljs@npm:^0.8.5": version: 0.8.5 resolution: "shelljs@npm:0.8.5" @@ -11228,13 +10452,6 @@ __metadata: languageName: node linkType: hard -"spawn-command@npm:0.0.2": - version: 0.0.2 - resolution: "spawn-command@npm:0.0.2" - checksum: 10c0/b22f2d71239e6e628a400831861ba747750bbb40c0a53323754cf7b84330b73d81e40ff1f9055e6d1971818679510208a9302e13d9ff3b32feb67e74d7a1b3ef - languageName: node - linkType: hard - "spdx-correct@npm:^3.0.0": version: 3.2.0 resolution: "spdx-correct@npm:3.2.0" @@ -11497,7 +10714,7 @@ __metadata: languageName: node linkType: hard -"supports-color@npm:^8.0.0, supports-color@npm:^8.1.1": +"supports-color@npm:^8.0.0": version: 8.1.1 resolution: "supports-color@npm:8.1.1" dependencies: @@ -11700,15 +10917,6 @@ __metadata: languageName: node linkType: hard -"tree-kill@npm:^1.2.2": - version: 1.2.2 - resolution: "tree-kill@npm:1.2.2" - bin: - tree-kill: cli.js - checksum: 10c0/7b1b7c7f17608a8f8d20a162e7957ac1ef6cd1636db1aba92f4e072dc31818c2ff0efac1e3d91064ede67ed5dc57c565420531a8134090a12ac10cf792ab14d2 - languageName: node - linkType: hard - "trim-newlines@npm:^3.0.0": version: 3.0.1 resolution: "trim-newlines@npm:3.0.1" @@ -11815,13 +11023,6 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.1.0": - version: 2.6.2 - resolution: "tslib@npm:2.6.2" - checksum: 10c0/e03a8a4271152c8b26604ed45535954c0a45296e32445b4b87f8a5abdb2421f40b59b4ca437c4346af0f28179780d604094eb64546bee2019d903d01c6c19bdb - languageName: node - linkType: hard - "tsscmp@npm:1.0.6": version: 1.0.6 resolution: "tsscmp@npm:1.0.6" @@ -11840,22 +11041,6 @@ __metadata: languageName: node linkType: hard -"tsx@npm:^4.19.2": - version: 4.19.2 - resolution: "tsx@npm:4.19.2" - dependencies: - esbuild: "npm:~0.23.0" - fsevents: "npm:~2.3.3" - get-tsconfig: "npm:^4.7.5" - dependenciesMeta: - fsevents: - optional: true - bin: - tsx: dist/cli.mjs - checksum: 10c0/63164b889b1d170403e4d8753a6755dec371f220f5ce29a8e88f1f4d6085a784a12d8dc2ee669116611f2c72757ac9beaa3eea5c452796f541bdd2dc11753721 - languageName: node - linkType: hard - "type-check@npm:^0.4.0, type-check@npm:~0.4.0": version: 0.4.0 resolution: "type-check@npm:0.4.0" @@ -11989,13 +11174,6 @@ __metadata: languageName: node linkType: hard -"undici@npm:^6.6.2": - version: 6.21.1 - resolution: "undici@npm:6.21.1" - checksum: 10c0/d604080e4f8db89b35a63b483b5f96a5f8b19ec9f716e934639345449405809d2997e1dd7212d67048f210e54534143384d712bd9075e4394f0788895ef9ca8e - languageName: node - linkType: hard - "unicorn-magic@npm:^0.1.0": version: 0.1.0 resolution: "unicorn-magic@npm:0.1.0" From a746efe520083f5cda6ccfdba7c2870b0ce77c54 Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Tue, 1 Jul 2025 14:35:27 +0200 Subject: [PATCH 28/62] chore: Replace ts-node completely with tsx --- package.json | 12 +- packages/uma/package.json | 7 +- scripts/test-private.ts | 2 +- scripts/test-public.ts | 2 +- scripts/test-registration.ts | 2 +- scripts/test-uma-ucp.ts | 2 +- yarn.lock | 302 ++++++++++++++++++++++++++++++++++- 7 files changed, 310 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index 5693dbe5..f52926eb 100644 --- a/package.json +++ b/package.json @@ -60,11 +60,11 @@ "start:demo": "yarn workspaces foreach --include 'packages/*' -A -pi -j unlimited run demo", "script:demo": "yarn exec tsx ./demo/flow.ts", "script:demo-test": "yarn exec tsx ./demo/flow-test.ts", - "script:public": "yarn exec ts-node ./scripts/test-public.ts", - "script:private": "yarn exec ts-node ./scripts/test-private.ts", - "script:registration": "yarn exec ts-node ./scripts/test-registration.ts", - "script:uma-ucp": "yarn exec ts-node ./scripts/test-uma-ucp.ts", - "script:uma-odrl": "yarn exec ts-node ./scripts/test-uma-ODRL.ts", + "script:public": "yarn exec tsx ./scripts/test-public.ts", + "script:private": "yarn exec tsx ./scripts/test-private.ts", + "script:registration": "yarn exec tsx ./scripts/test-registration.ts", + "script:uma-ucp": "yarn exec tsx ./scripts/test-uma-ucp.ts", + "script:uma-odrl": "yarn exec tsx ./scripts/test-uma-ODRL.ts", "script:flow": "yarn run script:public && yarn run script:private && yarn run script:uma-ucp && yarn run script:registration", "sync:list": "syncpack list-mismatches", "sync:fix": "syncpack fix-mismatches" @@ -84,7 +84,7 @@ "shx": "^0.3.4", "syncpack": "^13.0.2", "ts-jest": "^29.1.2", - "ts-node": "^10.9.2", + "tsx": "^4.19.2", "typescript": "^5.3.3" }, "resolutions": { diff --git a/packages/uma/package.json b/packages/uma/package.json index c057a164..1a0b64ee 100644 --- a/packages/uma/package.json +++ b/packages/uma/package.json @@ -56,9 +56,9 @@ "build:ts": "yarn run -T tsc", "build:components": "yarn run -T componentsjs-generator -r sai-uma -s src -c dist/components -i .componentsignore --lenient", "test": "yarn run -T jest --coverage", - "start": "yarn run -T ts-node bin/main.ts", - "start:odrl": "yarn run -T ts-node bin/odrl.ts", - "demo": "yarn run -T ts-node bin/demo.ts" + "start": "yarn run -T tsx bin/main.ts", + "start:odrl": "yarn run -T tsx bin/odrl.ts", + "demo": "yarn run -T tsx bin/demo.ts" }, "dependencies": { "@httpland/authorization-parser": "^1.1.0", @@ -73,7 +73,6 @@ "logform": "^2.6.0", "n3": "^1.17.2", "odrl-evaluator": "^0.3.0", - "ts-node": "^10.9.2", "uri-template-lite": "^23.4.0", "winston": "^3.11.0" }, diff --git a/scripts/test-private.ts b/scripts/test-private.ts index 7b02f6f4..a3411654 100644 --- a/scripts/test-private.ts +++ b/scripts/test-private.ts @@ -1,4 +1,4 @@ -#!/usr/bin/env ts-node +#!/usr/bin/env -S npx tsx const privateResource = "http://localhost:3000/alice/private/resource.txt" diff --git a/scripts/test-public.ts b/scripts/test-public.ts index ead0553c..7f745b5c 100644 --- a/scripts/test-public.ts +++ b/scripts/test-public.ts @@ -1,4 +1,4 @@ -#!/usr/bin/env ts-node +#!/usr/bin/env -S npx tsx const publicResource = "http://localhost:3000/alice/profile/card" diff --git a/scripts/test-registration.ts b/scripts/test-registration.ts index d69d330d..c58338a6 100644 --- a/scripts/test-registration.ts +++ b/scripts/test-registration.ts @@ -1,4 +1,4 @@ -#!/usr/bin/env ts-node +#!/usr/bin/env -S npx tsx const container = "http://localhost:3000/alice/public/"; const slug = "resource.txt"; diff --git a/scripts/test-uma-ucp.ts b/scripts/test-uma-ucp.ts index 889462c7..79b65e0c 100644 --- a/scripts/test-uma-ucp.ts +++ b/scripts/test-uma-ucp.ts @@ -1,4 +1,4 @@ -#!/usr/bin/env ts-node +#!/usr/bin/env -S npx tsx // Resource and WebID as set in config/rules/policy/policy0.ttl const resource = "http://localhost:3000/alice/other/resource.txt"; diff --git a/yarn.lock b/yarn.lock index baee7659..56f23312 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3095,6 +3095,181 @@ __metadata: languageName: node linkType: hard +"@esbuild/aix-ppc64@npm:0.25.5": + version: 0.25.5 + resolution: "@esbuild/aix-ppc64@npm:0.25.5" + conditions: os=aix & cpu=ppc64 + languageName: node + linkType: hard + +"@esbuild/android-arm64@npm:0.25.5": + version: 0.25.5 + resolution: "@esbuild/android-arm64@npm:0.25.5" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/android-arm@npm:0.25.5": + version: 0.25.5 + resolution: "@esbuild/android-arm@npm:0.25.5" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + +"@esbuild/android-x64@npm:0.25.5": + version: 0.25.5 + resolution: "@esbuild/android-x64@npm:0.25.5" + conditions: os=android & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/darwin-arm64@npm:0.25.5": + version: 0.25.5 + resolution: "@esbuild/darwin-arm64@npm:0.25.5" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/darwin-x64@npm:0.25.5": + version: 0.25.5 + resolution: "@esbuild/darwin-x64@npm:0.25.5" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/freebsd-arm64@npm:0.25.5": + version: 0.25.5 + resolution: "@esbuild/freebsd-arm64@npm:0.25.5" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/freebsd-x64@npm:0.25.5": + version: 0.25.5 + resolution: "@esbuild/freebsd-x64@npm:0.25.5" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/linux-arm64@npm:0.25.5": + version: 0.25.5 + resolution: "@esbuild/linux-arm64@npm:0.25.5" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/linux-arm@npm:0.25.5": + version: 0.25.5 + resolution: "@esbuild/linux-arm@npm:0.25.5" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@esbuild/linux-ia32@npm:0.25.5": + version: 0.25.5 + resolution: "@esbuild/linux-ia32@npm:0.25.5" + conditions: os=linux & cpu=ia32 + languageName: node + linkType: hard + +"@esbuild/linux-loong64@npm:0.25.5": + version: 0.25.5 + resolution: "@esbuild/linux-loong64@npm:0.25.5" + conditions: os=linux & cpu=loong64 + languageName: node + linkType: hard + +"@esbuild/linux-mips64el@npm:0.25.5": + version: 0.25.5 + resolution: "@esbuild/linux-mips64el@npm:0.25.5" + conditions: os=linux & cpu=mips64el + languageName: node + linkType: hard + +"@esbuild/linux-ppc64@npm:0.25.5": + version: 0.25.5 + resolution: "@esbuild/linux-ppc64@npm:0.25.5" + conditions: os=linux & cpu=ppc64 + languageName: node + linkType: hard + +"@esbuild/linux-riscv64@npm:0.25.5": + version: 0.25.5 + resolution: "@esbuild/linux-riscv64@npm:0.25.5" + conditions: os=linux & cpu=riscv64 + languageName: node + linkType: hard + +"@esbuild/linux-s390x@npm:0.25.5": + version: 0.25.5 + resolution: "@esbuild/linux-s390x@npm:0.25.5" + conditions: os=linux & cpu=s390x + languageName: node + linkType: hard + +"@esbuild/linux-x64@npm:0.25.5": + version: 0.25.5 + resolution: "@esbuild/linux-x64@npm:0.25.5" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/netbsd-arm64@npm:0.25.5": + version: 0.25.5 + resolution: "@esbuild/netbsd-arm64@npm:0.25.5" + conditions: os=netbsd & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/netbsd-x64@npm:0.25.5": + version: 0.25.5 + resolution: "@esbuild/netbsd-x64@npm:0.25.5" + conditions: os=netbsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/openbsd-arm64@npm:0.25.5": + version: 0.25.5 + resolution: "@esbuild/openbsd-arm64@npm:0.25.5" + conditions: os=openbsd & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/openbsd-x64@npm:0.25.5": + version: 0.25.5 + resolution: "@esbuild/openbsd-x64@npm:0.25.5" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/sunos-x64@npm:0.25.5": + version: 0.25.5 + resolution: "@esbuild/sunos-x64@npm:0.25.5" + conditions: os=sunos & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/win32-arm64@npm:0.25.5": + version: 0.25.5 + resolution: "@esbuild/win32-arm64@npm:0.25.5" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/win32-ia32@npm:0.25.5": + version: 0.25.5 + resolution: "@esbuild/win32-ia32@npm:0.25.5" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@esbuild/win32-x64@npm:0.25.5": + version: 0.25.5 + resolution: "@esbuild/win32-x64@npm:0.25.5" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@eslint-community/eslint-utils@npm:^4.2.0": version: 4.4.0 resolution: "@eslint-community/eslint-utils@npm:4.4.0" @@ -3924,7 +4099,6 @@ __metadata: logform: "npm:^2.6.0" n3: "npm:^1.17.2" odrl-evaluator: "npm:^0.3.0" - ts-node: "npm:^10.9.2" uri-template-lite: "npm:^23.4.0" winston: "npm:^3.11.0" languageName: unknown @@ -3948,7 +4122,7 @@ __metadata: shx: "npm:^0.3.4" syncpack: "npm:^13.0.2" ts-jest: "npm:^29.1.2" - ts-node: "npm:^10.9.2" + tsx: "npm:^4.19.2" typescript: "npm:^5.3.3" languageName: unknown linkType: soft @@ -6124,6 +6298,92 @@ __metadata: languageName: node linkType: hard +"esbuild@npm:~0.25.0": + version: 0.25.5 + resolution: "esbuild@npm:0.25.5" + dependencies: + "@esbuild/aix-ppc64": "npm:0.25.5" + "@esbuild/android-arm": "npm:0.25.5" + "@esbuild/android-arm64": "npm:0.25.5" + "@esbuild/android-x64": "npm:0.25.5" + "@esbuild/darwin-arm64": "npm:0.25.5" + "@esbuild/darwin-x64": "npm:0.25.5" + "@esbuild/freebsd-arm64": "npm:0.25.5" + "@esbuild/freebsd-x64": "npm:0.25.5" + "@esbuild/linux-arm": "npm:0.25.5" + "@esbuild/linux-arm64": "npm:0.25.5" + "@esbuild/linux-ia32": "npm:0.25.5" + "@esbuild/linux-loong64": "npm:0.25.5" + "@esbuild/linux-mips64el": "npm:0.25.5" + "@esbuild/linux-ppc64": "npm:0.25.5" + "@esbuild/linux-riscv64": "npm:0.25.5" + "@esbuild/linux-s390x": "npm:0.25.5" + "@esbuild/linux-x64": "npm:0.25.5" + "@esbuild/netbsd-arm64": "npm:0.25.5" + "@esbuild/netbsd-x64": "npm:0.25.5" + "@esbuild/openbsd-arm64": "npm:0.25.5" + "@esbuild/openbsd-x64": "npm:0.25.5" + "@esbuild/sunos-x64": "npm:0.25.5" + "@esbuild/win32-arm64": "npm:0.25.5" + "@esbuild/win32-ia32": "npm:0.25.5" + "@esbuild/win32-x64": "npm:0.25.5" + dependenciesMeta: + "@esbuild/aix-ppc64": + optional: true + "@esbuild/android-arm": + optional: true + "@esbuild/android-arm64": + optional: true + "@esbuild/android-x64": + optional: true + "@esbuild/darwin-arm64": + optional: true + "@esbuild/darwin-x64": + optional: true + "@esbuild/freebsd-arm64": + optional: true + "@esbuild/freebsd-x64": + optional: true + "@esbuild/linux-arm": + optional: true + "@esbuild/linux-arm64": + optional: true + "@esbuild/linux-ia32": + optional: true + "@esbuild/linux-loong64": + optional: true + "@esbuild/linux-mips64el": + optional: true + "@esbuild/linux-ppc64": + optional: true + "@esbuild/linux-riscv64": + optional: true + "@esbuild/linux-s390x": + optional: true + "@esbuild/linux-x64": + optional: true + "@esbuild/netbsd-arm64": + optional: true + "@esbuild/netbsd-x64": + optional: true + "@esbuild/openbsd-arm64": + optional: true + "@esbuild/openbsd-x64": + optional: true + "@esbuild/sunos-x64": + optional: true + "@esbuild/win32-arm64": + optional: true + "@esbuild/win32-ia32": + optional: true + "@esbuild/win32-x64": + optional: true + bin: + esbuild: bin/esbuild + checksum: 10c0/aba8cbc11927fa77562722ed5e95541ce2853f67ad7bdc40382b558abc2e0ec57d92ffb820f082ba2047b4ef9f3bc3da068cdebe30dfd3850cfa3827a78d604e + languageName: node + linkType: hard + "escalade@npm:^3.1.1": version: 3.1.1 resolution: "escalade@npm:3.1.1" @@ -6666,7 +6926,7 @@ __metadata: languageName: node linkType: hard -"fsevents@npm:^2.3.2": +"fsevents@npm:^2.3.2, fsevents@npm:~2.3.3": version: 2.3.3 resolution: "fsevents@npm:2.3.3" dependencies: @@ -6676,7 +6936,7 @@ __metadata: languageName: node linkType: hard -"fsevents@patch:fsevents@npm%3A^2.3.2#optional!builtin": +"fsevents@patch:fsevents@npm%3A^2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin": version: 2.3.3 resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1" dependencies: @@ -6738,6 +6998,15 @@ __metadata: languageName: node linkType: hard +"get-tsconfig@npm:^4.7.5": + version: 4.10.1 + resolution: "get-tsconfig@npm:4.10.1" + dependencies: + resolve-pkg-maps: "npm:^1.0.0" + checksum: 10c0/7f8e3dabc6a49b747920a800fb88e1952fef871cdf51b79e98db48275a5de6cdaf499c55ee67df5fa6fe7ce65f0063e26de0f2e53049b408c585aa74d39ffa21 + languageName: node + linkType: hard + "git-raw-commits@npm:^2.0.0": version: 2.0.11 resolution: "git-raw-commits@npm:2.0.11" @@ -10054,6 +10323,13 @@ __metadata: languageName: node linkType: hard +"resolve-pkg-maps@npm:^1.0.0": + version: 1.0.0 + resolution: "resolve-pkg-maps@npm:1.0.0" + checksum: 10c0/fb8f7bbe2ca281a73b7ef423a1cbc786fb244bd7a95cbe5c3fba25b27d327150beca8ba02f622baea65919a57e061eb5005204daa5f93ed590d9b77463a567ab + languageName: node + linkType: hard + "resolve.exports@npm:^2.0.0": version: 2.0.2 resolution: "resolve.exports@npm:2.0.2" @@ -10971,7 +11247,7 @@ __metadata: languageName: node linkType: hard -"ts-node@npm:^10.8.1, ts-node@npm:^10.9.2": +"ts-node@npm:^10.8.1": version: 10.9.2 resolution: "ts-node@npm:10.9.2" dependencies: @@ -11041,6 +11317,22 @@ __metadata: languageName: node linkType: hard +"tsx@npm:^4.19.2": + version: 4.20.3 + resolution: "tsx@npm:4.20.3" + dependencies: + esbuild: "npm:~0.25.0" + fsevents: "npm:~2.3.3" + get-tsconfig: "npm:^4.7.5" + dependenciesMeta: + fsevents: + optional: true + bin: + tsx: dist/cli.mjs + checksum: 10c0/6ff0d91ed046ec743fac7ed60a07f3c025e5b71a5aaf58f3d2a6b45e4db114c83e59ebbb078c8e079e48d3730b944a02bc0de87695088aef4ec8bbc705dc791b + languageName: node + linkType: hard + "type-check@npm:^0.4.0, type-check@npm:~0.4.0": version: 0.4.0 resolution: "type-check@npm:0.4.0" From 3f8c38814f5c7b0f1e9184cd3b255af186c2c815 Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Tue, 1 Jul 2025 14:28:50 +0200 Subject: [PATCH 29/62] test: Add integration tests with vitest --- demo/seed.json | 18 + package.json | 19 +- packages/css/package.json | 8 +- packages/ucp/jest.config.js | 20 - packages/ucp/jest.coverage.config.js | 13 - packages/ucp/package.json | 11 +- packages/uma/package.json | 8 +- .../src/policies/contracts/ContractManager.ts | 2 - test/integration/Base.test.ts | 175 ++ test/integration/Demo.test.ts | 334 +++ test/integration/Odrl.test.ts | 196 ++ test/util/ServerUtil.ts | 46 + vitest.config.mts | 12 + yarn.lock | 2588 +++++------------ 14 files changed, 1554 insertions(+), 1896 deletions(-) create mode 100644 demo/seed.json delete mode 100644 packages/ucp/jest.config.js delete mode 100644 packages/ucp/jest.coverage.config.js create mode 100644 test/integration/Base.test.ts create mode 100644 test/integration/Demo.test.ts create mode 100644 test/integration/Odrl.test.ts create mode 100644 test/util/ServerUtil.ts create mode 100644 vitest.config.mts diff --git a/demo/seed.json b/demo/seed.json new file mode 100644 index 00000000..37c598c6 --- /dev/null +++ b/demo/seed.json @@ -0,0 +1,18 @@ +[ + { + "email": "ruben@example.org", + "password": "abc123", + "pods": [{ + "name": "ruben" + }] + }, + { + "email": "demo@example.org", + "password": "abc123", + "pods": [ + { + "name": "demo" + } + ] + } +] diff --git a/package.json b/package.json index f52926eb..c3dc9136 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ } ], "private": true, - "packageManager": "yarn@4.1.0", + "packageManager": "yarn@4.9.2", "engines": { "node": ">=20.0", "yarn": ">=4.0" @@ -54,7 +54,7 @@ "postinstall": "yarn run sync:list && yarn build", "clean": "shx rm -rf ./**/node_modules", "build": "yarn workspaces foreach --include 'packages/*' -A -pi -j unlimited -t run build", - "test": "yarn workspaces foreach --include 'packages/*' -A -pi -j unlimited run test", + "test": "vitest run", "start": "yarn workspaces foreach --include 'packages/*' -A -pi -j unlimited run start", "start:odrl": "yarn workspace @solidlab/uma run start:odrl & yarn workspace @solidlab/uma-css run start", "start:demo": "yarn workspaces foreach --include 'packages/*' -A -pi -j unlimited run demo", @@ -73,31 +73,26 @@ "@commitlint/cli": "^16.1.0", "@commitlint/config-conventional": "^16.0.0", "@types/jest": "^29.5.12", - "@types/node": "^20.11.25", + "@types/node": "^20.19.1", "@typescript-eslint/eslint-plugin": "^5.12.1", "@typescript-eslint/parser": "^5.12.1", "chalk": "^5.4.1", "componentsjs-generator": "^3.1.2", "eslint": "^8.10.0", - "jest": "^29.7.0", "jest-rdf": "^1.8.1", "shx": "^0.3.4", "syncpack": "^13.0.2", - "ts-jest": "^29.1.2", "tsx": "^4.19.2", - "typescript": "^5.3.3" + "typescript": "^5.8.3", + "vite": "^6.3.5", + "vitest": "^3.2.3" }, "resolutions": { - "@types/node": "^20.11.25" + "@types/node": "^20.19.1" }, "workspaces": [ "packages/*" ], - "jest": { - "projects": [ - "./packages/*/package.json" - ] - }, "eslintConfig": { "env": { "browser": true, diff --git a/packages/css/package.json b/packages/css/package.json index a66a06c1..b6213d3f 100644 --- a/packages/css/package.json +++ b/packages/css/package.json @@ -36,7 +36,7 @@ } ], "private": true, - "packageManager": "yarn@4.0.2", + "packageManager": "yarn@4.9.2", "engines": { "node": ">=20.0", "yarn": ">=4.0" @@ -59,7 +59,6 @@ "build": "yarn build:ts && yarn build:components", "build:ts": "yarn run -T tsc", "build:components": "yarn run -T componentsjs-generator -r uma-css -s src/ -c dist/components -i .componentsignore --lenient", - "test": "yarn run -T jest --coverage", "start:unseeded": "yarn run community-solid-server -m . -c ./config/default.json -a http://localhost:4000/", "start": "yarn run community-solid-server -m . -c ./config/default.json --seedConfig ./config/seed.json -a http://localhost:4000/", "demo": "yarn run demo:setup && yarn run demo:start", @@ -76,11 +75,6 @@ "jose": "^5.2.2", "n3": "^1.17.2" }, - "jest": { - "preset": "ts-jest", - "testEnvironment": "node", - "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts)$" - }, "lsd:module": "https://linkedsoftwaredependencies.org/bundles/npm/@solidlab/uma-css", "lsd:components": "dist/components/components.jsonld", "lsd:contexts": { diff --git a/packages/ucp/jest.config.js b/packages/ucp/jest.config.js deleted file mode 100644 index 2561ff18..00000000 --- a/packages/ucp/jest.config.js +++ /dev/null @@ -1,20 +0,0 @@ -module.exports = { - clearMocks: true, - moduleFileExtensions: ['ts', 'js'], - roots: ['/test/'], - testEnvironment: 'node', - transform: { - '^.+\\.ts?$': 'ts-jest', - }, - collectCoverage: true, - coverageReporters: ['text', 'lcov'], - coveragePathIgnorePatterns: [ - '/dist/', - '/node_modules/', - ], - moduleNameMapper: { - '^jose/(.*)$': '/node_modules/jose/dist/node/cjs/$1', - }, - testTimeout: 60000, - setupFilesAfterEnv: ["jest-rdf"] -} \ No newline at end of file diff --git a/packages/ucp/jest.coverage.config.js b/packages/ucp/jest.coverage.config.js deleted file mode 100644 index 4e0062d7..00000000 --- a/packages/ucp/jest.coverage.config.js +++ /dev/null @@ -1,13 +0,0 @@ -const jestConfig = require('./jest.config'); - -module.exports = { - ...jestConfig, - coverageThreshold: { - './src': { - branches: 100, - functions: 100, - lines: 100, - statements: 100, - }, - }, -}; \ No newline at end of file diff --git a/packages/ucp/package.json b/packages/ucp/package.json index c2726fe4..18995280 100644 --- a/packages/ucp/package.json +++ b/packages/ucp/package.json @@ -31,7 +31,7 @@ } ], "private": true, - "packageManager": "yarn@4.0.2", + "packageManager": "yarn@4.9.2", "engines": { "node": ">=20.0", "yarn": ">=4.0" @@ -53,14 +53,7 @@ "scripts": { "build": "yarn build:ts && yarn build:components", "build:ts": "yarn run -T tsc", - "build:components": "yarn run -T componentsjs-generator -r ucp -s src -c dist/components -i .componentsignore --lenient", - "test": "yarn run test:unit && yarn run test:integration", - "test:unit": "jest test/unit", - "test:integration": "yarn run test:engines && yarn exec ts-node test/integration/ContainerRulesStorage.ts", - "test:engines": "yarn run test:log-engine && yarn run test:crud-engine && yarn run test:crud-temporal-engine", - "test:log-engine": "yarn exec ts-node test/integration/LogEngine.ts", - "test:crud-engine": "yarn exec ts-node test/integration/CrudEngine.ts", - "test:crud-temporal-engine": "yarn exec ts-node test/integration/CrudEngineTemporal.ts" + "build:components": "yarn run -T componentsjs-generator -r ucp -s src -c dist/components -i .componentsignore --lenient" }, "dependencies": { "@smessie/readable-web-to-node-stream": "^3.0.3", diff --git a/packages/uma/package.json b/packages/uma/package.json index 1a0b64ee..c9577482 100644 --- a/packages/uma/package.json +++ b/packages/uma/package.json @@ -32,7 +32,7 @@ } ], "private": true, - "packageManager": "yarn@4.0.2", + "packageManager": "yarn@4.9.2", "engines": { "node": ">=20.0", "yarn": ">=4.0" @@ -55,7 +55,6 @@ "build": "yarn build:ts && yarn build:components", "build:ts": "yarn run -T tsc", "build:components": "yarn run -T componentsjs-generator -r sai-uma -s src -c dist/components -i .componentsignore --lenient", - "test": "yarn run -T jest --coverage", "start": "yarn run -T tsx bin/main.ts", "start:odrl": "yarn run -T tsx bin/odrl.ts", "demo": "yarn run -T tsx bin/demo.ts" @@ -76,11 +75,6 @@ "uri-template-lite": "^23.4.0", "winston": "^3.11.0" }, - "jest": { - "preset": "ts-jest", - "testEnvironment": "node", - "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts)$" - }, "lsd:module": "https://linkedsoftwaredependencies.org/bundles/npm/@solidlab/uma", "lsd:components": "dist/components/components.jsonld", "lsd:contexts": { diff --git a/packages/uma/src/policies/contracts/ContractManager.ts b/packages/uma/src/policies/contracts/ContractManager.ts index e9f733ac..f05e3d78 100644 --- a/packages/uma/src/policies/contracts/ContractManager.ts +++ b/packages/uma/src/policies/contracts/ContractManager.ts @@ -25,8 +25,6 @@ export class ContractManager { } createContract(perms: Permission[]): ODRLContract { - console.log('Creating Contract', JSON.stringify(perms, null, 2)) - // todo: un-mock this!!! type Pair = { action: string, target: string }; const permissionPairs: Pair[] = perms.flatMap( diff --git a/test/integration/Base.test.ts b/test/integration/Base.test.ts new file mode 100644 index 00000000..2a5248a8 --- /dev/null +++ b/test/integration/Base.test.ts @@ -0,0 +1,175 @@ +import { App, setGlobalLoggerFactory, WinstonLoggerFactory } from '@solid/community-server'; +import * as path from 'node:path'; +import { getDefaultCssVariables, instantiateFromConfig } from '../util/ServerUtil'; + +const cssPort = 3001; +const umaPort = 4001; + +describe('A server setup', (): void => { + let umaApp: App; + let cssApp: App; + + beforeAll(async(): Promise => { + setGlobalLoggerFactory(new WinstonLoggerFactory('off')); + + umaApp = await instantiateFromConfig( + 'urn:uma:default:App', + path.join(__dirname, '../../packages/uma/config/default.json'), + { + 'urn:uma:variables:port': umaPort, + 'urn:uma:variables:baseUrl': `http://localhost:${umaPort}/uma`, + 'urn:uma:variables:policyBaseIRI': `http://localhost:${cssPort}/`, + 'urn:uma:variables:policyDir': path.join(__dirname, '../../packages/uma/config/rules/policy'), + 'urn:uma:variables:eyePath': 'eye', + } + ) as App; + + cssApp = await instantiateFromConfig( + 'urn:solid-server:default:App', + path.join(__dirname, '../../packages/css/config/default.json'), + { + ...getDefaultCssVariables(cssPort), + 'urn:solid-server:uma:variable:AuthorizationServer': `http://localhost:${umaPort}/`, + 'urn:solid-server:default:variable:seedConfig': path.join(__dirname, '../../packages/css/config/seed.json'), + }, + ) as App; + + await Promise.all([ umaApp.start(), cssApp.start() ]); + }); + + afterAll(async(): Promise => { + await Promise.all([ umaApp.stop(), cssApp.stop() ]); + }); + + describe('using public namespace authorization', (): void => { + const container = `http://localhost:${cssPort}/alice/public/`; + const slug = 'resource.txt'; + const body = 'This is a resource.'; + + it('RS: provides immediate read access.', async(): Promise => { + const publicResource = `http://localhost:${cssPort}/alice/profile/card`; + + const publicResponse = await fetch(publicResource); + + expect(publicResponse.status).toBe(200); + expect(publicResponse.headers.get('content-type')).toBe('text/turtle'); + }); + + it('RS: provides immediate create access to the container', async(): Promise => { + const containerResponse = await fetch(container, { + method: 'PUT', + }); + expect(containerResponse.status).toBe(201); + expect(containerResponse.headers.get('location')).toBe(container); + }); + + it('RS: provides immediate create access to the contents', async(): Promise => { + const createResponse = await fetch(container, { + method: 'POST', + headers: { slug }, + body + }); + expect(createResponse.status).toBe(201); + expect(createResponse.headers.get('location')).toBe(`${container}${slug}`); + }); + + it('RS: provides immediate read access to the contents', async(): Promise => { + const readResponse = await fetch(`${container}${slug}`); + expect(readResponse.status).toBe(200); + await expect(readResponse.text()).resolves.toBe(body); + }); + + it('RS: provides immediate delete access to the contents', async(): Promise => { + const deleteResponse = await fetch(`${container}${slug}`, { + method: 'DELETE', + }) + expect(deleteResponse.status).toBe(205); + + const readResponse = await fetch(`${container}${slug}`); + expect(readResponse.status).toBe(404); + }); + }); + + describe('using ODRL authorization', (): void => { + const privateResource = `http://localhost:${cssPort}/alice/private/resource.txt`; + let wwwAuthenticateHeader: string; + let ticket: string; + let tokenEndpoint: string; + let jsonResponse: { access_token: string, token_type: string }; + + it('RS: sends a WWW-Authenticate response when access is private.', async(): Promise => { + const noTokenResponse = await fetch(privateResource, { + method: 'PUT', + body: 'Some text ...' , + }); + + expect(noTokenResponse.status).toBe(401); + wwwAuthenticateHeader = noTokenResponse.headers.get('WWW-Authenticate'); + expect(typeof wwwAuthenticateHeader).toBe('string'); + }); + + it('AS: returns the token endpoint from the configuration.', async(): Promise => { + const parsedHeader = Object.fromEntries( + wwwAuthenticateHeader + .replace(/^UMA /,'') + .split(', ') + .map(param => param.split('=').map(s => s.replace(/"/g,''))) + ); + expect(typeof parsedHeader.as_uri).toBe('string'); + expect(typeof parsedHeader.ticket).toBe('string'); + ticket = parsedHeader.ticket; + + const configurationUrl = parsedHeader.as_uri + '/.well-known/uma2-configuration'; + const response = await fetch(configurationUrl); + expect(response.status).toBe(200); + const configuration = await response.json(); + expect(typeof configuration.token_endpoint).toBe('string'); + tokenEndpoint = configuration.token_endpoint; + }); + + it('AS: responds with a token when receiving the ticket.', async(): Promise => { + const claim_token = 'https://woslabbi.pod.knows.idlab.ugent.be/profile/card#me'; + + const content = { + grant_type: 'urn:ietf:params:oauth:grant-type:uma-ticket', + ticket, + claim_token: encodeURIComponent(claim_token), + claim_token_format: 'urn:solidlab:uma:claims:formats:webid', + }; + + const asRequestResponse = await fetch(tokenEndpoint, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(content), + }); + + expect(asRequestResponse.status).toBe(200); + expect(asRequestResponse.headers.get('content-type')).toBe('application/json'); + jsonResponse = await asRequestResponse.json(); + expect(typeof jsonResponse.access_token).toBe('string'); + expect(jsonResponse.token_type).toBe('Bearer'); + const token = JSON.parse(Buffer.from(jsonResponse.access_token.split('.')[1], 'base64').toString()); + expect(Array.isArray(token.permissions)).toBe(true); + expect(token.permissions).toHaveLength(2); + expect(token.permissions).toContainEqual({ + resource_id: `http://localhost:${cssPort}/alice/private/resource.txt`, + resource_scopes: [ 'urn:example:css:modes:append', 'urn:example:css:modes:create' ] + }); + expect(token.permissions).toContainEqual({ + resource_id: `http://localhost:${cssPort}/alice/private/`, + resource_scopes: [ 'urn:example:css:modes:create' ] + } + ); + }); + + it('RS: provides access when receiving a valid token.', async(): Promise => { + const response = await fetch(privateResource, { + method: 'PUT', + headers: { 'Authorization': `${jsonResponse.token_type} ${jsonResponse.access_token}` }, + body: 'Some text ...' , + }); + + expect(response.status).toBe(201); + }); + }); +}); diff --git a/test/integration/Demo.test.ts b/test/integration/Demo.test.ts new file mode 100644 index 00000000..15ca1f48 --- /dev/null +++ b/test/integration/Demo.test.ts @@ -0,0 +1,334 @@ +import { App, setGlobalLoggerFactory, WinstonLoggerFactory } from '@solid/community-server'; +import { DialogOutput } from '@solidlab/uma'; +import { Parser, Store } from 'n3'; +import * as path from 'node:path'; +import { getDefaultCssVariables, instantiateFromConfig } from '../util/ServerUtil'; + +const cssPort = 3003; +const umaPort = 4003; + +const terms = { + solid: { + umaServer: 'http://www.w3.org/ns/solid/terms#umaServer', + }, + resources: { + smartwatch: `http://localhost:${cssPort}/ruben/medical/smartwatch.ttl` + }, + agents: { + ruben: `http://localhost:${cssPort}/ruben/profile/card#me`, + alice: `http://localhost:${cssPort}/alice/profile/card#me`, + } +} + +async function noTokenFetch(input: string | URL | globalThis.Request, init?: RequestInit): Promise<{ as_uri: string, ticket: string }> { + const noTokenResponse = await fetch(input, init); + + expect(noTokenResponse.status).toBe(401); + + const wwwAuthenticateHeader = noTokenResponse.headers.get('WWW-Authenticate'); + expect(typeof wwwAuthenticateHeader).toBe('string'); + + const parsedHeader = Object.fromEntries( + wwwAuthenticateHeader + .replace(/^UMA /,'') + .split(', ') + .map(param => param.split('=').map(s => s.replace(/"/g,''))) + ); + expect(typeof parsedHeader.as_uri).toBe('string'); + expect(typeof parsedHeader.ticket).toBe('string'); + return parsedHeader; +} + +async function findTokenEndpoint(uri: string): Promise { + // TODO: cache this + const configurationUrl = uri + '/.well-known/uma2-configuration'; + const configResponse = await fetch(configurationUrl); + expect(configResponse.status).toBe(200); + const configuration = await configResponse.json(); + expect(typeof configuration.token_endpoint).toBe('string'); + return configuration.token_endpoint; +} + +async function getToken(ticket: string, endpoint: string, webId?: string): Promise { + const content: Record = { + grant_type: 'urn:ietf:params:oauth:grant-type:uma-ticket', + ticket: ticket, + }; + if (webId) { + content.claim_token = encodeURIComponent(webId); + content.claim_token_format = 'urn:solidlab:uma:claims:formats:webid'; + } + + const response = await fetch(endpoint, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(content), + }); + + expect(response.status).toBe(200); + expect(response.headers.get('content-type')).toBe('application/json'); + const jsonResponse: DialogOutput = await response.json(); + + expect(typeof jsonResponse.access_token).toBe('string'); + expect(jsonResponse.token_type).toBe('Bearer'); + const token = JSON.parse(Buffer.from(jsonResponse.access_token.split('.')[1], 'base64').toString()); + expect(Array.isArray(token.permissions)).toBe(true); + + return jsonResponse; +} + +async function tokenFetch(token: any, input: string | URL | globalThis.Request, init?: RequestInit): Promise { + return fetch(input, { + ...init, + headers: { + ...init?.headers, + 'Authorization': `${token.token_type} ${token.access_token}` + }, + }); +} + +// TODO: only call this function if you know there will be a 401 +async function umaFetch(input: string | URL | globalThis.Request, init?: RequestInit, webId?: string): Promise { + // Parse ticket and UMA server URL from header + const parsedHeader = await noTokenFetch(input, init); + + // Find UMA server token endpoint + const tokenEndpoint = await findTokenEndpoint(parsedHeader.as_uri); + + // Send ticket request to UMA server and extract token from response + const token = await getToken(parsedHeader.ticket, tokenEndpoint, webId); + + // Perform new call with token + return tokenFetch(token, input, init); +} + +describe('A demo server setup', (): void => { + let umaApp: App; + let cssApp: App; + const policyContainer = `http://localhost:${cssPort}/ruben/settings/policies/`; + + beforeAll(async(): Promise => { + setGlobalLoggerFactory(new WinstonLoggerFactory('off')); + + umaApp = await instantiateFromConfig( + 'urn:uma:default:App', + path.join(__dirname, '../../packages/uma/config/demo.json'), + { + 'urn:uma:variables:port': umaPort, + 'urn:uma:variables:baseUrl': `http://localhost:${umaPort}/uma`, + 'urn:uma:variables:eyePath': 'eye', + 'urn:uma:variables:policyContainer': policyContainer, + } + ) as App; + + cssApp = await instantiateFromConfig( + 'urn:solid-server:default:App', + // Not using the demo config as that one writes to disk, this is the same but in memory + path.join(__dirname, '../../packages/css/config/default.json'), + { + ...getDefaultCssVariables(cssPort), + 'urn:solid-server:uma:variable:AuthorizationServer': `http://localhost:${umaPort}/`, + 'urn:solid-server:default:variable:seedConfig': path.join(__dirname, '../../demo/seed.json'), + }, + ) as App; + + await Promise.all([umaApp.start(), cssApp.start()]); + }); + + afterAll(async(): Promise => { + await Promise.all([ umaApp.stop(), cssApp.stop() ]); + }); + + it('sets up the initial data.', async(): Promise => { + // Policy that allows the creation of all the initial resources + const policy = ` + @prefix ex: . + @prefix odrl: . + + ex:usagePolicy a odrl:Agreement ; + odrl:permission ex:permission . + ex:permission a odrl:Permission ; + odrl:action odrl:create, odrl:append ; + odrl:target , + , + , + ; + odrl:assignee <${terms.agents.ruben}> ; + odrl:assigner <${terms.agents.ruben}> . + `; + + // Create policies container + let response = await fetch(`http://localhost:${cssPort}/ruben/settings/policies/policy`, { + method: 'PUT', + headers: { 'content-type': 'text/turtle' }, + body: policy, + }); + expect(response.status).toBe(201); + + // Create smartwatch data + response = await umaFetch(`http://localhost:${cssPort}/ruben/medical/smartwatch.ttl`, { + method: 'PUT', + headers: { 'content-type': 'application/trig' }, + body: ' .', + }, terms.agents.ruben); + expect(response.status).toBe(201); + + // Create private data + response = await umaFetch(`http://localhost:${cssPort}/ruben/private/data`, { + method: 'PUT', + headers: { 'content-type': 'text/turtle' }, + body: ` +@prefix dbo: . +@prefix xsd: . + + dbo:birthDate "1987-02-28"^^xsd:date .`, + }, terms.agents.ruben); + expect(response.status).toBe(201); + + // Create derived resources. + // This is outdated and not actually needed for the test, + // but this did cause a bug about auxiliary resources to be discovered, + // so it should stay until there is a specific test for those. + response = await umaFetch(`http://localhost:${cssPort}/ruben/private/.meta`, { + method: 'PATCH', + headers: { 'content-type': 'text/n3' }, + body: ` +@prefix solid: . +@prefix ex: . +@prefix derived: . + +_:rename a solid:InsertDeletePatch; + solid:inserts { + derived:derivedResource ex:bday. + ex:bday derived:template "derived/bday"; + derived:selector ; + derived:filter . + + derived:derivedResource ex:age. + ex:age derived:template "derived/age"; + derived:selector ; + derived:filter . + }.`, + }, terms.agents.ruben); + expect(response.status).toBe(205); + + // TODO: Do I need this though + // Add necessary triples to WebID + response = await fetch(terms.agents.ruben, { + method: 'PATCH', + headers: { 'content-type': 'text/n3' }, + body: ` +@prefix solid: . + +_:rename a solid:InsertDeletePatch; + solid:inserts { + <${terms.agents.ruben}> solid:umaServer + }.`, + }); + expect(response.status).toBe(205); + }); + + it('finds the UMA server of the user in their WebID.', async(): Promise => { + // TODO: what is the point of any of this? the as_uri response should have this data? + // TODO: find out why it doesn't work though as the term does get added at the end of the previous test + const response = await fetch(terms.agents.ruben, { + headers: { 'accept': 'text/turtle' }, + }); + expect(response.status).toBe(200); + const parser = new Parser({ baseIRI: terms.agents.ruben }); + const store = new Store(parser.parse(await response.text())); + expect(store.countQuads(terms.agents.ruben, terms.solid.umaServer, null, null)).toBe(1); + const umaServer = store.getObjects(terms.agents.ruben, terms.solid.umaServer, null)[0].value; + }); + + it('can add a healthcare policy to the server.', async(): Promise => { + const healthcare_patient_policy = + `PREFIX dcterms: +PREFIX eu-gdpr: +PREFIX oac: +PREFIX odrl: +PREFIX xsd: + +PREFIX ex: + + a odrl:Agreement ; + odrl:uid ex:HCPX-agreement ; + odrl:profile oac: ; + odrl:permission . + + a odrl:Permission ; + odrl:action odrl:read ; + odrl:target <${terms.resources.smartwatch}> ; + odrl:assigner <${terms.agents.ruben}> ; + odrl:assignee <${terms.agents.alice}> ; + odrl:constraint , + . + + a odrl:Constraint ; + odrl:leftOperand odrl:purpose ; # can also be oac:Purpose, to conform with OAC profile + odrl:operator odrl:eq ; + odrl:rightOperand ex:bariatric-care . + + a odrl:Constraint ; + odrl:leftOperand oac:LegalBasis ; + odrl:operator odrl:eq ; + odrl:rightOperand eu-gdpr:A9-2-a .` + + const medicalPolicyCreationResponse = await fetch(policyContainer, { + method: 'POST', + headers: { 'content-type': 'text/turtle' }, + body: healthcare_patient_policy, + }); + expect(medicalPolicyCreationResponse.status).toBe(201); + }); + + it('requires authorized access for patient data.', async(): Promise => { + // TODO: should do the steps individually here so we can check the contents of the tokens/tickets + // Parse ticket and UMA server URL from header + const parsedHeader = await noTokenFetch(terms.resources.smartwatch); + + // Find UMA server token endpoint + const tokenEndpoint = await findTokenEndpoint(parsedHeader.as_uri); + + // Send ticket request to UMA server and extract token from response + const token = await getToken(parsedHeader.ticket, tokenEndpoint, terms.agents.alice); + const accessToken = JSON.parse(Buffer.from(token.access_token.split('.')[1], 'base64').toString()); + expect(accessToken).toMatchObject({ + permissions:[{ + resource_id: terms.resources.smartwatch, + resource_scopes: [ 'urn:example:css:modes:read' ] + }], + contract:{ + '@context': 'http://www.w3.org/ns/odrl.jsonld', + '@type': 'Agreement', + uid: expect.any(String), + 'http://purl.org/dc/terms/description': 'Agreement for HCP X to read Alice\'s health data for bariatric care.', + 'https://w3id.org/dpv#hasLegalBasis': { + '@id': 'https://w3id.org/dpv/legal/eu/gdpr#eu-gdpr:A9-2-a' + }, + permission:[{ + '@type': 'Permission', + action: 'https://w3id.org/oac#read', + target: terms.resources.smartwatch, + assigner: 'http://localhost:3000/ruben/profile/card#me', + assignee: 'http://localhost:3000/alice/profile/card#me', + constraint: [{ + '@type': 'Constraint', + leftOperand: 'purpose', + operator: 'eq', + rightOperand: { '@id':'http://example.org/bariatric-care' } + }] + }] + }, + iat: expect.any(Number), + iss: `http://localhost:${umaPort}/uma`, + aud: 'solid', + exp: expect.any(Number), + jti: expect.any(String), + }) + + // Perform new call with token + const response = await tokenFetch(token, terms.resources.smartwatch); + expect(response.status).toBe(200); + }); +}); diff --git a/test/integration/Odrl.test.ts b/test/integration/Odrl.test.ts new file mode 100644 index 00000000..6c47280b --- /dev/null +++ b/test/integration/Odrl.test.ts @@ -0,0 +1,196 @@ +import { App, setGlobalLoggerFactory, WinstonLoggerFactory } from '@solid/community-server'; +import * as path from 'node:path'; +import { getDefaultCssVariables, instantiateFromConfig } from '../util/ServerUtil'; + +const cssPort = 3002; +const umaPort = 4002; + +describe('An ODRL server setup', (): void => { + let umaApp: App; + let cssApp: App; + + const resource = `http://localhost:${cssPort}/alice/other/resource.txt`; + + beforeAll(async(): Promise => { + setGlobalLoggerFactory(new WinstonLoggerFactory('off')); + + umaApp = await instantiateFromConfig( + 'urn:uma:default:App', + path.join(__dirname, '../../packages/uma/config/odrl.json'), + { + 'urn:uma:variables:port': umaPort, + 'urn:uma:variables:baseUrl': `http://localhost:${umaPort}/uma`, + 'urn:uma:variables:policyBaseIRI': `http://localhost:${cssPort}/`, + 'urn:uma:variables:policyDir': path.join(__dirname, '../../packages/uma/config/rules/odrl'), + 'urn:uma:variables:eyePath': 'eye', + } + ) as App; + + cssApp = await instantiateFromConfig( + 'urn:solid-server:default:App', + path.join(__dirname, '../../packages/css/config/default.json'), + { + ...getDefaultCssVariables(cssPort), + 'urn:solid-server:uma:variable:AuthorizationServer': `http://localhost:${umaPort}/`, + 'urn:solid-server:default:variable:seedConfig': path.join(__dirname, '../../packages/css/config/seed.json'), + }, + ) as App; + + await Promise.all([umaApp.start(), cssApp.start()]); + }); + + describe('creating a resource', (): void => { + let wwwAuthenticateHeader: string; + let ticket: string; + let tokenEndpoint: string; + let jsonResponse: { access_token: string, token_type: string }; + + it('RS: sends a WWW-Authenticate response when access is private.', async(): Promise => { + const noTokenResponse = await fetch(resource, { + method: "PUT", + body: 'some text' , + }); + + expect(noTokenResponse.status).toBe(401); + wwwAuthenticateHeader = noTokenResponse.headers.get("WWW-Authenticate"); + expect(typeof wwwAuthenticateHeader).toBe('string'); + }); + + it('AS: returns the token endpoint from the configuration.', async(): Promise => { + const parsedHeader = Object.fromEntries( + wwwAuthenticateHeader + .replace(/^UMA /,'') + .split(', ') + .map(param => param.split('=').map(s => s.replace(/"/g,''))) + ); + expect(typeof parsedHeader.as_uri).toBe('string'); + expect(typeof parsedHeader.ticket).toBe('string'); + ticket = parsedHeader.ticket; + + const configurationUrl = parsedHeader.as_uri + '/.well-known/uma2-configuration'; + const response = await fetch(configurationUrl); + expect(response.status).toBe(200); + const configuration = await response.json(); + expect(typeof configuration.token_endpoint).toBe('string'); + tokenEndpoint = configuration.token_endpoint; + }); + + it('AS: responds with a token when receiving the ticket.', async(): Promise => { + const claim_token = 'https://woslabbi.pod.knows.idlab.ugent.be/profile/card#me'; + + const content = { + grant_type: 'urn:ietf:params:oauth:grant-type:uma-ticket', + ticket, + claim_token: encodeURIComponent(claim_token), + claim_token_format: 'urn:solidlab:uma:claims:formats:webid', + }; + + const asRequestResponse = await fetch(tokenEndpoint, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(content), + }); + + expect(asRequestResponse.status).toBe(200); + expect(asRequestResponse.headers.get('content-type')).toBe('application/json'); + jsonResponse = await asRequestResponse.json(); + expect(typeof jsonResponse.access_token).toBe('string'); + expect(jsonResponse.token_type).toBe('Bearer'); + const token = JSON.parse(Buffer.from(jsonResponse.access_token.split('.')[1], 'base64').toString()); + expect(Array.isArray(token.permissions)).toBe(true); + expect(token.permissions).toHaveLength(2); + expect(token.permissions).toContainEqual({ + resource_id: resource, + resource_scopes: [ 'urn:example:css:modes:append', 'urn:example:css:modes:create' ] + }); + expect(token.permissions).toContainEqual({ + resource_id: `http://localhost:${cssPort}/alice/other/`, + resource_scopes: [ 'urn:example:css:modes:create' ] + } + ); + }); + + it('RS: provides access when receiving a valid token.', async(): Promise => { + const response = await fetch(resource, { + method: "PUT", + headers: { 'Authorization': `${jsonResponse.token_type} ${jsonResponse.access_token}` }, + body: 'Some text ...' , + }); + + expect(response.status).toBe(201); + }); + }); + + describe('reading a resource', (): void => { + let wwwAuthenticateHeader: string; + let ticket: string; + let tokenEndpoint: string; + let jsonResponse: { access_token: string, token_type: string }; + + it('RS: sends a WWW-Authenticate response when access is private.', async(): Promise => { + const noTokenResponse = await fetch(resource); + + expect(noTokenResponse.status).toBe(401); + wwwAuthenticateHeader = noTokenResponse.headers.get("WWW-Authenticate"); + expect(typeof wwwAuthenticateHeader).toBe('string'); + }); + + it('AS: returns the token endpoint from the configuration.', async(): Promise => { + const parsedHeader = Object.fromEntries( + wwwAuthenticateHeader + .replace(/^UMA /,'') + .split(', ') + .map(param => param.split('=').map(s => s.replace(/"/g,''))) + ); + expect(typeof parsedHeader.as_uri).toBe('string'); + expect(typeof parsedHeader.ticket).toBe('string'); + ticket = parsedHeader.ticket; + + const configurationUrl = parsedHeader.as_uri + '/.well-known/uma2-configuration'; + const response = await fetch(configurationUrl); + expect(response.status).toBe(200); + const configuration = await response.json(); + expect(typeof configuration.token_endpoint).toBe('string'); + tokenEndpoint = configuration.token_endpoint; + }); + + it('AS: responds with a token when receiving the ticket.', async(): Promise => { + const claim_token = 'https://woslabbi.pod.knows.idlab.ugent.be/profile/card#me'; + + const content = { + grant_type: 'urn:ietf:params:oauth:grant-type:uma-ticket', + ticket, + claim_token: encodeURIComponent(claim_token), + claim_token_format: 'urn:solidlab:uma:claims:formats:webid', + }; + + const asRequestResponse = await fetch(tokenEndpoint, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(content), + }); + + expect(asRequestResponse.status).toBe(200); + expect(asRequestResponse.headers.get('content-type')).toBe('application/json'); + jsonResponse = await asRequestResponse.json(); + expect(typeof jsonResponse.access_token).toBe('string'); + expect(jsonResponse.token_type).toBe('Bearer'); + const token = JSON.parse(Buffer.from(jsonResponse.access_token.split('.')[1], 'base64').toString()); + expect(Array.isArray(token.permissions)).toBe(true); + expect(token.permissions).toHaveLength(1); + expect(token.permissions).toContainEqual({ + resource_id: resource, + resource_scopes: [ 'urn:example:css:modes:read' ], + }); + }); + + it('RS: provides access when receiving a valid token.', async(): Promise => { + const response = await fetch(resource, { + headers: { 'Authorization': `${jsonResponse.token_type} ${jsonResponse.access_token}` }, + }); + + expect(response.status).toBe(200); + await expect(response.text()).resolves.toBe('Some text ...'); + }); + }); +}); diff --git a/test/util/ServerUtil.ts b/test/util/ServerUtil.ts new file mode 100644 index 00000000..f488d673 --- /dev/null +++ b/test/util/ServerUtil.ts @@ -0,0 +1,46 @@ +import { ComponentsManager, IModuleState } from 'componentsjs'; +import * as path from 'node:path'; + +let cachedModuleState: IModuleState; + +/** + * Returns a component instantiated from a Components.js configuration. + */ +export async function instantiateFromConfig( + componentUrl: string, + configPaths: string | string[], + variables?: Record, +): Promise { + // Initialize the Components.js loader + const mainModulePath = path.join(__dirname, '../../'); + const manager = await ComponentsManager.build({ + mainModulePath, + logLevel: 'error', + moduleState: cachedModuleState, + typeChecking: false, + }); + cachedModuleState = manager.moduleState; + + if (!Array.isArray(configPaths)) { + configPaths = [ configPaths ]; + } + + // Instantiate the component from the config(s) + for (const configPath of configPaths) { + await manager.configRegistry.register(configPath); + } + return manager.instantiate(componentUrl, { variables }); +} + +export function getDefaultCssVariables(port: number, baseUrl?: string): Record { + return { + 'urn:solid-server:default:variable:baseUrl': baseUrl ?? `http://localhost:${port}/`, + 'urn:solid-server:default:variable:port': port, + 'urn:solid-server:default:variable:socket': null, + 'urn:solid-server:default:variable:loggingLevel': 'off', + 'urn:solid-server:default:variable:showStackTrace': true, + 'urn:solid-server:default:variable:seedConfig': null, + 'urn:solid-server:default:variable:workers': 1, + 'urn:solid-server:default:variable:confirmMigration': false, + }; +} diff --git a/vitest.config.mts b/vitest.config.mts new file mode 100644 index 00000000..4a280afd --- /dev/null +++ b/vitest.config.mts @@ -0,0 +1,12 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + // TODO: prolly want to put integration tests in separate package/folder/something + // projects: [ 'packages/*' ], + include: [ '**/test/(unit|integration)/**/*.test.ts' ], + hookTimeout: 60000, + testTimeout: 60000, + }, +}); diff --git a/yarn.lock b/yarn.lock index 56f23312..395e78df 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,17 +12,7 @@ __metadata: languageName: node linkType: hard -"@ampproject/remapping@npm:^2.2.0": - version: 2.2.1 - resolution: "@ampproject/remapping@npm:2.2.1" - dependencies: - "@jridgewell/gen-mapping": "npm:^0.3.0" - "@jridgewell/trace-mapping": "npm:^0.3.9" - checksum: 10c0/92ce5915f8901d8c7cd4f4e6e2fe7b9fd335a29955b400caa52e0e5b12ca3796ada7c2f10e78c9c5b0f9c2539dff0ffea7b19850a56e1487aa083531e1e46d43 - languageName: node - linkType: hard - -"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.22.13, @babel/code-frame@npm:^7.23.4": +"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.12.13": version: 7.23.4 resolution: "@babel/code-frame@npm:7.23.4" dependencies: @@ -32,143 +22,6 @@ __metadata: languageName: node linkType: hard -"@babel/compat-data@npm:^7.22.9": - version: 7.23.3 - resolution: "@babel/compat-data@npm:7.23.3" - checksum: 10c0/c6af331753c34ee8a5678bc94404320826cb56b1dda3efc1311ec8fb0774e78225132f3c1acc988440ace667f14a838e297a822692b95758aa63da406e1f97a1 - languageName: node - linkType: hard - -"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3": - version: 7.23.3 - resolution: "@babel/core@npm:7.23.3" - dependencies: - "@ampproject/remapping": "npm:^2.2.0" - "@babel/code-frame": "npm:^7.22.13" - "@babel/generator": "npm:^7.23.3" - "@babel/helper-compilation-targets": "npm:^7.22.15" - "@babel/helper-module-transforms": "npm:^7.23.3" - "@babel/helpers": "npm:^7.23.2" - "@babel/parser": "npm:^7.23.3" - "@babel/template": "npm:^7.22.15" - "@babel/traverse": "npm:^7.23.3" - "@babel/types": "npm:^7.23.3" - convert-source-map: "npm:^2.0.0" - debug: "npm:^4.1.0" - gensync: "npm:^1.0.0-beta.2" - json5: "npm:^2.2.3" - semver: "npm:^6.3.1" - checksum: 10c0/08d43b749e24052d12713a7fb1f0c0d1275d4fb056d00846faeb8da79ecf6d0ba91a11b6afec407b8b0f9388d00e2c2f485f282bef0ade4d6d0a17de191a4287 - languageName: node - linkType: hard - -"@babel/generator@npm:^7.23.3, @babel/generator@npm:^7.23.4, @babel/generator@npm:^7.7.2": - version: 7.23.4 - resolution: "@babel/generator@npm:7.23.4" - dependencies: - "@babel/types": "npm:^7.23.4" - "@jridgewell/gen-mapping": "npm:^0.3.2" - "@jridgewell/trace-mapping": "npm:^0.3.17" - jsesc: "npm:^2.5.1" - checksum: 10c0/79b87ef49c4af1b4356b2fcab80ed92dfcad7927c3d6d89c4f749fd947768de3ec129467fb8eee0fe53cf8fc38b4d34d44487f714a9c23bee981c9cba3a670e4 - languageName: node - linkType: hard - -"@babel/helper-compilation-targets@npm:^7.22.15": - version: 7.22.15 - resolution: "@babel/helper-compilation-targets@npm:7.22.15" - dependencies: - "@babel/compat-data": "npm:^7.22.9" - "@babel/helper-validator-option": "npm:^7.22.15" - browserslist: "npm:^4.21.9" - lru-cache: "npm:^5.1.1" - semver: "npm:^6.3.1" - checksum: 10c0/45b9286861296e890f674a3abb199efea14a962a27d9b8adeb44970a9fd5c54e73a9e342e8414d2851cf4f98d5994537352fbce7b05ade32e9849bbd327f9ff1 - languageName: node - linkType: hard - -"@babel/helper-environment-visitor@npm:^7.22.20": - version: 7.22.20 - resolution: "@babel/helper-environment-visitor@npm:7.22.20" - checksum: 10c0/e762c2d8f5d423af89bd7ae9abe35bd4836d2eb401af868a63bbb63220c513c783e25ef001019418560b3fdc6d9a6fb67e6c0b650bcdeb3a2ac44b5c3d2bdd94 - languageName: node - linkType: hard - -"@babel/helper-function-name@npm:^7.23.0": - version: 7.23.0 - resolution: "@babel/helper-function-name@npm:7.23.0" - dependencies: - "@babel/template": "npm:^7.22.15" - "@babel/types": "npm:^7.23.0" - checksum: 10c0/d771dd1f3222b120518176733c52b7cadac1c256ff49b1889dbbe5e3fed81db855b8cc4e40d949c9d3eae0e795e8229c1c8c24c0e83f27cfa6ee3766696c6428 - languageName: node - linkType: hard - -"@babel/helper-hoist-variables@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/helper-hoist-variables@npm:7.22.5" - dependencies: - "@babel/types": "npm:^7.22.5" - checksum: 10c0/60a3077f756a1cd9f14eb89f0037f487d81ede2b7cfe652ea6869cd4ec4c782b0fb1de01b8494b9a2d2050e3d154d7d5ad3be24806790acfb8cbe2073bf1e208 - languageName: node - linkType: hard - -"@babel/helper-module-imports@npm:^7.22.15": - version: 7.22.15 - resolution: "@babel/helper-module-imports@npm:7.22.15" - dependencies: - "@babel/types": "npm:^7.22.15" - checksum: 10c0/4e0d7fc36d02c1b8c8b3006dfbfeedf7a367d3334a04934255de5128115ea0bafdeb3e5736a2559917f0653e4e437400d54542da0468e08d3cbc86d3bbfa8f30 - languageName: node - linkType: hard - -"@babel/helper-module-transforms@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/helper-module-transforms@npm:7.23.3" - dependencies: - "@babel/helper-environment-visitor": "npm:^7.22.20" - "@babel/helper-module-imports": "npm:^7.22.15" - "@babel/helper-simple-access": "npm:^7.22.5" - "@babel/helper-split-export-declaration": "npm:^7.22.6" - "@babel/helper-validator-identifier": "npm:^7.22.20" - peerDependencies: - "@babel/core": ^7.0.0 - checksum: 10c0/211e1399d0c4993671e8e5c2b25383f08bee40004ace5404ed4065f0e9258cc85d99c1b82fd456c030ce5cfd4d8f310355b54ef35de9924eabfc3dff1331d946 - languageName: node - linkType: hard - -"@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.22.5, @babel/helper-plugin-utils@npm:^7.8.0": - version: 7.22.5 - resolution: "@babel/helper-plugin-utils@npm:7.22.5" - checksum: 10c0/d2c4bfe2fa91058bcdee4f4e57a3f4933aed7af843acfd169cd6179fab8d13c1d636474ecabb2af107dc77462c7e893199aa26632bac1c6d7e025a17cbb9d20d - languageName: node - linkType: hard - -"@babel/helper-simple-access@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/helper-simple-access@npm:7.22.5" - dependencies: - "@babel/types": "npm:^7.22.5" - checksum: 10c0/f0cf81a30ba3d09a625fd50e5a9069e575c5b6719234e04ee74247057f8104beca89ed03e9217b6e9b0493434cedc18c5ecca4cea6244990836f1f893e140369 - languageName: node - linkType: hard - -"@babel/helper-split-export-declaration@npm:^7.22.6": - version: 7.22.6 - resolution: "@babel/helper-split-export-declaration@npm:7.22.6" - dependencies: - "@babel/types": "npm:^7.22.5" - checksum: 10c0/d83e4b623eaa9622c267d3c83583b72f3aac567dc393dda18e559d79187961cb29ae9c57b2664137fc3d19508370b12ec6a81d28af73a50e0846819cb21c6e44 - languageName: node - linkType: hard - -"@babel/helper-string-parser@npm:^7.23.4": - version: 7.23.4 - resolution: "@babel/helper-string-parser@npm:7.23.4" - checksum: 10c0/f348d5637ad70b6b54b026d6544bd9040f78d24e7ec245a0fc42293968181f6ae9879c22d89744730d246ce8ec53588f716f102addd4df8bbc79b73ea10004ac - languageName: node - linkType: hard - "@babel/helper-validator-identifier@npm:^7.22.20": version: 7.22.20 resolution: "@babel/helper-validator-identifier@npm:7.22.20" @@ -176,24 +29,6 @@ __metadata: languageName: node linkType: hard -"@babel/helper-validator-option@npm:^7.22.15": - version: 7.22.15 - resolution: "@babel/helper-validator-option@npm:7.22.15" - checksum: 10c0/e9661bf80ba18e2dd978217b350fb07298e57ac417f4f1ab9fa011505e20e4857f2c3b4b538473516a9dc03af5ce3a831e5ed973311c28326f4c330b6be981c2 - languageName: node - linkType: hard - -"@babel/helpers@npm:^7.23.2": - version: 7.23.4 - resolution: "@babel/helpers@npm:7.23.4" - dependencies: - "@babel/template": "npm:^7.22.15" - "@babel/traverse": "npm:^7.23.4" - "@babel/types": "npm:^7.23.4" - checksum: 10c0/6bb552b3de530f5eaae99f5410826b5877bae38ccd95cb5809c9a0cef99bcdb9f5db373309c1cf873f5d68927993515323985bac0ff1b811f2437f2e3ae994b8 - languageName: node - linkType: hard - "@babel/highlight@npm:^7.23.4": version: 7.23.4 resolution: "@babel/highlight@npm:7.23.4" @@ -205,216 +40,6 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.22.15, @babel/parser@npm:^7.23.3, @babel/parser@npm:^7.23.4": - version: 7.23.4 - resolution: "@babel/parser@npm:7.23.4" - bin: - parser: ./bin/babel-parser.js - checksum: 10c0/9115cd9c5855a6c7a8dd246938b1316dc1014ad36e01240c1e94ada63218ca39aa63d953d1bff8074a2737933448bc50736eb3da52ffc5c11a256c66d0accc2b - languageName: node - linkType: hard - -"@babel/plugin-syntax-async-generators@npm:^7.8.4": - version: 7.8.4 - resolution: "@babel/plugin-syntax-async-generators@npm:7.8.4" - dependencies: - "@babel/helper-plugin-utils": "npm:^7.8.0" - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 10c0/d13efb282838481348c71073b6be6245b35d4f2f964a8f71e4174f235009f929ef7613df25f8d2338e2d3e44bc4265a9f8638c6aaa136d7a61fe95985f9725c8 - languageName: node - linkType: hard - -"@babel/plugin-syntax-bigint@npm:^7.8.3": - version: 7.8.3 - resolution: "@babel/plugin-syntax-bigint@npm:7.8.3" - dependencies: - "@babel/helper-plugin-utils": "npm:^7.8.0" - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 10c0/686891b81af2bc74c39013655da368a480f17dd237bf9fbc32048e5865cb706d5a8f65438030da535b332b1d6b22feba336da8fa931f663b6b34e13147d12dde - languageName: node - linkType: hard - -"@babel/plugin-syntax-class-properties@npm:^7.8.3": - version: 7.12.13 - resolution: "@babel/plugin-syntax-class-properties@npm:7.12.13" - dependencies: - "@babel/helper-plugin-utils": "npm:^7.12.13" - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 10c0/95168fa186416195280b1264fb18afcdcdcea780b3515537b766cb90de6ce042d42dd6a204a39002f794ae5845b02afb0fd4861a3308a861204a55e68310a120 - languageName: node - linkType: hard - -"@babel/plugin-syntax-import-meta@npm:^7.8.3": - version: 7.10.4 - resolution: "@babel/plugin-syntax-import-meta@npm:7.10.4" - dependencies: - "@babel/helper-plugin-utils": "npm:^7.10.4" - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 10c0/0b08b5e4c3128523d8e346f8cfc86824f0da2697b1be12d71af50a31aff7a56ceb873ed28779121051475010c28d6146a6bfea8518b150b71eeb4e46190172ee - languageName: node - linkType: hard - -"@babel/plugin-syntax-json-strings@npm:^7.8.3": - version: 7.8.3 - resolution: "@babel/plugin-syntax-json-strings@npm:7.8.3" - dependencies: - "@babel/helper-plugin-utils": "npm:^7.8.0" - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 10c0/e98f31b2ec406c57757d115aac81d0336e8434101c224edd9a5c93cefa53faf63eacc69f3138960c8b25401315af03df37f68d316c151c4b933136716ed6906e - languageName: node - linkType: hard - -"@babel/plugin-syntax-jsx@npm:^7.7.2": - version: 7.23.3 - resolution: "@babel/plugin-syntax-jsx@npm:7.23.3" - dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 10c0/563bb7599b868773f1c7c1d441ecc9bc53aeb7832775da36752c926fc402a1fa5421505b39e724f71eb217c13e4b93117e081cac39723b0e11dac4c897f33c3e - languageName: node - linkType: hard - -"@babel/plugin-syntax-logical-assignment-operators@npm:^7.8.3": - version: 7.10.4 - resolution: "@babel/plugin-syntax-logical-assignment-operators@npm:7.10.4" - dependencies: - "@babel/helper-plugin-utils": "npm:^7.10.4" - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 10c0/2594cfbe29411ad5bc2ad4058de7b2f6a8c5b86eda525a993959438615479e59c012c14aec979e538d60a584a1a799b60d1b8942c3b18468cb9d99b8fd34cd0b - languageName: node - linkType: hard - -"@babel/plugin-syntax-nullish-coalescing-operator@npm:^7.8.3": - version: 7.8.3 - resolution: "@babel/plugin-syntax-nullish-coalescing-operator@npm:7.8.3" - dependencies: - "@babel/helper-plugin-utils": "npm:^7.8.0" - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 10c0/2024fbb1162899094cfc81152449b12bd0cc7053c6d4bda8ac2852545c87d0a851b1b72ed9560673cbf3ef6248257262c3c04aabf73117215c1b9cc7dd2542ce - languageName: node - linkType: hard - -"@babel/plugin-syntax-numeric-separator@npm:^7.8.3": - version: 7.10.4 - resolution: "@babel/plugin-syntax-numeric-separator@npm:7.10.4" - dependencies: - "@babel/helper-plugin-utils": "npm:^7.10.4" - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 10c0/c55a82b3113480942c6aa2fcbe976ff9caa74b7b1109ff4369641dfbc88d1da348aceb3c31b6ed311c84d1e7c479440b961906c735d0ab494f688bf2fd5b9bb9 - languageName: node - linkType: hard - -"@babel/plugin-syntax-object-rest-spread@npm:^7.8.3": - version: 7.8.3 - resolution: "@babel/plugin-syntax-object-rest-spread@npm:7.8.3" - dependencies: - "@babel/helper-plugin-utils": "npm:^7.8.0" - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 10c0/ee1eab52ea6437e3101a0a7018b0da698545230015fc8ab129d292980ec6dff94d265e9e90070e8ae5fed42f08f1622c14c94552c77bcac784b37f503a82ff26 - languageName: node - linkType: hard - -"@babel/plugin-syntax-optional-catch-binding@npm:^7.8.3": - version: 7.8.3 - resolution: "@babel/plugin-syntax-optional-catch-binding@npm:7.8.3" - dependencies: - "@babel/helper-plugin-utils": "npm:^7.8.0" - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 10c0/27e2493ab67a8ea6d693af1287f7e9acec206d1213ff107a928e85e173741e1d594196f99fec50e9dde404b09164f39dec5864c767212154ffe1caa6af0bc5af - languageName: node - linkType: hard - -"@babel/plugin-syntax-optional-chaining@npm:^7.8.3": - version: 7.8.3 - resolution: "@babel/plugin-syntax-optional-chaining@npm:7.8.3" - dependencies: - "@babel/helper-plugin-utils": "npm:^7.8.0" - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 10c0/46edddf2faa6ebf94147b8e8540dfc60a5ab718e2de4d01b2c0bdf250a4d642c2bd47cbcbb739febcb2bf75514dbcefad3c52208787994b8d0f8822490f55e81 - languageName: node - linkType: hard - -"@babel/plugin-syntax-top-level-await@npm:^7.8.3": - version: 7.14.5 - resolution: "@babel/plugin-syntax-top-level-await@npm:7.14.5" - dependencies: - "@babel/helper-plugin-utils": "npm:^7.14.5" - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 10c0/14bf6e65d5bc1231ffa9def5f0ef30b19b51c218fcecaa78cd1bdf7939dfdf23f90336080b7f5196916368e399934ce5d581492d8292b46a2fb569d8b2da106f - languageName: node - linkType: hard - -"@babel/plugin-syntax-typescript@npm:^7.7.2": - version: 7.23.3 - resolution: "@babel/plugin-syntax-typescript@npm:7.23.3" - dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 10c0/4d6e9cdb9d0bfb9bd9b220fc951d937fce2ca69135ec121153572cebe81d86abc9a489208d6b69ee5f10cadcaeffa10d0425340a5029e40e14a6025021b90948 - languageName: node - linkType: hard - -"@babel/template@npm:^7.22.15, @babel/template@npm:^7.3.3": - version: 7.22.15 - resolution: "@babel/template@npm:7.22.15" - dependencies: - "@babel/code-frame": "npm:^7.22.13" - "@babel/parser": "npm:^7.22.15" - "@babel/types": "npm:^7.22.15" - checksum: 10c0/9312edd37cf1311d738907003f2aa321a88a42ba223c69209abe4d7111db019d321805504f606c7fd75f21c6cf9d24d0a8223104cd21ebd207e241b6c551f454 - languageName: node - linkType: hard - -"@babel/traverse@npm:^7.23.3, @babel/traverse@npm:^7.23.4": - version: 7.23.4 - resolution: "@babel/traverse@npm:7.23.4" - dependencies: - "@babel/code-frame": "npm:^7.23.4" - "@babel/generator": "npm:^7.23.4" - "@babel/helper-environment-visitor": "npm:^7.22.20" - "@babel/helper-function-name": "npm:^7.23.0" - "@babel/helper-hoist-variables": "npm:^7.22.5" - "@babel/helper-split-export-declaration": "npm:^7.22.6" - "@babel/parser": "npm:^7.23.4" - "@babel/types": "npm:^7.23.4" - debug: "npm:^4.1.0" - globals: "npm:^11.1.0" - checksum: 10c0/a423d2b90934efe4ed423d67d91d6aa33ad035d6a175420fa9715376720584b4e7a7002be4bf55b085b7e675b7aba8c615e01560d6d9c47341427e1fe8039c68 - languageName: node - linkType: hard - -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.23.3, @babel/types@npm:^7.23.4, @babel/types@npm:^7.3.3, @babel/types@npm:^7.8.3": - version: 7.23.4 - resolution: "@babel/types@npm:7.23.4" - dependencies: - "@babel/helper-string-parser": "npm:^7.23.4" - "@babel/helper-validator-identifier": "npm:^7.22.20" - to-fast-properties: "npm:^2.0.0" - checksum: 10c0/231954418e0d052a8e69c9d84dde31baffd91d38d99624d18f160e14aa32b094b9e3e91c9c065ea88ea80c6e1589b17bb8b843b950c20c112f32c17482f7cf1f - languageName: node - linkType: hard - -"@bcoe/v8-coverage@npm:^0.2.3": - version: 0.2.3 - resolution: "@bcoe/v8-coverage@npm:0.2.3" - checksum: 10c0/6b80ae4cb3db53f486da2dc63b6e190a74c8c3cca16bb2733f234a0b6a9382b09b146488ae08e2b22cf00f6c83e20f3e040a2f7894f05c045c946d6a090b1d52 - languageName: node - linkType: hard - "@bergos/jsonparse@npm:^1.4.0, @bergos/jsonparse@npm:^1.4.1": version: 1.4.1 resolution: "@bergos/jsonparse@npm:1.4.1" @@ -3404,340 +3029,94 @@ __metadata: languageName: node linkType: hard -"@istanbuljs/load-nyc-config@npm:^1.0.0": - version: 1.1.0 - resolution: "@istanbuljs/load-nyc-config@npm:1.1.0" +"@jest/expect-utils@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/expect-utils@npm:29.7.0" dependencies: - camelcase: "npm:^5.3.1" - find-up: "npm:^4.1.0" - get-package-type: "npm:^0.1.0" - js-yaml: "npm:^3.13.1" - resolve-from: "npm:^5.0.0" - checksum: 10c0/dd2a8b094887da5a1a2339543a4933d06db2e63cbbc2e288eb6431bd832065df0c099d091b6a67436e71b7d6bf85f01ce7c15f9253b4cbebcc3b9a496165ba42 + jest-get-type: "npm:^29.6.3" + checksum: 10c0/60b79d23a5358dc50d9510d726443316253ecda3a7fb8072e1526b3e0d3b14f066ee112db95699b7a43ad3f0b61b750c72e28a5a1cac361d7a2bb34747fa938a languageName: node linkType: hard -"@istanbuljs/schema@npm:^0.1.2": - version: 0.1.3 - resolution: "@istanbuljs/schema@npm:0.1.3" - checksum: 10c0/61c5286771676c9ca3eb2bd8a7310a9c063fb6e0e9712225c8471c582d157392c88f5353581c8c9adbe0dff98892317d2fdfc56c3499aa42e0194405206a963a +"@jest/schemas@npm:^29.6.3": + version: 29.6.3 + resolution: "@jest/schemas@npm:29.6.3" + dependencies: + "@sinclair/typebox": "npm:^0.27.8" + checksum: 10c0/b329e89cd5f20b9278ae1233df74016ebf7b385e0d14b9f4c1ad18d096c4c19d1e687aa113a9c976b16ec07f021ae53dea811fb8c1248a50ac34fbe009fdf6be languageName: node linkType: hard -"@jest/console@npm:^29.7.0": - version: 29.7.0 - resolution: "@jest/console@npm:29.7.0" +"@jest/types@npm:^29.6.3": + version: 29.6.3 + resolution: "@jest/types@npm:29.6.3" dependencies: - "@jest/types": "npm:^29.6.3" + "@jest/schemas": "npm:^29.6.3" + "@types/istanbul-lib-coverage": "npm:^2.0.0" + "@types/istanbul-reports": "npm:^3.0.0" "@types/node": "npm:*" + "@types/yargs": "npm:^17.0.8" chalk: "npm:^4.0.0" - jest-message-util: "npm:^29.7.0" - jest-util: "npm:^29.7.0" - slash: "npm:^3.0.0" - checksum: 10c0/7be408781d0a6f657e969cbec13b540c329671819c2f57acfad0dae9dbfe2c9be859f38fe99b35dba9ff1536937dc6ddc69fdcd2794812fa3c647a1619797f6c + checksum: 10c0/ea4e493dd3fb47933b8ccab201ae573dcc451f951dc44ed2a86123cd8541b82aa9d2b1031caf9b1080d6673c517e2dcc25a44b2dc4f3fbc37bfc965d444888c0 languageName: node linkType: hard -"@jest/core@npm:^29.7.0": - version: 29.7.0 - resolution: "@jest/core@npm:29.7.0" +"@jeswr/prefixcc@npm:^1.2.1": + version: 1.2.1 + resolution: "@jeswr/prefixcc@npm:1.2.1" dependencies: - "@jest/console": "npm:^29.7.0" - "@jest/reporters": "npm:^29.7.0" - "@jest/test-result": "npm:^29.7.0" - "@jest/transform": "npm:^29.7.0" - "@jest/types": "npm:^29.6.3" - "@types/node": "npm:*" - ansi-escapes: "npm:^4.2.1" - chalk: "npm:^4.0.0" - ci-info: "npm:^3.2.0" - exit: "npm:^0.1.2" - graceful-fs: "npm:^4.2.9" - jest-changed-files: "npm:^29.7.0" - jest-config: "npm:^29.7.0" - jest-haste-map: "npm:^29.7.0" - jest-message-util: "npm:^29.7.0" - jest-regex-util: "npm:^29.6.3" - jest-resolve: "npm:^29.7.0" - jest-resolve-dependencies: "npm:^29.7.0" - jest-runner: "npm:^29.7.0" - jest-runtime: "npm:^29.7.0" - jest-snapshot: "npm:^29.7.0" - jest-util: "npm:^29.7.0" - jest-validate: "npm:^29.7.0" - jest-watcher: "npm:^29.7.0" - micromatch: "npm:^4.0.4" - pretty-format: "npm:^29.7.0" - slash: "npm:^3.0.0" - strip-ansi: "npm:^6.0.0" - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: + cross-fetch: "npm:^3.1.5" + fsevents: "npm:^2.3.2" + dependenciesMeta: + fsevents: optional: true - checksum: 10c0/934f7bf73190f029ac0f96662c85cd276ec460d407baf6b0dbaec2872e157db4d55a7ee0b1c43b18874602f662b37cb973dda469a4e6d88b4e4845b521adeeb2 + checksum: 10c0/f2b7dfb953d6648655659207dd9a8474b6022d744312c2bacb76376a87006f2b50dba0a133926d1bd23fb6e0ff2ae2c33c524815865690e9511ccd0fa4edf788 languageName: node linkType: hard -"@jest/environment@npm:^29.7.0": - version: 29.7.0 - resolution: "@jest/environment@npm:29.7.0" - dependencies: - "@jest/fake-timers": "npm:^29.7.0" - "@jest/types": "npm:^29.6.3" - "@types/node": "npm:*" - jest-mock: "npm:^29.7.0" - checksum: 10c0/c7b1b40c618f8baf4d00609022d2afa086d9c6acc706f303a70bb4b67275868f620ad2e1a9efc5edd418906157337cce50589a627a6400bbdf117d351b91ef86 +"@jridgewell/resolve-uri@npm:^3.0.3": + version: 3.1.1 + resolution: "@jridgewell/resolve-uri@npm:3.1.1" + checksum: 10c0/0dbc9e29bc640bbbdc5b9876d2859c69042bfcf1423c1e6421bcca53e826660bff4e41c7d4bcb8dbea696404231a6f902f76ba41835d049e20f2dd6cffb713bf languageName: node linkType: hard -"@jest/expect-utils@npm:^29.7.0": - version: 29.7.0 - resolution: "@jest/expect-utils@npm:29.7.0" - dependencies: - jest-get-type: "npm:^29.6.3" - checksum: 10c0/60b79d23a5358dc50d9510d726443316253ecda3a7fb8072e1526b3e0d3b14f066ee112db95699b7a43ad3f0b61b750c72e28a5a1cac361d7a2bb34747fa938a +"@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.5.0": + version: 1.5.0 + resolution: "@jridgewell/sourcemap-codec@npm:1.5.0" + checksum: 10c0/2eb864f276eb1096c3c11da3e9bb518f6d9fc0023c78344cdc037abadc725172c70314bdb360f2d4b7bffec7f5d657ce006816bc5d4ecb35e61b66132db00c18 languageName: node linkType: hard -"@jest/expect@npm:^29.7.0": - version: 29.7.0 - resolution: "@jest/expect@npm:29.7.0" +"@jridgewell/trace-mapping@npm:0.3.9": + version: 0.3.9 + resolution: "@jridgewell/trace-mapping@npm:0.3.9" dependencies: - expect: "npm:^29.7.0" - jest-snapshot: "npm:^29.7.0" - checksum: 10c0/b41f193fb697d3ced134349250aed6ccea075e48c4f803159db102b826a4e473397c68c31118259868fd69a5cba70e97e1c26d2c2ff716ca39dc73a2ccec037e + "@jridgewell/resolve-uri": "npm:^3.0.3" + "@jridgewell/sourcemap-codec": "npm:^1.4.10" + checksum: 10c0/fa425b606d7c7ee5bfa6a31a7b050dd5814b4082f318e0e4190f991902181b4330f43f4805db1dd4f2433fd0ed9cc7a7b9c2683f1deeab1df1b0a98b1e24055b languageName: node linkType: hard -"@jest/fake-timers@npm:^29.7.0": - version: 29.7.0 - resolution: "@jest/fake-timers@npm:29.7.0" +"@koa/cors@npm:^5.0.0": + version: 5.0.0 + resolution: "@koa/cors@npm:5.0.0" dependencies: - "@jest/types": "npm:^29.6.3" - "@sinonjs/fake-timers": "npm:^10.0.2" - "@types/node": "npm:*" - jest-message-util: "npm:^29.7.0" - jest-mock: "npm:^29.7.0" - jest-util: "npm:^29.7.0" - checksum: 10c0/cf0a8bcda801b28dc2e2b2ba36302200ee8104a45ad7a21e6c234148932f826cb3bc57c8df3b7b815aeea0861d7b6ca6f0d4778f93b9219398ef28749e03595c + vary: "npm:^1.1.2" + checksum: 10c0/49e5f3b861590bd81aa3663a2f0658234a9b378840bb54a2947b3c5f2067f9d966b6fa2e9049fdc7c74c787456d1885bacd0b7ee1f134274d28282c7df99c3fd languageName: node linkType: hard -"@jest/globals@npm:^29.7.0": - version: 29.7.0 - resolution: "@jest/globals@npm:29.7.0" +"@koa/router@npm:^12.0.1": + version: 12.0.1 + resolution: "@koa/router@npm:12.0.1" dependencies: - "@jest/environment": "npm:^29.7.0" - "@jest/expect": "npm:^29.7.0" - "@jest/types": "npm:^29.6.3" - jest-mock: "npm:^29.7.0" - checksum: 10c0/a385c99396878fe6e4460c43bd7bb0a5cc52befb462cc6e7f2a3810f9e7bcce7cdeb51908fd530391ee452dc856c98baa2c5f5fa8a5b30b071d31ef7f6955cea - languageName: node - linkType: hard - -"@jest/reporters@npm:^29.7.0": - version: 29.7.0 - resolution: "@jest/reporters@npm:29.7.0" - dependencies: - "@bcoe/v8-coverage": "npm:^0.2.3" - "@jest/console": "npm:^29.7.0" - "@jest/test-result": "npm:^29.7.0" - "@jest/transform": "npm:^29.7.0" - "@jest/types": "npm:^29.6.3" - "@jridgewell/trace-mapping": "npm:^0.3.18" - "@types/node": "npm:*" - chalk: "npm:^4.0.0" - collect-v8-coverage: "npm:^1.0.0" - exit: "npm:^0.1.2" - glob: "npm:^7.1.3" - graceful-fs: "npm:^4.2.9" - istanbul-lib-coverage: "npm:^3.0.0" - istanbul-lib-instrument: "npm:^6.0.0" - istanbul-lib-report: "npm:^3.0.0" - istanbul-lib-source-maps: "npm:^4.0.0" - istanbul-reports: "npm:^3.1.3" - jest-message-util: "npm:^29.7.0" - jest-util: "npm:^29.7.0" - jest-worker: "npm:^29.7.0" - slash: "npm:^3.0.0" - string-length: "npm:^4.0.1" - strip-ansi: "npm:^6.0.0" - v8-to-istanbul: "npm:^9.0.1" - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - checksum: 10c0/a754402a799541c6e5aff2c8160562525e2a47e7d568f01ebfc4da66522de39cbb809bbb0a841c7052e4270d79214e70aec3c169e4eae42a03bc1a8a20cb9fa2 - languageName: node - linkType: hard - -"@jest/schemas@npm:^29.6.3": - version: 29.6.3 - resolution: "@jest/schemas@npm:29.6.3" - dependencies: - "@sinclair/typebox": "npm:^0.27.8" - checksum: 10c0/b329e89cd5f20b9278ae1233df74016ebf7b385e0d14b9f4c1ad18d096c4c19d1e687aa113a9c976b16ec07f021ae53dea811fb8c1248a50ac34fbe009fdf6be - languageName: node - linkType: hard - -"@jest/source-map@npm:^29.6.3": - version: 29.6.3 - resolution: "@jest/source-map@npm:29.6.3" - dependencies: - "@jridgewell/trace-mapping": "npm:^0.3.18" - callsites: "npm:^3.0.0" - graceful-fs: "npm:^4.2.9" - checksum: 10c0/a2f177081830a2e8ad3f2e29e20b63bd40bade294880b595acf2fc09ec74b6a9dd98f126a2baa2bf4941acd89b13a4ade5351b3885c224107083a0059b60a219 - languageName: node - linkType: hard - -"@jest/test-result@npm:^29.7.0": - version: 29.7.0 - resolution: "@jest/test-result@npm:29.7.0" - dependencies: - "@jest/console": "npm:^29.7.0" - "@jest/types": "npm:^29.6.3" - "@types/istanbul-lib-coverage": "npm:^2.0.0" - collect-v8-coverage: "npm:^1.0.0" - checksum: 10c0/7de54090e54a674ca173470b55dc1afdee994f2d70d185c80236003efd3fa2b753fff51ffcdda8e2890244c411fd2267529d42c4a50a8303755041ee493e6a04 - languageName: node - linkType: hard - -"@jest/test-sequencer@npm:^29.7.0": - version: 29.7.0 - resolution: "@jest/test-sequencer@npm:29.7.0" - dependencies: - "@jest/test-result": "npm:^29.7.0" - graceful-fs: "npm:^4.2.9" - jest-haste-map: "npm:^29.7.0" - slash: "npm:^3.0.0" - checksum: 10c0/593a8c4272797bb5628984486080cbf57aed09c7cfdc0a634e8c06c38c6bef329c46c0016e84555ee55d1cd1f381518cf1890990ff845524c1123720c8c1481b - languageName: node - linkType: hard - -"@jest/transform@npm:^29.7.0": - version: 29.7.0 - resolution: "@jest/transform@npm:29.7.0" - dependencies: - "@babel/core": "npm:^7.11.6" - "@jest/types": "npm:^29.6.3" - "@jridgewell/trace-mapping": "npm:^0.3.18" - babel-plugin-istanbul: "npm:^6.1.1" - chalk: "npm:^4.0.0" - convert-source-map: "npm:^2.0.0" - fast-json-stable-stringify: "npm:^2.1.0" - graceful-fs: "npm:^4.2.9" - jest-haste-map: "npm:^29.7.0" - jest-regex-util: "npm:^29.6.3" - jest-util: "npm:^29.7.0" - micromatch: "npm:^4.0.4" - pirates: "npm:^4.0.4" - slash: "npm:^3.0.0" - write-file-atomic: "npm:^4.0.2" - checksum: 10c0/7f4a7f73dcf45dfdf280c7aa283cbac7b6e5a904813c3a93ead7e55873761fc20d5c4f0191d2019004fac6f55f061c82eb3249c2901164ad80e362e7a7ede5a6 - languageName: node - linkType: hard - -"@jest/types@npm:^29.6.3": - version: 29.6.3 - resolution: "@jest/types@npm:29.6.3" - dependencies: - "@jest/schemas": "npm:^29.6.3" - "@types/istanbul-lib-coverage": "npm:^2.0.0" - "@types/istanbul-reports": "npm:^3.0.0" - "@types/node": "npm:*" - "@types/yargs": "npm:^17.0.8" - chalk: "npm:^4.0.0" - checksum: 10c0/ea4e493dd3fb47933b8ccab201ae573dcc451f951dc44ed2a86123cd8541b82aa9d2b1031caf9b1080d6673c517e2dcc25a44b2dc4f3fbc37bfc965d444888c0 - languageName: node - linkType: hard - -"@jeswr/prefixcc@npm:^1.2.1": - version: 1.2.1 - resolution: "@jeswr/prefixcc@npm:1.2.1" - dependencies: - cross-fetch: "npm:^3.1.5" - fsevents: "npm:^2.3.2" - dependenciesMeta: - fsevents: - optional: true - checksum: 10c0/f2b7dfb953d6648655659207dd9a8474b6022d744312c2bacb76376a87006f2b50dba0a133926d1bd23fb6e0ff2ae2c33c524815865690e9511ccd0fa4edf788 - languageName: node - linkType: hard - -"@jridgewell/gen-mapping@npm:^0.3.0, @jridgewell/gen-mapping@npm:^0.3.2": - version: 0.3.3 - resolution: "@jridgewell/gen-mapping@npm:0.3.3" - dependencies: - "@jridgewell/set-array": "npm:^1.0.1" - "@jridgewell/sourcemap-codec": "npm:^1.4.10" - "@jridgewell/trace-mapping": "npm:^0.3.9" - checksum: 10c0/376fc11cf5a967318ba3ddd9d8e91be528eab6af66810a713c49b0c3f8dc67e9949452c51c38ab1b19aa618fb5e8594da5a249977e26b1e7fea1ee5a1fcacc74 - languageName: node - linkType: hard - -"@jridgewell/resolve-uri@npm:^3.0.3, @jridgewell/resolve-uri@npm:^3.1.0": - version: 3.1.1 - resolution: "@jridgewell/resolve-uri@npm:3.1.1" - checksum: 10c0/0dbc9e29bc640bbbdc5b9876d2859c69042bfcf1423c1e6421bcca53e826660bff4e41c7d4bcb8dbea696404231a6f902f76ba41835d049e20f2dd6cffb713bf - languageName: node - linkType: hard - -"@jridgewell/set-array@npm:^1.0.1": - version: 1.1.2 - resolution: "@jridgewell/set-array@npm:1.1.2" - checksum: 10c0/bc7ab4c4c00470de4e7562ecac3c0c84f53e7ee8a711e546d67c47da7febe7c45cd67d4d84ee3c9b2c05ae8e872656cdded8a707a283d30bd54fbc65aef821ab - languageName: node - linkType: hard - -"@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.4.14": - version: 1.4.15 - resolution: "@jridgewell/sourcemap-codec@npm:1.4.15" - checksum: 10c0/0c6b5ae663087558039052a626d2d7ed5208da36cfd707dcc5cea4a07cfc918248403dcb5989a8f7afaf245ce0573b7cc6fd94c4a30453bd10e44d9363940ba5 - languageName: node - linkType: hard - -"@jridgewell/trace-mapping@npm:0.3.9": - version: 0.3.9 - resolution: "@jridgewell/trace-mapping@npm:0.3.9" - dependencies: - "@jridgewell/resolve-uri": "npm:^3.0.3" - "@jridgewell/sourcemap-codec": "npm:^1.4.10" - checksum: 10c0/fa425b606d7c7ee5bfa6a31a7b050dd5814b4082f318e0e4190f991902181b4330f43f4805db1dd4f2433fd0ed9cc7a7b9c2683f1deeab1df1b0a98b1e24055b - languageName: node - linkType: hard - -"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.17, @jridgewell/trace-mapping@npm:^0.3.18, @jridgewell/trace-mapping@npm:^0.3.9": - version: 0.3.20 - resolution: "@jridgewell/trace-mapping@npm:0.3.20" - dependencies: - "@jridgewell/resolve-uri": "npm:^3.1.0" - "@jridgewell/sourcemap-codec": "npm:^1.4.14" - checksum: 10c0/0ea0b2675cf513ec44dc25605616a3c9b808b9832e74b5b63c44260d66b58558bba65764f81928fc1033ead911f8718dca1134049c3e7a93937faf436671df31 - languageName: node - linkType: hard - -"@koa/cors@npm:^5.0.0": - version: 5.0.0 - resolution: "@koa/cors@npm:5.0.0" - dependencies: - vary: "npm:^1.1.2" - checksum: 10c0/49e5f3b861590bd81aa3663a2f0658234a9b378840bb54a2947b3c5f2067f9d966b6fa2e9049fdc7c74c787456d1885bacd0b7ee1f134274d28282c7df99c3fd - languageName: node - linkType: hard - -"@koa/router@npm:^12.0.1": - version: 12.0.1 - resolution: "@koa/router@npm:12.0.1" - dependencies: - debug: "npm:^4.3.4" - http-errors: "npm:^2.0.0" - koa-compose: "npm:^4.1.0" - methods: "npm:^1.1.2" - path-to-regexp: "npm:^6.2.1" - checksum: 10c0/978a668a88dad8cba38afe0df537b95f6d49bdaa60af5f479240fde3a87a7046cf8c068a58c355442335794db5cb583b88ca07a4b65f217b44925a7ce99ccc1d + debug: "npm:^4.3.4" + http-errors: "npm:^2.0.0" + koa-compose: "npm:^4.1.0" + methods: "npm:^1.1.2" + path-to-regexp: "npm:^6.2.1" + checksum: 10c0/978a668a88dad8cba38afe0df537b95f6d49bdaa60af5f479240fde3a87a7046cf8c068a58c355442335794db5cb583b88ca07a4b65f217b44925a7ce99ccc1d languageName: node linkType: hard @@ -3885,6 +3264,146 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-android-arm-eabi@npm:4.44.0": + version: 4.44.0 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.44.0" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + +"@rollup/rollup-android-arm64@npm:4.44.0": + version: 4.44.0 + resolution: "@rollup/rollup-android-arm64@npm:4.44.0" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@rollup/rollup-darwin-arm64@npm:4.44.0": + version: 4.44.0 + resolution: "@rollup/rollup-darwin-arm64@npm:4.44.0" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@rollup/rollup-darwin-x64@npm:4.44.0": + version: 4.44.0 + resolution: "@rollup/rollup-darwin-x64@npm:4.44.0" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@rollup/rollup-freebsd-arm64@npm:4.44.0": + version: 4.44.0 + resolution: "@rollup/rollup-freebsd-arm64@npm:4.44.0" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + +"@rollup/rollup-freebsd-x64@npm:4.44.0": + version: 4.44.0 + resolution: "@rollup/rollup-freebsd-x64@npm:4.44.0" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@rollup/rollup-linux-arm-gnueabihf@npm:4.44.0": + version: 4.44.0 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.44.0" + conditions: os=linux & cpu=arm & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-arm-musleabihf@npm:4.44.0": + version: 4.44.0 + resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.44.0" + conditions: os=linux & cpu=arm & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-arm64-gnu@npm:4.44.0": + version: 4.44.0 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.44.0" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-arm64-musl@npm:4.44.0": + version: 4.44.0 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.44.0" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-loongarch64-gnu@npm:4.44.0": + version: 4.44.0 + resolution: "@rollup/rollup-linux-loongarch64-gnu@npm:4.44.0" + conditions: os=linux & cpu=loong64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-powerpc64le-gnu@npm:4.44.0": + version: 4.44.0 + resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.44.0" + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-riscv64-gnu@npm:4.44.0": + version: 4.44.0 + resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.44.0" + conditions: os=linux & cpu=riscv64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-riscv64-musl@npm:4.44.0": + version: 4.44.0 + resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.44.0" + conditions: os=linux & cpu=riscv64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-s390x-gnu@npm:4.44.0": + version: 4.44.0 + resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.44.0" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-x64-gnu@npm:4.44.0": + version: 4.44.0 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.44.0" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-x64-musl@npm:4.44.0": + version: 4.44.0 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.44.0" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-win32-arm64-msvc@npm:4.44.0": + version: 4.44.0 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.44.0" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@rollup/rollup-win32-ia32-msvc@npm:4.44.0": + version: 4.44.0 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.44.0" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@rollup/rollup-win32-x64-msvc@npm:4.44.0": + version: 4.44.0 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.44.0" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@rubensworks/saxes@npm:^6.0.1": version: 6.0.1 resolution: "@rubensworks/saxes@npm:6.0.1" @@ -3915,24 +3434,6 @@ __metadata: languageName: node linkType: hard -"@sinonjs/commons@npm:^3.0.0": - version: 3.0.0 - resolution: "@sinonjs/commons@npm:3.0.0" - dependencies: - type-detect: "npm:4.0.8" - checksum: 10c0/1df9cd257942f4e4960dfb9fd339d9e97b6a3da135f3d5b8646562918e863809cb8e00268535f4f4723535d2097881c8fc03d545c414d8555183376cfc54ee84 - languageName: node - linkType: hard - -"@sinonjs/fake-timers@npm:^10.0.2": - version: 10.3.0 - resolution: "@sinonjs/fake-timers@npm:10.3.0" - dependencies: - "@sinonjs/commons": "npm:^3.0.0" - checksum: 10c0/2e2fb6cc57f227912814085b7b01fede050cd4746ea8d49a1e44d5a0e56a804663b0340ae2f11af7559ea9bf4d087a11f2f646197a660ea3cb04e19efc04aa63 - languageName: node - linkType: hard - "@smessie/readable-web-to-node-stream@npm:^3.0.3": version: 3.0.3 resolution: "@smessie/readable-web-to-node-stream@npm:3.0.3" @@ -4111,19 +3612,19 @@ __metadata: "@commitlint/cli": "npm:^16.1.0" "@commitlint/config-conventional": "npm:^16.0.0" "@types/jest": "npm:^29.5.12" - "@types/node": "npm:^20.11.25" + "@types/node": "npm:^20.19.1" "@typescript-eslint/eslint-plugin": "npm:^5.12.1" "@typescript-eslint/parser": "npm:^5.12.1" chalk: "npm:^5.4.1" componentsjs-generator: "npm:^3.1.2" eslint: "npm:^8.10.0" - jest: "npm:^29.7.0" jest-rdf: "npm:^1.8.1" shx: "npm:^0.3.4" syncpack: "npm:^13.0.2" - ts-jest: "npm:^29.1.2" tsx: "npm:^4.19.2" - typescript: "npm:^5.3.3" + typescript: "npm:^5.8.3" + vite: "npm:^6.3.5" + vitest: "npm:^3.2.3" languageName: unknown linkType: soft @@ -4192,47 +3693,6 @@ __metadata: languageName: node linkType: hard -"@types/babel__core@npm:^7.1.14": - version: 7.20.5 - resolution: "@types/babel__core@npm:7.20.5" - dependencies: - "@babel/parser": "npm:^7.20.7" - "@babel/types": "npm:^7.20.7" - "@types/babel__generator": "npm:*" - "@types/babel__template": "npm:*" - "@types/babel__traverse": "npm:*" - checksum: 10c0/bdee3bb69951e833a4b811b8ee9356b69a61ed5b7a23e1a081ec9249769117fa83aaaf023bb06562a038eb5845155ff663e2d5c75dd95c1d5ccc91db012868ff - languageName: node - linkType: hard - -"@types/babel__generator@npm:*": - version: 7.6.7 - resolution: "@types/babel__generator@npm:7.6.7" - dependencies: - "@babel/types": "npm:^7.0.0" - checksum: 10c0/2427203864ef231857e102eeb32b731a419164863983119cdd4dac9f1503c2831eb4262d05ade95d4574aa410b94c16e54e36a616758452f685a34881f4596d9 - languageName: node - linkType: hard - -"@types/babel__template@npm:*": - version: 7.4.4 - resolution: "@types/babel__template@npm:7.4.4" - dependencies: - "@babel/parser": "npm:^7.1.0" - "@babel/types": "npm:^7.0.0" - checksum: 10c0/cc84f6c6ab1eab1427e90dd2b76ccee65ce940b778a9a67be2c8c39e1994e6f5bbc8efa309f6cea8dc6754994524cd4d2896558df76d92e7a1f46ecffee7112b - languageName: node - linkType: hard - -"@types/babel__traverse@npm:*, @types/babel__traverse@npm:^7.0.6": - version: 7.20.4 - resolution: "@types/babel__traverse@npm:7.20.4" - dependencies: - "@babel/types": "npm:^7.20.7" - checksum: 10c0/e76cb4974c7740fd61311152dc497e7b05c1c46ba554aab875544ab0a7457f343cafcad34ba8fb2ff543ab0e012ef2d3fa0c13f1a4e9a4cd9c4c703c7a2a8d62 - languageName: node - linkType: hard - "@types/bcryptjs@npm:^2.4.4": version: 2.4.6 resolution: "@types/bcryptjs@npm:2.4.6" @@ -4250,6 +3710,15 @@ __metadata: languageName: node linkType: hard +"@types/chai@npm:^5.2.2": + version: 5.2.2 + resolution: "@types/chai@npm:5.2.2" + dependencies: + "@types/deep-eql": "npm:*" + checksum: 10c0/49282bf0e8246800ebb36f17256f97bd3a8c4fb31f92ad3c0eaa7623518d7e87f1eaad4ad206960fcaf7175854bdff4cb167e4fe96811e0081b4ada83dd533ec + languageName: node + linkType: hard + "@types/clownface@npm:*": version: 2.0.5 resolution: "@types/clownface@npm:2.0.5" @@ -4303,6 +3772,13 @@ __metadata: languageName: node linkType: hard +"@types/deep-eql@npm:*": + version: 4.0.2 + resolution: "@types/deep-eql@npm:4.0.2" + checksum: 10c0/bf3f811843117900d7084b9d0c852da9a044d12eb40e6de73b552598a6843c21291a8a381b0532644574beecd5e3491c5ff3a0365ab86b15d59862c025384844 + languageName: node + linkType: hard + "@types/ejs@npm:^3.1.3": version: 3.1.5 resolution: "@types/ejs@npm:3.1.5" @@ -4326,6 +3802,13 @@ __metadata: languageName: node linkType: hard +"@types/estree@npm:1.0.8, @types/estree@npm:^1.0.0": + version: 1.0.8 + resolution: "@types/estree@npm:1.0.8" + checksum: 10c0/39d34d1afaa338ab9763f37ad6066e3f349444f9052b9676a7cc0252ef9485a41c6d81c9c4e0d26e9077993354edf25efc853f3224dd4b447175ef62bdcc86a5 + languageName: node + linkType: hard + "@types/express-serve-static-core@npm:^4.17.33": version: 4.17.41 resolution: "@types/express-serve-static-core@npm:4.17.41" @@ -4360,15 +3843,6 @@ __metadata: languageName: node linkType: hard -"@types/graceful-fs@npm:^4.1.3": - version: 4.1.9 - resolution: "@types/graceful-fs@npm:4.1.9" - dependencies: - "@types/node": "npm:*" - checksum: 10c0/235d2fc69741448e853333b7c3d1180a966dd2b8972c8cbcd6b2a0c6cd7f8d582ab2b8e58219dbc62cce8f1b40aa317ff78ea2201cdd8249da5025adebed6f0b - languageName: node - linkType: hard - "@types/http-assert@npm:*": version: 1.5.5 resolution: "@types/http-assert@npm:1.5.5" @@ -4399,7 +3873,7 @@ __metadata: languageName: node linkType: hard -"@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0, @types/istanbul-lib-coverage@npm:^2.0.1": +"@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0": version: 2.0.6 resolution: "@types/istanbul-lib-coverage@npm:2.0.6" checksum: 10c0/3948088654f3eeb45363f1db158354fb013b362dba2a5c2c18c559484d5eb9f6fd85b23d66c0a7c2fcfab7308d0a585b14dadaca6cc8bf89ebfdc7f8f5102fb7 @@ -4550,12 +4024,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^20.11.25": - version: 20.17.17 - resolution: "@types/node@npm:20.17.17" +"@types/node@npm:^20.19.1": + version: 20.19.1 + resolution: "@types/node@npm:20.19.1" dependencies: - undici-types: "npm:~6.19.2" - checksum: 10c0/6aebc8053b96f1b6d82c3a3fe5fc20d9f7eeb507d79d967221bbecaa2b26665fa1c9efe460be2a2f4aaae4e98f37797df7d5a4189f2b6edf8abf9f82f7fd8b1f + undici-types: "npm:~6.21.0" + checksum: 10c0/4f1c3c8ec24c79af6802b376fa307904abf19accb9ac291de0bfc02220494c8b027d3ef733dbf64cc09b37594f22f679a15eabb30f3785bcfcc13bd9bbd8c0e2 languageName: node linkType: hard @@ -4898,14 +4372,97 @@ __metadata: languageName: node linkType: hard -"JSONStream@npm:^1.0.4": - version: 1.3.5 - resolution: "JSONStream@npm:1.3.5" +"@vitest/expect@npm:3.2.4": + version: 3.2.4 + resolution: "@vitest/expect@npm:3.2.4" dependencies: - jsonparse: "npm:^1.2.0" - through: "npm:>=2.2.7 <3" - bin: - JSONStream: ./bin.js + "@types/chai": "npm:^5.2.2" + "@vitest/spy": "npm:3.2.4" + "@vitest/utils": "npm:3.2.4" + chai: "npm:^5.2.0" + tinyrainbow: "npm:^2.0.0" + checksum: 10c0/7586104e3fd31dbe1e6ecaafb9a70131e4197dce2940f727b6a84131eee3decac7b10f9c7c72fa5edbdb68b6f854353bd4c0fa84779e274207fb7379563b10db + languageName: node + linkType: hard + +"@vitest/mocker@npm:3.2.4": + version: 3.2.4 + resolution: "@vitest/mocker@npm:3.2.4" + dependencies: + "@vitest/spy": "npm:3.2.4" + estree-walker: "npm:^3.0.3" + magic-string: "npm:^0.30.17" + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + checksum: 10c0/f7a4aea19bbbf8f15905847ee9143b6298b2c110f8b64789224cb0ffdc2e96f9802876aa2ca83f1ec1b6e1ff45e822abb34f0054c24d57b29ab18add06536ccd + languageName: node + linkType: hard + +"@vitest/pretty-format@npm:3.2.4, @vitest/pretty-format@npm:^3.2.4": + version: 3.2.4 + resolution: "@vitest/pretty-format@npm:3.2.4" + dependencies: + tinyrainbow: "npm:^2.0.0" + checksum: 10c0/5ad7d4278e067390d7d633e307fee8103958806a419ca380aec0e33fae71b44a64415f7a9b4bc11635d3c13d4a9186111c581d3cef9c65cc317e68f077456887 + languageName: node + linkType: hard + +"@vitest/runner@npm:3.2.4": + version: 3.2.4 + resolution: "@vitest/runner@npm:3.2.4" + dependencies: + "@vitest/utils": "npm:3.2.4" + pathe: "npm:^2.0.3" + strip-literal: "npm:^3.0.0" + checksum: 10c0/e8be51666c72b3668ae3ea348b0196656a4a5adb836cb5e270720885d9517421815b0d6c98bfdf1795ed02b994b7bfb2b21566ee356a40021f5bf4f6ed4e418a + languageName: node + linkType: hard + +"@vitest/snapshot@npm:3.2.4": + version: 3.2.4 + resolution: "@vitest/snapshot@npm:3.2.4" + dependencies: + "@vitest/pretty-format": "npm:3.2.4" + magic-string: "npm:^0.30.17" + pathe: "npm:^2.0.3" + checksum: 10c0/f8301a3d7d1559fd3d59ed51176dd52e1ed5c2d23aa6d8d6aa18787ef46e295056bc726a021698d8454c16ed825ecba163362f42fa90258bb4a98cfd2c9424fc + languageName: node + linkType: hard + +"@vitest/spy@npm:3.2.4": + version: 3.2.4 + resolution: "@vitest/spy@npm:3.2.4" + dependencies: + tinyspy: "npm:^4.0.3" + checksum: 10c0/6ebf0b4697dc238476d6b6a60c76ba9eb1dd8167a307e30f08f64149612fd50227682b876420e4c2e09a76334e73f72e3ebf0e350714dc22474258292e202024 + languageName: node + linkType: hard + +"@vitest/utils@npm:3.2.4": + version: 3.2.4 + resolution: "@vitest/utils@npm:3.2.4" + dependencies: + "@vitest/pretty-format": "npm:3.2.4" + loupe: "npm:^3.1.4" + tinyrainbow: "npm:^2.0.0" + checksum: 10c0/024a9b8c8bcc12cf40183c246c244b52ecff861c6deb3477cbf487ac8781ad44c68a9c5fd69f8c1361878e55b97c10d99d511f2597f1f7244b5e5101d028ba64 + languageName: node + linkType: hard + +"JSONStream@npm:^1.0.4": + version: 1.3.5 + resolution: "JSONStream@npm:1.3.5" + dependencies: + jsonparse: "npm:^1.2.0" + through: "npm:>=2.2.7 <3" + bin: + JSONStream: ./bin.js checksum: 10c0/0f54694da32224d57b715385d4a6b668d2117379d1f3223dc758459246cca58fdc4c628b83e8a8883334e454a0a30aa198ede77c788b55537c1844f686a751f2 languageName: node linkType: hard @@ -4999,15 +4556,6 @@ __metadata: languageName: node linkType: hard -"ansi-escapes@npm:^4.2.1": - version: 4.3.2 - resolution: "ansi-escapes@npm:4.3.2" - dependencies: - type-fest: "npm:^0.21.3" - checksum: 10c0/da917be01871525a3dfcf925ae2977bc59e8c513d4423368645634bf5d4ceba5401574eb705c1e92b79f7292af5a656f78c5725a4b0e1cec97c4b413705c1d50 - languageName: node - linkType: hard - "ansi-regex@npm:^5.0.1": version: 5.0.1 resolution: "ansi-regex@npm:5.0.1" @@ -5054,16 +4602,6 @@ __metadata: languageName: node linkType: hard -"anymatch@npm:^3.0.3": - version: 3.1.3 - resolution: "anymatch@npm:3.1.3" - dependencies: - normalize-path: "npm:^3.0.0" - picomatch: "npm:^2.0.4" - checksum: 10c0/57b06ae984bc32a0d22592c87384cd88fe4511b1dd7581497831c56d41939c8a001b28e7b853e1450f2bf61992dfcaa8ae2d0d161a0a90c4fb631ef07098fbac - languageName: node - linkType: hard - "arg@npm:^4.1.0": version: 4.1.3 resolution: "arg@npm:4.1.3" @@ -5071,15 +4609,6 @@ __metadata: languageName: node linkType: hard -"argparse@npm:^1.0.7": - version: 1.0.10 - resolution: "argparse@npm:1.0.10" - dependencies: - sprintf-js: "npm:~1.0.2" - checksum: 10c0/b2972c5c23c63df66bca144dbc65d180efa74f25f8fd9b7d9a0a6c88ae839db32df3d54770dcb6460cf840d232b60695d1a6b1053f599d84e73f7437087712de - languageName: node - linkType: hard - "argparse@npm:^2.0.1": version: 2.0.1 resolution: "argparse@npm:2.0.1" @@ -5127,6 +4656,13 @@ __metadata: languageName: node linkType: hard +"assertion-error@npm:^2.0.1": + version: 2.0.1 + resolution: "assertion-error@npm:2.0.1" + checksum: 10c0/bbbcb117ac6480138f8c93cf7f535614282dea9dc828f540cdece85e3c665e8f78958b96afac52f29ff883c72638e6a87d469ecc9fe5bc902df03ed24a55dba8 + languageName: node + linkType: hard + "async-lock@npm:^1.4.0": version: 1.4.1 resolution: "async-lock@npm:1.4.1" @@ -5157,82 +4693,6 @@ __metadata: languageName: node linkType: hard -"babel-jest@npm:^29.7.0": - version: 29.7.0 - resolution: "babel-jest@npm:29.7.0" - dependencies: - "@jest/transform": "npm:^29.7.0" - "@types/babel__core": "npm:^7.1.14" - babel-plugin-istanbul: "npm:^6.1.1" - babel-preset-jest: "npm:^29.6.3" - chalk: "npm:^4.0.0" - graceful-fs: "npm:^4.2.9" - slash: "npm:^3.0.0" - peerDependencies: - "@babel/core": ^7.8.0 - checksum: 10c0/2eda9c1391e51936ca573dd1aedfee07b14c59b33dbe16ef347873ddd777bcf6e2fc739681e9e9661ab54ef84a3109a03725be2ac32cd2124c07ea4401cbe8c1 - languageName: node - linkType: hard - -"babel-plugin-istanbul@npm:^6.1.1": - version: 6.1.1 - resolution: "babel-plugin-istanbul@npm:6.1.1" - dependencies: - "@babel/helper-plugin-utils": "npm:^7.0.0" - "@istanbuljs/load-nyc-config": "npm:^1.0.0" - "@istanbuljs/schema": "npm:^0.1.2" - istanbul-lib-instrument: "npm:^5.0.4" - test-exclude: "npm:^6.0.0" - checksum: 10c0/1075657feb705e00fd9463b329921856d3775d9867c5054b449317d39153f8fbcebd3e02ebf00432824e647faff3683a9ca0a941325ef1afe9b3c4dd51b24beb - languageName: node - linkType: hard - -"babel-plugin-jest-hoist@npm:^29.6.3": - version: 29.6.3 - resolution: "babel-plugin-jest-hoist@npm:29.6.3" - dependencies: - "@babel/template": "npm:^7.3.3" - "@babel/types": "npm:^7.3.3" - "@types/babel__core": "npm:^7.1.14" - "@types/babel__traverse": "npm:^7.0.6" - checksum: 10c0/7e6451caaf7dce33d010b8aafb970e62f1b0c0b57f4978c37b0d457bbcf0874d75a395a102daf0bae0bd14eafb9f6e9a165ee5e899c0a4f1f3bb2e07b304ed2e - languageName: node - linkType: hard - -"babel-preset-current-node-syntax@npm:^1.0.0": - version: 1.0.1 - resolution: "babel-preset-current-node-syntax@npm:1.0.1" - dependencies: - "@babel/plugin-syntax-async-generators": "npm:^7.8.4" - "@babel/plugin-syntax-bigint": "npm:^7.8.3" - "@babel/plugin-syntax-class-properties": "npm:^7.8.3" - "@babel/plugin-syntax-import-meta": "npm:^7.8.3" - "@babel/plugin-syntax-json-strings": "npm:^7.8.3" - "@babel/plugin-syntax-logical-assignment-operators": "npm:^7.8.3" - "@babel/plugin-syntax-nullish-coalescing-operator": "npm:^7.8.3" - "@babel/plugin-syntax-numeric-separator": "npm:^7.8.3" - "@babel/plugin-syntax-object-rest-spread": "npm:^7.8.3" - "@babel/plugin-syntax-optional-catch-binding": "npm:^7.8.3" - "@babel/plugin-syntax-optional-chaining": "npm:^7.8.3" - "@babel/plugin-syntax-top-level-await": "npm:^7.8.3" - peerDependencies: - "@babel/core": ^7.0.0 - checksum: 10c0/5ba39a3a0e6c37d25e56a4fb843be632dac98d54706d8a0933f9bcb1a07987a96d55c2b5a6c11788a74063fb2534fe68c1f1dbb6c93626850c785e0938495627 - languageName: node - linkType: hard - -"babel-preset-jest@npm:^29.6.3": - version: 29.6.3 - resolution: "babel-preset-jest@npm:29.6.3" - dependencies: - babel-plugin-jest-hoist: "npm:^29.6.3" - babel-preset-current-node-syntax: "npm:^1.0.0" - peerDependencies: - "@babel/core": ^7.0.0 - checksum: 10c0/ec5fd0276b5630b05f0c14bb97cc3815c6b31600c683ebb51372e54dcb776cff790bdeeabd5b8d01ede375a040337ccbf6a3ccd68d3a34219125945e167ad943 - languageName: node - linkType: hard - "balanced-match@npm:^1.0.0": version: 1.0.2 resolution: "balanced-match@npm:1.0.2" @@ -5303,45 +4763,6 @@ __metadata: languageName: node linkType: hard -"browserslist@npm:^4.21.9": - version: 4.22.1 - resolution: "browserslist@npm:4.22.1" - dependencies: - caniuse-lite: "npm:^1.0.30001541" - electron-to-chromium: "npm:^1.4.535" - node-releases: "npm:^2.0.13" - update-browserslist-db: "npm:^1.0.13" - bin: - browserslist: cli.js - checksum: 10c0/6810f2d63f171d0b7b8d38cf091708e00cb31525501810a507839607839320d66e657293b0aa3d7f051ecbc025cb07390a90c037682c1d05d12604991e41050b - languageName: node - linkType: hard - -"bs-logger@npm:0.x": - version: 0.2.6 - resolution: "bs-logger@npm:0.2.6" - dependencies: - fast-json-stable-stringify: "npm:2.x" - checksum: 10c0/80e89aaaed4b68e3374ce936f2eb097456a0dddbf11f75238dbd53140b1e39259f0d248a5089ed456f1158984f22191c3658d54a713982f676709fbe1a6fa5a0 - languageName: node - linkType: hard - -"bser@npm:2.1.1": - version: 2.1.1 - resolution: "bser@npm:2.1.1" - dependencies: - node-int64: "npm:^0.4.0" - checksum: 10c0/24d8dfb7b6d457d73f32744e678a60cc553e4ec0e9e1a01cf614b44d85c3c87e188d3cc78ef0442ce5032ee6818de20a0162ba1074725c0d08908f62ea979227 - languageName: node - linkType: hard - -"buffer-from@npm:^1.0.0": - version: 1.1.2 - resolution: "buffer-from@npm:1.1.2" - checksum: 10c0/124fff9d66d691a86d3b062eff4663fe437a9d9ee4b47b1b9e97f5a5d14f6d5399345db80f796827be7c95e70a8e765dd404b7c3ff3b3324f98e9b0c8826cc34 - languageName: node - linkType: hard - "buffer@npm:^6.0.3": version: 6.0.3 resolution: "buffer@npm:6.0.3" @@ -5359,6 +4780,13 @@ __metadata: languageName: node linkType: hard +"cac@npm:^6.7.14": + version: 6.7.14 + resolution: "cac@npm:6.7.14" + checksum: 10c0/4ee06aaa7bab8981f0d54e5f5f9d4adcd64058e9697563ce336d8a3878ed018ee18ebe5359b2430eceae87e0758e62ea2019c3f52ae6e211b1bd2e133856cd10 + languageName: node + linkType: hard + "cacache@npm:^18.0.0": version: 18.0.0 resolution: "cacache@npm:18.0.0" @@ -5436,20 +4864,6 @@ __metadata: languageName: node linkType: hard -"camelcase@npm:^6.2.0": - version: 6.3.0 - resolution: "camelcase@npm:6.3.0" - checksum: 10c0/0d701658219bd3116d12da3eab31acddb3f9440790c0792e0d398f0a520a6a4058018e546862b6fba89d7ae990efaeb97da71e1913e9ebf5a8b5621a3d55c710 - languageName: node - linkType: hard - -"caniuse-lite@npm:^1.0.30001541": - version: 1.0.30001563 - resolution: "caniuse-lite@npm:1.0.30001563" - checksum: 10c0/a8b367d43e0307ec243d8df8515d563fa3de895e9698eec4539037aed400da81e0df737164da2a6b7104ab6e75b4ea63db0adebcaabe326f2792841980259256 - languageName: node - linkType: hard - "canonicalize@npm:^1.0.1": version: 1.0.8 resolution: "canonicalize@npm:1.0.8" @@ -5464,6 +4878,19 @@ __metadata: languageName: node linkType: hard +"chai@npm:^5.2.0": + version: 5.2.0 + resolution: "chai@npm:5.2.0" + dependencies: + assertion-error: "npm:^2.0.1" + check-error: "npm:^2.1.1" + deep-eql: "npm:^5.0.1" + loupe: "npm:^3.1.0" + pathval: "npm:^2.0.0" + checksum: 10c0/dfd1cb719c7cebb051b727672d382a35338af1470065cb12adb01f4ee451bbf528e0e0f9ab2016af5fc1eea4df6e7f4504dc8443f8f00bd8fb87ad32dc516f7d + languageName: node + linkType: hard + "chalk-template@npm:1.1.0": version: 1.1.0 resolution: "chalk-template@npm:1.1.0" @@ -5501,10 +4928,10 @@ __metadata: languageName: node linkType: hard -"char-regex@npm:^1.0.2": - version: 1.0.2 - resolution: "char-regex@npm:1.0.2" - checksum: 10c0/57a09a86371331e0be35d9083ba429e86c4f4648ecbe27455dbfb343037c16ee6fdc7f6b61f433a57cc5ded5561d71c56a150e018f40c2ffb7bc93a26dae341e +"check-error@npm:^2.1.1": + version: 2.1.1 + resolution: "check-error@npm:2.1.1" + checksum: 10c0/979f13eccab306cf1785fa10941a590b4e7ea9916ea2a4f8c87f0316fc3eab07eabefb6e587424ef0f88cbcd3805791f172ea739863ca3d7ce2afc54641c7f0e languageName: node linkType: hard @@ -5522,13 +4949,6 @@ __metadata: languageName: node linkType: hard -"cjs-module-lexer@npm:^1.0.0": - version: 1.2.3 - resolution: "cjs-module-lexer@npm:1.2.3" - checksum: 10c0/0de9a9c3fad03a46804c0d38e7b712fb282584a9c7ef1ed44cae22fb71d9bb600309d66a9711ac36a596fd03422f5bb03e021e8f369c12a39fa1786ae531baab - languageName: node - linkType: hard - "clean-stack@npm:^2.0.0": version: 2.2.0 resolution: "clean-stack@npm:2.2.0" @@ -5587,13 +5007,6 @@ __metadata: languageName: node linkType: hard -"collect-v8-coverage@npm:^1.0.0": - version: 1.0.2 - resolution: "collect-v8-coverage@npm:1.0.2" - checksum: 10c0/ed7008e2e8b6852c5483b444a3ae6e976e088d4335a85aa0a9db2861c5f1d31bd2d7ff97a60469b3388deeba661a619753afbe201279fb159b4b9548ab8269a1 - languageName: node - linkType: hard - "color-convert@npm:^1.9.0, color-convert@npm:^1.9.3": version: 1.9.3 resolution: "color-convert@npm:1.9.3" @@ -5791,13 +5204,6 @@ __metadata: languageName: node linkType: hard -"convert-source-map@npm:^2.0.0": - version: 2.0.0 - resolution: "convert-source-map@npm:2.0.0" - checksum: 10c0/8f2f7a27a1a011cc6cc88cc4da2d7d0cfa5ee0369508baae3d98c260bb3ac520691464e5bbe4ae7cdf09860c1d69ecc6f70c63c6e7c7f7e3f18ec08484dc7d9b - languageName: node - linkType: hard - "cookie@npm:^0.7.0": version: 0.7.2 resolution: "cookie@npm:0.7.2" @@ -5869,23 +5275,6 @@ __metadata: languageName: node linkType: hard -"create-jest@npm:^29.7.0": - version: 29.7.0 - resolution: "create-jest@npm:29.7.0" - dependencies: - "@jest/types": "npm:^29.6.3" - chalk: "npm:^4.0.0" - exit: "npm:^0.1.2" - graceful-fs: "npm:^4.2.9" - jest-config: "npm:^29.7.0" - jest-util: "npm:^29.7.0" - prompts: "npm:^2.0.1" - bin: - create-jest: bin/create-jest.js - checksum: 10c0/e7e54c280692470d3398f62a6238fd396327e01c6a0757002833f06d00afc62dd7bfe04ff2b9cd145264460e6b4d1eb8386f2925b7e567f97939843b7b0e812f - languageName: node - linkType: hard - "create-require@npm:^1.1.0": version: 1.1.1 resolution: "create-require@npm:1.1.1" @@ -5943,15 +5332,15 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.2, debug@npm:^4.3.4": - version: 4.3.4 - resolution: "debug@npm:4.3.4" +"debug@npm:4, debug@npm:^4.1.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.4.1": + version: 4.4.1 + resolution: "debug@npm:4.4.1" dependencies: - ms: "npm:2.1.2" + ms: "npm:^2.1.3" peerDependenciesMeta: supports-color: optional: true - checksum: 10c0/cedbec45298dd5c501d01b92b119cd3faebe5438c3917ff11ae1bff86a6c722930ac9c8659792824013168ba6db7c4668225d845c633fbdafbbf902a6389f736 + checksum: 10c0/d2b44bc1afd912b49bb7ebb0d50a860dc93a4dd7d946e8de94abc957bb63726b7dd5aa48c18c2386c379ec024c46692e15ed3ed97d481729f929201e671fcd55 languageName: node linkType: hard @@ -5981,15 +5370,10 @@ __metadata: languageName: node linkType: hard -"dedent@npm:^1.0.0": - version: 1.5.1 - resolution: "dedent@npm:1.5.1" - peerDependencies: - babel-plugin-macros: ^3.1.0 - peerDependenciesMeta: - babel-plugin-macros: - optional: true - checksum: 10c0/f8612cd5b00aab58b18bb95572dca08dc2d49720bfa7201a444c3dae430291e8a06d4928614a6ec8764d713927f44bce9c990d3b8238fca2f430990ddc17c070 +"deep-eql@npm:^5.0.1": + version: 5.0.2 + resolution: "deep-eql@npm:5.0.2" + checksum: 10c0/7102cf3b7bb719c6b9c0db2e19bf0aa9318d141581befe8c7ce8ccd39af9eaa4346e5e05adef7f9bd7015da0f13a3a25dcfe306ef79dc8668aedbecb658dd247 languageName: node linkType: hard @@ -6007,13 +5391,6 @@ __metadata: languageName: node linkType: hard -"deepmerge@npm:^4.2.2": - version: 4.3.1 - resolution: "deepmerge@npm:4.3.1" - checksum: 10c0/e53481aaf1aa2c4082b5342be6b6d8ad9dfe387bc92ce197a66dea08bd4265904a087e75e464f14d1347cf2ac8afe1e4c16b266e0561cc5df29382d3c5f80044 - languageName: node - linkType: hard - "defer-to-connect@npm:^2.0.1": version: 2.0.1 resolution: "defer-to-connect@npm:2.0.1" @@ -6056,13 +5433,6 @@ __metadata: languageName: node linkType: hard -"detect-newline@npm:^3.0.0": - version: 3.1.0 - resolution: "detect-newline@npm:3.1.0" - checksum: 10c0/c38cfc8eeb9fda09febb44bcd85e467c970d4e3bf526095394e5a4f18bc26dd0cf6b22c69c1fa9969261521c593836db335c2795218f6d781a512aea2fb8209d - languageName: node - linkType: hard - "diff-sequences@npm:^29.6.3": version: 29.6.3 resolution: "diff-sequences@npm:29.6.3" @@ -6176,13 +5546,6 @@ __metadata: languageName: node linkType: hard -"electron-to-chromium@npm:^1.4.535": - version: 1.4.589 - resolution: "electron-to-chromium@npm:1.4.589" - checksum: 10c0/6d632d8367c7fae10d4ba68bc255305b676c2f5924035f29a6e6ea818a1f1e92d0aee3ac210ad097e7de0fcd248f932c12638c1e458144de9340c5889cad2ceb - languageName: node - linkType: hard - "elliptic@npm:^6.5.4": version: 6.5.5 resolution: "elliptic@npm:6.5.5" @@ -6198,13 +5561,6 @@ __metadata: languageName: node linkType: hard -"emittery@npm:^0.13.1": - version: 0.13.1 - resolution: "emittery@npm:0.13.1" - checksum: 10c0/1573d0ae29ab34661b6c63251ff8f5facd24ccf6a823f19417ae8ba8c88ea450325788c67f16c99edec8de4b52ce93a10fe441ece389fd156e88ee7dab9bfa35 - languageName: node - linkType: hard - "emoji-regex@npm:^10.3.0": version: 10.4.0 resolution: "emoji-regex@npm:10.4.0" @@ -6298,7 +5654,14 @@ __metadata: languageName: node linkType: hard -"esbuild@npm:~0.25.0": +"es-module-lexer@npm:^1.7.0": + version: 1.7.0 + resolution: "es-module-lexer@npm:1.7.0" + checksum: 10c0/4c935affcbfeba7fb4533e1da10fa8568043df1e3574b869385980de9e2d475ddc36769891936dbb07036edb3c3786a8b78ccf44964cd130dedc1f2c984b6c7b + languageName: node + linkType: hard + +"esbuild@npm:^0.25.0, esbuild@npm:~0.25.0": version: 0.25.5 resolution: "esbuild@npm:0.25.5" dependencies: @@ -6512,16 +5875,6 @@ __metadata: languageName: node linkType: hard -"esprima@npm:^4.0.0": - version: 4.0.1 - resolution: "esprima@npm:4.0.1" - bin: - esparse: ./bin/esparse.js - esvalidate: ./bin/esvalidate.js - checksum: 10c0/ad4bab9ead0808cf56501750fd9d3fb276f6b105f987707d059005d57e182d18a7c9ec7f3a01794ebddcca676773e42ca48a32d67a250c9d35e009ca613caba3 - languageName: node - linkType: hard - "esquery@npm:^1.4.2": version: 1.5.0 resolution: "esquery@npm:1.5.0" @@ -6554,6 +5907,15 @@ __metadata: languageName: node linkType: hard +"estree-walker@npm:^3.0.3": + version: 3.0.3 + resolution: "estree-walker@npm:3.0.3" + dependencies: + "@types/estree": "npm:^1.0.0" + checksum: 10c0/c12e3c2b2642d2bcae7d5aa495c60fa2f299160946535763969a1c83fc74518ffa9c2cd3a8b69ac56aea547df6a8aac25f729a342992ef0bbac5f1c73e78995d + languageName: node + linkType: hard + "esutils@npm:^2.0.2": version: 2.0.3 resolution: "esutils@npm:2.0.3" @@ -6599,14 +5961,14 @@ __metadata: languageName: node linkType: hard -"exit@npm:^0.1.2": - version: 0.1.2 - resolution: "exit@npm:0.1.2" - checksum: 10c0/71d2ad9b36bc25bb8b104b17e830b40a08989be7f7d100b13269aaae7c3784c3e6e1e88a797e9e87523993a25ba27c8958959a554535370672cfb4d824af8989 +"expect-type@npm:^1.2.1": + version: 1.2.1 + resolution: "expect-type@npm:1.2.1" + checksum: 10c0/b775c9adab3c190dd0d398c722531726cdd6022849b4adba19dceab58dda7e000a7c6c872408cd73d665baa20d381eca36af4f7b393a4ba60dd10232d1fb8898 languageName: node linkType: hard -"expect@npm:^29.0.0, expect@npm:^29.7.0": +"expect@npm:^29.0.0": version: 29.7.0 resolution: "expect@npm:29.7.0" dependencies: @@ -6683,7 +6045,7 @@ __metadata: languageName: node linkType: hard -"fast-json-stable-stringify@npm:2.x, fast-json-stable-stringify@npm:^2.0.0, fast-json-stable-stringify@npm:^2.1.0": +"fast-json-stable-stringify@npm:^2.0.0": version: 2.1.0 resolution: "fast-json-stable-stringify@npm:2.1.0" checksum: 10c0/7f081eb0b8a64e0057b3bb03f974b3ef00135fbf36c1c710895cd9300f13c94ba809bb3a81cf4e1b03f6e5285610a61abbd7602d0652de423144dfee5a389c9b @@ -6706,12 +6068,15 @@ __metadata: languageName: node linkType: hard -"fb-watchman@npm:^2.0.0": - version: 2.0.2 - resolution: "fb-watchman@npm:2.0.2" - dependencies: - bser: "npm:2.1.1" - checksum: 10c0/feae89ac148adb8f6ae8ccd87632e62b13563e6fb114cacb5265c51f585b17e2e268084519fb2edd133872f1d47a18e6bfd7e5e08625c0d41b93149694187581 +"fdir@npm:^6.4.4": + version: 6.4.6 + resolution: "fdir@npm:6.4.6" + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + checksum: 10c0/45b559cff889934ebb8bc498351e5acba40750ada7e7d6bde197768d2fa67c149be8ae7f8ff34d03f4e1eb20f2764116e56440aaa2f6689e9a4aa7ef06acafe9 languageName: node linkType: hard @@ -6790,7 +6155,7 @@ __metadata: languageName: node linkType: hard -"find-up@npm:^4.0.0, find-up@npm:^4.1.0": +"find-up@npm:^4.1.0": version: 4.1.0 resolution: "find-up@npm:4.1.0" dependencies: @@ -6926,7 +6291,7 @@ __metadata: languageName: node linkType: hard -"fsevents@npm:^2.3.2, fsevents@npm:~2.3.3": +"fsevents@npm:^2.3.2, fsevents@npm:~2.3.2, fsevents@npm:~2.3.3": version: 2.3.3 resolution: "fsevents@npm:2.3.3" dependencies: @@ -6936,7 +6301,7 @@ __metadata: languageName: node linkType: hard -"fsevents@patch:fsevents@npm%3A^2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin": +"fsevents@patch:fsevents@npm%3A^2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin": version: 2.3.3 resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1" dependencies: @@ -6952,13 +6317,6 @@ __metadata: languageName: node linkType: hard -"gensync@npm:^1.0.0-beta.2": - version: 1.0.0-beta.2 - resolution: "gensync@npm:1.0.0-beta.2" - checksum: 10c0/782aba6cba65b1bb5af3b095d96249d20edbe8df32dbf4696fd49be2583faf676173bf4809386588828e4dd76a3354fcbeb577bab1c833ccd9fc4577f26103f8 - languageName: node - linkType: hard - "get-caller-file@npm:^2.0.5": version: 2.0.5 resolution: "get-caller-file@npm:2.0.5" @@ -6984,13 +6342,6 @@ __metadata: languageName: node linkType: hard -"get-package-type@npm:^0.1.0": - version: 0.1.0 - resolution: "get-package-type@npm:0.1.0" - checksum: 10c0/e34cdf447fdf1902a1f6d5af737eaadf606d2ee3518287abde8910e04159368c268568174b2e71102b87b26c2020486f126bfca9c4fb1ceb986ff99b52ecd1be - languageName: node - linkType: hard - "get-stream@npm:^6.0.0, get-stream@npm:^6.0.1": version: 6.0.1 resolution: "get-stream@npm:6.0.1" @@ -7055,7 +6406,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^7.0.0, glob@npm:^7.1.3, glob@npm:^7.1.4": +"glob@npm:^7.0.0, glob@npm:^7.1.3": version: 7.2.3 resolution: "glob@npm:7.2.3" dependencies: @@ -7078,13 +6429,6 @@ __metadata: languageName: node linkType: hard -"globals@npm:^11.1.0": - version: 11.12.0 - resolution: "globals@npm:11.12.0" - checksum: 10c0/758f9f258e7b19226bd8d4af5d3b0dcf7038780fb23d82e6f98932c44e239f884847f1766e8fa9cc5635ccb3204f7fa7314d4408dd4002a5e8ea827b4018f0a1 - languageName: node - linkType: hard - "globals@npm:^13.19.0": version: 13.23.0 resolution: "globals@npm:13.23.0" @@ -7288,13 +6632,6 @@ __metadata: languageName: node linkType: hard -"html-escaper@npm:^2.0.0": - version: 2.0.2 - resolution: "html-escaper@npm:2.0.2" - checksum: 10c0/208e8a12de1a6569edbb14544f4567e6ce8ecc30b9394fcaa4e7bb1e60c12a7c9a1ed27e31290817157e8626f3a4f29e76c8747030822eb84a6abb15c255f0a0 - languageName: node - linkType: hard - "htmlparser2@npm:^8.0.0": version: 8.0.2 resolution: "htmlparser2@npm:8.0.2" @@ -7464,18 +6801,6 @@ __metadata: languageName: node linkType: hard -"import-local@npm:^3.0.2": - version: 3.1.0 - resolution: "import-local@npm:3.1.0" - dependencies: - pkg-dir: "npm:^4.2.0" - resolve-cwd: "npm:^3.0.0" - bin: - import-local-fixture: fixtures/cli.js - checksum: 10c0/c67ecea72f775fe8684ca3d057e54bdb2ae28c14bf261d2607c269c18ea0da7b730924c06262eca9aed4b8ab31e31d65bc60b50e7296c85908a56e2f7d41ecd2 - languageName: node - linkType: hard - "imurmurhash@npm:^0.1.4": version: 0.1.4 resolution: "imurmurhash@npm:0.1.4" @@ -7582,13 +6907,6 @@ __metadata: languageName: node linkType: hard -"is-generator-fn@npm:^2.0.0": - version: 2.1.0 - resolution: "is-generator-fn@npm:2.1.0" - checksum: 10c0/2957cab387997a466cd0bf5c1b6047bd21ecb32bdcfd8996b15747aa01002c1c88731802f1b3d34ac99f4f6874b626418bd118658cf39380fe5fff32a3af9c4d - languageName: node - linkType: hard - "is-generator-function@npm:^1.0.7": version: 1.0.10 resolution: "is-generator-function@npm:1.0.10" @@ -7711,71 +7029,6 @@ __metadata: languageName: node linkType: hard -"istanbul-lib-coverage@npm:^3.0.0, istanbul-lib-coverage@npm:^3.2.0": - version: 3.2.2 - resolution: "istanbul-lib-coverage@npm:3.2.2" - checksum: 10c0/6c7ff2106769e5f592ded1fb418f9f73b4411fd5a084387a5410538332b6567cd1763ff6b6cadca9b9eb2c443cce2f7ea7d7f1b8d315f9ce58539793b1e0922b - languageName: node - linkType: hard - -"istanbul-lib-instrument@npm:^5.0.4": - version: 5.2.1 - resolution: "istanbul-lib-instrument@npm:5.2.1" - dependencies: - "@babel/core": "npm:^7.12.3" - "@babel/parser": "npm:^7.14.7" - "@istanbuljs/schema": "npm:^0.1.2" - istanbul-lib-coverage: "npm:^3.2.0" - semver: "npm:^6.3.0" - checksum: 10c0/8a1bdf3e377dcc0d33ec32fe2b6ecacdb1e4358fd0eb923d4326bb11c67622c0ceb99600a680f3dad5d29c66fc1991306081e339b4d43d0b8a2ab2e1d910a6ee - languageName: node - linkType: hard - -"istanbul-lib-instrument@npm:^6.0.0": - version: 6.0.1 - resolution: "istanbul-lib-instrument@npm:6.0.1" - dependencies: - "@babel/core": "npm:^7.12.3" - "@babel/parser": "npm:^7.14.7" - "@istanbuljs/schema": "npm:^0.1.2" - istanbul-lib-coverage: "npm:^3.2.0" - semver: "npm:^7.5.4" - checksum: 10c0/313d61aca3f82a04ad9377841d05061d603ea3d4a4dd281fdda2479ec4ddbc86dc1792c73651f21c93480570d1ecadc5f63011e2df86f30ee662b62c0c00e3d8 - languageName: node - linkType: hard - -"istanbul-lib-report@npm:^3.0.0": - version: 3.0.1 - resolution: "istanbul-lib-report@npm:3.0.1" - dependencies: - istanbul-lib-coverage: "npm:^3.0.0" - make-dir: "npm:^4.0.0" - supports-color: "npm:^7.1.0" - checksum: 10c0/84323afb14392de8b6a5714bd7e9af845cfbd56cfe71ed276cda2f5f1201aea673c7111901227ee33e68e4364e288d73861eb2ed48f6679d1e69a43b6d9b3ba7 - languageName: node - linkType: hard - -"istanbul-lib-source-maps@npm:^4.0.0": - version: 4.0.1 - resolution: "istanbul-lib-source-maps@npm:4.0.1" - dependencies: - debug: "npm:^4.1.1" - istanbul-lib-coverage: "npm:^3.0.0" - source-map: "npm:^0.6.1" - checksum: 10c0/19e4cc405016f2c906dff271a76715b3e881fa9faeb3f09a86cb99b8512b3a5ed19cadfe0b54c17ca0e54c1142c9c6de9330d65506e35873994e06634eebeb66 - languageName: node - linkType: hard - -"istanbul-reports@npm:^3.1.3": - version: 3.1.6 - resolution: "istanbul-reports@npm:3.1.6" - dependencies: - html-escaper: "npm:^2.0.0" - istanbul-lib-report: "npm:^3.0.0" - checksum: 10c0/ec3f1bdbc51b3e0b325a5b9f4ad31a247697f31001df4e81075f7980413f14da1b5adfec574fd156efd3b0464023f61320f6718efc66ee72b32d89611cef99dd - languageName: node - linkType: hard - "jackspeak@npm:^2.3.5": version: 2.3.6 resolution: "jackspeak@npm:2.3.6" @@ -7803,109 +7056,6 @@ __metadata: languageName: node linkType: hard -"jest-changed-files@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-changed-files@npm:29.7.0" - dependencies: - execa: "npm:^5.0.0" - jest-util: "npm:^29.7.0" - p-limit: "npm:^3.1.0" - checksum: 10c0/e071384d9e2f6bb462231ac53f29bff86f0e12394c1b49ccafbad225ce2ab7da226279a8a94f421949920bef9be7ef574fd86aee22e8adfa149be73554ab828b - languageName: node - linkType: hard - -"jest-circus@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-circus@npm:29.7.0" - dependencies: - "@jest/environment": "npm:^29.7.0" - "@jest/expect": "npm:^29.7.0" - "@jest/test-result": "npm:^29.7.0" - "@jest/types": "npm:^29.6.3" - "@types/node": "npm:*" - chalk: "npm:^4.0.0" - co: "npm:^4.6.0" - dedent: "npm:^1.0.0" - is-generator-fn: "npm:^2.0.0" - jest-each: "npm:^29.7.0" - jest-matcher-utils: "npm:^29.7.0" - jest-message-util: "npm:^29.7.0" - jest-runtime: "npm:^29.7.0" - jest-snapshot: "npm:^29.7.0" - jest-util: "npm:^29.7.0" - p-limit: "npm:^3.1.0" - pretty-format: "npm:^29.7.0" - pure-rand: "npm:^6.0.0" - slash: "npm:^3.0.0" - stack-utils: "npm:^2.0.3" - checksum: 10c0/8d15344cf7a9f14e926f0deed64ed190c7a4fa1ed1acfcd81e4cc094d3cc5bf7902ebb7b874edc98ada4185688f90c91e1747e0dfd7ac12463b097968ae74b5e - languageName: node - linkType: hard - -"jest-cli@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-cli@npm:29.7.0" - dependencies: - "@jest/core": "npm:^29.7.0" - "@jest/test-result": "npm:^29.7.0" - "@jest/types": "npm:^29.6.3" - chalk: "npm:^4.0.0" - create-jest: "npm:^29.7.0" - exit: "npm:^0.1.2" - import-local: "npm:^3.0.2" - jest-config: "npm:^29.7.0" - jest-util: "npm:^29.7.0" - jest-validate: "npm:^29.7.0" - yargs: "npm:^17.3.1" - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - bin: - jest: bin/jest.js - checksum: 10c0/a658fd55050d4075d65c1066364595962ead7661711495cfa1dfeecf3d6d0a8ffec532f3dbd8afbb3e172dd5fd2fb2e813c5e10256e7cf2fea766314942fb43a - languageName: node - linkType: hard - -"jest-config@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-config@npm:29.7.0" - dependencies: - "@babel/core": "npm:^7.11.6" - "@jest/test-sequencer": "npm:^29.7.0" - "@jest/types": "npm:^29.6.3" - babel-jest: "npm:^29.7.0" - chalk: "npm:^4.0.0" - ci-info: "npm:^3.2.0" - deepmerge: "npm:^4.2.2" - glob: "npm:^7.1.3" - graceful-fs: "npm:^4.2.9" - jest-circus: "npm:^29.7.0" - jest-environment-node: "npm:^29.7.0" - jest-get-type: "npm:^29.6.3" - jest-regex-util: "npm:^29.6.3" - jest-resolve: "npm:^29.7.0" - jest-runner: "npm:^29.7.0" - jest-util: "npm:^29.7.0" - jest-validate: "npm:^29.7.0" - micromatch: "npm:^4.0.4" - parse-json: "npm:^5.2.0" - pretty-format: "npm:^29.7.0" - slash: "npm:^3.0.0" - strip-json-comments: "npm:^3.1.1" - peerDependencies: - "@types/node": "*" - ts-node: ">=9.0.0" - peerDependenciesMeta: - "@types/node": - optional: true - ts-node: - optional: true - checksum: 10c0/bab23c2eda1fff06e0d104b00d6adfb1d1aabb7128441899c9bff2247bd26710b050a5364281ce8d52b46b499153bf7e3ee88b19831a8f3451f1477a0246a0f1 - languageName: node - linkType: hard - "jest-diff@npm:^29.7.0": version: 29.7.0 resolution: "jest-diff@npm:29.7.0" @@ -7918,42 +7068,6 @@ __metadata: languageName: node linkType: hard -"jest-docblock@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-docblock@npm:29.7.0" - dependencies: - detect-newline: "npm:^3.0.0" - checksum: 10c0/d932a8272345cf6b6142bb70a2bb63e0856cc0093f082821577ea5bdf4643916a98744dfc992189d2b1417c38a11fa42466f6111526bc1fb81366f56410f3be9 - languageName: node - linkType: hard - -"jest-each@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-each@npm:29.7.0" - dependencies: - "@jest/types": "npm:^29.6.3" - chalk: "npm:^4.0.0" - jest-get-type: "npm:^29.6.3" - jest-util: "npm:^29.7.0" - pretty-format: "npm:^29.7.0" - checksum: 10c0/f7f9a90ebee80cc688e825feceb2613627826ac41ea76a366fa58e669c3b2403d364c7c0a74d862d469b103c843154f8456d3b1c02b487509a12afa8b59edbb4 - languageName: node - linkType: hard - -"jest-environment-node@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-environment-node@npm:29.7.0" - dependencies: - "@jest/environment": "npm:^29.7.0" - "@jest/fake-timers": "npm:^29.7.0" - "@jest/types": "npm:^29.6.3" - "@types/node": "npm:*" - jest-mock: "npm:^29.7.0" - jest-util: "npm:^29.7.0" - checksum: 10c0/61f04fec077f8b1b5c1a633e3612fc0c9aa79a0ab7b05600683428f1e01a4d35346c474bde6f439f9fcc1a4aa9a2861ff852d079a43ab64b02105d1004b2592b - languageName: node - linkType: hard - "jest-get-type@npm:^29.6.3": version: 29.6.3 resolution: "jest-get-type@npm:29.6.3" @@ -7961,39 +7075,6 @@ __metadata: languageName: node linkType: hard -"jest-haste-map@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-haste-map@npm:29.7.0" - dependencies: - "@jest/types": "npm:^29.6.3" - "@types/graceful-fs": "npm:^4.1.3" - "@types/node": "npm:*" - anymatch: "npm:^3.0.3" - fb-watchman: "npm:^2.0.0" - fsevents: "npm:^2.3.2" - graceful-fs: "npm:^4.2.9" - jest-regex-util: "npm:^29.6.3" - jest-util: "npm:^29.7.0" - jest-worker: "npm:^29.7.0" - micromatch: "npm:^4.0.4" - walker: "npm:^1.0.8" - dependenciesMeta: - fsevents: - optional: true - checksum: 10c0/2683a8f29793c75a4728787662972fedd9267704c8f7ef9d84f2beed9a977f1cf5e998c07b6f36ba5603f53cb010c911fe8cd0ac9886e073fe28ca66beefd30c - languageName: node - linkType: hard - -"jest-leak-detector@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-leak-detector@npm:29.7.0" - dependencies: - jest-get-type: "npm:^29.6.3" - pretty-format: "npm:^29.7.0" - checksum: 10c0/71bb9f77fc489acb842a5c7be030f2b9acb18574dc9fb98b3100fc57d422b1abc55f08040884bd6e6dbf455047a62f7eaff12aa4058f7cbdc11558718ca6a395 - languageName: node - linkType: hard - "jest-matcher-utils@npm:^29.7.0": version: 29.7.0 resolution: "jest-matcher-utils@npm:29.7.0" @@ -8023,29 +7104,6 @@ __metadata: languageName: node linkType: hard -"jest-mock@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-mock@npm:29.7.0" - dependencies: - "@jest/types": "npm:^29.6.3" - "@types/node": "npm:*" - jest-util: "npm:^29.7.0" - checksum: 10c0/7b9f8349ee87695a309fe15c46a74ab04c853369e5c40952d68061d9dc3159a0f0ed73e215f81b07ee97a9faaf10aebe5877a9d6255068a0977eae6a9ff1d5ac - languageName: node - linkType: hard - -"jest-pnp-resolver@npm:^1.2.2": - version: 1.2.3 - resolution: "jest-pnp-resolver@npm:1.2.3" - peerDependencies: - jest-resolve: "*" - peerDependenciesMeta: - jest-resolve: - optional: true - checksum: 10c0/86eec0c78449a2de733a6d3e316d49461af6a858070e113c97f75fb742a48c2396ea94150cbca44159ffd4a959f743a47a8b37a792ef6fdad2cf0a5cba973fac - languageName: node - linkType: hard - "jest-rdf@npm:^1.8.1": version: 1.8.1 resolution: "jest-rdf@npm:1.8.1" @@ -8058,128 +7116,7 @@ __metadata: languageName: node linkType: hard -"jest-regex-util@npm:^29.6.3": - version: 29.6.3 - resolution: "jest-regex-util@npm:29.6.3" - checksum: 10c0/4e33fb16c4f42111159cafe26397118dcfc4cf08bc178a67149fb05f45546a91928b820894572679d62559839d0992e21080a1527faad65daaae8743a5705a3b - languageName: node - linkType: hard - -"jest-resolve-dependencies@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-resolve-dependencies@npm:29.7.0" - dependencies: - jest-regex-util: "npm:^29.6.3" - jest-snapshot: "npm:^29.7.0" - checksum: 10c0/b6e9ad8ae5b6049474118ea6441dfddd385b6d1fc471db0136f7c8fbcfe97137a9665e4f837a9f49f15a29a1deb95a14439b7aec812f3f99d08f228464930f0d - languageName: node - linkType: hard - -"jest-resolve@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-resolve@npm:29.7.0" - dependencies: - chalk: "npm:^4.0.0" - graceful-fs: "npm:^4.2.9" - jest-haste-map: "npm:^29.7.0" - jest-pnp-resolver: "npm:^1.2.2" - jest-util: "npm:^29.7.0" - jest-validate: "npm:^29.7.0" - resolve: "npm:^1.20.0" - resolve.exports: "npm:^2.0.0" - slash: "npm:^3.0.0" - checksum: 10c0/59da5c9c5b50563e959a45e09e2eace783d7f9ac0b5dcc6375dea4c0db938d2ebda97124c8161310082760e8ebbeff9f6b177c15ca2f57fb424f637a5d2adb47 - languageName: node - linkType: hard - -"jest-runner@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-runner@npm:29.7.0" - dependencies: - "@jest/console": "npm:^29.7.0" - "@jest/environment": "npm:^29.7.0" - "@jest/test-result": "npm:^29.7.0" - "@jest/transform": "npm:^29.7.0" - "@jest/types": "npm:^29.6.3" - "@types/node": "npm:*" - chalk: "npm:^4.0.0" - emittery: "npm:^0.13.1" - graceful-fs: "npm:^4.2.9" - jest-docblock: "npm:^29.7.0" - jest-environment-node: "npm:^29.7.0" - jest-haste-map: "npm:^29.7.0" - jest-leak-detector: "npm:^29.7.0" - jest-message-util: "npm:^29.7.0" - jest-resolve: "npm:^29.7.0" - jest-runtime: "npm:^29.7.0" - jest-util: "npm:^29.7.0" - jest-watcher: "npm:^29.7.0" - jest-worker: "npm:^29.7.0" - p-limit: "npm:^3.1.0" - source-map-support: "npm:0.5.13" - checksum: 10c0/2194b4531068d939f14c8d3274fe5938b77fa73126aedf9c09ec9dec57d13f22c72a3b5af01ac04f5c1cf2e28d0ac0b4a54212a61b05f10b5d6b47f2a1097bb4 - languageName: node - linkType: hard - -"jest-runtime@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-runtime@npm:29.7.0" - dependencies: - "@jest/environment": "npm:^29.7.0" - "@jest/fake-timers": "npm:^29.7.0" - "@jest/globals": "npm:^29.7.0" - "@jest/source-map": "npm:^29.6.3" - "@jest/test-result": "npm:^29.7.0" - "@jest/transform": "npm:^29.7.0" - "@jest/types": "npm:^29.6.3" - "@types/node": "npm:*" - chalk: "npm:^4.0.0" - cjs-module-lexer: "npm:^1.0.0" - collect-v8-coverage: "npm:^1.0.0" - glob: "npm:^7.1.3" - graceful-fs: "npm:^4.2.9" - jest-haste-map: "npm:^29.7.0" - jest-message-util: "npm:^29.7.0" - jest-mock: "npm:^29.7.0" - jest-regex-util: "npm:^29.6.3" - jest-resolve: "npm:^29.7.0" - jest-snapshot: "npm:^29.7.0" - jest-util: "npm:^29.7.0" - slash: "npm:^3.0.0" - strip-bom: "npm:^4.0.0" - checksum: 10c0/7cd89a1deda0bda7d0941835434e44f9d6b7bd50b5c5d9b0fc9a6c990b2d4d2cab59685ab3cb2850ed4cc37059f6de903af5a50565d7f7f1192a77d3fd6dd2a6 - languageName: node - linkType: hard - -"jest-snapshot@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-snapshot@npm:29.7.0" - dependencies: - "@babel/core": "npm:^7.11.6" - "@babel/generator": "npm:^7.7.2" - "@babel/plugin-syntax-jsx": "npm:^7.7.2" - "@babel/plugin-syntax-typescript": "npm:^7.7.2" - "@babel/types": "npm:^7.3.3" - "@jest/expect-utils": "npm:^29.7.0" - "@jest/transform": "npm:^29.7.0" - "@jest/types": "npm:^29.6.3" - babel-preset-current-node-syntax: "npm:^1.0.0" - chalk: "npm:^4.0.0" - expect: "npm:^29.7.0" - graceful-fs: "npm:^4.2.9" - jest-diff: "npm:^29.7.0" - jest-get-type: "npm:^29.6.3" - jest-matcher-utils: "npm:^29.7.0" - jest-message-util: "npm:^29.7.0" - jest-util: "npm:^29.7.0" - natural-compare: "npm:^1.4.0" - pretty-format: "npm:^29.7.0" - semver: "npm:^7.5.3" - checksum: 10c0/6e9003c94ec58172b4a62864a91c0146513207bedf4e0a06e1e2ac70a4484088a2683e3a0538d8ea913bcfd53dc54a9b98a98cdfa562e7fe1d1339aeae1da570 - languageName: node - linkType: hard - -"jest-util@npm:^29.0.0, jest-util@npm:^29.7.0": +"jest-util@npm:^29.7.0": version: 29.7.0 resolution: "jest-util@npm:29.7.0" dependencies: @@ -8193,67 +7130,6 @@ __metadata: languageName: node linkType: hard -"jest-validate@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-validate@npm:29.7.0" - dependencies: - "@jest/types": "npm:^29.6.3" - camelcase: "npm:^6.2.0" - chalk: "npm:^4.0.0" - jest-get-type: "npm:^29.6.3" - leven: "npm:^3.1.0" - pretty-format: "npm:^29.7.0" - checksum: 10c0/a20b930480c1ed68778c739f4739dce39423131bc070cd2505ddede762a5570a256212e9c2401b7ae9ba4d7b7c0803f03c5b8f1561c62348213aba18d9dbece2 - languageName: node - linkType: hard - -"jest-watcher@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-watcher@npm:29.7.0" - dependencies: - "@jest/test-result": "npm:^29.7.0" - "@jest/types": "npm:^29.6.3" - "@types/node": "npm:*" - ansi-escapes: "npm:^4.2.1" - chalk: "npm:^4.0.0" - emittery: "npm:^0.13.1" - jest-util: "npm:^29.7.0" - string-length: "npm:^4.0.1" - checksum: 10c0/ec6c75030562fc8f8c727cb8f3b94e75d831fc718785abfc196e1f2a2ebc9a2e38744a15147170039628a853d77a3b695561ce850375ede3a4ee6037a2574567 - languageName: node - linkType: hard - -"jest-worker@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-worker@npm:29.7.0" - dependencies: - "@types/node": "npm:*" - jest-util: "npm:^29.7.0" - merge-stream: "npm:^2.0.0" - supports-color: "npm:^8.0.0" - checksum: 10c0/5570a3a005b16f46c131968b8a5b56d291f9bbb85ff4217e31c80bd8a02e7de799e59a54b95ca28d5c302f248b54cbffde2d177c2f0f52ffcee7504c6eabf660 - languageName: node - linkType: hard - -"jest@npm:^29.7.0": - version: 29.7.0 - resolution: "jest@npm:29.7.0" - dependencies: - "@jest/core": "npm:^29.7.0" - "@jest/types": "npm:^29.6.3" - import-local: "npm:^3.0.2" - jest-cli: "npm:^29.7.0" - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - bin: - jest: bin/jest.js - checksum: 10c0/f40eb8171cf147c617cc6ada49d062fbb03b4da666cb8d39cdbfb739a7d75eea4c3ca150fb072d0d273dce0c753db4d0467d54906ad0293f59c54f9db4a09d8b - languageName: node - linkType: hard - "jose@npm:^4.15.2, jose@npm:^4.7.0": version: 4.15.4 resolution: "jose@npm:4.15.4" @@ -8274,16 +7150,11 @@ __metadata: checksum: 10c0/e248708d377aa058eacf2037b07ded847790e6de892bbad3dac0abba2e759cb9f121b00099a65195616badcb6eca8d14d975cb3e89eb1cfda644756402c8aeed languageName: node linkType: hard - -"js-yaml@npm:^3.13.1": - version: 3.14.1 - resolution: "js-yaml@npm:3.14.1" - dependencies: - argparse: "npm:^1.0.7" - esprima: "npm:^4.0.0" - bin: - js-yaml: bin/js-yaml.js - checksum: 10c0/6746baaaeac312c4db8e75fa22331d9a04cccb7792d126ed8ce6a0bbcfef0cedaddd0c5098fade53db067c09fe00aa1c957674b4765610a8b06a5a189e46433b + +"js-tokens@npm:^9.0.1": + version: 9.0.1 + resolution: "js-tokens@npm:9.0.1" + checksum: 10c0/68dcab8f233dde211a6b5fd98079783cbcd04b53617c1250e3553ee16ab3e6134f5e65478e41d82f6d351a052a63d71024553933808570f04dbf828d7921e80e languageName: node linkType: hard @@ -8298,15 +7169,6 @@ __metadata: languageName: node linkType: hard -"jsesc@npm:^2.5.1": - version: 2.5.2 - resolution: "jsesc@npm:2.5.2" - bin: - jsesc: bin/jsesc - checksum: 10c0/dbf59312e0ebf2b4405ef413ec2b25abb5f8f4d9bc5fb8d9f90381622ebca5f2af6a6aa9a8578f65903f9e33990a6dc798edd0ce5586894bf0e9e31803a1de88 - languageName: node - linkType: hard - "jsesc@npm:^3.0.2": version: 3.0.2 resolution: "jsesc@npm:3.0.2" @@ -8344,15 +7206,6 @@ __metadata: languageName: node linkType: hard -"json5@npm:^2.2.3": - version: 2.2.3 - resolution: "json5@npm:2.2.3" - bin: - json5: lib/cli.js - checksum: 10c0/5a04eed94810fa55c5ea138b2f7a5c12b97c3750bc63d11e511dcecbfef758003861522a070c2272764ee0f4e3e323862f386945aeb5b85b87ee43f084ba586c - languageName: node - linkType: hard - "jsonc-parser@npm:3.3.1": version: 3.3.1 resolution: "jsonc-parser@npm:3.3.1" @@ -8600,13 +7453,6 @@ __metadata: languageName: node linkType: hard -"leven@npm:^3.1.0": - version: 3.1.0 - resolution: "leven@npm:3.1.0" - checksum: 10c0/cd778ba3fbab0f4d0500b7e87d1f6e1f041507c56fdcd47e8256a3012c98aaee371d4c15e0a76e0386107af2d42e2b7466160a2d80688aaa03e66e49949f42df - languageName: node - linkType: hard - "levn@npm:^0.4.1": version: 0.4.1 resolution: "levn@npm:0.4.1" @@ -8656,13 +7502,6 @@ __metadata: languageName: node linkType: hard -"lodash.memoize@npm:4.x": - version: 4.1.2 - resolution: "lodash.memoize@npm:4.1.2" - checksum: 10c0/c8713e51eccc650422716a14cece1809cfe34bc5ab5e242b7f8b4e2241c2483697b971a604252807689b9dd69bfe3a98852e19a5b89d506b000b4187a1285df8 - languageName: node - linkType: hard - "lodash.merge@npm:^4.6.2": version: 4.6.2 resolution: "lodash.merge@npm:4.6.2" @@ -8735,6 +7574,13 @@ __metadata: languageName: node linkType: hard +"loupe@npm:^3.1.0, loupe@npm:^3.1.4": + version: 3.1.4 + resolution: "loupe@npm:3.1.4" + checksum: 10c0/5c2e6aefaad25f812d361c750b8cf4ff91d68de289f141d7c85c2ce9bb79eeefa06a93c85f7b87cba940531ed8f15e492f32681d47eed23842ad1963eb3a154d + languageName: node + linkType: hard + "lowercase-keys@npm:^3.0.0": version: 3.0.0 resolution: "lowercase-keys@npm:3.0.0" @@ -8749,15 +7595,6 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:^5.1.1": - version: 5.1.1 - resolution: "lru-cache@npm:5.1.1" - dependencies: - yallist: "npm:^3.0.2" - checksum: 10c0/89b2ef2ef45f543011e38737b8a8622a2f8998cddf0e5437174ef8f1f70a8b9d14a918ab3e232cb3ba343b7abddffa667f0b59075b2b80e6b4d63c3de6127482 - languageName: node - linkType: hard - "lru-cache@npm:^6.0.0": version: 6.0.0 resolution: "lru-cache@npm:6.0.0" @@ -8767,16 +7604,16 @@ __metadata: languageName: node linkType: hard -"make-dir@npm:^4.0.0": - version: 4.0.0 - resolution: "make-dir@npm:4.0.0" +"magic-string@npm:^0.30.17": + version: 0.30.17 + resolution: "magic-string@npm:0.30.17" dependencies: - semver: "npm:^7.5.3" - checksum: 10c0/69b98a6c0b8e5c4fe9acb61608a9fbcfca1756d910f51e5dbe7a9e5cfb74fca9b8a0c8a0ffdf1294a740826c1ab4871d5bf3f62f72a3049e5eac6541ddffed68 + "@jridgewell/sourcemap-codec": "npm:^1.5.0" + checksum: 10c0/16826e415d04b88378f200fe022b53e638e3838b9e496edda6c0e086d7753a44a6ed187adc72d19f3623810589bf139af1a315541cd6a26ae0771a0193eaf7b8 languageName: node linkType: hard -"make-error@npm:1.x, make-error@npm:^1.1.1": +"make-error@npm:^1.1.1": version: 1.3.6 resolution: "make-error@npm:1.3.6" checksum: 10c0/171e458d86854c6b3fc46610cfacf0b45149ba043782558c6875d9f42f222124384ad0b468c92e996d815a8a2003817a710c0a160e49c1c394626f76fa45396f @@ -8802,15 +7639,6 @@ __metadata: languageName: node linkType: hard -"makeerror@npm:1.0.12": - version: 1.0.12 - resolution: "makeerror@npm:1.0.12" - dependencies: - tmpl: "npm:1.0.5" - checksum: 10c0/b0e6e599780ce6bab49cc413eba822f7d1f0dfebd1c103eaa3785c59e43e22c59018323cf9e1708f0ef5329e94a745d163fcbb6bff8e4c6742f9be9e86f3500c - languageName: node - linkType: hard - "map-obj@npm:^1.0.0": version: 1.0.1 resolution: "map-obj@npm:1.0.1" @@ -8978,7 +7806,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^3.0.4, minimatch@npm:^3.0.5, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": +"minimatch@npm:^3.0.5, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": version: 3.1.2 resolution: "minimatch@npm:3.1.2" dependencies: @@ -9107,14 +7935,7 @@ __metadata: languageName: node linkType: hard -"ms@npm:2.1.2": - version: 2.1.2 - resolution: "ms@npm:2.1.2" - checksum: 10c0/a437714e2f90dbf881b5191d35a6db792efbca5badf112f87b9e1c712aace4b4b9b742dd6537f3edf90fd6f684de897cec230abde57e87883766712ddda297cc - languageName: node - linkType: hard - -"ms@npm:^2.1.1": +"ms@npm:^2.1.1, ms@npm:^2.1.3": version: 2.1.3 resolution: "ms@npm:2.1.3" checksum: 10c0/d924b57e7312b3b63ad21fc5b3dc0af5e78d61a1fc7cfb5457edaf26326bf62be5307cc87ffb6862ef1c2b33b0233cdb5d4f01c4c958cc0d660948b65a287a48 @@ -9131,6 +7952,15 @@ __metadata: languageName: node linkType: hard +"nanoid@npm:^3.3.11": + version: 3.3.11 + resolution: "nanoid@npm:3.3.11" + bin: + nanoid: bin/nanoid.cjs + checksum: 10c0/40e7f70b3d15f725ca072dfc4f74e81fcf1fbb02e491cf58ac0c79093adc9b0a73b152bcde57df4b79cd097e13023d7504acb38404a4da7bc1cd8e887b82fe0b + languageName: node + linkType: hard + "nanoid@npm:^5.0.4": version: 5.0.4 resolution: "nanoid@npm:5.0.4" @@ -9227,20 +8057,6 @@ __metadata: languageName: node linkType: hard -"node-int64@npm:^0.4.0": - version: 0.4.0 - resolution: "node-int64@npm:0.4.0" - checksum: 10c0/a6a4d8369e2f2720e9c645255ffde909c0fbd41c92ea92a5607fc17055955daac99c1ff589d421eee12a0d24e99f7bfc2aabfeb1a4c14742f6c099a51863f31a - languageName: node - linkType: hard - -"node-releases@npm:^2.0.13": - version: 2.0.13 - resolution: "node-releases@npm:2.0.13" - checksum: 10c0/2fb44bf70fc949d27f3a48a7fd1a9d1d603ddad4ccd091f26b3fb8b1da976605d919330d7388ccd55ca2ade0dc8b2e12841ba19ef249c8bb29bf82532d401af7 - languageName: node - linkType: hard - "nodemailer@npm:^6.9.9": version: 6.10.0 resolution: "nodemailer@npm:6.10.0" @@ -9283,13 +8099,6 @@ __metadata: languageName: node linkType: hard -"normalize-path@npm:^3.0.0": - version: 3.0.0 - resolution: "normalize-path@npm:3.0.0" - checksum: 10c0/e008c8142bcc335b5e38cf0d63cfd39d6cf2d97480af9abdbe9a439221fd4d749763bab492a8ee708ce7a194bb00c9da6d0a115018672310850489137b3da046 - languageName: node - linkType: hard - "normalize-url@npm:^8.0.0": version: 8.0.0 resolution: "normalize-url@npm:8.0.0" @@ -9486,7 +8295,7 @@ __metadata: languageName: node linkType: hard -"p-limit@npm:^3.0.2, p-limit@npm:^3.1.0": +"p-limit@npm:^3.0.2": version: 3.1.0 resolution: "p-limit@npm:3.1.0" dependencies: @@ -9616,33 +8425,49 @@ __metadata: languageName: node linkType: hard -"picocolors@npm:^1.0.0": - version: 1.0.0 - resolution: "picocolors@npm:1.0.0" - checksum: 10c0/20a5b249e331c14479d94ec6817a182fd7a5680debae82705747b2db7ec50009a5f6648d0621c561b0572703f84dbef0858abcbd5856d3c5511426afcb1961f7 +"pathe@npm:^2.0.3": + version: 2.0.3 + resolution: "pathe@npm:2.0.3" + checksum: 10c0/c118dc5a8b5c4166011b2b70608762e260085180bb9e33e80a50dcdb1e78c010b1624f4280c492c92b05fc276715a4c357d1f9edc570f8f1b3d90b6839ebaca1 + languageName: node + linkType: hard + +"pathval@npm:^2.0.0": + version: 2.0.0 + resolution: "pathval@npm:2.0.0" + checksum: 10c0/602e4ee347fba8a599115af2ccd8179836a63c925c23e04bd056d0674a64b39e3a081b643cc7bc0b84390517df2d800a46fcc5598d42c155fe4977095c2f77c5 + languageName: node + linkType: hard + +"picocolors@npm:^1.1.1": + version: 1.1.1 + resolution: "picocolors@npm:1.1.1" + checksum: 10c0/e2e3e8170ab9d7c7421969adaa7e1b31434f789afb9b3f115f6b96d91945041ac3ceb02e9ec6fe6510ff036bcc0bf91e69a1772edc0b707e12b19c0f2d6bcf58 languageName: node linkType: hard -"picomatch@npm:^2.0.4, picomatch@npm:^2.2.3, picomatch@npm:^2.3.1": +"picomatch@npm:^2.2.3, picomatch@npm:^2.3.1": version: 2.3.1 resolution: "picomatch@npm:2.3.1" checksum: 10c0/26c02b8d06f03206fc2ab8d16f19960f2ff9e81a658f831ecb656d8f17d9edc799e8364b1f4a7873e89d9702dff96204be0fa26fe4181f6843f040f819dac4be languageName: node linkType: hard -"pirates@npm:^4.0.4": - version: 4.0.6 - resolution: "pirates@npm:4.0.6" - checksum: 10c0/00d5fa51f8dded94d7429700fb91a0c1ead00ae2c7fd27089f0c5b63e6eca36197fe46384631872690a66f390c5e27198e99006ab77ae472692ab9c2ca903f36 +"picomatch@npm:^4.0.2": + version: 4.0.2 + resolution: "picomatch@npm:4.0.2" + checksum: 10c0/7c51f3ad2bb42c776f49ebf964c644958158be30d0a510efd5a395e8d49cb5acfed5b82c0c5b365523ce18e6ab85013c9ebe574f60305892ec3fa8eee8304ccc languageName: node linkType: hard -"pkg-dir@npm:^4.2.0": - version: 4.2.0 - resolution: "pkg-dir@npm:4.2.0" +"postcss@npm:^8.5.3": + version: 8.5.6 + resolution: "postcss@npm:8.5.6" dependencies: - find-up: "npm:^4.0.0" - checksum: 10c0/c56bda7769e04907a88423feb320babaed0711af8c436ce3e56763ab1021ba107c7b0cafb11cde7529f669cfc22bffcaebffb573645cbd63842ea9fb17cd7728 + nanoid: "npm:^3.3.11" + picocolors: "npm:^1.1.1" + source-map-js: "npm:^1.2.1" + checksum: 10c0/5127cc7c91ed7a133a1b7318012d8bfa112da9ef092dddf369ae699a1f10ebbd89b1b9f25f3228795b84585c72aabd5ced5fc11f2ba467eedf7b081a66fad024 languageName: node linkType: hard @@ -9702,7 +8527,7 @@ __metadata: languageName: node linkType: hard -"prompts@npm:2.4.2, prompts@npm:^2.0.1": +"prompts@npm:2.4.2": version: 2.4.2 resolution: "prompts@npm:2.4.2" dependencies: @@ -9747,7 +8572,7 @@ __metadata: languageName: node linkType: hard -"pure-rand@npm:^6.0.0, pure-rand@npm:^6.1.0": +"pure-rand@npm:^6.1.0": version: 6.1.0 resolution: "pure-rand@npm:6.1.0" checksum: 10c0/1abe217897bf74dcb3a0c9aba3555fe975023147b48db540aa2faf507aee91c03bf54f6aef0eb2bf59cc259a16d06b28eca37f0dc426d94f4692aeff02fb0e65 @@ -10291,15 +9116,6 @@ __metadata: languageName: node linkType: hard -"resolve-cwd@npm:^3.0.0": - version: 3.0.0 - resolution: "resolve-cwd@npm:3.0.0" - dependencies: - resolve-from: "npm:^5.0.0" - checksum: 10c0/e608a3ebd15356264653c32d7ecbc8fd702f94c6703ea4ac2fb81d9c359180cba0ae2e6b71faa446631ed6145454d5a56b227efc33a2d40638ac13f8beb20ee4 - languageName: node - linkType: hard - "resolve-from@npm:5.0.0, resolve-from@npm:^5.0.0": version: 5.0.0 resolution: "resolve-from@npm:5.0.0" @@ -10330,14 +9146,7 @@ __metadata: languageName: node linkType: hard -"resolve.exports@npm:^2.0.0": - version: 2.0.2 - resolution: "resolve.exports@npm:2.0.2" - checksum: 10c0/cc4cffdc25447cf34730f388dca5021156ba9302a3bad3d7f168e790dc74b2827dff603f1bc6ad3d299bac269828dca96dd77e036dc9fba6a2a1807c47ab5c98 - languageName: node - linkType: hard - -"resolve@npm:^1.1.6, resolve@npm:^1.10.0, resolve@npm:^1.20.0": +"resolve@npm:^1.1.6, resolve@npm:^1.10.0": version: 1.22.8 resolution: "resolve@npm:1.22.8" dependencies: @@ -10350,7 +9159,7 @@ __metadata: languageName: node linkType: hard -"resolve@patch:resolve@npm%3A^1.1.6#optional!builtin, resolve@patch:resolve@npm%3A^1.10.0#optional!builtin, resolve@patch:resolve@npm%3A^1.20.0#optional!builtin": +"resolve@patch:resolve@npm%3A^1.1.6#optional!builtin, resolve@patch:resolve@npm%3A^1.10.0#optional!builtin": version: 1.22.8 resolution: "resolve@patch:resolve@npm%3A1.22.8#optional!builtin::version=1.22.8&hash=c3c19d" dependencies: @@ -10414,6 +9223,81 @@ __metadata: languageName: node linkType: hard +"rollup@npm:^4.34.9": + version: 4.44.0 + resolution: "rollup@npm:4.44.0" + dependencies: + "@rollup/rollup-android-arm-eabi": "npm:4.44.0" + "@rollup/rollup-android-arm64": "npm:4.44.0" + "@rollup/rollup-darwin-arm64": "npm:4.44.0" + "@rollup/rollup-darwin-x64": "npm:4.44.0" + "@rollup/rollup-freebsd-arm64": "npm:4.44.0" + "@rollup/rollup-freebsd-x64": "npm:4.44.0" + "@rollup/rollup-linux-arm-gnueabihf": "npm:4.44.0" + "@rollup/rollup-linux-arm-musleabihf": "npm:4.44.0" + "@rollup/rollup-linux-arm64-gnu": "npm:4.44.0" + "@rollup/rollup-linux-arm64-musl": "npm:4.44.0" + "@rollup/rollup-linux-loongarch64-gnu": "npm:4.44.0" + "@rollup/rollup-linux-powerpc64le-gnu": "npm:4.44.0" + "@rollup/rollup-linux-riscv64-gnu": "npm:4.44.0" + "@rollup/rollup-linux-riscv64-musl": "npm:4.44.0" + "@rollup/rollup-linux-s390x-gnu": "npm:4.44.0" + "@rollup/rollup-linux-x64-gnu": "npm:4.44.0" + "@rollup/rollup-linux-x64-musl": "npm:4.44.0" + "@rollup/rollup-win32-arm64-msvc": "npm:4.44.0" + "@rollup/rollup-win32-ia32-msvc": "npm:4.44.0" + "@rollup/rollup-win32-x64-msvc": "npm:4.44.0" + "@types/estree": "npm:1.0.8" + fsevents: "npm:~2.3.2" + dependenciesMeta: + "@rollup/rollup-android-arm-eabi": + optional: true + "@rollup/rollup-android-arm64": + optional: true + "@rollup/rollup-darwin-arm64": + optional: true + "@rollup/rollup-darwin-x64": + optional: true + "@rollup/rollup-freebsd-arm64": + optional: true + "@rollup/rollup-freebsd-x64": + optional: true + "@rollup/rollup-linux-arm-gnueabihf": + optional: true + "@rollup/rollup-linux-arm-musleabihf": + optional: true + "@rollup/rollup-linux-arm64-gnu": + optional: true + "@rollup/rollup-linux-arm64-musl": + optional: true + "@rollup/rollup-linux-loongarch64-gnu": + optional: true + "@rollup/rollup-linux-powerpc64le-gnu": + optional: true + "@rollup/rollup-linux-riscv64-gnu": + optional: true + "@rollup/rollup-linux-riscv64-musl": + optional: true + "@rollup/rollup-linux-s390x-gnu": + optional: true + "@rollup/rollup-linux-x64-gnu": + optional: true + "@rollup/rollup-linux-x64-musl": + optional: true + "@rollup/rollup-win32-arm64-msvc": + optional: true + "@rollup/rollup-win32-ia32-msvc": + optional: true + "@rollup/rollup-win32-x64-msvc": + optional: true + fsevents: + optional: true + bin: + rollup: dist/bin/rollup + checksum: 10c0/ff3e0741f2fc7b7b183079628cf50fcfc9163bef86ecfbc9f4e4023adfdee375b7075940963514e2bc4969764688d38d67095bce038b0ad5d572207f114afff5 + languageName: node + linkType: hard + "run-parallel@npm:^1.1.9": version: 1.2.0 resolution: "run-parallel@npm:1.2.0" @@ -10471,7 +9355,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:7.7.0, semver@npm:^7.3.2, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.5.3, semver@npm:^7.5.4": +"semver@npm:7.7.0, semver@npm:^7.3.2, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7": version: 7.7.0 resolution: "semver@npm:7.7.0" bin: @@ -10480,15 +9364,6 @@ __metadata: languageName: node linkType: hard -"semver@npm:^6.3.0, semver@npm:^6.3.1": - version: 6.3.1 - resolution: "semver@npm:6.3.1" - bin: - semver: bin/semver.js - checksum: 10c0/e3d79b609071caa78bcb6ce2ad81c7966a46a7431d9d58b8800cfa9cb6a63699b3899a0e4bcce36167a284578212d9ae6942b6929ba4aa5015c079a67751d42d - languageName: node - linkType: hard - "setimmediate@npm:^1.0.5": version: 1.0.5 resolution: "setimmediate@npm:1.0.5" @@ -10565,7 +9440,14 @@ __metadata: languageName: node linkType: hard -"signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": +"siginfo@npm:^2.0.0": + version: 2.0.0 + resolution: "siginfo@npm:2.0.0" + checksum: 10c0/3def8f8e516fbb34cb6ae415b07ccc5d9c018d85b4b8611e3dc6f8be6d1899f693a4382913c9ed51a06babb5201639d76453ab297d1c54a456544acf5c892e34 + languageName: node + linkType: hard + +"signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.3": version: 3.0.7 resolution: "signal-exit@npm:3.0.7" checksum: 10c0/25d272fa73e146048565e08f3309d5b942c1979a6f4a58a8c59d5fa299728e9c2fcd1a759ec870863b1fd38653670240cd420dad2ad9330c71f36608a6a1c912 @@ -10637,17 +9519,14 @@ __metadata: languageName: node linkType: hard -"source-map-support@npm:0.5.13": - version: 0.5.13 - resolution: "source-map-support@npm:0.5.13" - dependencies: - buffer-from: "npm:^1.0.0" - source-map: "npm:^0.6.0" - checksum: 10c0/137539f8c453fa0f496ea42049ab5da4569f96781f6ac8e5bfda26937be9494f4e8891f523c5f98f0e85f71b35d74127a00c46f83f6a4f54672b58d53202565e +"source-map-js@npm:^1.2.1": + version: 1.2.1 + resolution: "source-map-js@npm:1.2.1" + checksum: 10c0/7bda1fc4c197e3c6ff17de1b8b2c20e60af81b63a52cb32ec5a5d67a20a7d42651e2cb34ebe93833c5a2a084377e17455854fee3e21e7925c64a51b6a52b0faf languageName: node linkType: hard -"source-map@npm:^0.6.0, source-map@npm:^0.6.1": +"source-map@npm:^0.6.1": version: 0.6.1 resolution: "source-map@npm:0.6.1" checksum: 10c0/ab55398007c5e5532957cb0beee2368529618ac0ab372d789806f5718123cc4367d57de3904b4e6a4170eb5a0b0f41373066d02ca0735a0c4d75c7d328d3e011 @@ -10771,13 +9650,6 @@ __metadata: languageName: node linkType: hard -"sprintf-js@npm:~1.0.2": - version: 1.0.3 - resolution: "sprintf-js@npm:1.0.3" - checksum: 10c0/ecadcfe4c771890140da5023d43e190b7566d9cf8b2d238600f31bec0fc653f328da4450eb04bd59a431771a8e9cc0e118f0aa3974b683a4981b4e07abc2a5bb - languageName: node - linkType: hard - "ssri@npm:^10.0.0": version: 10.0.5 resolution: "ssri@npm:10.0.5" @@ -10803,6 +9675,13 @@ __metadata: languageName: node linkType: hard +"stackback@npm:0.0.2": + version: 0.0.2 + resolution: "stackback@npm:0.0.2" + checksum: 10c0/89a1416668f950236dd5ac9f9a6b2588e1b9b62b1b6ad8dff1bfc5d1a15dbf0aafc9b52d2226d00c28dffff212da464eaeebfc6b7578b9d180cef3e3782c5983 + languageName: node + linkType: hard + "standard-as-callback@npm:^2.1.0": version: 2.1.0 resolution: "standard-as-callback@npm:2.1.0" @@ -10824,6 +9703,13 @@ __metadata: languageName: node linkType: hard +"std-env@npm:^3.9.0": + version: 3.9.0 + resolution: "std-env@npm:3.9.0" + checksum: 10c0/4a6f9218aef3f41046c3c7ecf1f98df00b30a07f4f35c6d47b28329bc2531eef820828951c7d7b39a1c5eb19ad8a46e3ddfc7deb28f0a2f3ceebee11bab7ba50 + languageName: node + linkType: hard + "stdin-discarder@npm:^0.2.2": version: 0.2.2 resolution: "stdin-discarder@npm:0.2.2" @@ -10865,16 +9751,6 @@ __metadata: languageName: node linkType: hard -"string-length@npm:^4.0.1": - version: 4.0.2 - resolution: "string-length@npm:4.0.2" - dependencies: - char-regex: "npm:^1.0.2" - strip-ansi: "npm:^6.0.0" - checksum: 10c0/1cd77409c3d7db7bc59406f6bcc9ef0783671dcbabb23597a1177c166906ef2ee7c8290f78cae73a8aec858768f189d2cb417797df5e15ec4eb5e16b3346340c - languageName: node - linkType: hard - "string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": version: 4.2.3 resolution: "string-width@npm:4.2.3" @@ -10965,6 +9841,15 @@ __metadata: languageName: node linkType: hard +"strip-literal@npm:^3.0.0": + version: 3.0.0 + resolution: "strip-literal@npm:3.0.0" + dependencies: + js-tokens: "npm:^9.0.1" + checksum: 10c0/d81657f84aba42d4bbaf2a677f7e7f34c1f3de5a6726db8bc1797f9c0b303ba54d4660383a74bde43df401cf37cce1dff2c842c55b077a4ceee11f9e31fba828 + languageName: node + linkType: hard + "structured-headers@npm:^1.0.1": version: 1.0.1 resolution: "structured-headers@npm:1.0.1" @@ -10990,15 +9875,6 @@ __metadata: languageName: node linkType: hard -"supports-color@npm:^8.0.0": - version: 8.1.1 - resolution: "supports-color@npm:8.1.1" - dependencies: - has-flag: "npm:^4.0.0" - checksum: 10c0/ea1d3c275dd604c974670f63943ed9bd83623edc102430c05adb8efc56ba492746b6e95386e7831b872ec3807fd89dd8eb43f735195f37b5ec343e4234cc7e89 - languageName: node - linkType: hard - "supports-preserve-symlinks-flag@npm:^1.0.0": version: 1.0.0 resolution: "supports-preserve-symlinks-flag@npm:1.0.0" @@ -11080,17 +9956,6 @@ __metadata: languageName: node linkType: hard -"test-exclude@npm:^6.0.0": - version: 6.0.0 - resolution: "test-exclude@npm:6.0.0" - dependencies: - "@istanbuljs/schema": "npm:^0.1.2" - glob: "npm:^7.1.4" - minimatch: "npm:^3.0.4" - checksum: 10c0/019d33d81adff3f9f1bfcff18125fb2d3c65564f437d9be539270ee74b994986abb8260c7c2ce90e8f30162178b09dbbce33c6389273afac4f36069c48521f57 - languageName: node - linkType: hard - "text-extensions@npm:^1.0.0": version: 1.9.0 resolution: "text-extensions@npm:1.9.0" @@ -11142,24 +10007,55 @@ __metadata: languageName: node linkType: hard -"tmp@npm:^0.2.1, tmp@npm:^0.2.3": - version: 0.2.3 - resolution: "tmp@npm:0.2.3" - checksum: 10c0/3e809d9c2f46817475b452725c2aaa5d11985cf18d32a7a970ff25b568438e2c076c2e8609224feef3b7923fa9749b74428e3e634f6b8e520c534eef2fd24125 +"tinybench@npm:^2.9.0": + version: 2.9.0 + resolution: "tinybench@npm:2.9.0" + checksum: 10c0/c3500b0f60d2eb8db65250afe750b66d51623057ee88720b7f064894a6cb7eb93360ca824a60a31ab16dab30c7b1f06efe0795b352e37914a9d4bad86386a20c languageName: node linkType: hard -"tmpl@npm:1.0.5": - version: 1.0.5 - resolution: "tmpl@npm:1.0.5" - checksum: 10c0/f935537799c2d1922cb5d6d3805f594388f75338fe7a4a9dac41504dd539704ca4db45b883b52e7b0aa5b2fd5ddadb1452bf95cd23a69da2f793a843f9451cc9 +"tinyexec@npm:^0.3.2": + version: 0.3.2 + resolution: "tinyexec@npm:0.3.2" + checksum: 10c0/3efbf791a911be0bf0821eab37a3445c2ba07acc1522b1fa84ae1e55f10425076f1290f680286345ed919549ad67527d07281f1c19d584df3b74326909eb1f90 + languageName: node + linkType: hard + +"tinyglobby@npm:^0.2.13, tinyglobby@npm:^0.2.14": + version: 0.2.14 + resolution: "tinyglobby@npm:0.2.14" + dependencies: + fdir: "npm:^6.4.4" + picomatch: "npm:^4.0.2" + checksum: 10c0/f789ed6c924287a9b7d3612056ed0cda67306cd2c80c249fd280cf1504742b12583a2089b61f4abbd24605f390809017240e250241f09938054c9b363e51c0a6 + languageName: node + linkType: hard + +"tinypool@npm:^1.1.1": + version: 1.1.1 + resolution: "tinypool@npm:1.1.1" + checksum: 10c0/bf26727d01443061b04fa863f571016950888ea994ba0cd8cba3a1c51e2458d84574341ab8dbc3664f1c3ab20885c8cf9ff1cc4b18201f04c2cde7d317fff69b languageName: node linkType: hard -"to-fast-properties@npm:^2.0.0": +"tinyrainbow@npm:^2.0.0": version: 2.0.0 - resolution: "to-fast-properties@npm:2.0.0" - checksum: 10c0/b214d21dbfb4bce3452b6244b336806ffea9c05297148d32ebb428d5c43ce7545bdfc65a1ceb58c9ef4376a65c0cb2854d645f33961658b3e3b4f84910ddcdd7 + resolution: "tinyrainbow@npm:2.0.0" + checksum: 10c0/c83c52bef4e0ae7fb8ec6a722f70b5b6fa8d8be1c85792e829f56c0e1be94ab70b293c032dc5048d4d37cfe678f1f5babb04bdc65fd123098800148ca989184f + languageName: node + linkType: hard + +"tinyspy@npm:^4.0.3": + version: 4.0.3 + resolution: "tinyspy@npm:4.0.3" + checksum: 10c0/0a92a18b5350945cc8a1da3a22c9ad9f4e2945df80aaa0c43e1b3a3cfb64d8501e607ebf0305e048e3c3d3e0e7f8eb10cea27dc17c21effb73e66c4a3be36373 + languageName: node + linkType: hard + +"tmp@npm:^0.2.1, tmp@npm:^0.2.3": + version: 0.2.3 + resolution: "tmp@npm:0.2.3" + checksum: 10c0/3e809d9c2f46817475b452725c2aaa5d11985cf18d32a7a970ff25b568438e2c076c2e8609224feef3b7923fa9749b74428e3e634f6b8e520c534eef2fd24125 languageName: node linkType: hard @@ -11214,39 +10110,6 @@ __metadata: languageName: node linkType: hard -"ts-jest@npm:^29.1.2": - version: 29.1.2 - resolution: "ts-jest@npm:29.1.2" - dependencies: - bs-logger: "npm:0.x" - fast-json-stable-stringify: "npm:2.x" - jest-util: "npm:^29.0.0" - json5: "npm:^2.2.3" - lodash.memoize: "npm:4.x" - make-error: "npm:1.x" - semver: "npm:^7.5.3" - yargs-parser: "npm:^21.0.1" - peerDependencies: - "@babel/core": ">=7.0.0-beta.0 <8" - "@jest/types": ^29.0.0 - babel-jest: ^29.0.0 - jest: ^29.0.0 - typescript: ">=4.3 <6" - peerDependenciesMeta: - "@babel/core": - optional: true - "@jest/types": - optional: true - babel-jest: - optional: true - esbuild: - optional: true - bin: - ts-jest: cli.js - checksum: 10c0/c2f51f0241f89d127d41392decbcb83b5dfd5e57ab9d50220aa7b7e2f9b3f3b07ccdbba33311284df1c41941879e4ddfad44b15a9d0da4b74bd1b98702b729df - languageName: node - linkType: hard - "ts-node@npm:^10.8.1": version: 10.9.2 resolution: "ts-node@npm:10.9.2" @@ -11342,13 +10205,6 @@ __metadata: languageName: node linkType: hard -"type-detect@npm:4.0.8": - version: 4.0.8 - resolution: "type-detect@npm:4.0.8" - checksum: 10c0/8fb9a51d3f365a7de84ab7f73b653534b61b622aa6800aecdb0f1095a4a646d3f5eb295322127b6573db7982afcd40ab492d038cf825a42093a58b1e1353e0bd - languageName: node - linkType: hard - "type-fest@npm:^0.18.0": version: 0.18.1 resolution: "type-fest@npm:0.18.1" @@ -11363,13 +10219,6 @@ __metadata: languageName: node linkType: hard -"type-fest@npm:^0.21.3": - version: 0.21.3 - resolution: "type-fest@npm:0.21.3" - checksum: 10c0/902bd57bfa30d51d4779b641c2bc403cdf1371fb9c91d3c058b0133694fcfdb817aef07a47f40faf79039eecbaa39ee9d3c532deff244f3a19ce68cea71a61e8 - languageName: node - linkType: hard - "type-fest@npm:^0.6.0": version: 0.6.0 resolution: "type-fest@npm:0.6.0" @@ -11411,13 +10260,13 @@ __metadata: languageName: node linkType: hard -"typescript@npm:^5.3.3": - version: 5.3.3 - resolution: "typescript@npm:5.3.3" +"typescript@npm:^5.8.3": + version: 5.8.3 + resolution: "typescript@npm:5.8.3" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 10c0/e33cef99d82573624fc0f854a2980322714986bc35b9cb4d1ce736ed182aeab78e2cb32b385efa493b2a976ef52c53e20d6c6918312353a91850e2b76f1ea44f + checksum: 10c0/5f8bb01196e542e64d44db3d16ee0e4063ce4f3e3966df6005f2588e86d91c03e1fb131c2581baf0fb65ee79669eea6e161cd448178986587e9f6844446dbb48 languageName: node linkType: hard @@ -11431,13 +10280,13 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@npm%3A^5.3.3#optional!builtin": - version: 5.3.3 - resolution: "typescript@patch:typescript@npm%3A5.3.3#optional!builtin::version=5.3.3&hash=e012d7" +"typescript@patch:typescript@npm%3A^5.8.3#optional!builtin": + version: 5.8.3 + resolution: "typescript@patch:typescript@npm%3A5.8.3#optional!builtin::version=5.8.3&hash=5786d5" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 10c0/1d0a5f4ce496c42caa9a30e659c467c5686eae15d54b027ee7866744952547f1be1262f2d40de911618c242b510029d51d43ff605dba8fb740ec85ca2d3f9500 + checksum: 10c0/39117e346ff8ebd87ae1510b3a77d5d92dae5a89bde588c747d25da5c146603a99c8ee588c7ef80faaf123d89ed46f6dbd918d534d641083177d5fac38b8a1cb languageName: node linkType: hard @@ -11450,10 +10299,10 @@ __metadata: languageName: node linkType: hard -"undici-types@npm:~6.19.2": - version: 6.19.8 - resolution: "undici-types@npm:6.19.8" - checksum: 10c0/078afa5990fba110f6824823ace86073b4638f1d5112ee26e790155f481f2a868cc3e0615505b6f4282bdf74a3d8caad715fd809e870c2bb0704e3ea6082f344 +"undici-types@npm:~6.21.0": + version: 6.21.0 + resolution: "undici-types@npm:6.21.0" + checksum: 10c0/c01ed51829b10aa72fc3ce64b747f8e74ae9b60eafa19a7b46ef624403508a54c526ffab06a14a26b3120d055e1104d7abe7c9017e83ced038ea5cf52f8d5e04 languageName: node linkType: hard @@ -11512,20 +10361,6 @@ __metadata: languageName: node linkType: hard -"update-browserslist-db@npm:^1.0.13": - version: 1.0.13 - resolution: "update-browserslist-db@npm:1.0.13" - dependencies: - escalade: "npm:^3.1.1" - picocolors: "npm:^1.0.0" - peerDependencies: - browserslist: ">= 4.21.0" - bin: - update-browserslist-db: cli.js - checksum: 10c0/e52b8b521c78ce1e0c775f356cd16a9c22c70d25f3e01180839c407a5dc787fb05a13f67560cbaf316770d26fa99f78f1acd711b1b54a4f35d4820d4ea7136e6 - languageName: node - linkType: hard - "uri-js@npm:^4.2.2": version: 4.4.1 resolution: "uri-js@npm:4.4.1" @@ -11598,17 +10433,6 @@ __metadata: languageName: node linkType: hard -"v8-to-istanbul@npm:^9.0.1": - version: 9.1.3 - resolution: "v8-to-istanbul@npm:9.1.3" - dependencies: - "@jridgewell/trace-mapping": "npm:^0.3.12" - "@types/istanbul-lib-coverage": "npm:^2.0.1" - convert-source-map: "npm:^2.0.0" - checksum: 10c0/7acfc460731b629a0d547b231e9d510aaa826df67f4deeaeeb991b492f78faf3bb1aa4b54fa0f9b06d815bc69eb0a04a6c2180c16ba43a83cc5e5490fa160a96 - languageName: node - linkType: hard - "validate-iri@npm:^1.0.0": version: 1.0.1 resolution: "validate-iri@npm:1.0.1" @@ -11640,12 +10464,129 @@ __metadata: languageName: node linkType: hard -"walker@npm:^1.0.8": - version: 1.0.8 - resolution: "walker@npm:1.0.8" +"vite-node@npm:3.2.4": + version: 3.2.4 + resolution: "vite-node@npm:3.2.4" + dependencies: + cac: "npm:^6.7.14" + debug: "npm:^4.4.1" + es-module-lexer: "npm:^1.7.0" + pathe: "npm:^2.0.3" + vite: "npm:^5.0.0 || ^6.0.0 || ^7.0.0-0" + bin: + vite-node: vite-node.mjs + checksum: 10c0/6ceca67c002f8ef6397d58b9539f80f2b5d79e103a18367288b3f00a8ab55affa3d711d86d9112fce5a7fa658a212a087a005a045eb8f4758947dd99af2a6c6b + languageName: node + linkType: hard + +"vite@npm:^5.0.0 || ^6.0.0 || ^7.0.0-0, vite@npm:^6.3.5": + version: 6.3.5 + resolution: "vite@npm:6.3.5" dependencies: - makeerror: "npm:1.0.12" - checksum: 10c0/a17e037bccd3ca8a25a80cb850903facdfed0de4864bd8728f1782370715d679fa72e0a0f5da7c1c1379365159901e5935f35be531229da53bbfc0efdabdb48e + esbuild: "npm:^0.25.0" + fdir: "npm:^6.4.4" + fsevents: "npm:~2.3.3" + picomatch: "npm:^4.0.2" + postcss: "npm:^8.5.3" + rollup: "npm:^4.34.9" + tinyglobby: "npm:^0.2.13" + peerDependencies: + "@types/node": ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: ">=1.21.0" + less: "*" + lightningcss: ^1.21.0 + sass: "*" + sass-embedded: "*" + stylus: "*" + sugarss: "*" + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + dependenciesMeta: + fsevents: + optional: true + peerDependenciesMeta: + "@types/node": + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + bin: + vite: bin/vite.js + checksum: 10c0/df70201659085133abffc6b88dcdb8a57ef35f742a01311fc56a4cfcda6a404202860729cc65a2c401a724f6e25f9ab40ce4339ed4946f550541531ced6fe41c + languageName: node + linkType: hard + +"vitest@npm:^3.2.3": + version: 3.2.4 + resolution: "vitest@npm:3.2.4" + dependencies: + "@types/chai": "npm:^5.2.2" + "@vitest/expect": "npm:3.2.4" + "@vitest/mocker": "npm:3.2.4" + "@vitest/pretty-format": "npm:^3.2.4" + "@vitest/runner": "npm:3.2.4" + "@vitest/snapshot": "npm:3.2.4" + "@vitest/spy": "npm:3.2.4" + "@vitest/utils": "npm:3.2.4" + chai: "npm:^5.2.0" + debug: "npm:^4.4.1" + expect-type: "npm:^1.2.1" + magic-string: "npm:^0.30.17" + pathe: "npm:^2.0.3" + picomatch: "npm:^4.0.2" + std-env: "npm:^3.9.0" + tinybench: "npm:^2.9.0" + tinyexec: "npm:^0.3.2" + tinyglobby: "npm:^0.2.14" + tinypool: "npm:^1.1.1" + tinyrainbow: "npm:^2.0.0" + vite: "npm:^5.0.0 || ^6.0.0 || ^7.0.0-0" + vite-node: "npm:3.2.4" + why-is-node-running: "npm:^2.3.0" + peerDependencies: + "@edge-runtime/vm": "*" + "@types/debug": ^4.1.12 + "@types/node": ^18.0.0 || ^20.0.0 || >=22.0.0 + "@vitest/browser": 3.2.4 + "@vitest/ui": 3.2.4 + happy-dom: "*" + jsdom: "*" + peerDependenciesMeta: + "@edge-runtime/vm": + optional: true + "@types/debug": + optional: true + "@types/node": + optional: true + "@vitest/browser": + optional: true + "@vitest/ui": + optional: true + happy-dom: + optional: true + jsdom: + optional: true + bin: + vitest: vitest.mjs + checksum: 10c0/5bf53ede3ae6a0e08956d72dab279ae90503f6b5a05298a6a5e6ef47d2fd1ab386aaf48fafa61ed07a0ebfe9e371772f1ccbe5c258dd765206a8218bf2eb79eb languageName: node linkType: hard @@ -11702,6 +10643,18 @@ __metadata: languageName: node linkType: hard +"why-is-node-running@npm:^2.3.0": + version: 2.3.0 + resolution: "why-is-node-running@npm:2.3.0" + dependencies: + siginfo: "npm:^2.0.0" + stackback: "npm:0.0.2" + bin: + why-is-node-running: cli.js + checksum: 10c0/1cde0b01b827d2cf4cb11db962f3958b9175d5d9e7ac7361d1a7b0e2dc6069a263e69118bd974c4f6d0a890ef4eedfe34cf3d5167ec14203dbc9a18620537054 + languageName: node + linkType: hard + "winston-transport@npm:^4.5.0": version: 4.6.0 resolution: "winston-transport@npm:4.6.0" @@ -11768,16 +10721,6 @@ __metadata: languageName: node linkType: hard -"write-file-atomic@npm:^4.0.2": - version: 4.0.2 - resolution: "write-file-atomic@npm:4.0.2" - dependencies: - imurmurhash: "npm:^0.1.4" - signal-exit: "npm:^3.0.7" - checksum: 10c0/a2c282c95ef5d8e1c27b335ae897b5eca00e85590d92a3fd69a437919b7b93ff36a69ea04145da55829d2164e724bc62202cdb5f4b208b425aba0807889375c7 - languageName: node - linkType: hard - "ws@npm:^8.14.2": version: 8.16.0 resolution: "ws@npm:8.16.0" @@ -11807,13 +10750,6 @@ __metadata: languageName: node linkType: hard -"yallist@npm:^3.0.2": - version: 3.1.1 - resolution: "yallist@npm:3.1.1" - checksum: 10c0/c66a5c46bc89af1625476f7f0f2ec3653c1a1791d2f9407cfb4c2ba812a1e1c9941416d71ba9719876530e3340a99925f697142989371b72d93b9ee628afd8c1 - languageName: node - linkType: hard - "yallist@npm:^4.0.0": version: 4.0.0 resolution: "yallist@npm:4.0.0" @@ -11835,14 +10771,14 @@ __metadata: languageName: node linkType: hard -"yargs-parser@npm:^21.0.1, yargs-parser@npm:^21.1.1": +"yargs-parser@npm:^21.1.1": version: 21.1.1 resolution: "yargs-parser@npm:21.1.1" checksum: 10c0/f84b5e48169479d2f402239c59f084cfd1c3acc197a05c59b98bab067452e6b3ea46d4dd8ba2985ba7b3d32a343d77df0debd6b343e5dae3da2aab2cdf5886b2 languageName: node linkType: hard -"yargs@npm:^17.0.0, yargs@npm:^17.3.1, yargs@npm:^17.7.2": +"yargs@npm:^17.0.0, yargs@npm:^17.7.2": version: 17.7.2 resolution: "yargs@npm:17.7.2" dependencies: From 4cde160bda57500c5ef26036e56da7735b577808 Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Wed, 2 Jul 2025 08:42:08 +0200 Subject: [PATCH 30/62] test: Add testing to CI --- .github/workflows/push.yml | 34 ----------------------- .github/workflows/test.yml | 57 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 34 deletions(-) delete mode 100644 .github/workflows/push.yml create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml deleted file mode 100644 index efcaea13..00000000 --- a/.github/workflows/push.yml +++ /dev/null @@ -1,34 +0,0 @@ - -name: Push - -on: [ push ] - -jobs: - - build: - - runs-on: ubuntu-latest - - strategy: - fail-fast: false - matrix: - node-version: - - 20.x # Maintenance - - 22.x # Active - - 23.x # Current - - steps: - - - name: Checkout main branch - uses: actions/checkout@v4 - - - name: Enable Node.js Corepack - run: corepack enable - - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - - - name: Yarn install - run: yarn install diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..0e4f4078 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,57 @@ + +name: Test + +on: + push: + +concurrency: + group: ${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + + build: + + runs-on: ${{ matrix.operating-system }} + + strategy: + fail-fast: false + matrix: + operating-system: + - ubuntu-latest + # logtalk-actions/setup-swi-prolog@master does not work with windows, so a different solution would be required + # - windows-latest + node-version: + - 20.x + - 22.x + - 24.x + + steps: + + - name: Install prolog + uses: logtalk-actions/setup-swi-prolog@master + + - name: Clone EYE repo + uses: actions/checkout@v4 + with: + repository: eyereasoner/eye + + - name: Build EYE + run: bash install.sh --prefix=$HOME/.local # This folder is available on $PATH already + + - name: Checkout main branch + uses: actions/checkout@v4 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Enable Node.js Corepack + run: corepack enable + + - name: Yarn install + run: yarn install + + - name: Test + run: yarn test From de2fab00d4d4ad9501c4e415ba55f5fb716846b7 Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Tue, 8 Jul 2025 14:50:56 +0200 Subject: [PATCH 31/62] chore: Build startup scripts instead of using tsx For some reason, tsx could not exit gracefully when running these. Either it got stuck, or exited but left the process running. These issues did not occur with ts-node, but the solution in this commit seemed more acceptable than having a new dependency. Future work could include investigating if this is an issue with the server. --- packages/uma/bin/{demo.ts => demo.js} | 12 ++++++------ packages/uma/bin/{main.ts => main.js} | 12 ++++++------ packages/uma/bin/{odrl.ts => odrl.js} | 12 ++++++------ packages/uma/package.json | 6 +++--- 4 files changed, 21 insertions(+), 21 deletions(-) rename packages/uma/bin/{demo.ts => demo.js} (70%) rename packages/uma/bin/{main.ts => main.js} (69%) rename packages/uma/bin/{odrl.ts => odrl.js} (70%) diff --git a/packages/uma/bin/demo.ts b/packages/uma/bin/demo.js similarity index 70% rename from packages/uma/bin/demo.ts rename to packages/uma/bin/demo.js index eba751b7..4bde38ec 100644 --- a/packages/uma/bin/demo.ts +++ b/packages/uma/bin/demo.js @@ -1,6 +1,6 @@ -import * as path from 'path'; -import { ComponentsManager } from 'componentsjs'; -import { App, setGlobalLoggerFactory, WinstonLoggerFactory } from '@solid/community-server'; +const path = require('path'); +const { ComponentsManager } = require('componentsjs'); +const { setGlobalLoggerFactory, WinstonLoggerFactory } = require('@solid/community-server'); const protocol = 'http'; const host = 'localhost'; @@ -9,8 +9,8 @@ const port = 4000; const baseUrl = `${protocol}://${host}:${port}/uma`; const rootDir = path.join(__dirname, '../'); -export const launch: () => Promise = async () => { - const variables: Record = {}; +const launch = async () => { + const variables = {}; variables['urn:uma:variables:port'] = port; variables['urn:uma:variables:baseUrl'] = baseUrl; @@ -31,7 +31,7 @@ export const launch: () => Promise = async () => { await manager.configRegistry.register(configPath); - const umaServer: App = await manager.instantiate('urn:uma:default:App',{variables}); + const umaServer = await manager.instantiate('urn:uma:default:App',{variables}); await umaServer.start(); }; diff --git a/packages/uma/bin/main.ts b/packages/uma/bin/main.js similarity index 69% rename from packages/uma/bin/main.ts rename to packages/uma/bin/main.js index ef3d34a2..1f8dcc57 100644 --- a/packages/uma/bin/main.ts +++ b/packages/uma/bin/main.js @@ -1,6 +1,6 @@ -import * as path from 'path'; -import { ComponentsManager } from 'componentsjs'; -import { App, setGlobalLoggerFactory, WinstonLoggerFactory } from '@solid/community-server'; +const path = require('path'); +const { ComponentsManager } = require('componentsjs'); +const { setGlobalLoggerFactory, WinstonLoggerFactory } = require('@solid/community-server'); const protocol = 'http'; const host = 'localhost'; @@ -9,8 +9,8 @@ const port = 4000; const baseUrl = `${protocol}://${host}:${port}/uma`; const rootDir = path.join(__dirname, '../'); -export const launch: () => Promise = async () => { - const variables: Record = {}; +const launch = async () => { + const variables = {}; variables['urn:uma:variables:port'] = port; variables['urn:uma:variables:baseUrl'] = baseUrl; @@ -31,7 +31,7 @@ export const launch: () => Promise = async () => { await manager.configRegistry.register(configPath); - const umaServer: App = await manager.instantiate('urn:uma:default:App',{variables}); + const umaServer = await manager.instantiate('urn:uma:default:App',{variables}); await umaServer.start(); }; diff --git a/packages/uma/bin/odrl.ts b/packages/uma/bin/odrl.js similarity index 70% rename from packages/uma/bin/odrl.ts rename to packages/uma/bin/odrl.js index 3845767c..97785bdd 100644 --- a/packages/uma/bin/odrl.ts +++ b/packages/uma/bin/odrl.js @@ -1,6 +1,6 @@ -import { App, setGlobalLoggerFactory, WinstonLoggerFactory } from '@solid/community-server'; -import * as path from 'path'; -import { ComponentsManager } from 'componentsjs'; +const path = require('path'); +const { ComponentsManager } = require('componentsjs'); +const { setGlobalLoggerFactory, WinstonLoggerFactory } = require('@solid/community-server'); const protocol = 'http'; const host = 'localhost'; @@ -9,8 +9,8 @@ const port = 4000; const baseUrl = `${protocol}://${host}:${port}/uma`; const rootDir = path.join(__dirname, '../'); -export const launch: () => Promise = async () => { - const variables: Record = {}; +const launch = async () => { + const variables = {}; variables['urn:uma:variables:port'] = port; variables['urn:uma:variables:baseUrl'] = baseUrl; @@ -31,7 +31,7 @@ export const launch: () => Promise = async () => { await manager.configRegistry.register(configPath); - const umaServer: App = await manager.instantiate('urn:uma:default:App',{variables}); + const umaServer = await manager.instantiate('urn:uma:default:App',{variables}); await umaServer.start(); }; diff --git a/packages/uma/package.json b/packages/uma/package.json index c9577482..2938203f 100644 --- a/packages/uma/package.json +++ b/packages/uma/package.json @@ -55,9 +55,9 @@ "build": "yarn build:ts && yarn build:components", "build:ts": "yarn run -T tsc", "build:components": "yarn run -T componentsjs-generator -r sai-uma -s src -c dist/components -i .componentsignore --lenient", - "start": "yarn run -T tsx bin/main.ts", - "start:odrl": "yarn run -T tsx bin/odrl.ts", - "demo": "yarn run -T tsx bin/demo.ts" + "start": "node bin/main.js", + "start:odrl": "node bin/odrl.js", + "demo": "node bin/demo.js" }, "dependencies": { "@httpland/authorization-parser": "^1.1.0", From 3be468a01693363f5b09557aa8d08fe34e731802 Mon Sep 17 00:00:00 2001 From: lennertdr Date: Wed, 9 Jul 2025 10:27:58 +0200 Subject: [PATCH 32/62] Tests for DELETE endpoint --- docs/policy-management.md | 9 +++- packages/uma/src/routes/Policy.ts | 4 +- .../routeSpecific/policies/DeletePolicies.ts | 38 ++++++------- .../routeSpecific/policies/GetPolicies.ts | 7 ++- scripts/test-uma-ODRL-policy.ts | 54 ++++++++++++++++++- 5 files changed, 83 insertions(+), 29 deletions(-) diff --git a/docs/policy-management.md b/docs/policy-management.md index c07d33ac..ffde1696 100644 --- a/docs/policy-management.md +++ b/docs/policy-management.md @@ -15,7 +15,7 @@ The body is expected to represent a proper ODRL policy, although some [sanitizat ### Reading policies To read policies, two endpoints are implemented: - GET `/uma/policies`: get policy information that you are authorized to see, for every policy -- GET `/uma/policies/`: get policy information that you are authorized to see, for the policy with the requested ID +- GET `/uma/policies/`: get policy information that you are authorized to see, for the policy with the requested [URL encoded](#uri-encodig-decision) ID. The current algorithm will retrieve the IDs of the policies and its rules that you are authorized to see. It will seek information about those properties with **depth 1**. This is not representative for a lot of policies, hence a recursive algorithm will be implemented in the future. @@ -23,7 +23,12 @@ The current algorithm will retrieve the IDs of the policies and its rules that y Yet to be implemented... ### Deleting policies -Yet to be implemented... +To delete a policy, send a DELETE request to `/uma/policies/` with the URL encoded ID of the policy. The DELETE works like this: +1. Find the rules defined in the policy +2. Filter the rules that are assigned by the client, and delete them +3. Find out if there are rules not assigned by the client + * if there are other rules, we cannot delete the policy information as well + * if there are no other rules, we can delete the entire policy extra info diff --git a/packages/uma/src/routes/Policy.ts b/packages/uma/src/routes/Policy.ts index b1f40637..6f202637 100644 --- a/packages/uma/src/routes/Policy.ts +++ b/packages/uma/src/routes/Policy.ts @@ -3,7 +3,7 @@ import { UCRulesStorage } from "@solidlab/ucp"; import { HttpHandlerContext, HttpHandlerResponse, HttpHandler, HttpHandlerRequest } from "../util/http/models/HttpHandler"; import { getPolicies } from "../util/routeSpecific/policies/GetPolicies"; import { addPolicies } from "../util/routeSpecific/policies/CreatePolicies"; -import { deletePolicies } from "../util/routeSpecific/policies/DeletePolicies"; +import { deletePolicy } from "../util/routeSpecific/policies/DeletePolicies"; /** * Endpoint to handle policies, this implementation gives all policies that have the @@ -48,7 +48,7 @@ export class PolicyRequestHandler extends HttpHandler { switch (request.method) { case 'GET': return getPolicies(request, store, client, this.baseUrl); case 'POST': return addPolicies(request, this.storage, client); - case 'DELETE': return deletePolicies(request, store, this.storage, client, this.baseUrl); + case 'DELETE': return deletePolicy(request, store, this.storage, client, this.baseUrl); // TODO: add other endpoints default: throw new MethodNotAllowedHttpError(); } diff --git a/packages/uma/src/util/routeSpecific/policies/DeletePolicies.ts b/packages/uma/src/util/routeSpecific/policies/DeletePolicies.ts index 8813f554..0c773eae 100644 --- a/packages/uma/src/util/routeSpecific/policies/DeletePolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/DeletePolicies.ts @@ -13,42 +13,42 @@ import { InternalServerError } from "@solid/community-server"; * @param baseUrl * @returns */ -export async function deletePolicies(request: HttpHandlerRequest, store: Store, storage: UCRulesStorage, clientId: string, baseUrl: string): Promise> { +export async function deletePolicy(request: HttpHandlerRequest, store: Store, storage: UCRulesStorage, clientId: string, baseUrl: string): Promise> { // 1. Retrieve Policy ID const policyId = decodeURIComponent(retrieveID(checkBaseURL(request, baseUrl))); // 2. Collect the IDs of the rules we want to delete - let policyRules: Quad[] = [] - for (const relation of relations) { - policyRules = [...policyRules, ...store.getQuads(namedNode(policyId), relation, null, null)] - } - - // Nothing to delete - // TODO: change status code, nothing to delete - if (policyRules.length === 0) { - return { - status: 200, - }; - } + const policyRules: Quad[] = relations.flatMap(relation => + store.getQuads(namedNode(policyId), relation, null, null) + ) - // Keep track of IDs that are not within the clients reach + // Keep track of IDs that are (not) within the clients reach const otherRules: string[] = []; const ownedRules: string[] = []; policyRules.forEach(quad => { - if (store.getQuads(quad.object, odrlAssigner, namedNode(clientId), null).length > 0) + if (store.getQuads(quad.object, odrlAssigner, namedNode(clientId), null).length === 1) ownedRules.push(quad.object.id); else otherRules.push(quad.object.id); }); - // 3. If the policy has rules that do not belong to the client, we cannot remove the policy quads - const rulesToDelete = otherRules.length === 0 ? ownedRules : ownedRules.concat(store.getQuads(namedNode(policyId), null, null, null).map(quad => quad.subject.id)); + // Nothing to delete + if (ownedRules.length === 0) { + return { + status: 204, + }; + } + + // 3. If the policy contains only rules assigned by the client, we can remove the entire policy + // Otherwise, we only remove the rules within our reach + const idsToDelete = otherRules.length === 0 ? [policyId] : ownedRules; // 4. Remove the specified quads try { - rulesToDelete.forEach(id => storage.deleteRule(id)); + await Promise.all(idsToDelete.map(id => storage.deleteRule(id))); + } catch (error) { - throw new InternalServerError("Failed to delete rules"); + throw new InternalServerError(`Failed to delete rules: ${error}`); } // Delete succesful diff --git a/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts b/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts index bfe6df02..57f23d4f 100644 --- a/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts @@ -39,10 +39,9 @@ async function getOnePolicy(policyId: string, store: Store, clientId: string): P const policyMatches = store.getQuads(namedNode(policyId), null, null, null); // 2. Find the rules that this policy defines - let policyRules: Quad[] = [] - for (const relation of relations) { - policyRules = [...policyRules, ...store.getQuads(namedNode(policyId), relation, null, null)] - } + const policyRules: Quad[] = relations.flatMap(relation => + store.getQuads(namedNode(policyId), relation, null, null) + ) // 3. Only keep the rules assigned by the client const ownedRules = policyRules.filter(quad => store.getQuads(quad.object, odrlAssigner, namedNode(clientId), null).length > 0); diff --git a/scripts/test-uma-ODRL-policy.ts b/scripts/test-uma-ODRL-policy.ts index 7193758f..5fd3107b 100644 --- a/scripts/test-uma-ODRL-policy.ts +++ b/scripts/test-uma-ODRL-policy.ts @@ -9,13 +9,14 @@ import { policyA, policyB, policyC, badPolicy1 } from "./util/policyExampels"; const endpoint = (extra: string = '') => 'http://localhost:4000/uma/policies' + extra; const client = (client: string = 'a') => `https://pod.${client}.com/profile/card#me`; const policyId = 'http://example.org/usagePolicy1'; +const policyToDelete = 'urn:uuid:95efe0e8-4fb7-496d-8f3c-4d78c97829bc' const badPolicyId = 'nonExistentPolicy'; let errorCounter = 0; // Test if the first digit of the status code equals the second arg, or match the entire code when specific is false -const testCode = (code: number, shouldbe: number = 2, specific: boolean = true) => { - if ((specific ? Math.trunc(Number(code) / 100) : code) !== shouldbe) errorCounter++; +const testCode = (code: number, shouldbe: number = 2, trunc: boolean = true) => { + if ((trunc ? Math.trunc(Number(code) / 100) : code) !== shouldbe) errorCounter++; } async function getAllPolicies() { @@ -77,6 +78,52 @@ async function postPolicy() { testCode(response.status); } +async function testDelete() { + const encodedPolicyId = encodeURIComponent(policyToDelete); + console.log("Testing Delete endpoint"); + + let response = await fetch(endpoint(`/${encodedPolicyId}`), { method: 'DELETE', headers: { 'Authorization': client('c') } }); + console.log(`expecting status 204, nothing to delete: ${response.status}`); + testCode(response.status, 204, false); + + response = await fetch(endpoint(`/${encodedPolicyId}`), { method: 'DELETE', headers: { 'Authorization': client('a') } }); + console.log(`expecting status 200: ${response.status}\n`); + testCode(response.status, 200, false); + + console.log('testing if the policy is deleted for client a, but not for client b\n'); + response = await fetch(endpoint(`/${encodedPolicyId}`), { headers: { 'Authorization': client('a') } }); + let resText = await response.text(); + console.log(`expecting an empty body, the policy should be deleted for client a: ${resText}`); + testCode(resText.length, 0, false); + + response = await fetch(endpoint(`/${encodedPolicyId}`), { headers: { 'Authorization': client('b') } }); + resText = await response.text(); + console.log(`expecting the policy with one rule: ${resText}\n\n`); + + console.log('now we delete the policy for client b. It should delete the rules AND the policy information'); + response = await fetch(endpoint(`/${encodedPolicyId}`), { method: 'DELETE', headers: { 'Authorization': client('b') } }); + console.log(`expecting status 200: ${response.status}`); + testCode(response.status, 200, false); + response = await fetch(endpoint(`/${encodedPolicyId}`), { headers: { 'Authorization': client('a') } }); + resText = await response.text(); + console.log(`expecting an empty body, the policy should be deleted for client a: ${resText}`); + testCode(resText.length, 0, false); +} + +async function deleteAll() { + const obj = { + 'a': ['http://example.org/usagePolicy1', 'http://example.org/usagePolicy1a', 'urn:uuid:95efe0e8-4fb7-496d-8f3c-4d78c97829bc'], + 'b': ['http://example.org/usagePolicy2', 'http://example.org/usagePolicy2a', 'urn:uuid:95efe0e8-4fb7-496d-8f3c-4d78c97829bc'], 'c': ['http://example.org/usagePolicy3'] + } + console.log("deleting all policies") + for (const [clientId, policyIds] of Object.entries(obj)) { + for (const policyId of policyIds) { + const response = await fetch(endpoint(`/${encodeURIComponent(policyId)}`), { method: 'DELETE', headers: { 'Authorization': client(clientId) } }) + console.log(`DELETE ${policyId} from client ${clientId} responded with status ${response.status}`) + } + } +} + /** * As explained in the docs, the order of execution is extremely important. * The storage is filled with the POST requests, so this must precede the other tests! @@ -90,6 +137,9 @@ async function main() { console.log("\n\n\n"); await getOnePolicy(); console.log("\n\n\n"); + await testDelete(); + console.log("\n\n\n"); + await deleteAll() console.log(errorCounter === 0 ? `No fails detected` : `${errorCounter} tests have failed`); } From de05066b483e85fb3c7c69990ef589f6aab3b4f0 Mon Sep 17 00:00:00 2001 From: lennertdr Date: Wed, 9 Jul 2025 14:33:56 +0200 Subject: [PATCH 33/62] edit policy setup --- packages/uma/config/default.json | 6 +- packages/uma/config/routes/policies.json | 6 +- packages/uma/package.json | 1 + packages/uma/src/routes/Policy.ts | 2 + .../routeSpecific/policies/CreatePolicies.ts | 12 +-- .../routeSpecific/policies/EditPolicies.ts | 33 +++++++++ .../routeSpecific/policies/GetPolicies.ts | 74 +++++++++++++------ .../util/routeSpecific/policies/PolicyUtil.ts | 12 ++- .../routeSpecific/policies/rewritePolicies.ts | 0 9 files changed, 111 insertions(+), 35 deletions(-) create mode 100644 packages/uma/src/util/routeSpecific/policies/rewritePolicies.ts diff --git a/packages/uma/config/default.json b/packages/uma/config/default.json index acf5f3ba..5361a7bc 100644 --- a/packages/uma/config/default.json +++ b/packages/uma/config/default.json @@ -106,8 +106,7 @@ { "@id": "urn:uma:default:PermissionRegistrationRoute" }, { "@id": "urn:uma:default:ResourceRegistrationRoute" }, { "@id": "urn:uma:default:ResourceRegistrationOpsRoute" }, - { "@id": "urn:uma:default:IntrospectionRoute" }, - { "@id": "urn:uma:default:GetOnePolicyRoute"} + { "@id": "urn:uma:default:IntrospectionRoute" } ] } }, @@ -119,7 +118,8 @@ { "@id": "urn:uma:default:ContractRoute" }, { "@id": "urn:uma:default:LogRoute" }, { "@id": "urn:uma:default:VCRoute" }, - { "@id": "urn:uma:default:PolicyRoute" } + { "@id": "urn:uma:default:PolicyRoute" }, + { "@id": "urn:uma:default:OnePolicyRoute"} ] }, { diff --git a/packages/uma/config/routes/policies.json b/packages/uma/config/routes/policies.json index afa22405..3f751e45 100644 --- a/packages/uma/config/routes/policies.json +++ b/packages/uma/config/routes/policies.json @@ -20,11 +20,13 @@ "path": "/uma/policies" }, { - "@id": "urn:uma:default:GetOnePolicyRoute", + "@id": "urn:uma:default:OnePolicyRoute", "@type": "HttpHandlerRoute", "methods": [ "GET", - "DELETE" + "DELETE", + "PATCH", + "PUT" ], "handler": { "@type": "PolicyRequestHandler", diff --git a/packages/uma/package.json b/packages/uma/package.json index 59a154c7..5a0c43f8 100644 --- a/packages/uma/package.json +++ b/packages/uma/package.json @@ -65,6 +65,7 @@ "@solid/access-token-verifier": "^1.2.0", "@solid/community-server": "^7.1.7", "@solidlab/ucp": "workspace:^", + "@comunica/query-sparql": "^2.9.0", "@types/n3": "^1.16.4", "componentsjs": "^5.5.1", "get-jwks": "^9.0.1", diff --git a/packages/uma/src/routes/Policy.ts b/packages/uma/src/routes/Policy.ts index 6f202637..f7fe7f6a 100644 --- a/packages/uma/src/routes/Policy.ts +++ b/packages/uma/src/routes/Policy.ts @@ -4,6 +4,7 @@ import { HttpHandlerContext, HttpHandlerResponse, HttpHandler, HttpHandlerReques import { getPolicies } from "../util/routeSpecific/policies/GetPolicies"; import { addPolicies } from "../util/routeSpecific/policies/CreatePolicies"; import { deletePolicy } from "../util/routeSpecific/policies/DeletePolicies"; +import { editPolicy } from "../util/routeSpecific/policies/EditPolicies"; /** * Endpoint to handle policies, this implementation gives all policies that have the @@ -49,6 +50,7 @@ export class PolicyRequestHandler extends HttpHandler { case 'GET': return getPolicies(request, store, client, this.baseUrl); case 'POST': return addPolicies(request, this.storage, client); case 'DELETE': return deletePolicy(request, store, this.storage, client, this.baseUrl); + case 'PATCH': return editPolicy(request, store, client, this.baseUrl); // TODO: add other endpoints default: throw new MethodNotAllowedHttpError(); } diff --git a/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts b/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts index f17619ef..f55f19ea 100644 --- a/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts @@ -1,6 +1,6 @@ import { Quad, Store } from "n3"; import { HttpHandlerRequest, HttpHandlerResponse } from "../../http/models/HttpHandler"; -import { odrlAssigner, relations } from "./PolicyUtil"; +import { odrlAssigner, parsePolicyBody, relations } from "./PolicyUtil"; import { BadRequestHttpError, InternalServerError } from "@solid/community-server"; import { parseStringAsN3Store } from "koreografeye"; import { UCRulesStorage } from "@solidlab/ucp"; @@ -45,13 +45,9 @@ export async function addPolicies(request: HttpHandlerRequest, storage: UCRulesS throw new BadRequestHttpError(`Content-Type ${contentType} is not supported.`); } - let requestedPolicy; - if (Buffer.isBuffer(request.body)) { - requestedPolicy = request.body.toString('utf-8'); - console.log('RDF body:', requestedPolicy); - } else { - throw new Error("Expected Buffer body"); - } + // Try to parse the body + const requestedPolicy = parsePolicyBody(request.body); + let parsedPolicy: Store; try { parsedPolicy = await parseStringAsN3Store(requestedPolicy, { format: contentType }); diff --git a/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts b/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts index e69de29b..6f4d03a9 100644 --- a/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts @@ -0,0 +1,33 @@ +import { Store } from "n3"; +import { HttpHandlerRequest, HttpHandlerResponse } from "../../http/models/HttpHandler"; +import { UCRulesStorage } from "@solidlab/ucp"; +import { checkBaseURL, parsePolicyBody, quadsToText, retrieveID } from "./PolicyUtil"; +import { QueryEngine } from '@comunica/query-sparql'; +import { BadRequestHttpError } from "@solid/community-server"; +import { getOnePolicyInfo } from "./GetPolicies"; + +export async function editPolicy(request: HttpHandlerRequest, store: Store, clientId: string, baseUrl: string): Promise> { + // 1. Retrieve Policy ID + const policyId = decodeURIComponent(retrieveID(checkBaseURL(request, baseUrl))); + + // 2. Retrieve the Policy Body + const contentType = request.headers['content-type']; + if (!/(?:application\/sparql-update)$/i.test(contentType)) { + throw new BadRequestHttpError(`Content-Type ${contentType} is not supported.`); + } + const query = parsePolicyBody(request.body); + + // 3. Retrieve the existing policy + const { policyQuads, ownedRules, otherRules } = getOnePolicyInfo(policyId, store, clientId); + + // 4. Execute the query on the policy + const policyStore = new Store([...policyQuads, ...ownedRules, ...otherRules]); + const engine = new QueryEngine(); + await engine.queryVoid(query, { sources: [policyStore] }); + + // 5. Check if we stayed between our boundaries + const newState = getOnePolicyInfo(policyId, policyStore, clientId); + if (newState.otherRules != otherRules) throw new BadRequestHttpError("UPDATE rules within authorization reach"); + + return quadsToText([...newState.policyQuads, ...newState.ownedRules, ...newState.otherRules]); +} \ No newline at end of file diff --git a/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts b/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts index 57f23d4f..aa84c311 100644 --- a/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts @@ -23,6 +23,53 @@ export async function getPolicies(request: HttpHandlerRequest, store: Store, cli } +// Interface to represent a policy based on a client +export interface OnePolicy { + clientId: string; + policyId: string; + policyQuads: Set; + ownedRules: Set; + otherRules: Set; +} + +// Functional implementation to get one policy +export function getOnePolicyInfo(policyId: string, store: Store, clientId: string): OnePolicy { + // 1. Search the policy by ID + const policyMatches = store.getQuads(namedNode(policyId), null, null, null); + + // 2. Find the rules that this policy defines + const policyRules: Quad[] = relations.flatMap(relation => + store.getQuads(namedNode(policyId), relation, null, null) + ) + + // 3. Separate the rules owned by the client from the others + const otherRules: Set = new Set(); + const ownedRules: Set = new Set(); + policyRules.forEach(quad => { + if (store.getQuads(quad.object, odrlAssigner, namedNode(clientId), null).length === 1) + // This is the step to be replaced with the recursive variant + store.getQuads(quad.object, null, null, null).forEach( + quad => ownedRules.add(quad) + ) + else + // Once again, this is to be replaced with the recursive variant + store.getQuads(quad.object, null, null, null).forEach( + quad => otherRules.add(quad) + ) + }); + + // 4. Return the detailed object + return { + policyId: policyId, + clientId: clientId, + policyQuads: new Set(policyMatches), + ownedRules: ownedRules, + otherRules: otherRules + } + +} + + /** * Function to implement the GET /uma/policies/ endpoint, it retrieves all information about a certain * policy if available. @@ -35,34 +82,18 @@ export async function getPolicies(request: HttpHandlerRequest, store: Store, cli async function getOnePolicy(policyId: string, store: Store, clientId: string): Promise> { policyId = decodeURIComponent(policyId); - // 1. Search the policy by ID - const policyMatches = store.getQuads(namedNode(policyId), null, null, null); - - // 2. Find the rules that this policy defines - const policyRules: Quad[] = relations.flatMap(relation => - store.getQuads(namedNode(policyId), relation, null, null) - ) + const { policyQuads, ownedRules } = getOnePolicyInfo(policyId, store, clientId); - // 3. Only keep the rules assigned by the client - const ownedRules = policyRules.filter(quad => store.getQuads(quad.object, odrlAssigner, namedNode(clientId), null).length > 0); - // Return an empty body when no rules are found - if (ownedRules.length === 0) { + if (ownedRules.size === 0) return { - status: 200, + status: 204, headers: { 'content-type': 'text/turtle', }, body: '', - }; - } - - // 4. Search all info about the policy AND the rules, for now with depth 1 but a recursive variant needs to be implemented here. - let details: Set = new Set(policyMatches); - ownedRules.forEach((rule) => { - store.getQuads(rule.object, null, null, null).forEach(q => details.add(q)); - }); + } - return quadsToText(Array.from(details)); + return quadsToText([...policyQuads, ...ownedRules]); } @@ -92,6 +123,7 @@ async function getAllPolicies(store: Store, clientId: string): Promise 0) { // Because an ODRL policy may only have one assigner, we can now add all policy and rule information + // Note that this is the only part of the function to be replaced with the recursive variant store.getQuads(policy, null, null, null).forEach(quad => policyDetails.add(quad)); store.getQuads(rule, null, null, null).forEach(quad => policyDetails.add(quad)); } diff --git a/packages/uma/src/util/routeSpecific/policies/PolicyUtil.ts b/packages/uma/src/util/routeSpecific/policies/PolicyUtil.ts index 29e29a2d..161209d0 100644 --- a/packages/uma/src/util/routeSpecific/policies/PolicyUtil.ts +++ b/packages/uma/src/util/routeSpecific/policies/PolicyUtil.ts @@ -1,7 +1,7 @@ import { ODRL } from "@solidlab/ucp"; import { DataFactory, Quad, Writer } from "n3"; import { HttpHandlerRequest, HttpHandlerResponse } from "../../http/models/HttpHandler"; -import { MethodNotAllowedHttpError } from "@solid/community-server"; +import { BadRequestHttpError, MethodNotAllowedHttpError } from "@solid/community-server"; // relevant ODRL implementations export const odrlAssigner = ODRL.terms.assigner; @@ -59,4 +59,14 @@ export async function quadsToText(quads: Quad[]): Promise Date: Wed, 9 Jul 2025 15:14:22 +0200 Subject: [PATCH 34/62] Basic edit implementation --- packages/uma/src/routes/Policy.ts | 2 +- .../routeSpecific/policies/EditPolicies.ts | 29 ++++++++++++++----- scripts/test-uma-ODRL-policy.ts | 12 ++++---- 3 files changed, 29 insertions(+), 14 deletions(-) diff --git a/packages/uma/src/routes/Policy.ts b/packages/uma/src/routes/Policy.ts index f7fe7f6a..87b10413 100644 --- a/packages/uma/src/routes/Policy.ts +++ b/packages/uma/src/routes/Policy.ts @@ -50,7 +50,7 @@ export class PolicyRequestHandler extends HttpHandler { case 'GET': return getPolicies(request, store, client, this.baseUrl); case 'POST': return addPolicies(request, this.storage, client); case 'DELETE': return deletePolicy(request, store, this.storage, client, this.baseUrl); - case 'PATCH': return editPolicy(request, store, client, this.baseUrl); + case 'PATCH': return editPolicy(request, store, this.storage, client, this.baseUrl); // TODO: add other endpoints default: throw new MethodNotAllowedHttpError(); } diff --git a/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts b/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts index 6f4d03a9..c1e3ce1d 100644 --- a/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts @@ -3,10 +3,15 @@ import { HttpHandlerRequest, HttpHandlerResponse } from "../../http/models/HttpH import { UCRulesStorage } from "@solidlab/ucp"; import { checkBaseURL, parsePolicyBody, quadsToText, retrieveID } from "./PolicyUtil"; import { QueryEngine } from '@comunica/query-sparql'; -import { BadRequestHttpError } from "@solid/community-server"; +import { BadRequestHttpError, InternalServerError } from "@solid/community-server"; import { getOnePolicyInfo } from "./GetPolicies"; -export async function editPolicy(request: HttpHandlerRequest, store: Store, clientId: string, baseUrl: string): Promise> { +// Apparently js does not have this +const setIsEqual = (xs: Set, ys: Set) => + xs.size === ys.size && + [...xs].every((x) => ys.has(x)) + +export async function editPolicy(request: HttpHandlerRequest, store: Store, storage: UCRulesStorage, clientId: string, baseUrl: string): Promise> { // 1. Retrieve Policy ID const policyId = decodeURIComponent(retrieveID(checkBaseURL(request, baseUrl))); @@ -17,17 +22,27 @@ export async function editPolicy(request: HttpHandlerRequest, store: Store, clie } const query = parsePolicyBody(request.body); - // 3. Retrieve the existing policy + // 3. Retrieve the existing policy info const { policyQuads, ownedRules, otherRules } = getOnePolicyInfo(policyId, store, clientId); // 4. Execute the query on the policy const policyStore = new Store([...policyQuads, ...ownedRules, ...otherRules]); - const engine = new QueryEngine(); - await engine.queryVoid(query, { sources: [policyStore] }); + try { + await new QueryEngine().queryVoid(query, { sources: [policyStore] }); + } catch (error) { + throw new BadRequestHttpError("Query could not be executed:", error); + } // 5. Check if we stayed between our boundaries const newState = getOnePolicyInfo(policyId, policyStore, clientId); - if (newState.otherRules != otherRules) throw new BadRequestHttpError("UPDATE rules within authorization reach"); + if (!setIsEqual(newState.otherRules, otherRules)) throw new BadRequestHttpError("Update not allowed: attempted to modify rules not owned by client"); - return quadsToText([...newState.policyQuads, ...newState.ownedRules, ...newState.otherRules]); + // 6. Modify the storage to the updated version + try { + await storage.deleteRule(policyId); + await storage.addRule(policyStore); + } catch (error) { + throw new InternalServerError("Something went wrong while editting the policy:", error); + } + return quadsToText(policyStore.getQuads(null, null, null, null)); } \ No newline at end of file diff --git a/scripts/test-uma-ODRL-policy.ts b/scripts/test-uma-ODRL-policy.ts index 5fd3107b..6fc780ed 100644 --- a/scripts/test-uma-ODRL-policy.ts +++ b/scripts/test-uma-ODRL-policy.ts @@ -11,7 +11,7 @@ const client = (client: string = 'a') => `https://pod.${client}.com/profile/card const policyId = 'http://example.org/usagePolicy1'; const policyToDelete = 'urn:uuid:95efe0e8-4fb7-496d-8f3c-4d78c97829bc' const badPolicyId = 'nonExistentPolicy'; - +const quickBuffer = (text: string) => Buffer.from(text, 'utf-8'); let errorCounter = 0; // Test if the first digit of the status code equals the second arg, or match the entire code when specific is false @@ -57,23 +57,23 @@ async function getOnePolicy() { async function postPolicy() { console.log("Simple test for the POST policy endpoint"); - let response = await fetch(endpoint(), { method: 'POST', headers: { 'Authorization': client('a'), 'Content-Type': 'text/turtle' }, body: Buffer.from(policyB, 'utf-8') }); + let response = await fetch(endpoint(), { method: 'POST', headers: { 'Authorization': client('a'), 'Content-Type': 'text/turtle' }, body: quickBuffer(policyB) }); console.log(`expecting a negative response since assigner != client: status code ${response.status}\nmessage: ${await response.text()}`); testCode(response.status, 4); - response = await fetch(endpoint(), { method: 'POST', headers: { 'Authorization': client('b'), 'Content-Type': 'text/turtle' }, body: Buffer.from(badPolicy1, 'utf-8') }); + response = await fetch(endpoint(), { method: 'POST', headers: { 'Authorization': client('b'), 'Content-Type': 'text/turtle' }, body: quickBuffer(badPolicy1) }); console.log(`expecting a negative response since policy has a rule with no assigner ${response.status}\nmessage: ${await response.text()}`); testCode(response.status, 4); - response = await fetch(endpoint(), { method: 'POST', headers: { 'Authorization': client('a'), 'Content-Type': 'text/turtle' }, body: Buffer.from(policyA, 'utf-8') }); + response = await fetch(endpoint(), { method: 'POST', headers: { 'Authorization': client('a'), 'Content-Type': 'text/turtle' }, body: quickBuffer(policyA) }); console.log(`expecting a positive response: status code ${response.status}, ${await response.text()}`); testCode(response.status); - response = await fetch(endpoint(), { method: 'POST', headers: { 'Authorization': client('b'), 'Content-Type': 'text/turtle' }, body: Buffer.from(policyB, 'utf-8') }); + response = await fetch(endpoint(), { method: 'POST', headers: { 'Authorization': client('b'), 'Content-Type': 'text/turtle' }, body: quickBuffer(policyB) }); console.log(`expecting a positive response: status code ${response.status}, ${await response.text()}`); testCode(response.status); - response = await fetch(endpoint(), { method: 'POST', headers: { 'Authorization': client('c'), 'Content-Type': 'text/turtle' }, body: Buffer.from(policyC, 'utf-8') }); + response = await fetch(endpoint(), { method: 'POST', headers: { 'Authorization': client('c'), 'Content-Type': 'text/turtle' }, body: quickBuffer(policyC) }); console.log(`expecting a positive response: status code ${response.status}, ${await response.text()}`); testCode(response.status); } From f06de5050c91b7a311987dd14adf7306bb15c5d4 Mon Sep 17 00:00:00 2001 From: lennertdr Date: Wed, 9 Jul 2025 16:02:29 +0200 Subject: [PATCH 35/62] added simple tests for PATCH --- scripts/test-uma-ODRL-policy.ts | 33 +++++++++++++---- scripts/util/policyExampels.ts | 63 +++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 7 deletions(-) diff --git a/scripts/test-uma-ODRL-policy.ts b/scripts/test-uma-ODRL-policy.ts index 6fc780ed..40bc2f79 100644 --- a/scripts/test-uma-ODRL-policy.ts +++ b/scripts/test-uma-ODRL-policy.ts @@ -4,7 +4,7 @@ * The purpose of this file is to test the /policies endpoint. */ -import { policyA, policyB, policyC, badPolicy1 } from "./util/policyExampels"; +import { policyA, policyB, policyC, badPolicy1, changePolicy1, changePolicy95e } from "./util/policyExampels"; const endpoint = (extra: string = '') => 'http://localhost:4000/uma/policies' + extra; const client = (client: string = 'a') => `https://pod.${client}.com/profile/card#me`; @@ -19,6 +19,24 @@ const testCode = (code: number, shouldbe: number = 2, trunc: boolean = true) => if ((trunc ? Math.trunc(Number(code) / 100) : code) !== shouldbe) errorCounter++; } +async function patchPolicies() { + console.log("Simple test for the PATCH policy endpoint"); + const encoded = encodeURIComponent(policyId); + + let response = await fetch(endpoint(`/${encoded}`), { method: 'PATCH', headers: { 'Authorization': client('a'), 'Content-Type': 'application/sparql-update' }, body: quickBuffer(changePolicy1) }); + console.log(`expecting a positive response: status code ${response.status}\n expecting to see policy100 as its only rule: \n${await response.text()}`); + // console.log(`expecting a negative response since assigner != client: status code ${response.status}\nmessage: ${await response.text()}`); + testCode(response.status); + + response = await fetch(endpoint(`/${encoded}`), { method: 'PATCH', headers: { 'Authorization': client('a'), 'Content-Type': 'application/sparql-update' }, body: quickBuffer(changePolicy95e) }); + console.log(`expecting a positive response: status code ${response.status}\nexpecting the old rule to delete and two rules to take its place: \n${await response.text()}`); + testCode(response.status); + + response = await fetch(endpoint(`/${encoded}`), { method: 'PATCH', headers: { 'Authorization': client('c'), 'Content-Type': 'application/sparql-update' }, body: quickBuffer(changePolicy1) }); + console.log(`expecting a negative response since the query changes another client's rules ${response.status}\nmessage: ${await response.text()}`); + testCode(response.status, 4); +} + async function getAllPolicies() { console.log("Simple test for the GET All Policies endpoint\n"); @@ -133,12 +151,13 @@ async function main() { console.log("Testing all implemented Policy Endpoints:\n\n\n"); await postPolicy(); console.log("\n\n\n"); - await getAllPolicies(); - console.log("\n\n\n"); - await getOnePolicy(); - console.log("\n\n\n"); - await testDelete(); - console.log("\n\n\n"); + await patchPolicies(); + // await getAllPolicies(); + // console.log("\n\n\n"); + // await getOnePolicy(); + // console.log("\n\n\n"); + // await testDelete(); + // console.log("\n\n\n"); await deleteAll() console.log(errorCounter === 0 ? `No fails detected` : `${errorCounter} tests have failed`); diff --git a/scripts/util/policyExampels.ts b/scripts/util/policyExampels.ts index 732f2e2e..0c9a9e5c 100644 --- a/scripts/util/policyExampels.ts +++ b/scripts/util/policyExampels.ts @@ -82,4 +82,67 @@ ex:permissionBad a odrl:Permission . ex:permissionBad odrl:action odrl:modify . ex:permissionBad odrl:target . ex:permissionBad odrl:assignee . +` + +export const changePolicy1 = ` +PREFIX ex: +PREFIX odrl: +PREFIX dct: + +DELETE { + ex:usagePolicy1 odrl:permission ex:permission1 . + ex:permission1 a odrl:Permission ; + odrl:action odrl:modify ; + odrl:target ; + odrl:assignee ; + odrl:assigner . +} +INSERT { + ex:usagePolicy1 odrl:permission ex:permission100 . + ex:permission100 a odrl:Permission ; + odrl:action odrl:read ; + odrl:target ; + odrl:assignee ; + odrl:assigner . +} +WHERE { + ex:usagePolicy1 odrl:permission ex:permission1 . + ex:permission1 a odrl:Permission ; + odrl:action odrl:modify ; + odrl:target ; + odrl:assignee ; + odrl:assigner . +} +` + +export const changePolicy95e = ` +PREFIX ex: +PREFIX odrl: +PREFIX dct: + +DELETE { + odrl:permission . + ?p ?o . +} +INSERT { + odrl:permission . + odrl:permission . + + a odrl:Permission ; + odrl:assignee ex:alice ; + odrl:assigner ; + odrl:action odrl:write ; + odrl:target ex:x . + + a odrl:Permission ; + odrl:assignee ex:alice ; + odrl:assigner ; + odrl:action odrl:delete ; + odrl:target ex:x . +} +WHERE { + OPTIONAL { + ?p ?o . + } +} ` \ No newline at end of file From aecd278134bb225485ded546a511180a1919fa76 Mon Sep 17 00:00:00 2001 From: lennertdr Date: Wed, 9 Jul 2025 17:27:03 +0200 Subject: [PATCH 36/62] patch + tests --- docs/policy-management.md | 5 +- packages/uma/src/routes/Policy.ts | 3 +- .../routeSpecific/policies/CreatePolicies.ts | 19 +------ .../routeSpecific/policies/DeletePolicies.ts | 17 +++--- .../routeSpecific/policies/EditPolicies.ts | 4 +- .../util/routeSpecific/policies/PolicyUtil.ts | 24 ++++++++- .../routeSpecific/policies/rewritePolicies.ts | 44 +++++++++++++++ scripts/test-uma-ODRL-policy.ts | 54 ++++++++++++++----- scripts/util/policyExampels.ts | 16 ++++++ 9 files changed, 144 insertions(+), 42 deletions(-) diff --git a/docs/policy-management.md b/docs/policy-management.md index ffde1696..26726693 100644 --- a/docs/policy-management.md +++ b/docs/policy-management.md @@ -51,4 +51,7 @@ Sanitization Limitations Some operations require the client to specify a policy ID in the URL. Since policy ID's might contain reserved characters (e.g. `/`, `:`, ...), we have chosen to encode them with the builtin [`encodeURIComponent()` function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent). Using this method, reserved characters will be converted to their respective UTF-8 encodings. ## Testing -The current implementation is tested only by the script in `scripts\test-uma-ODRL-policy.ts`. This script tests every implemented endpoint with a designated flow. Since the script initiates with an empty storage, and there is no endpoint or other way to seed it, the first requests must test the POST endpoint. These tests are designed to ensure that the storage is filled. After the POST tests, the access endpoints can be tested. \ No newline at end of file +The current implementation is tested only by the script in `scripts\test-uma-ODRL-policy.ts`. This script tests every implemented endpoint with a designated flow. Since the script initiates with an empty storage, and there is no endpoint or other way to seed it, the first requests must test the POST endpoint. These tests are designed to ensure that the storage is filled. After the POST tests, the access endpoints can be tested. + +## Problems +- When you have a policy with multiple rules that have different assigners, DELETE on every rule of one assigner will succesfully delete the rule itself, but not the definition of the rule within the policy. This is due to the fact that you can currently only DELETE based on the ID of the rule/policy you want to delete, and you cannot delete the entire policy since other assigners depend on it. \ No newline at end of file diff --git a/packages/uma/src/routes/Policy.ts b/packages/uma/src/routes/Policy.ts index 87b10413..2618233c 100644 --- a/packages/uma/src/routes/Policy.ts +++ b/packages/uma/src/routes/Policy.ts @@ -5,6 +5,7 @@ import { getPolicies } from "../util/routeSpecific/policies/GetPolicies"; import { addPolicies } from "../util/routeSpecific/policies/CreatePolicies"; import { deletePolicy } from "../util/routeSpecific/policies/DeletePolicies"; import { editPolicy } from "../util/routeSpecific/policies/EditPolicies"; +import { rewritePolicy } from "../util/routeSpecific/policies/rewritePolicies"; /** * Endpoint to handle policies, this implementation gives all policies that have the @@ -51,7 +52,7 @@ export class PolicyRequestHandler extends HttpHandler { case 'POST': return addPolicies(request, this.storage, client); case 'DELETE': return deletePolicy(request, store, this.storage, client, this.baseUrl); case 'PATCH': return editPolicy(request, store, this.storage, client, this.baseUrl); - // TODO: add other endpoints + case 'PUT': return rewritePolicy(request, store, this.storage, client, this.baseUrl); default: throw new MethodNotAllowedHttpError(); } } diff --git a/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts b/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts index f55f19ea..700bfd74 100644 --- a/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts @@ -1,6 +1,6 @@ import { Quad, Store } from "n3"; import { HttpHandlerRequest, HttpHandlerResponse } from "../../http/models/HttpHandler"; -import { odrlAssigner, parsePolicyBody, relations } from "./PolicyUtil"; +import { odrlAssigner, parseBodyToStore, parseBufferToString, relations } from "./PolicyUtil"; import { BadRequestHttpError, InternalServerError } from "@solid/community-server"; import { parseStringAsN3Store } from "koreografeye"; import { UCRulesStorage } from "@solidlab/ucp"; @@ -38,22 +38,7 @@ export function sanitizeRule(parsedPolicy: Store, clientId: string): void { export async function addPolicies(request: HttpHandlerRequest, storage: UCRulesStorage, clientId: string): Promise> { // 1. Parse the requested policy - - // Regex check for content type - const contentType = request.headers['content-type']; - if (!/(?:n3|trig|turtle|nquads?|ntriples?)$/i.test(contentType)) { - throw new BadRequestHttpError(`Content-Type ${contentType} is not supported.`); - } - - // Try to parse the body - const requestedPolicy = parsePolicyBody(request.body); - - let parsedPolicy: Store; - try { - parsedPolicy = await parseStringAsN3Store(requestedPolicy, { format: contentType }); - } catch (error) { - throw new BadRequestHttpError(`Policy string can not be parsed: ${error}`) - } + const parsedPolicy = await parseBodyToStore(request); // 2. Sanitization checks (error is thrown when checks fail) sanitizeRule(parsedPolicy, clientId); diff --git a/packages/uma/src/util/routeSpecific/policies/DeletePolicies.ts b/packages/uma/src/util/routeSpecific/policies/DeletePolicies.ts index 0c773eae..d91efb09 100644 --- a/packages/uma/src/util/routeSpecific/policies/DeletePolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/DeletePolicies.ts @@ -4,6 +4,13 @@ import { checkBaseURL, namedNode, odrlAssigner, quadsToText, relations, retrieve import { Quad, Store } from "n3"; import { InternalServerError } from "@solid/community-server"; +export async function deletePolicy(request: HttpHandlerRequest, store: Store, storage: UCRulesStorage, clientId: string, baseUrl: string): Promise> { + + const policyId = decodeURIComponent(retrieveID(checkBaseURL(request, baseUrl))); + return deleteOnePolicy(policyId, store, storage, clientId) +} + + /** * TODO: documentation * @param request @@ -13,11 +20,9 @@ import { InternalServerError } from "@solid/community-server"; * @param baseUrl * @returns */ -export async function deletePolicy(request: HttpHandlerRequest, store: Store, storage: UCRulesStorage, clientId: string, baseUrl: string): Promise> { - // 1. Retrieve Policy ID - const policyId = decodeURIComponent(retrieveID(checkBaseURL(request, baseUrl))); +export async function deleteOnePolicy(policyId: string, store: Store, storage: UCRulesStorage, clientId: string): Promise> { - // 2. Collect the IDs of the rules we want to delete + // 1. Collect the IDs of the rules we want to delete const policyRules: Quad[] = relations.flatMap(relation => store.getQuads(namedNode(policyId), relation, null, null) ) @@ -39,11 +44,11 @@ export async function deletePolicy(request: HttpHandlerRequest, store: Store, st }; } - // 3. If the policy contains only rules assigned by the client, we can remove the entire policy + // 2. If the policy contains only rules assigned by the client, we can remove the entire policy // Otherwise, we only remove the rules within our reach const idsToDelete = otherRules.length === 0 ? [policyId] : ownedRules; - // 4. Remove the specified quads + // 3. Remove the specified quads try { await Promise.all(idsToDelete.map(id => storage.deleteRule(id))); diff --git a/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts b/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts index c1e3ce1d..e71b9ca6 100644 --- a/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts @@ -1,7 +1,7 @@ import { Store } from "n3"; import { HttpHandlerRequest, HttpHandlerResponse } from "../../http/models/HttpHandler"; import { UCRulesStorage } from "@solidlab/ucp"; -import { checkBaseURL, parsePolicyBody, quadsToText, retrieveID } from "./PolicyUtil"; +import { checkBaseURL, parseBufferToString, quadsToText, retrieveID } from "./PolicyUtil"; import { QueryEngine } from '@comunica/query-sparql'; import { BadRequestHttpError, InternalServerError } from "@solid/community-server"; import { getOnePolicyInfo } from "./GetPolicies"; @@ -20,7 +20,7 @@ export async function editPolicy(request: HttpHandlerRequest, store: Store, stor if (!/(?:application\/sparql-update)$/i.test(contentType)) { throw new BadRequestHttpError(`Content-Type ${contentType} is not supported.`); } - const query = parsePolicyBody(request.body); + const query = parseBufferToString(request.body); // 3. Retrieve the existing policy info const { policyQuads, ownedRules, otherRules } = getOnePolicyInfo(policyId, store, clientId); diff --git a/packages/uma/src/util/routeSpecific/policies/PolicyUtil.ts b/packages/uma/src/util/routeSpecific/policies/PolicyUtil.ts index 161209d0..7a06d962 100644 --- a/packages/uma/src/util/routeSpecific/policies/PolicyUtil.ts +++ b/packages/uma/src/util/routeSpecific/policies/PolicyUtil.ts @@ -1,7 +1,8 @@ import { ODRL } from "@solidlab/ucp"; -import { DataFactory, Quad, Writer } from "n3"; +import { DataFactory, Quad, Store, Writer } from "n3"; import { HttpHandlerRequest, HttpHandlerResponse } from "../../http/models/HttpHandler"; import { BadRequestHttpError, MethodNotAllowedHttpError } from "@solid/community-server"; +import { parseStringAsN3Store } from "koreografeye"; // relevant ODRL implementations export const odrlAssigner = ODRL.terms.assigner; @@ -61,7 +62,7 @@ export async function quadsToText(quads: Quad[]): Promise { + // Regex check for content type + const contentType = request.headers['content-type']; + if (!/(?:n3|trig|turtle|nquads?|ntriples?)$/i.test(contentType)) { + throw new BadRequestHttpError(`Content-Type ${contentType} is not supported.`); + } + + // Try to parse the body + const requestedPolicy = parseBufferToString(request.body); + + let parsedPolicy: Store; + try { + parsedPolicy = await parseStringAsN3Store(requestedPolicy, { format: contentType }); + } catch (error) { + throw new BadRequestHttpError(`Policy string can not be parsed: ${error}`) + } + return parsedPolicy } \ No newline at end of file diff --git a/packages/uma/src/util/routeSpecific/policies/rewritePolicies.ts b/packages/uma/src/util/routeSpecific/policies/rewritePolicies.ts index e69de29b..0b272d03 100644 --- a/packages/uma/src/util/routeSpecific/policies/rewritePolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/rewritePolicies.ts @@ -0,0 +1,44 @@ +import { UCRulesStorage } from "@solidlab/ucp"; +import { HttpHandlerRequest, HttpHandlerResponse } from "../../http/models/HttpHandler"; +import { BadRequestHttpError, InternalServerError } from "@solid/community-server"; +import { checkBaseURL, namedNode, parseBodyToStore, quadsToText, retrieveID } from "./PolicyUtil"; +import { Store } from "n3"; +import { sanitizeRule } from "./CreatePolicies"; +import { deleteOnePolicy } from "./DeletePolicies"; +import { getOnePolicyInfo } from "./GetPolicies"; + +export async function rewritePolicy(request: HttpHandlerRequest, store: Store, storage: UCRulesStorage, clientId: string, baseUrl: string): Promise> { + // 1. Retrieve Policy ID + const policyId = decodeURIComponent(retrieveID(checkBaseURL(request, baseUrl))); + + // 2: Get all reachable policy information + const { policyQuads, ownedRules, otherRules } = getOnePolicyInfo(policyId, store, clientId); + + if (policyQuads.size === 0) + throw new BadRequestHttpError("You cannot PATCH a nonexistent policy"); + + // 3. Parse the requested policy + const parsedPolicy = await parseBodyToStore(request); + + // 4. Sanitization checks (error is thrown when checks fail) + sanitizeRule(parsedPolicy, clientId); + + // 5. Delete the old policy information + const oldQuads = [...policyQuads, ...ownedRules, ...otherRules]; + await deleteOnePolicy(policyId, store, storage, clientId); + + try { + // 6. Add the new policy information + await storage.addRule(parsedPolicy); + } catch (error) { + try { + await storage.addRule(new Store(oldQuads)); + } catch (error) { + throw new InternalServerError("Deleted, but not rewritten\n", error); + } + + throw new InternalServerError("Failed to PATCH policy\n", error); + } + + return quadsToText(parsedPolicy.getQuads(null, null, null, null)); +} \ No newline at end of file diff --git a/scripts/test-uma-ODRL-policy.ts b/scripts/test-uma-ODRL-policy.ts index 40bc2f79..cfece5ba 100644 --- a/scripts/test-uma-ODRL-policy.ts +++ b/scripts/test-uma-ODRL-policy.ts @@ -4,12 +4,12 @@ * The purpose of this file is to test the /policies endpoint. */ -import { policyA, policyB, policyC, badPolicy1, changePolicy1, changePolicy95e } from "./util/policyExampels"; +import { policyA, policyB, policyC, badPolicy1, changePolicy1, changePolicy95e, putPolicy95e } from "./util/policyExampels"; const endpoint = (extra: string = '') => 'http://localhost:4000/uma/policies' + extra; const client = (client: string = 'a') => `https://pod.${client}.com/profile/card#me`; -const policyId = 'http://example.org/usagePolicy1'; -const policyToDelete = 'urn:uuid:95efe0e8-4fb7-496d-8f3c-4d78c97829bc' +const policyId1 = 'http://example.org/usagePolicy1'; +const policyId95e = 'urn:uuid:95efe0e8-4fb7-496d-8f3c-4d78c97829bc' const badPolicyId = 'nonExistentPolicy'; const quickBuffer = (text: string) => Buffer.from(text, 'utf-8'); let errorCounter = 0; @@ -19,9 +19,33 @@ const testCode = (code: number, shouldbe: number = 2, trunc: boolean = true) => if ((trunc ? Math.trunc(Number(code) / 100) : code) !== shouldbe) errorCounter++; } +async function putPolicies() { + const encoded = encodeURIComponent(policyId95e); + console.log("test PUT policies"); + + // Delete the policy to be sure + await fetch(endpoint(`/${encoded}`), { method: 'DELETE', headers: { 'Authorization': client('a') } }); + + // POST it again + let response = await fetch(endpoint(), { method: 'POST', headers: { 'Authorization': client('a'), 'Content-Type': 'text/turtle' }, body: quickBuffer(policyA) }); + console.log(`expecting a positive response: status code ${response.status}, ${await response.text()}`); + testCode(response.status); + + response = await fetch(endpoint(), { method: 'POST', headers: { 'Authorization': client('b'), 'Content-Type': 'text/turtle' }, body: quickBuffer(policyB) }); + console.log(`expecting a positive response: status code ${response.status}, ${await response.text()}`); + testCode(response.status); + + response = await fetch(endpoint(`/${encoded}`), { method: 'PUT', headers: { 'Authorization': client('a'), 'Content-Type': 'text/turtle' }, body: quickBuffer(putPolicy95e) }); + console.log(`expecting Policy header to mistakenly contain the old policy, and (correctly) the new policies: ${response.status}\n${await response.text()}`); + + response = await fetch(endpoint(`/${encoded}`), { headers: { 'Authorization': client('b') } }); + console.log(`expecting to stay the same as before ${policyId95e}`, await response.text()); + +} + async function patchPolicies() { console.log("Simple test for the PATCH policy endpoint"); - const encoded = encodeURIComponent(policyId); + const encoded = encodeURIComponent(policyId1); let response = await fetch(endpoint(`/${encoded}`), { method: 'PATCH', headers: { 'Authorization': client('a'), 'Content-Type': 'application/sparql-update' }, body: quickBuffer(changePolicy1) }); console.log(`expecting a positive response: status code ${response.status}\n expecting to see policy100 as its only rule: \n${await response.text()}`); @@ -56,10 +80,10 @@ async function getAllPolicies() { async function getOnePolicy() { console.log("Simple test for the GET One Policy endpoint"); - const encoded = encodeURIComponent(policyId); + const encoded = encodeURIComponent(policyId1); let response = await fetch(endpoint(`/${encoded}`), { headers: { 'Authorization': client('a') } }); - console.log(`expecting to return relevent information about ${policyId}`, await response.text()); + console.log(`expecting to return relevent information about ${policyId1}`, await response.text()); response = await fetch(endpoint(`/${badPolicyId}`), { headers: { 'Authorization': client('b') } }); let resText = await response.text(); @@ -97,7 +121,7 @@ async function postPolicy() { } async function testDelete() { - const encodedPolicyId = encodeURIComponent(policyToDelete); + const encodedPolicyId = encodeURIComponent(policyId95e); console.log("Testing Delete endpoint"); let response = await fetch(endpoint(`/${encodedPolicyId}`), { method: 'DELETE', headers: { 'Authorization': client('c') } }); @@ -152,13 +176,17 @@ async function main() { await postPolicy(); console.log("\n\n\n"); await patchPolicies(); - // await getAllPolicies(); - // console.log("\n\n\n"); - // await getOnePolicy(); - // console.log("\n\n\n"); - // await testDelete(); - // console.log("\n\n\n"); + console.log("\n\n\n"); + await getAllPolicies(); + console.log("\n\n\n"); + await getOnePolicy(); + console.log("\n\n\n"); + await testDelete(); + console.log("\n\n\n"); + await putPolicies(); + console.log("\n\n\n"); await deleteAll() + console.log("\n\n\n"); console.log(errorCounter === 0 ? `No fails detected` : `${errorCounter} tests have failed`); } diff --git a/scripts/util/policyExampels.ts b/scripts/util/policyExampels.ts index 0c9a9e5c..c9107650 100644 --- a/scripts/util/policyExampels.ts +++ b/scripts/util/policyExampels.ts @@ -145,4 +145,20 @@ WHERE { ?p ?o . } } +` + +export const putPolicy95e = ` + a ; + "ZENO is data owner of resource X. ALICE may READ resource X."; + , . + a ; + ; + ; + ; + . + a ; + ; + ; + ; + . ` \ No newline at end of file From f0ea4c744885ee084e3f3d257cb4149a5ee7448d Mon Sep 17 00:00:00 2001 From: lennertdr Date: Thu, 10 Jul 2025 09:57:51 +0200 Subject: [PATCH 37/62] extra check for PATCH --- .../routeSpecific/policies/EditPolicies.ts | 73 +++++++++++++++++-- 1 file changed, 67 insertions(+), 6 deletions(-) diff --git a/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts b/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts index e71b9ca6..ba508114 100644 --- a/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts @@ -1,4 +1,4 @@ -import { Store } from "n3"; +import { Quad, Store, Writer } from "n3"; import { HttpHandlerRequest, HttpHandlerResponse } from "../../http/models/HttpHandler"; import { UCRulesStorage } from "@solidlab/ucp"; import { checkBaseURL, parseBufferToString, quadsToText, retrieveID } from "./PolicyUtil"; @@ -7,9 +7,15 @@ import { BadRequestHttpError, InternalServerError } from "@solid/community-serve import { getOnePolicyInfo } from "./GetPolicies"; // Apparently js does not have this -const setIsEqual = (xs: Set, ys: Set) => - xs.size === ys.size && - [...xs].every((x) => ys.has(x)) +const setIsEqual = (xs: Set, ys: Set) => { + if (xs.size === ys.size) + [...xs].every((x) => { + console.log([...ys].some(y => x.equals(y))) + return [...ys].some(y => x.equals(y)); + }) + return xs.size === ys.size && + [...xs].every((x) => [...ys].some(y => x.equals(y))) +}; export async function editPolicy(request: HttpHandlerRequest, store: Store, storage: UCRulesStorage, clientId: string, baseUrl: string): Promise> { // 1. Retrieve Policy ID @@ -27,15 +33,70 @@ export async function editPolicy(request: HttpHandlerRequest, store: Store, stor // 4. Execute the query on the policy const policyStore = new Store([...policyQuads, ...ownedRules, ...otherRules]); + const writer = new Writer('Turtle'); + const initialQuads = policyStore.getQuads(null, null, null, null); + console.log( + ` + ----------------------------------------------- + INITIAL LENGTHS AND LISTS: + policy quads: ${policyQuads.size} + ${writer.quadsToString([...policyQuads])} + + owned rules: ${ownedRules.size} + ${writer.quadsToString([...ownedRules])} + + other rules: ${otherRules.size} + ${writer.quadsToString([...otherRules])} + + initial length: ${initialQuads.length} + #rules out of reach: ${initialQuads.length - policyQuads.size - ownedRules.size} + ----------------------------------------------- + ` + ); try { await new QueryEngine().queryVoid(query, { sources: [policyStore] }); } catch (error) { throw new BadRequestHttpError("Query could not be executed:", error); } - // 5. Check if we stayed between our boundaries + // 5. Simple safety checks + // Check that the other rules are unchanged + const newState = getOnePolicyInfo(policyId, policyStore, clientId); - if (!setIsEqual(newState.otherRules, otherRules)) throw new BadRequestHttpError("Update not allowed: attempted to modify rules not owned by client"); + console.log(writer.quadsToString([...newState.otherRules])) + if (!setIsEqual(newState.otherRules, otherRules)) + throw new BadRequestHttpError("Update not allowed: attempted to modify rules not owned by client"); + + // Check that only Policy/Rule changing quads are introduced and removed + const newQuads = policyStore.getQuads(null, null, null, null); + console.log( + writer.quadsToString(newQuads) + ); + console.log( + ` + ----------------------------------------------- + NEW LENGTHS AND LISTS: + policy quads: ${newState.policyQuads.size} + ${writer.quadsToString([...newState.policyQuads])} + + owned rules: ${newState.ownedRules.size} + ${writer.quadsToString([...newState.ownedRules])} + + other rules: ${newState.otherRules.size} + ${writer.quadsToString([...otherRules])} + + new length: ${newQuads.length} + #rules out of reach: ${newQuads.length - newState.policyQuads.size - newState.ownedRules.size} + ----------------------------------------------- + + + check object reference + old policy quads ${policyQuads.size} + old owned rule quads ${ownedRules.size} + ` + ); + if (newQuads.length - newState.ownedRules.size - newState.policyQuads.size !== initialQuads.length - policyQuads.size - ownedRules.size) + throw new BadRequestHttpError("Update not allowed: this query introduces quads that have nothing to do with the policy/rules you own") // 6. Modify the storage to the updated version try { From c807bb7ee8095546c9ba51a35075ee4efbc7d0fb Mon Sep 17 00:00:00 2001 From: lennertdr Date: Thu, 10 Jul 2025 09:59:54 +0200 Subject: [PATCH 38/62] remove console.logs --- .../routeSpecific/policies/EditPolicies.ts | 52 +------------------ 1 file changed, 1 insertion(+), 51 deletions(-) diff --git a/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts b/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts index ba508114..701f7d8a 100644 --- a/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts @@ -8,11 +8,6 @@ import { getOnePolicyInfo } from "./GetPolicies"; // Apparently js does not have this const setIsEqual = (xs: Set, ys: Set) => { - if (xs.size === ys.size) - [...xs].every((x) => { - console.log([...ys].some(y => x.equals(y))) - return [...ys].some(y => x.equals(y)); - }) return xs.size === ys.size && [...xs].every((x) => [...ys].some(y => x.equals(y))) }; @@ -35,24 +30,6 @@ export async function editPolicy(request: HttpHandlerRequest, store: Store, stor const policyStore = new Store([...policyQuads, ...ownedRules, ...otherRules]); const writer = new Writer('Turtle'); const initialQuads = policyStore.getQuads(null, null, null, null); - console.log( - ` - ----------------------------------------------- - INITIAL LENGTHS AND LISTS: - policy quads: ${policyQuads.size} - ${writer.quadsToString([...policyQuads])} - - owned rules: ${ownedRules.size} - ${writer.quadsToString([...ownedRules])} - - other rules: ${otherRules.size} - ${writer.quadsToString([...otherRules])} - - initial length: ${initialQuads.length} - #rules out of reach: ${initialQuads.length - policyQuads.size - ownedRules.size} - ----------------------------------------------- - ` - ); try { await new QueryEngine().queryVoid(query, { sources: [policyStore] }); } catch (error) { @@ -63,42 +40,15 @@ export async function editPolicy(request: HttpHandlerRequest, store: Store, stor // Check that the other rules are unchanged const newState = getOnePolicyInfo(policyId, policyStore, clientId); - console.log(writer.quadsToString([...newState.otherRules])) if (!setIsEqual(newState.otherRules, otherRules)) throw new BadRequestHttpError("Update not allowed: attempted to modify rules not owned by client"); // Check that only Policy/Rule changing quads are introduced and removed const newQuads = policyStore.getQuads(null, null, null, null); - console.log( - writer.quadsToString(newQuads) - ); - console.log( - ` - ----------------------------------------------- - NEW LENGTHS AND LISTS: - policy quads: ${newState.policyQuads.size} - ${writer.quadsToString([...newState.policyQuads])} - - owned rules: ${newState.ownedRules.size} - ${writer.quadsToString([...newState.ownedRules])} - - other rules: ${newState.otherRules.size} - ${writer.quadsToString([...otherRules])} - - new length: ${newQuads.length} - #rules out of reach: ${newQuads.length - newState.policyQuads.size - newState.ownedRules.size} - ----------------------------------------------- - - - check object reference - old policy quads ${policyQuads.size} - old owned rule quads ${ownedRules.size} - ` - ); if (newQuads.length - newState.ownedRules.size - newState.policyQuads.size !== initialQuads.length - policyQuads.size - ownedRules.size) throw new BadRequestHttpError("Update not allowed: this query introduces quads that have nothing to do with the policy/rules you own") - // 6. Modify the storage to the updated version + // 6. Modify the storage to the updated version, there is currently no real modification method. try { await storage.deleteRule(policyId); await storage.addRule(policyStore); From 308e6c113d801421c66314695cb86b04c1a490ff Mon Sep 17 00:00:00 2001 From: lennertdr Date: Thu, 10 Jul 2025 13:29:15 +0200 Subject: [PATCH 39/62] Seperate rule definitions for a policy based on the client --- docs/policy-management.md | 32 +++++--- .../routeSpecific/policies/DeletePolicies.ts | 1 + .../routeSpecific/policies/EditPolicies.ts | 78 ++++++++++++++++--- .../routeSpecific/policies/GetPolicies.ts | 62 +++++++++------ .../routeSpecific/policies/rewritePolicies.ts | 17 ++-- scripts/test-uma-ODRL-policy.ts | 14 ++-- 6 files changed, 148 insertions(+), 56 deletions(-) diff --git a/docs/policy-management.md b/docs/policy-management.md index 26726693..8383e9ee 100644 --- a/docs/policy-management.md +++ b/docs/policy-management.md @@ -15,36 +15,42 @@ The body is expected to represent a proper ODRL policy, although some [sanitizat ### Reading policies To read policies, two endpoints are implemented: - GET `/uma/policies`: get policy information that you are authorized to see, for every policy -- GET `/uma/policies/`: get policy information that you are authorized to see, for the policy with the requested [URL encoded](#uri-encodig-decision) ID. +- GET `/uma/policies/`: get policy information that you are authorized to see, for the policy with the requested [URL encoded](#uri-encodig-decision) ID. The current algorithm will retrieve the IDs of the policies and its rules that you are authorized to see. It will seek information about those properties with **depth 1**. This is not representative for a lot of policies, hence a recursive algorithm will be implemented in the future. ### Updating policies -Yet to be implemented... +Updating a policy can be done through a PUT or a PATCH request. Both requests have different implementations. Both requests are sent to endpoint `/uma/policies/` + +#### PUT +A PUT request to the policy endpoint will redefine the entire policy to the requested body, whithin the reach of the client. + +#### PATCH ### Deleting policies -To delete a policy, send a DELETE request to `/uma/policies/` with the URL encoded ID of the policy. The DELETE works like this: +To delete a policy, send a DELETE request to `/uma/policies/` with the URL encoded ID of the policy. The DELETE works like this: 1. Find the rules defined in the policy 2. Filter the rules that are assigned by the client, and delete them 3. Find out if there are rules not assigned by the client * if there are other rules, we cannot delete the policy information as well * if there are no other rules, we can delete the entire policy -extra info + +## Implementation details #### Authorization/Authentication decisions The current implementation has insufficient authentication restrictions. Currently, the only requirement is that the 'Authorization' header is to be set to the webID of the "logged on" client. Proper procedures to authenticate this client are still to be implemented. #### Sanitization decisions Some endpoints allow new policies to be created, or existing policies to be modified. This introduces the possibility of invalid syntactic or semantic policies, hence a sanitization strategy is required. In the current implementation, only POST could introduce such problems. We provided the following basic checks: -- Every defined rule must have a unique ID -- Every rule must have exactly one assigner -- Every assigner must match the authenticated client +- Every defined rule must have a unique ID. +- Every rule must have exactly one assigner. +- Every assigner must match the authenticated client. Sanitization Limitations -- It is possible to `POST` a policy with an ID that already exists, or with rules that reuse already existing IDs +- It is possible to `POST` a policy with an ID that already exists, or with rules that reuse already existing IDs. - There are currently no checks to verify whether a client is sufficiently authorized to create or modify a policy/rule for a specific target. - * A client should not be in able to alter rights about a target it does not have access to + * A client should not be in able to alter rights about a target it does not have access to. - There are plenty of other sanitization checks to be considered. #### URI encodig decision @@ -54,4 +60,10 @@ Some operations require the client to specify a policy ID in the URL. Since poli The current implementation is tested only by the script in `scripts\test-uma-ODRL-policy.ts`. This script tests every implemented endpoint with a designated flow. Since the script initiates with an empty storage, and there is no endpoint or other way to seed it, the first requests must test the POST endpoint. These tests are designed to ensure that the storage is filled. After the POST tests, the access endpoints can be tested. ## Problems -- When you have a policy with multiple rules that have different assigners, DELETE on every rule of one assigner will succesfully delete the rule itself, but not the definition of the rule within the policy. This is due to the fact that you can currently only DELETE based on the ID of the rule/policy you want to delete, and you cannot delete the entire policy since other assigners depend on it. \ No newline at end of file +- When you have a policy with multiple rules that have different assigners, DELETE on every rule of one assigner will succesfully delete the rule itself, but not the definition of the rule within the policy. This is due to the fact that you can currently only DELETE based on the ID of the rule/policy you want to delete, and you cannot delete the entire policy since other assigners depend on it. + +- Because PATCH currently works with sets, it contains a safety hazard. When client A has a certain policy/rule, or even just a certain quad, this can be discovered by an intrusive client B. Client B can simply PATCH an INSERT of a random quad that does NOT belong to its own rules/policies, which can have one of three outcomes: + 1. The PATCH resolves in an error saying that you cannot change rules that do not belong to you. This means that client A does not have this quad, since a modification was detected. + 2. The PATCH resolves in an error saying that you cannot change rules that belong to nobody. This means that the quad is not affiliated with any client. + 3. The PATCH completes with code 200. Since the inserted quad does NOT belong to you, there must be another client that owns the quad. In this way, any policy can be discovered. + An extra constraint, disabling clients to PATCH policies it has no rules in, would still enable the client to exploit policies that it has rules in. \ No newline at end of file diff --git a/packages/uma/src/util/routeSpecific/policies/DeletePolicies.ts b/packages/uma/src/util/routeSpecific/policies/DeletePolicies.ts index d91efb09..f9f069d9 100644 --- a/packages/uma/src/util/routeSpecific/policies/DeletePolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/DeletePolicies.ts @@ -49,6 +49,7 @@ export async function deleteOnePolicy(policyId: string, store: Store, storage: U const idsToDelete = otherRules.length === 0 ? [policyId] : ownedRules; // 3. Remove the specified quads + // Note that the current implementation of the storages 'deleteRule' cannot delete the quads that define the deleted rules try { await Promise.all(idsToDelete.map(id => storage.deleteRule(id))); diff --git a/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts b/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts index 701f7d8a..031b4d2c 100644 --- a/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts @@ -7,9 +7,14 @@ import { BadRequestHttpError, InternalServerError } from "@solid/community-serve import { getOnePolicyInfo } from "./GetPolicies"; // Apparently js does not have this -const setIsEqual = (xs: Set, ys: Set) => { - return xs.size === ys.size && - [...xs].every((x) => [...ys].some(y => x.equals(y))) +const setIsEqual = (xs: Quad[], ys: Quad[]) => { + // if (xs.length === ys.length) + // [...xs].every((x) => { + // console.log([...ys].some(y => x.equals(y))) + // return [...ys].some(y => x.equals(y)); + // }) + return xs.length === ys.length && + xs.every((x) => ys.some(y => x.equals(y))) }; export async function editPolicy(request: HttpHandlerRequest, store: Store, storage: UCRulesStorage, clientId: string, baseUrl: string): Promise> { @@ -24,12 +29,35 @@ export async function editPolicy(request: HttpHandlerRequest, store: Store, stor const query = parseBufferToString(request.body); // 3. Retrieve the existing policy info - const { policyQuads, ownedRules, otherRules } = getOnePolicyInfo(policyId, store, clientId); + const { policyDefinitions, ownedPolicyRules, otherPolicyRules, ownedRules, otherRules } = getOnePolicyInfo(policyId, store, clientId); + + // Implementation Choice: You cannot PATCH a policy that you are not affiliated with + if (ownedRules.length === 0) + throw new BadRequestHttpError("Update not allowed: You cannot update policies that you are not affiliated with"); // 4. Execute the query on the policy - const policyStore = new Store([...policyQuads, ...ownedRules, ...otherRules]); - const writer = new Writer('Turtle'); + const policyStore = new Store([...policyDefinitions, ...ownedPolicyRules, ...otherPolicyRules, ...ownedRules, ...otherRules]); const initialQuads = policyStore.getQuads(null, null, null, null); + // const writer = new Writer('Turtle'); + // console.log( + // ` + // ----------------------------------------------- + // INITIAL LENGTHS AND LISTS: + // policy quads: ${policyQuads.length} + // ${writer.quadsToString([...policyQuads])} + + // owned rules: ${ownedRules.length} + // ${writer.quadsToString([...ownedRules])} + + // other rules: ${otherRules.length} + // ${writer.quadsToString([...otherRules])} + + // initial length: ${initialQuads.length} + // #rules out of reach: ${initialQuads.length - policyQuads.length - ownedRules.length} + // ----------------------------------------------- + // ` + // ); + try { await new QueryEngine().queryVoid(query, { sources: [policyStore] }); } catch (error) { @@ -40,20 +68,52 @@ export async function editPolicy(request: HttpHandlerRequest, store: Store, stor // Check that the other rules are unchanged const newState = getOnePolicyInfo(policyId, policyStore, clientId); + // console.log(writer.quadsToString([...newState.otherRules])) if (!setIsEqual(newState.otherRules, otherRules)) throw new BadRequestHttpError("Update not allowed: attempted to modify rules not owned by client"); // Check that only Policy/Rule changing quads are introduced and removed + // The only modifications we allow are policy definitions, policy rules that define owned rules and owned rules themselves const newQuads = policyStore.getQuads(null, null, null, null); - if (newQuads.length - newState.ownedRules.size - newState.policyQuads.size !== initialQuads.length - policyQuads.size - ownedRules.size) + // console.log( + // writer.quadsToString(newQuads) + // ); + // console.log( + // ` + // ----------------------------------------------- + // NEW LENGTHS AND LISTS: + // policy quads: ${newState.policyQuads.length} + // ${writer.quadsToString([...newState.policyQuads])} + + // owned rules: ${newState.ownedRules.length} + // ${writer.quadsToString([...newState.ownedRules])} + + // other rules: ${newState.otherRules.length} + // ${writer.quadsToString([...otherRules])} + + // new length: ${newQuads.length} + // #rules out of reach: ${newQuads.length - newState.policyQuads.length - newState.ownedRules.length} + // ----------------------------------------------- + + + // check object reference + // old policy quads ${policyQuads.length} + // old owned rule quads ${ownedRules.length} + // ` + // ); + if (newQuads.length - newState.ownedRules.length - newState.ownedPolicyRules.length - newState.policyDefinitions.length + !== initialQuads.length - ownedPolicyRules.length - ownedRules.length - policyDefinitions.length) throw new BadRequestHttpError("Update not allowed: this query introduces quads that have nothing to do with the policy/rules you own") - // 6. Modify the storage to the updated version, there is currently no real modification method. + // 6. Modify the storage to the updated version try { await storage.deleteRule(policyId); await storage.addRule(policyStore); } catch (error) { throw new InternalServerError("Something went wrong while editting the policy:", error); } - return quadsToText(policyStore.getQuads(null, null, null, null)); + + // 7. Print information within reach + const finalState = getOnePolicyInfo(policyId, policyStore, clientId); + return quadsToText([...finalState.policyDefinitions, ...finalState.ownedPolicyRules, ...finalState.ownedRules]); } \ No newline at end of file diff --git a/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts b/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts index aa84c311..3caa1015 100644 --- a/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts @@ -27,45 +27,60 @@ export async function getPolicies(request: HttpHandlerRequest, store: Store, cli export interface OnePolicy { clientId: string; policyId: string; - policyQuads: Set; - ownedRules: Set; - otherRules: Set; + + // Policy quads are split in three parts, to not leak information out of the clients reach + policyDefinitions: Quad[]; + ownedPolicyRules: Quad[]; + otherPolicyRules: Quad[]; + ownedRules: Quad[]; + otherRules: Quad[]; } // Functional implementation to get one policy export function getOnePolicyInfo(policyId: string, store: Store, clientId: string): OnePolicy { - // 1. Search the policy by ID - const policyMatches = store.getQuads(namedNode(policyId), null, null, null); - // 2. Find the rules that this policy defines + // 1. Find the rules that this policy defines const policyRules: Quad[] = relations.flatMap(relation => store.getQuads(namedNode(policyId), relation, null, null) - ) + ); + + // 2. Collect the policy quads unrelated to rules + const policyDefinitions = store.getQuads(namedNode(policyId), null, null, null).filter(quad => + !policyRules.some(rule => quad.equals(rule)) + ); // 3. Separate the rules owned by the client from the others - const otherRules: Set = new Set(); - const ownedRules: Set = new Set(); + const otherRules: Quad[] = []; + const ownedRules: Quad[] = []; + const ownedPolicyRules: Quad[] = []; + const otherPolicyRules: Quad[] = []; policyRules.forEach(quad => { - if (store.getQuads(quad.object, odrlAssigner, namedNode(clientId), null).length === 1) + if (store.getQuads(quad.object, odrlAssigner, namedNode(clientId), null).length === 1) { // This is the step to be replaced with the recursive variant store.getQuads(quad.object, null, null, null).forEach( - quad => ownedRules.add(quad) - ) - else + quad => ownedRules.push(quad) + ); + ownedPolicyRules.push(quad); + } else { // Once again, this is to be replaced with the recursive variant store.getQuads(quad.object, null, null, null).forEach( - quad => otherRules.add(quad) - ) + quad => otherRules.push(quad) + ); + otherPolicyRules.push(quad); + } + }); // 4. Return the detailed object return { policyId: policyId, clientId: clientId, - policyQuads: new Set(policyMatches), + policyDefinitions: policyDefinitions, + ownedPolicyRules: ownedPolicyRules, + otherPolicyRules: otherPolicyRules, ownedRules: ownedRules, otherRules: otherRules - } + }; } @@ -82,9 +97,9 @@ export function getOnePolicyInfo(policyId: string, store: Store, clientId: strin async function getOnePolicy(policyId: string, store: Store, clientId: string): Promise> { policyId = decodeURIComponent(policyId); - const { policyQuads, ownedRules } = getOnePolicyInfo(policyId, store, clientId); + const { policyDefinitions, ownedPolicyRules, ownedRules } = getOnePolicyInfo(policyId, store, clientId); - if (ownedRules.size === 0) + if (ownedRules.length === 0) return { status: 204, headers: { @@ -93,7 +108,7 @@ async function getOnePolicy(policyId: string, store: Store, clientId: string): P body: '', } - return quadsToText([...policyQuads, ...ownedRules]); + return quadsToText([...policyDefinitions, ...ownedPolicyRules, ...ownedRules]); } @@ -122,9 +137,12 @@ async function getAllPolicies(store: Store, clientId: string): Promise 0) { - // Because an ODRL policy may only have one assigner, we can now add all policy and rule information + // Because an ODRL policy may only have one assigner, we can now add all policy and rule information, except the policy quads that define other client's rules // Note that this is the only part of the function to be replaced with the recursive variant - store.getQuads(policy, null, null, null).forEach(quad => policyDetails.add(quad)); + store.getQuads(policy, null, null, null).forEach(quad => { + if (!(relations.map(r => r.value as string).includes(quad.predicate.id)) || store.getQuads(quad.object, odrlAssigner, namedNode(clientId), null).length > 0) + policyDetails.add(quad); + }); store.getQuads(rule, null, null, null).forEach(quad => policyDetails.add(quad)); } } diff --git a/packages/uma/src/util/routeSpecific/policies/rewritePolicies.ts b/packages/uma/src/util/routeSpecific/policies/rewritePolicies.ts index 0b272d03..83ff3783 100644 --- a/packages/uma/src/util/routeSpecific/policies/rewritePolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/rewritePolicies.ts @@ -1,8 +1,8 @@ import { UCRulesStorage } from "@solidlab/ucp"; import { HttpHandlerRequest, HttpHandlerResponse } from "../../http/models/HttpHandler"; import { BadRequestHttpError, InternalServerError } from "@solid/community-server"; -import { checkBaseURL, namedNode, parseBodyToStore, quadsToText, retrieveID } from "./PolicyUtil"; -import { Store } from "n3"; +import { checkBaseURL, parseBodyToStore, quadsToText, retrieveID } from "./PolicyUtil"; +import { Quad, Store } from "n3"; import { sanitizeRule } from "./CreatePolicies"; import { deleteOnePolicy } from "./DeletePolicies"; import { getOnePolicyInfo } from "./GetPolicies"; @@ -12,9 +12,8 @@ export async function rewritePolicy(request: HttpHandlerRequest, store: Store, s const policyId = decodeURIComponent(retrieveID(checkBaseURL(request, baseUrl))); // 2: Get all reachable policy information - const { policyQuads, ownedRules, otherRules } = getOnePolicyInfo(policyId, store, clientId); - - if (policyQuads.size === 0) + const { policyDefinitions, ownedPolicyRules, otherPolicyRules, ownedRules, otherRules } = getOnePolicyInfo(policyId, store, clientId); + if (policyDefinitions.length === 0) throw new BadRequestHttpError("You cannot PATCH a nonexistent policy"); // 3. Parse the requested policy @@ -23,20 +22,22 @@ export async function rewritePolicy(request: HttpHandlerRequest, store: Store, s // 4. Sanitization checks (error is thrown when checks fail) sanitizeRule(parsedPolicy, clientId); - // 5. Delete the old policy information - const oldQuads = [...policyQuads, ...ownedRules, ...otherRules]; + // 5. Delete the old policy information and keep track of the old ones for possible rollback + const oldQuads: Quad[] = [...policyDefinitions, ...ownedPolicyRules, ...otherPolicyRules, ...ownedRules, ...otherRules]; await deleteOnePolicy(policyId, store, storage, clientId); try { // 6. Add the new policy information await storage.addRule(parsedPolicy); } catch (error) { + // If addition fails, try to restore the old quads try { await storage.addRule(new Store(oldQuads)); } catch (error) { + // The restored quads could also not be added, the patch failed and the old policy (whithin your reach) is deleted throw new InternalServerError("Deleted, but not rewritten\n", error); } - + // The restored quads have been added, the patch failed and nothing changed throw new InternalServerError("Failed to PATCH policy\n", error); } diff --git a/scripts/test-uma-ODRL-policy.ts b/scripts/test-uma-ODRL-policy.ts index cfece5ba..1833bfe3 100644 --- a/scripts/test-uma-ODRL-policy.ts +++ b/scripts/test-uma-ODRL-policy.ts @@ -45,18 +45,18 @@ async function putPolicies() { async function patchPolicies() { console.log("Simple test for the PATCH policy endpoint"); - const encoded = encodeURIComponent(policyId1); + const encoded1 = encodeURIComponent(policyId1); + const encoded95e = encodeURIComponent(policyId95e); - let response = await fetch(endpoint(`/${encoded}`), { method: 'PATCH', headers: { 'Authorization': client('a'), 'Content-Type': 'application/sparql-update' }, body: quickBuffer(changePolicy1) }); + let response = await fetch(endpoint(`/${encoded1}`), { method: 'PATCH', headers: { 'Authorization': client('a'), 'Content-Type': 'application/sparql-update' }, body: quickBuffer(changePolicy1) }); console.log(`expecting a positive response: status code ${response.status}\n expecting to see policy100 as its only rule: \n${await response.text()}`); - // console.log(`expecting a negative response since assigner != client: status code ${response.status}\nmessage: ${await response.text()}`); testCode(response.status); - response = await fetch(endpoint(`/${encoded}`), { method: 'PATCH', headers: { 'Authorization': client('a'), 'Content-Type': 'application/sparql-update' }, body: quickBuffer(changePolicy95e) }); + response = await fetch(endpoint(`/${encoded95e}`), { method: 'PATCH', headers: { 'Authorization': client('a'), 'Content-Type': 'application/sparql-update' }, body: quickBuffer(changePolicy95e) }); console.log(`expecting a positive response: status code ${response.status}\nexpecting the old rule to delete and two rules to take its place: \n${await response.text()}`); testCode(response.status); - response = await fetch(endpoint(`/${encoded}`), { method: 'PATCH', headers: { 'Authorization': client('c'), 'Content-Type': 'application/sparql-update' }, body: quickBuffer(changePolicy1) }); + response = await fetch(endpoint(`/${encoded1}`), { method: 'PATCH', headers: { 'Authorization': client('c'), 'Content-Type': 'application/sparql-update' }, body: quickBuffer(changePolicy1) }); console.log(`expecting a negative response since the query changes another client's rules ${response.status}\nmessage: ${await response.text()}`); testCode(response.status, 4); } @@ -175,12 +175,12 @@ async function main() { console.log("Testing all implemented Policy Endpoints:\n\n\n"); await postPolicy(); console.log("\n\n\n"); - await patchPolicies(); - console.log("\n\n\n"); await getAllPolicies(); console.log("\n\n\n"); await getOnePolicy(); console.log("\n\n\n"); + await patchPolicies(); + console.log("\n\n\n"); await testDelete(); console.log("\n\n\n"); await putPolicies(); From f98fbc9deaf3356958c5eacb0dcd1cba58cd81be Mon Sep 17 00:00:00 2001 From: lennertdr Date: Thu, 10 Jul 2025 14:47:53 +0200 Subject: [PATCH 40/62] PATCH safety fix, GET duplicate fix --- docs/policy-management.md | 16 ++- .../routeSpecific/policies/DeletePolicies.ts | 1 + .../routeSpecific/policies/EditPolicies.ts | 111 +++++++++--------- .../routeSpecific/policies/GetPolicies.ts | 12 +- scripts/test-uma-ODRL-policy.ts | 4 + 5 files changed, 75 insertions(+), 69 deletions(-) diff --git a/docs/policy-management.md b/docs/policy-management.md index 8383e9ee..a47b6717 100644 --- a/docs/policy-management.md +++ b/docs/policy-management.md @@ -62,8 +62,14 @@ The current implementation is tested only by the script in `scripts\test-uma-ODR ## Problems - When you have a policy with multiple rules that have different assigners, DELETE on every rule of one assigner will succesfully delete the rule itself, but not the definition of the rule within the policy. This is due to the fact that you can currently only DELETE based on the ID of the rule/policy you want to delete, and you cannot delete the entire policy since other assigners depend on it. -- Because PATCH currently works with sets, it contains a safety hazard. When client A has a certain policy/rule, or even just a certain quad, this can be discovered by an intrusive client B. Client B can simply PATCH an INSERT of a random quad that does NOT belong to its own rules/policies, which can have one of three outcomes: - 1. The PATCH resolves in an error saying that you cannot change rules that do not belong to you. This means that client A does not have this quad, since a modification was detected. - 2. The PATCH resolves in an error saying that you cannot change rules that belong to nobody. This means that the quad is not affiliated with any client. - 3. The PATCH completes with code 200. Since the inserted quad does NOT belong to you, there must be another client that owns the quad. In this way, any policy can be discovered. - An extra constraint, disabling clients to PATCH policies it has no rules in, would still enable the client to exploit policies that it has rules in. \ No newline at end of file + +### Solved Problems + +#### PATCH fix +Because PATCH currently works with sets, it contains a safety hazard. When client A has a certain policy/rule, or even just a certain quad, this can be discovered by an intrusive client B. Client B can simply PATCH an INSERT of a random quad that does NOT belong to its own rules/policies, which can have one of three outcomes: +1. The PATCH resolves in an error saying that you cannot change rules that do not belong to you. This means that client A does not have this quad, since a modification was detected. +2. The PATCH resolves in an error saying that you cannot change rules that belong to nobody. This means that the quad is not affiliated with any client. +3. The PATCH completes with code 200. Since the inserted quad does NOT belong to you, there must be another client that owns the quad. In this way, any policy can be discovered. +An extra constraint, disabling clients to PATCH policies it has no rules in, would still enable the client to exploit policies that it has rules in. + +This problem was solved by splitting the policy into the parts where the client has access to, and the parts where it does not. By executing the query only on the parts that the client has access to, it would be easier to analyse the resulting store of the query. If this store has rules that the client does not have access to, they must have been added by the client and the operation gets cancelled. This method is also protected from deleting rules out of our reach. \ No newline at end of file diff --git a/packages/uma/src/util/routeSpecific/policies/DeletePolicies.ts b/packages/uma/src/util/routeSpecific/policies/DeletePolicies.ts index f9f069d9..f19a785a 100644 --- a/packages/uma/src/util/routeSpecific/policies/DeletePolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/DeletePolicies.ts @@ -50,6 +50,7 @@ export async function deleteOnePolicy(policyId: string, store: Store, storage: U // 3. Remove the specified quads // Note that the current implementation of the storages 'deleteRule' cannot delete the quads that define the deleted rules + // A way to deal with this could be updating the store using dedicated DELETE sparql queries, or by introducing a variant of the storage.deleteRule try { await Promise.all(idsToDelete.map(id => storage.deleteRule(id))); diff --git a/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts b/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts index 031b4d2c..773697f2 100644 --- a/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts @@ -7,12 +7,7 @@ import { BadRequestHttpError, InternalServerError } from "@solid/community-serve import { getOnePolicyInfo } from "./GetPolicies"; // Apparently js does not have this -const setIsEqual = (xs: Quad[], ys: Quad[]) => { - // if (xs.length === ys.length) - // [...xs].every((x) => { - // console.log([...ys].some(y => x.equals(y))) - // return [...ys].some(y => x.equals(y)); - // }) +const sameContent = (xs: Quad[], ys: Quad[]) => { return xs.length === ys.length && xs.every((x) => ys.some(y => x.equals(y))) }; @@ -35,79 +30,79 @@ export async function editPolicy(request: HttpHandlerRequest, store: Store, stor if (ownedRules.length === 0) throw new BadRequestHttpError("Update not allowed: You cannot update policies that you are not affiliated with"); - // 4. Execute the query on the policy - const policyStore = new Store([...policyDefinitions, ...ownedPolicyRules, ...otherPolicyRules, ...ownedRules, ...otherRules]); + // 4. Execute the query on the part of the policy that lays within reach + const policyStore = new Store([...policyDefinitions, ...ownedPolicyRules, ...ownedRules]); const initialQuads = policyStore.getQuads(null, null, null, null); - // const writer = new Writer('Turtle'); - // console.log( - // ` - // ----------------------------------------------- - // INITIAL LENGTHS AND LISTS: - // policy quads: ${policyQuads.length} - // ${writer.quadsToString([...policyQuads])} - - // owned rules: ${ownedRules.length} - // ${writer.quadsToString([...ownedRules])} - - // other rules: ${otherRules.length} - // ${writer.quadsToString([...otherRules])} - - // initial length: ${initialQuads.length} - // #rules out of reach: ${initialQuads.length - policyQuads.length - ownedRules.length} - // ----------------------------------------------- - // ` - // ); - try { await new QueryEngine().queryVoid(query, { sources: [policyStore] }); } catch (error) { throw new BadRequestHttpError("Query could not be executed:", error); } + const writer = new Writer('Turtle') + + console.log( + ` + ----------------------------------------------- + INITIAL LENGTHS AND LISTS: + policy quads: + Definitions: ${policyDefinitions.length} + ${writer.quadsToString([...policyDefinitions])} + Owned: ${ownedPolicyRules.length} + ${writer.quadsToString([...ownedPolicyRules])} + Other: ${otherPolicyRules.length} + ${writer.quadsToString([...otherPolicyRules])} + owned rules: ${ownedRules.length} + ${writer.quadsToString([...ownedRules])} + other rules: ${otherRules.length} + ${writer.quadsToString([...otherRules])} + initial length: ${initialQuads.length} + #rules out of reach: ${initialQuads.length - policyDefinitions.length - ownedPolicyRules.length - ownedRules.length} + ----------------------------------------------- + ` + ); // 5. Simple safety checks // Check that the other rules are unchanged - const newState = getOnePolicyInfo(policyId, policyStore, clientId); - // console.log(writer.quadsToString([...newState.otherRules])) - if (!setIsEqual(newState.otherRules, otherRules)) + const newQuads = policyStore.getQuads(null, null, null, null); + + console.log( + ` + ----------------------------------------------- + NEW LENGTHS AND LISTS: + policy quads: + Definitions: ${newState.policyDefinitions.length} + ${writer.quadsToString([...newState.policyDefinitions])} + Owned: ${newState.ownedPolicyRules.length} + ${writer.quadsToString([...newState.ownedPolicyRules])} + Other: ${otherPolicyRules.length} + ${writer.quadsToString([...newState.otherPolicyRules])} + owned rules: ${newState.ownedRules.length} + ${writer.quadsToString([...newState.ownedRules])} + other rules: ${newState.otherRules.length} + ${writer.quadsToString([...newState.otherRules])} + initial length: ${newQuads.length} + #rules out of reach: ${newQuads.length - newState.policyDefinitions.length - newState.ownedPolicyRules.length - newState.ownedRules.length} + ----------------------------------------------- + ` + ); + if (newState.otherRules.length !== 0) throw new BadRequestHttpError("Update not allowed: attempted to modify rules not owned by client"); // Check that only Policy/Rule changing quads are introduced and removed // The only modifications we allow are policy definitions, policy rules that define owned rules and owned rules themselves - const newQuads = policyStore.getQuads(null, null, null, null); - // console.log( - // writer.quadsToString(newQuads) - // ); - // console.log( - // ` - // ----------------------------------------------- - // NEW LENGTHS AND LISTS: - // policy quads: ${newState.policyQuads.length} - // ${writer.quadsToString([...newState.policyQuads])} - - // owned rules: ${newState.ownedRules.length} - // ${writer.quadsToString([...newState.ownedRules])} - - // other rules: ${newState.otherRules.length} - // ${writer.quadsToString([...otherRules])} - // new length: ${newQuads.length} - // #rules out of reach: ${newQuads.length - newState.policyQuads.length - newState.ownedRules.length} - // ----------------------------------------------- - - - // check object reference - // old policy quads ${policyQuads.length} - // old owned rule quads ${ownedRules.length} - // ` - // ); if (newQuads.length - newState.ownedRules.length - newState.ownedPolicyRules.length - newState.policyDefinitions.length !== initialQuads.length - ownedPolicyRules.length - ownedRules.length - policyDefinitions.length) - throw new BadRequestHttpError("Update not allowed: this query introduces quads that have nothing to do with the policy/rules you own") + throw new BadRequestHttpError("Update not allowed: this query introduces quads that have nothing to do with the policy/rules you own"); // 6. Modify the storage to the updated version try { + // Since no update function is available, we need to remove the old one and set the updated one await storage.deleteRule(policyId); + + // Add the other quads back into the policy + policyStore.addQuads([...otherPolicyRules, ...otherRules]); await storage.addRule(policyStore); } catch (error) { throw new InternalServerError("Something went wrong while editting the policy:", error); diff --git a/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts b/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts index 3caa1015..3c50bff3 100644 --- a/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts @@ -121,8 +121,8 @@ async function getOnePolicy(policyId: string, store: Store, clientId: string): P */ async function getAllPolicies(store: Store, clientId: string): Promise> { - // Keep track of all the matching policies - const policyDetails: Set = new Set(); + // Keep track of all the matching policies (use store because javascript does not know how sets work) + const policyDetails: Store = new Store(); for (const relation of relations) { // Collect every quad that matches with the relation (one of Permission, Prohibition or Duty) @@ -137,15 +137,15 @@ async function getAllPolicies(store: Store, clientId: string): Promise 0) { - // Because an ODRL policy may only have one assigner, we can now add all policy and rule information, except the policy quads that define other client's rules + // Because an ODRL policy may only have one assigner, we can now add all policy and rule information, except the policy quads that define other clients rules // Note that this is the only part of the function to be replaced with the recursive variant store.getQuads(policy, null, null, null).forEach(quad => { if (!(relations.map(r => r.value as string).includes(quad.predicate.id)) || store.getQuads(quad.object, odrlAssigner, namedNode(clientId), null).length > 0) - policyDetails.add(quad); + policyDetails.addQuad(quad); }); - store.getQuads(rule, null, null, null).forEach(quad => policyDetails.add(quad)); + store.getQuads(rule, null, null, null).forEach(quad => policyDetails.addQuad(quad)); } } } - return quadsToText(Array.from(policyDetails)); + return quadsToText(policyDetails.getQuads(null, null, null, null)); } \ No newline at end of file diff --git a/scripts/test-uma-ODRL-policy.ts b/scripts/test-uma-ODRL-policy.ts index 1833bfe3..3a73b70a 100644 --- a/scripts/test-uma-ODRL-policy.ts +++ b/scripts/test-uma-ODRL-policy.ts @@ -59,6 +59,10 @@ async function patchPolicies() { response = await fetch(endpoint(`/${encoded1}`), { method: 'PATCH', headers: { 'Authorization': client('c'), 'Content-Type': 'application/sparql-update' }, body: quickBuffer(changePolicy1) }); console.log(`expecting a negative response since the query changes another client's rules ${response.status}\nmessage: ${await response.text()}`); testCode(response.status, 4); + + response = await fetch(endpoint(), { headers: { 'Authorization': client('a') } }) + console.log("expecting to see the patched policy for client a: \n", await response.text()) + testCode(response.status); } async function getAllPolicies() { From 15d4619fce96882eeaa1edfb00a3ebcc17d701e6 Mon Sep 17 00:00:00 2001 From: lennertdr Date: Thu, 10 Jul 2025 15:38:45 +0200 Subject: [PATCH 41/62] cleanup, fix PUT, less redundant GET --- .../routeSpecific/policies/EditPolicies.ts | 56 +------------------ .../routeSpecific/policies/GetPolicies.ts | 25 +++++++-- .../routeSpecific/policies/rewritePolicies.ts | 12 ++-- 3 files changed, 30 insertions(+), 63 deletions(-) diff --git a/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts b/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts index 773697f2..b6f06efa 100644 --- a/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts @@ -1,4 +1,4 @@ -import { Quad, Store, Writer } from "n3"; +import { Store } from "n3"; import { HttpHandlerRequest, HttpHandlerResponse } from "../../http/models/HttpHandler"; import { UCRulesStorage } from "@solidlab/ucp"; import { checkBaseURL, parseBufferToString, quadsToText, retrieveID } from "./PolicyUtil"; @@ -6,12 +6,6 @@ import { QueryEngine } from '@comunica/query-sparql'; import { BadRequestHttpError, InternalServerError } from "@solid/community-server"; import { getOnePolicyInfo } from "./GetPolicies"; -// Apparently js does not have this -const sameContent = (xs: Quad[], ys: Quad[]) => { - return xs.length === ys.length && - xs.every((x) => ys.some(y => x.equals(y))) -}; - export async function editPolicy(request: HttpHandlerRequest, store: Store, storage: UCRulesStorage, clientId: string, baseUrl: string): Promise> { // 1. Retrieve Policy ID const policyId = decodeURIComponent(retrieveID(checkBaseURL(request, baseUrl))); @@ -38,60 +32,16 @@ export async function editPolicy(request: HttpHandlerRequest, store: Store, stor } catch (error) { throw new BadRequestHttpError("Query could not be executed:", error); } - const writer = new Writer('Turtle') - - console.log( - ` - ----------------------------------------------- - INITIAL LENGTHS AND LISTS: - policy quads: - Definitions: ${policyDefinitions.length} - ${writer.quadsToString([...policyDefinitions])} - Owned: ${ownedPolicyRules.length} - ${writer.quadsToString([...ownedPolicyRules])} - Other: ${otherPolicyRules.length} - ${writer.quadsToString([...otherPolicyRules])} - owned rules: ${ownedRules.length} - ${writer.quadsToString([...ownedRules])} - other rules: ${otherRules.length} - ${writer.quadsToString([...otherRules])} - initial length: ${initialQuads.length} - #rules out of reach: ${initialQuads.length - policyDefinitions.length - ownedPolicyRules.length - ownedRules.length} - ----------------------------------------------- - ` - ); // 5. Simple safety checks // Check that the other rules are unchanged const newState = getOnePolicyInfo(policyId, policyStore, clientId); - const newQuads = policyStore.getQuads(null, null, null, null); - - console.log( - ` - ----------------------------------------------- - NEW LENGTHS AND LISTS: - policy quads: - Definitions: ${newState.policyDefinitions.length} - ${writer.quadsToString([...newState.policyDefinitions])} - Owned: ${newState.ownedPolicyRules.length} - ${writer.quadsToString([...newState.ownedPolicyRules])} - Other: ${otherPolicyRules.length} - ${writer.quadsToString([...newState.otherPolicyRules])} - owned rules: ${newState.ownedRules.length} - ${writer.quadsToString([...newState.ownedRules])} - other rules: ${newState.otherRules.length} - ${writer.quadsToString([...newState.otherRules])} - initial length: ${newQuads.length} - #rules out of reach: ${newQuads.length - newState.policyDefinitions.length - newState.ownedPolicyRules.length - newState.ownedRules.length} - ----------------------------------------------- - ` - ); - if (newState.otherRules.length !== 0) + if (newState.otherRules.length !== 0 || newState.otherPolicyRules.length !== 0) throw new BadRequestHttpError("Update not allowed: attempted to modify rules not owned by client"); // Check that only Policy/Rule changing quads are introduced and removed // The only modifications we allow are policy definitions, policy rules that define owned rules and owned rules themselves - + const newQuads = policyStore.getQuads(null, null, null, null); if (newQuads.length - newState.ownedRules.length - newState.ownedPolicyRules.length - newState.policyDefinitions.length !== initialQuads.length - ownedPolicyRules.length - ownedRules.length - policyDefinitions.length) throw new BadRequestHttpError("Update not allowed: this query introduces quads that have nothing to do with the policy/rules you own"); diff --git a/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts b/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts index 3c50bff3..3021075f 100644 --- a/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts @@ -29,6 +29,7 @@ export interface OnePolicy { policyId: string; // Policy quads are split in three parts, to not leak information out of the clients reach + // The definitions are every policy quad that do not declare a rule policyDefinitions: Quad[]; ownedPolicyRules: Quad[]; otherPolicyRules: Quad[]; @@ -56,13 +57,13 @@ export function getOnePolicyInfo(policyId: string, store: Store, clientId: strin const otherPolicyRules: Quad[] = []; policyRules.forEach(quad => { if (store.getQuads(quad.object, odrlAssigner, namedNode(clientId), null).length === 1) { - // This is the step to be replaced with the recursive variant + // TODO: This is the step to be replaced with the recursive variant store.getQuads(quad.object, null, null, null).forEach( quad => ownedRules.push(quad) ); ownedPolicyRules.push(quad); } else { - // Once again, this is to be replaced with the recursive variant + // TODO: Once again, this is to be replaced with the recursive variant store.getQuads(quad.object, null, null, null).forEach( quad => otherRules.push(quad) ); @@ -111,6 +112,17 @@ async function getOnePolicy(policyId: string, store: Store, clientId: string): P return quadsToText([...policyDefinitions, ...ownedPolicyRules, ...ownedRules]); } +// Fill the policyDetails store with +// 1. The policy definitions +// 2. The owned rules declared in the policy +// TODO: This is a depth 1 algorithm that has to be replaced +export function getFilteredPolicyQuads(dataStore: Store, policyDetails: Store, policyId: string, clientId: string) { + dataStore.getQuads(namedNode(policyId), null, null, null).forEach(quad => { + if (!(relations.map(r => r.value as string).includes(quad.predicate.id)) || dataStore.getQuads(quad.object, odrlAssigner, namedNode(clientId), null).length > 0) + policyDetails.addQuad(quad); + }); +} + /** * Get all policy information relevant to the client in the request. @@ -123,6 +135,7 @@ async function getAllPolicies(store: Store, clientId: string): Promise = new Set(); for (const relation of relations) { // Collect every quad that matches with the relation (one of Permission, Prohibition or Duty) @@ -139,10 +152,10 @@ async function getAllPolicies(store: Store, clientId: string): Promise { - if (!(relations.map(r => r.value as string).includes(quad.predicate.id)) || store.getQuads(quad.object, odrlAssigner, namedNode(clientId), null).length > 0) - policyDetails.addQuad(quad); - }); + if (!memory.has(policy.id)) { + getFilteredPolicyQuads(store, policyDetails, policy.id, clientId); + memory.add(policy.id); + } store.getQuads(rule, null, null, null).forEach(quad => policyDetails.addQuad(quad)); } } diff --git a/packages/uma/src/util/routeSpecific/policies/rewritePolicies.ts b/packages/uma/src/util/routeSpecific/policies/rewritePolicies.ts index 83ff3783..e7fd440c 100644 --- a/packages/uma/src/util/routeSpecific/policies/rewritePolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/rewritePolicies.ts @@ -12,8 +12,8 @@ export async function rewritePolicy(request: HttpHandlerRequest, store: Store, s const policyId = decodeURIComponent(retrieveID(checkBaseURL(request, baseUrl))); // 2: Get all reachable policy information - const { policyDefinitions, ownedPolicyRules, otherPolicyRules, ownedRules, otherRules } = getOnePolicyInfo(policyId, store, clientId); - if (policyDefinitions.length === 0) + const policyInfo = getOnePolicyInfo(policyId, store, clientId); + if (policyInfo.policyDefinitions.length === 0) throw new BadRequestHttpError("You cannot PATCH a nonexistent policy"); // 3. Parse the requested policy @@ -23,7 +23,9 @@ export async function rewritePolicy(request: HttpHandlerRequest, store: Store, s sanitizeRule(parsedPolicy, clientId); // 5. Delete the old policy information and keep track of the old ones for possible rollback - const oldQuads: Quad[] = [...policyDefinitions, ...ownedPolicyRules, ...otherPolicyRules, ...ownedRules, ...otherRules]; + const oldQuads: Quad[] = [...policyInfo.policyDefinitions, ...policyInfo.ownedPolicyRules, ...policyInfo.otherPolicyRules, ...policyInfo.ownedRules, ...policyInfo.otherRules]; + + // TODO: this deletion does not delete rule definitions in the Policy declaration when there are multiple clients in the Policy await deleteOnePolicy(policyId, store, storage, clientId); try { @@ -41,5 +43,7 @@ export async function rewritePolicy(request: HttpHandlerRequest, store: Store, s throw new InternalServerError("Failed to PATCH policy\n", error); } - return quadsToText(parsedPolicy.getQuads(null, null, null, null)); + // Delete only what is in your reach + const { policyDefinitions, ownedPolicyRules, ownedRules } = getOnePolicyInfo(policyId, parsedPolicy, clientId); + return quadsToText([...policyDefinitions, ...ownedPolicyRules, ...ownedRules]); } \ No newline at end of file From d16071b0224993a946f399b25583d286eb9df29a Mon Sep 17 00:00:00 2001 From: lennertdr Date: Thu, 10 Jul 2025 16:28:31 +0200 Subject: [PATCH 42/62] extra PUT checks, extra documentation --- docs/policy-management.md | 8 +++++--- .../routeSpecific/policies/CreatePolicies.ts | 5 ++--- .../routeSpecific/policies/rewritePolicies.ts | 20 +++++++++++++++++-- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/docs/policy-management.md b/docs/policy-management.md index a47b6717..9e619953 100644 --- a/docs/policy-management.md +++ b/docs/policy-management.md @@ -17,15 +17,16 @@ To read policies, two endpoints are implemented: - GET `/uma/policies`: get policy information that you are authorized to see, for every policy - GET `/uma/policies/`: get policy information that you are authorized to see, for the policy with the requested [URL encoded](#uri-encodig-decision) ID. -The current algorithm will retrieve the IDs of the policies and its rules that you are authorized to see. It will seek information about those properties with **depth 1**. This is not representative for a lot of policies, hence a recursive algorithm will be implemented in the future. +The current algorithm will retrieve the IDs of the policies and its rules that you are authorized to see. It will seek information about those properties with **depth 1**. This is not representative for a lot of policies, hence a recursive algorithm will be implemented in the future. The procedures will only display information within the scope of the client. This means that other clients' rules and their definition in the policy quads will not be displayed. ### Updating policies Updating a policy can be done through a PUT or a PATCH request. Both requests have different implementations. Both requests are sent to endpoint `/uma/policies/` #### PUT -A PUT request to the policy endpoint will redefine the entire policy to the requested body, whithin the reach of the client. +A PUT request to the policy endpoint will redefine the entire policy to the requested body, whithin the reach of the client. The PUT works as a combination of DELETE and POST. It requires a body with the same content type as the [POST request](#creating-policies). This body will be interpreted as a policy with some rules. It executes the same sanitization checks as the POST request. Extra checks are implemented to make sure the new policy does not alter out of scope quads. After that, every rule within our reach (and possibly the entire policy) will be deleted. Upon success, the new policy and rule definitions will be added to the storage. When this addition fails, the procedure tries to restore the old policy. In this case, an internal server error will indicate that the patch failed. When the restore could not be completed, the internal server error will indicate that the policy with `` has been removed as if a DELETE request was sent. #### PATCH +A PATCH request to the policy endpoint will update the requested policy. The request expects a body with content type [`application/sparql-query`](https://www.w3.org/TR/rdf-sparql-query/). The INSERT and DELETE properties can be used to modify the requested policy. The PATCH request will get the existing information about the policy, and it will execute the query in an isolated environment. This environment is a store that contains only the relevant quads whithin the scope of the client. This means that a quad ` .` will not be a part of the environment if the rule has not been assigned by the client. Quads that are part of a rule not assigned by the client will also not be a part of the environment. This means that, after executing the query, we can analyse it as desired: we can check that no quads out of scope are added. Only in that case, the update will proceed. The procedure deletes the old policy and replaces it with the updated version, since no better way to update the storage exists. Note that we ### Deleting policies To delete a policy, send a DELETE request to `/uma/policies/` with the URL encoded ID of the policy. The DELETE works like this: @@ -34,6 +35,7 @@ To delete a policy, send a DELETE request to `/uma/policies/` with the 3. Find out if there are rules not assigned by the client * if there are other rules, we cannot delete the policy information as well * if there are no other rules, we can delete the entire policy +This method has one rather significant issue. When a client wishes to delete a policy, but other clients are still part of it, we will only remove the rules of the client whithin the policy. We will not remove the definitions of those rules in the policy itself, because there is currently no way to do this. A way to deal with this could be updating the store using dedicated DELETE sparql queries, or by introducing a variant of the storage.deleteRule. ## Implementation details @@ -60,7 +62,7 @@ Some operations require the client to specify a policy ID in the URL. Since poli The current implementation is tested only by the script in `scripts\test-uma-ODRL-policy.ts`. This script tests every implemented endpoint with a designated flow. Since the script initiates with an empty storage, and there is no endpoint or other way to seed it, the first requests must test the POST endpoint. These tests are designed to ensure that the storage is filled. After the POST tests, the access endpoints can be tested. ## Problems -- When you have a policy with multiple rules that have different assigners, DELETE on every rule of one assigner will succesfully delete the rule itself, but not the definition of the rule within the policy. This is due to the fact that you can currently only DELETE based on the ID of the rule/policy you want to delete, and you cannot delete the entire policy since other assigners depend on it. +- When you have a policy with multiple rules that have different assigners, DELETE on every rule of one assigner will succesfully delete the rule itself, but not the definition of the rule within the policy. This is due to the fact that you can currently only DELETE based on the ID of the rule/policy you want to delete, and you cannot delete the entire policy since other assigners depend on it. Currently, the only problem with this is filling space, since the quads defining deleted rules will not be returned in GET requests. ### Solved Problems diff --git a/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts b/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts index 700bfd74..cc248ed4 100644 --- a/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts @@ -1,8 +1,7 @@ -import { Quad, Store } from "n3"; +import { Store } from "n3"; import { HttpHandlerRequest, HttpHandlerResponse } from "../../http/models/HttpHandler"; -import { odrlAssigner, parseBodyToStore, parseBufferToString, relations } from "./PolicyUtil"; +import { odrlAssigner, parseBodyToStore, relations } from "./PolicyUtil"; import { BadRequestHttpError, InternalServerError } from "@solid/community-server"; -import { parseStringAsN3Store } from "koreografeye"; import { UCRulesStorage } from "@solidlab/ucp"; export function sanitizeRule(parsedPolicy: Store, clientId: string): void { diff --git a/packages/uma/src/util/routeSpecific/policies/rewritePolicies.ts b/packages/uma/src/util/routeSpecific/policies/rewritePolicies.ts index e7fd440c..243dc6df 100644 --- a/packages/uma/src/util/routeSpecific/policies/rewritePolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/rewritePolicies.ts @@ -1,7 +1,7 @@ import { UCRulesStorage } from "@solidlab/ucp"; import { HttpHandlerRequest, HttpHandlerResponse } from "../../http/models/HttpHandler"; import { BadRequestHttpError, InternalServerError } from "@solid/community-server"; -import { checkBaseURL, parseBodyToStore, quadsToText, retrieveID } from "./PolicyUtil"; +import { checkBaseURL, parseBodyToStore, quadsToText, relations, retrieveID } from "./PolicyUtil"; import { Quad, Store } from "n3"; import { sanitizeRule } from "./CreatePolicies"; import { deleteOnePolicy } from "./DeletePolicies"; @@ -14,7 +14,7 @@ export async function rewritePolicy(request: HttpHandlerRequest, store: Store, s // 2: Get all reachable policy information const policyInfo = getOnePolicyInfo(policyId, store, clientId); if (policyInfo.policyDefinitions.length === 0) - throw new BadRequestHttpError("You cannot PATCH a nonexistent policy"); + throw new BadRequestHttpError("Patch not allowed: policy does not exist"); // 3. Parse the requested policy const parsedPolicy = await parseBodyToStore(request); @@ -22,6 +22,22 @@ export async function rewritePolicy(request: HttpHandlerRequest, store: Store, s // 4. Sanitization checks (error is thrown when checks fail) sanitizeRule(parsedPolicy, clientId); + // Extra checks: this newly defined policy should not define other policies + if (relations.some(relation => parsedPolicy.getQuads(null, relation, null, null).some(quad => quad.subject.id !== policyId))) + throw new BadRequestHttpError("Patch not allowed: the request went out of scope"); + + // Extra checks: new policy should not contain out of scope rules + const newState = getOnePolicyInfo(policyId, parsedPolicy, clientId); + if (newState.otherRules.length !== 0 || newState.otherPolicyRules.length !== 0) + throw new BadRequestHttpError("Patch not allowed: attempted to modify rules not owned by client"); + + // Extra checks: only Policy/Rule changing quads are introduced and removed + // The only modifications we allow are policy definitions, policy rules that define owned rules and owned rules themselves + const newQuads = parsedPolicy.getQuads(null, null, null, null); + if (newQuads.length - newState.ownedRules.length - newState.ownedPolicyRules.length - newState.policyDefinitions.length !== 0) + throw new BadRequestHttpError("Patch not allowed: this query introduces quads that have nothing to do with the policy/rules you own"); + + // 5. Delete the old policy information and keep track of the old ones for possible rollback const oldQuads: Quad[] = [...policyInfo.policyDefinitions, ...policyInfo.ownedPolicyRules, ...policyInfo.otherPolicyRules, ...policyInfo.ownedRules, ...policyInfo.otherRules]; From a67b3d4d96664949ec032ad80134da3af25c06a0 Mon Sep 17 00:00:00 2001 From: lennertdr Date: Thu, 10 Jul 2025 16:32:52 +0200 Subject: [PATCH 43/62] doc layout fix --- docs/policy-management.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/policy-management.md b/docs/policy-management.md index 9e619953..b6525746 100644 --- a/docs/policy-management.md +++ b/docs/policy-management.md @@ -35,6 +35,7 @@ To delete a policy, send a DELETE request to `/uma/policies/` with the 3. Find out if there are rules not assigned by the client * if there are other rules, we cannot delete the policy information as well * if there are no other rules, we can delete the entire policy + This method has one rather significant issue. When a client wishes to delete a policy, but other clients are still part of it, we will only remove the rules of the client whithin the policy. We will not remove the definitions of those rules in the policy itself, because there is currently no way to do this. A way to deal with this could be updating the store using dedicated DELETE sparql queries, or by introducing a variant of the storage.deleteRule. From 615a9a9aecb25eb0a18704d052a73cbcdd0e31db Mon Sep 17 00:00:00 2001 From: lennertdr Date: Fri, 11 Jul 2025 13:30:21 +0200 Subject: [PATCH 44/62] detailed documentation --- docs/policy-management.md | 169 +++++++++++++++--- .../routeSpecific/policies/DeletePolicies.ts | 15 +- .../routeSpecific/policies/EditPolicies.ts | 33 ++-- .../routeSpecific/policies/rewritePolicies.ts | 13 +- 4 files changed, 181 insertions(+), 49 deletions(-) diff --git a/docs/policy-management.md b/docs/policy-management.md index b6525746..59a2746f 100644 --- a/docs/policy-management.md +++ b/docs/policy-management.md @@ -1,40 +1,162 @@ # Policy Management In this document we describe the *policy adminstration endpoint*. -It contains the methods to describe how to create, read, update and delete policies +It contains the methods to describe how to create, read, update and delete policies. + + ## Supported endpoints -The current implementation supports `GET` and `POST` requests, which comply with some restrictions: -- The request has its `'Authorization'` header set to the clients webID. More on that [later](#authorizationauthentication-decisions) -- The requests as discussed in this document use the same base URL: `/uma/policies` +The current implementation supports the following requests: +- [`GET`](#reading-policies) to both `uma/policies` and `uma/policies/` +- [`POST`](#creating-policies) to `uma/policies` +- [`PATCH`](#updating-policies) to `uma/policies/` +- [`PUT`](#updating-policies) to `uma/policies/` +- [`DELETE`](#deleting-policies) to `uma/policies/` + + +These requests comply with some restrictions: +- When the URL contains a policy ID, it must be [URI encoded](#uri-encodig-decision). +- The request must have its `'Authorization'` header set to the clients webID. More on that [later](#authorizationauthentication-decisions). + ### Creating policies -Create a policy/multiple policies through a POST request to `/uma/policies`. Apart from its Authorization header, the `'Content-Type'` must be set to the RDF serialization format in which the body is written. The accepted formats are those accepted by the [N3 Parser](https://github.com/rdfjs/N3.js/?tab=readme-ov-file#parsing). +Create a policy/multiple policies through a POST request to `/uma/policies`. +Apart from its Authorization header, the `'Content-Type'` must be set to the RDF serialization format in which the body is written. +The accepted formats are those accepted by the [N3 Parser](https://github.com/rdfjs/N3.js/?tab=readme-ov-file#parsing), represented by the following content types: +- `text/turtle` +- `application/trig` +- `application/n-triples` +- `application/n-quads` +- `text/n3` + +The body is expected to represent a valid ODRL policy, although some [sanitization](#sanitization-decisions) is applied to ensure minimal validity. +Upon success, the server responds with **status code 201**. +Bad requests, possibly due to an improper policy definition, will respond with **status code 400**. +When the policy has been validated, but adding it to the storage fails, the response will have **status code 500**. + +#### Example Request +This example creates a policy `http://example.org/usagePolicy` for client `https://pod.example.com/profile/card#me:` +```curl +curl --location 'http://localhost:4000/uma/policies' \ +--header 'Authorization: https://pod.example.com/profile/card#me' \ +--header 'Content-Type: text/turtle' \ +--data-raw '@prefix ex: . +@prefix odrl: . +@prefix dct: . + +ex:usagePolicy a odrl:Agreement . +ex:usagePolicy odrl:permission ex:permission . +ex:permission a odrl:Permission . +ex:permission odrl:action odrl:read . +ex:permission odrl:target . +ex:permission odrl:assignee . +ex:permission odrl:assigner .' +``` + -The body is expected to represent a proper ODRL policy, although some [sanitization](#sanitization-decisions) is applied to ensure minimal validity. ### Reading policies To read policies, two endpoints are implemented: -- GET `/uma/policies`: get policy information that you are authorized to see, for every policy -- GET `/uma/policies/`: get policy information that you are authorized to see, for the policy with the requested [URL encoded](#uri-encodig-decision) ID. +- GET `/uma/policies`: get policy information you are authorized to see, for every policy. +- GET `/uma/policies/`: get policy information you are authorized to see, for the policy with the specified [URI encoded](#uri-encodig-decision) ID. + +#### GET one policy +The algorithm to GET a single policy will use a procedure to separate the policy into different parts: +1. Policy Quads + - Some quads in the policy define rules. Their general form looks like ` .`. These quads are split into rules owned by the client **(1)**, and those owned by another client **(2)**. + - Other quads define other parts about the policy. These quads are set into a group of `policy definitions` **(3)**. +2. Rule Quads + - Owned rule quads are those where the client as an assigner **(4)**. + - Other rule quads are owned by other clients **(5)**. + +The procedure returns an object containing these five groups. Note that the information is discovered through a **depth 1** algorithm, which can not handle `Duty` constructions. +The same procedure is also used in other endpoints, in cases where it is also important what groups **(2)** and **(5)** contain, which is why we also return these. + +The GET endpoint for one policy will call this procedure, processes groups **(1)**, **(3)** and **(4)**. +If group **(1)** is not empty, it will respond with **status code 200** and returns the information in the body with `Content-type: text/turtle`. +If group **(1)** is empty, it will respond with **status code 204** and an empty body. + +An example request to get policy `http://example.org/usagePolicy` for the client with webID `https://pod.example.com/profile/card#me` looks like this: + +```curl +curl --location 'http://localhost:4000/uma/policies/http%3A%2F%2Fexample.org%2FusagePolicy' \ +--header 'Authorization: https://pod.example.com/profile/card#me' +``` + +If the client has viable information about this policy, the server would respond with the information about the policy: +``` + a ; + . + a ; + ; + ; + ; + . + +``` + +#### GET all polices +The current implementation iterates over every quad in the policy that defines a rule, performing a lookup to see if the client is the assigner. +Only then, additional information about the policy AND the rule will be collected, once again only with **depth 1**. The information about the policy will be filtered, because you shouldn't be in able to see the rule definitions of another client. + +This is not done with the procedure from the GET One Policy endpoint, because using it over every policy would be quite exhaustive. + +An example request would look like this: +```curl +curl --location 'http://localhost:4000/uma/policies' \ +--header 'Authorization: https://pod.aexample.com/profile/card#me' +``` + +The response is similar to the GET One Policy response, but for every policy *owned* by the client. + -The current algorithm will retrieve the IDs of the policies and its rules that you are authorized to see. It will seek information about those properties with **depth 1**. This is not representative for a lot of policies, hence a recursive algorithm will be implemented in the future. The procedures will only display information within the scope of the client. This means that other clients' rules and their definition in the policy quads will not be displayed. ### Updating policies -Updating a policy can be done through a PUT or a PATCH request. Both requests have different implementations. Both requests are sent to endpoint `/uma/policies/` +Updating a policy can be done through a PUT or a PATCH request to `/uma/policies/`, each with different semantics. #### PUT -A PUT request to the policy endpoint will redefine the entire policy to the requested body, whithin the reach of the client. The PUT works as a combination of DELETE and POST. It requires a body with the same content type as the [POST request](#creating-policies). This body will be interpreted as a policy with some rules. It executes the same sanitization checks as the POST request. Extra checks are implemented to make sure the new policy does not alter out of scope quads. After that, every rule within our reach (and possibly the entire policy) will be deleted. Upon success, the new policy and rule definitions will be added to the storage. When this addition fails, the procedure tries to restore the old policy. In this case, an internal server error will indicate that the patch failed. When the restore could not be completed, the internal server error will indicate that the policy with `` has been removed as if a DELETE request was sent. +A PUT completely replaces the policy within the scope of the client. +The PUT works as a combination of DELETE and POST. It requires a body with the same content type as the [POST request](#creating-policies). This body will be interpreted as the requested policy with some rules. + +The PUT process: +1. Find information about the policy. If it does not exist, return with a **status code 400** to indicate that you cannot rewrite a nonexistent policy. +2. Parse and validate the body, with the same procedure used in the POST endpoint. First, we perform the basic sanitization checks. Upon success, extra checks are performed to see if the new definition stays whithin the scope of the client: + - Check that the newly defined policy does not define other policies + - Check that the new policy does not contain any rules that do not belong to the client + - Check that no unrelated quads to the policy and its rules are added. + + Failed checks will result in a response with **status code 400** and a dedicated message. +3. Delete the old policy, but keep a copy for a possible rollback. The deletion uses the procedure used in the [DELETE](#deleting-policies) endpoint. +4. Add the new policy. On success, the server will respond with **status code 200** and a body containing the new policy and its rules (whithin scope of the client). When this does not succeed, a rollback will be set up: + - The server will try to reset the state of the policy by adding the old quads. If this succeeds, an internal server error with **status code 500** will indicate that nothing has been rewritten, and the old version is restored. + - When the rollback fails, we basically deleted the policy information whithin our reach. An internal server error with **status code 500** will indicate this. + +Note that this endpoint uses the POST and DELETE functionality to implement the PUT. #### PATCH -A PATCH request to the policy endpoint will update the requested policy. The request expects a body with content type [`application/sparql-query`](https://www.w3.org/TR/rdf-sparql-query/). The INSERT and DELETE properties can be used to modify the requested policy. The PATCH request will get the existing information about the policy, and it will execute the query in an isolated environment. This environment is a store that contains only the relevant quads whithin the scope of the client. This means that a quad ` .` will not be a part of the environment if the rule has not been assigned by the client. Quads that are part of a rule not assigned by the client will also not be a part of the environment. This means that, after executing the query, we can analyse it as desired: we can check that no quads out of scope are added. Only in that case, the update will proceed. The procedure deletes the old policy and replaces it with the updated version, since no better way to update the storage exists. Note that we +A PATCH request will update the specified policy. The request expects a body with content type [`application/sparql-query`](https://www.w3.org/TR/rdf-sparql-query/). The INSERT and DELETE properties can be used to modify the requested policy. These modifications can only be applied to parts of the policy within the client's scope. + +The PATCH process: +1. Collect all information about the existing policy with the procedure explained in the [GET One Policy endpoint](#get-one-policy). If the policy does not exist, the server responds with **status code 400**. In the current implementation, we have chosen that the client cannot PATCH a policy in which it has no rules. Doing so will also result in a response with **status code 400**. +2. The content type of the body gets validated. The content-type must be set to `application/sparql-query`. Any other type will result in a response with **status code 400**. +3. We use the policy information to create a store which only contains groups **(1)**, **(3)** and **(4)** as explained [above](#get-one-policy). This will serve as an isolated store, on which we can execute the update query. This implementation has its advantages: + - We do not need to validate the sparql query, since we execute it on an isolated store. + - Performing DELETE queries on rules out of your scope will simply not work, since they are not part of the isolated store. + - We can easily see exactly when the query goes out of scope by testing the resulting store, separating it in the 5 groups and performing the following checks: + 1. If the resulting store has rules out of the clients' scope (indicated by groups **(2)** and **(5)**), we can abort the update and respond with **status code 400**. + 2. We can analyze the size of the resulting store. Substracting the amount of quads whithin reach should result in 0, since no other rules may be added. This test will fail when the client inserts any unrelated quads to its own policy. Upon failure, the server responds with **status code 400**. +4. The old definition will be replaced with the updated version. Since no real update function for our storage exists, we delete the old policy and add the resulting store from the query, together with the quads out of scope as collected in step 1. + +Note that any quads in the original policy that could not be collected by the procedure defined in [GET One Policy](#get-one-policy), will not be part of the newly defined policy. ### Deleting policies -To delete a policy, send a DELETE request to `/uma/policies/` with the URL encoded ID of the policy. The DELETE works like this: -1. Find the rules defined in the policy -2. Filter the rules that are assigned by the client, and delete them -3. Find out if there are rules not assigned by the client - * if there are other rules, we cannot delete the policy information as well - * if there are no other rules, we can delete the entire policy +To delete a policy, send a DELETE request to `/uma/policies/` with the URI encoded ID of the policy. + +The DELETE process: +1. Find the rules defined in the policy. +2. Filter the rules that are assigned by the client, and delete them. +3. Find out if there are rules not assigned by the client. + * if there are other rules, we cannot delete the policy information as well. + * if there are no other rules, we can delete the entire policy. This method has one rather significant issue. When a client wishes to delete a policy, but other clients are still part of it, we will only remove the rules of the client whithin the policy. We will not remove the definitions of those rules in the policy itself, because there is currently no way to do this. A way to deal with this could be updating the store using dedicated DELETE sparql queries, or by introducing a variant of the storage.deleteRule. @@ -45,7 +167,7 @@ This method has one rather significant issue. When a client wishes to delete a p The current implementation has insufficient authentication restrictions. Currently, the only requirement is that the 'Authorization' header is to be set to the webID of the "logged on" client. Proper procedures to authenticate this client are still to be implemented. #### Sanitization decisions -Some endpoints allow new policies to be created, or existing policies to be modified. This introduces the possibility of invalid syntactic or semantic policies, hence a sanitization strategy is required. In the current implementation, only POST could introduce such problems. We provided the following basic checks: +Some endpoints allow new policies to be created, or existing policies to be modified. This introduces the possibility of invalid syntactic or semantic policies, hence a sanitization strategy is required. In the current implementation, only POST, PUT and PATCH could introduce such problems. We provided the following basic checks: - Every defined rule must have a unique ID. - Every rule must have exactly one assigner. - Every assigner must match the authenticated client. @@ -60,19 +182,20 @@ Sanitization Limitations Some operations require the client to specify a policy ID in the URL. Since policy ID's might contain reserved characters (e.g. `/`, `:`, ...), we have chosen to encode them with the builtin [`encodeURIComponent()` function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent). Using this method, reserved characters will be converted to their respective UTF-8 encodings. ## Testing -The current implementation is tested only by the script in `scripts\test-uma-ODRL-policy.ts`. This script tests every implemented endpoint with a designated flow. Since the script initiates with an empty storage, and there is no endpoint or other way to seed it, the first requests must test the POST endpoint. These tests are designed to ensure that the storage is filled. After the POST tests, the access endpoints can be tested. +The current implementation is tested only by the script in `scripts\test-uma-ODRL-policy.ts`. This script tests every implemented endpoint with a designated flow. Since the script initiates with an empty storage, and there is no endpoint or other way to seed it, the first requests must test the POST endpoint. These tests are designed to ensure that the storage is filled. After the POST tests, the access endpoints can be tested. Every endpoint gets tested in this script, which makes sure that the added data is removed. The current testing will be replaced with proper unit tests in the near future. ## Problems - When you have a policy with multiple rules that have different assigners, DELETE on every rule of one assigner will succesfully delete the rule itself, but not the definition of the rule within the policy. This is due to the fact that you can currently only DELETE based on the ID of the rule/policy you want to delete, and you cannot delete the entire policy since other assigners depend on it. Currently, the only problem with this is filling space, since the quads defining deleted rules will not be returned in GET requests. +- The current [sanitization limitations](#sanitization-decisions) are to be considered ### Solved Problems #### PATCH fix -Because PATCH currently works with sets, it contains a safety hazard. When client A has a certain policy/rule, or even just a certain quad, this can be discovered by an intrusive client B. Client B can simply PATCH an INSERT of a random quad that does NOT belong to its own rules/policies, which can have one of three outcomes: -1. The PATCH resolves in an error saying that you cannot change rules that do not belong to you. This means that client A does not have this quad, since a modification was detected. +PATCH used to contain a safety hazard. When client A has a certain policy/rule, or even just a certain quad, this could be discovered by an intrusive client B. Client B could simply PATCH an INSERT of a random quad that does NOT belong to its own rules/policies, which can have one of three outcomes: +1. The PATCH resolves in an error saying that you cannot change rules that do not belong to you. This means that the quad belongs to some other client, since it has been detected as a quad owned by someone else. 2. The PATCH resolves in an error saying that you cannot change rules that belong to nobody. This means that the quad is not affiliated with any client. -3. The PATCH completes with code 200. Since the inserted quad does NOT belong to you, there must be another client that owns the quad. In this way, any policy can be discovered. +3. The PATCH completes with code 200. Since the inserted quad does NOT belong to you, there must be another client that owns the quad. In this way, any policy can be discovered (exhaustively). An extra constraint, disabling clients to PATCH policies it has no rules in, would still enable the client to exploit policies that it has rules in. This problem was solved by splitting the policy into the parts where the client has access to, and the parts where it does not. By executing the query only on the parts that the client has access to, it would be easier to analyse the resulting store of the query. If this store has rules that the client does not have access to, they must have been added by the client and the operation gets cancelled. This method is also protected from deleting rules out of our reach. \ No newline at end of file diff --git a/packages/uma/src/util/routeSpecific/policies/DeletePolicies.ts b/packages/uma/src/util/routeSpecific/policies/DeletePolicies.ts index f19a785a..6c2416e7 100644 --- a/packages/uma/src/util/routeSpecific/policies/DeletePolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/DeletePolicies.ts @@ -12,13 +12,14 @@ export async function deletePolicy(request: HttpHandlerRequest, store: Store, st /** - * TODO: documentation - * @param request - * @param store - * @param storage - * @param clientId - * @param baseUrl - * @returns + * To delete a policy, send a DELETE request to `/uma/policies/` with the URL encoded ID of the policy. The DELETE works like this: + * 1. Find the rules defined in the policy + * 2. Filter the rules that are assigned by the client, and delete them + * 3. Find out if there are rules not assigned by the client + * if there are other rules, we cannot delete the policy information as well + * if there are no other rules, we can delete the entire policy + * + * As read in /docs/policy-managament.md */ export async function deleteOnePolicy(policyId: string, store: Store, storage: UCRulesStorage, clientId: string): Promise> { diff --git a/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts b/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts index b6f06efa..75a821d3 100644 --- a/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts @@ -5,11 +5,23 @@ import { checkBaseURL, parseBufferToString, quadsToText, retrieveID } from "./Po import { QueryEngine } from '@comunica/query-sparql'; import { BadRequestHttpError, InternalServerError } from "@solid/community-server"; import { getOnePolicyInfo } from "./GetPolicies"; +import { sanitizeRule } from "./CreatePolicies"; export async function editPolicy(request: HttpHandlerRequest, store: Store, storage: UCRulesStorage, clientId: string, baseUrl: string): Promise> { - // 1. Retrieve Policy ID + // Retrieve Policy ID const policyId = decodeURIComponent(retrieveID(checkBaseURL(request, baseUrl))); + // 1. Retrieve the existing policy info + const { policyDefinitions, ownedPolicyRules, otherPolicyRules, ownedRules, otherRules } = getOnePolicyInfo(policyId, store, clientId); + + // Cannot update a nonexistent policy + if (policyDefinitions.length === 0) + throw new BadRequestHttpError("Update not allowed: You cannot update a nonexistent policy"); + + // Implementation Choice: You cannot PATCH a policy that you are not affiliated with + if (ownedRules.length === 0) + throw new BadRequestHttpError("Update not allowed: You cannot update policies that you are not affiliated with"); + // 2. Retrieve the Policy Body const contentType = request.headers['content-type']; if (!/(?:application\/sparql-update)$/i.test(contentType)) { @@ -17,14 +29,7 @@ export async function editPolicy(request: HttpHandlerRequest, store: Store, stor } const query = parseBufferToString(request.body); - // 3. Retrieve the existing policy info - const { policyDefinitions, ownedPolicyRules, otherPolicyRules, ownedRules, otherRules } = getOnePolicyInfo(policyId, store, clientId); - - // Implementation Choice: You cannot PATCH a policy that you are not affiliated with - if (ownedRules.length === 0) - throw new BadRequestHttpError("Update not allowed: You cannot update policies that you are not affiliated with"); - - // 4. Execute the query on the part of the policy that lays within reach + // 3. Execute the query on the part of the policy that lays within reach const policyStore = new Store([...policyDefinitions, ...ownedPolicyRules, ...ownedRules]); const initialQuads = policyStore.getQuads(null, null, null, null); try { @@ -33,20 +38,22 @@ export async function editPolicy(request: HttpHandlerRequest, store: Store, stor throw new BadRequestHttpError("Query could not be executed:", error); } - // 5. Simple safety checks - // Check that the other rules are unchanged + // Sanitization + sanitizeRule(policyStore, clientId); + + // 3.1 Check that the other rules are unchanged const newState = getOnePolicyInfo(policyId, policyStore, clientId); if (newState.otherRules.length !== 0 || newState.otherPolicyRules.length !== 0) throw new BadRequestHttpError("Update not allowed: attempted to modify rules not owned by client"); - // Check that only Policy/Rule changing quads are introduced and removed + // 3.2 Check that only Policy/Rule changing quads are introduced and removed // The only modifications we allow are policy definitions, policy rules that define owned rules and owned rules themselves const newQuads = policyStore.getQuads(null, null, null, null); if (newQuads.length - newState.ownedRules.length - newState.ownedPolicyRules.length - newState.policyDefinitions.length !== initialQuads.length - ownedPolicyRules.length - ownedRules.length - policyDefinitions.length) throw new BadRequestHttpError("Update not allowed: this query introduces quads that have nothing to do with the policy/rules you own"); - // 6. Modify the storage to the updated version + // 4 Modify the storage to the updated version try { // Since no update function is available, we need to remove the old one and set the updated one await storage.deleteRule(policyId); diff --git a/packages/uma/src/util/routeSpecific/policies/rewritePolicies.ts b/packages/uma/src/util/routeSpecific/policies/rewritePolicies.ts index 243dc6df..6ee9ef6e 100644 --- a/packages/uma/src/util/routeSpecific/policies/rewritePolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/rewritePolicies.ts @@ -8,18 +8,19 @@ import { deleteOnePolicy } from "./DeletePolicies"; import { getOnePolicyInfo } from "./GetPolicies"; export async function rewritePolicy(request: HttpHandlerRequest, store: Store, storage: UCRulesStorage, clientId: string, baseUrl: string): Promise> { - // 1. Retrieve Policy ID + // Retrieve Policy ID const policyId = decodeURIComponent(retrieveID(checkBaseURL(request, baseUrl))); - // 2: Get all reachable policy information + // 1: Get all reachable policy information const policyInfo = getOnePolicyInfo(policyId, store, clientId); if (policyInfo.policyDefinitions.length === 0) throw new BadRequestHttpError("Patch not allowed: policy does not exist"); - // 3. Parse the requested policy + + // 2. Parse the requested policy, perform checks const parsedPolicy = await parseBodyToStore(request); - // 4. Sanitization checks (error is thrown when checks fail) + // Sanitization checks (error is thrown when checks fail) sanitizeRule(parsedPolicy, clientId); // Extra checks: this newly defined policy should not define other policies @@ -38,14 +39,14 @@ export async function rewritePolicy(request: HttpHandlerRequest, store: Store, s throw new BadRequestHttpError("Patch not allowed: this query introduces quads that have nothing to do with the policy/rules you own"); - // 5. Delete the old policy information and keep track of the old ones for possible rollback + // 3. Delete the old policy information and keep track of the old ones for possible rollback const oldQuads: Quad[] = [...policyInfo.policyDefinitions, ...policyInfo.ownedPolicyRules, ...policyInfo.otherPolicyRules, ...policyInfo.ownedRules, ...policyInfo.otherRules]; // TODO: this deletion does not delete rule definitions in the Policy declaration when there are multiple clients in the Policy await deleteOnePolicy(policyId, store, storage, clientId); try { - // 6. Add the new policy information + // 4. Add the new policy information await storage.addRule(parsedPolicy); } catch (error) { // If addition fails, try to restore the old quads From d66d1f41ed96e13584a8299622697cd1356b04e5 Mon Sep 17 00:00:00 2001 From: lennertdr Date: Fri, 11 Jul 2025 16:36:48 +0200 Subject: [PATCH 45/62] Stronger POST checks --- docs/policy-management.md | 20 +++-- packages/uma/src/routes/Policy.ts | 2 +- .../routeSpecific/policies/CreatePolicies.ts | 76 +++++++++++++++++-- .../routeSpecific/policies/EditPolicies.ts | 8 +- .../routeSpecific/policies/GetPolicies.ts | 4 +- .../routeSpecific/policies/rewritePolicies.ts | 14 ++-- scripts/test-uma-ODRL-policy.ts | 11 ++- scripts/util/policyExampels.ts | 5 +- 8 files changed, 109 insertions(+), 31 deletions(-) diff --git a/docs/policy-management.md b/docs/policy-management.md index 59a2746f..3ce21a72 100644 --- a/docs/policy-management.md +++ b/docs/policy-management.md @@ -28,7 +28,7 @@ The accepted formats are those accepted by the [N3 Parser](https://github.com/rd - `application/n-quads` - `text/n3` -The body is expected to represent a valid ODRL policy, although some [sanitization](#sanitization-decisions) is applied to ensure minimal validity. +The body is expected to represent a valid ODRL policy, although some [sanitization](#sanitization-decisions) is applied to ensure minimal validity. It is possible to POST multiple policies at once, but they have to remain in scope of the client. Upon success, the server responds with **status code 201**. Bad requests, possibly due to an improper policy definition, will respond with **status code 400**. When the policy has been validated, but adding it to the storage fails, the response will have **status code 500**. @@ -119,16 +119,16 @@ The PUT works as a combination of DELETE and POST. It requires a body with the s The PUT process: 1. Find information about the policy. If it does not exist, return with a **status code 400** to indicate that you cannot rewrite a nonexistent policy. -2. Parse and validate the body, with the same procedure used in the POST endpoint. First, we perform the basic sanitization checks. Upon success, extra checks are performed to see if the new definition stays whithin the scope of the client: +2. Parse and validate the body, with the same procedure used in the POST endpoint. First, we perform the basic sanitization checks. Upon success, extra checks are performed to see if the new definition stays within the scope of the client: - Check that the newly defined policy does not define other policies - Check that the new policy does not contain any rules that do not belong to the client - Check that no unrelated quads to the policy and its rules are added. Failed checks will result in a response with **status code 400** and a dedicated message. 3. Delete the old policy, but keep a copy for a possible rollback. The deletion uses the procedure used in the [DELETE](#deleting-policies) endpoint. -4. Add the new policy. On success, the server will respond with **status code 200** and a body containing the new policy and its rules (whithin scope of the client). When this does not succeed, a rollback will be set up: +4. Add the new policy. On success, the server will respond with **status code 200** and a body containing the new policy and its rules (within scope of the client). When this does not succeed, a rollback will be set up: - The server will try to reset the state of the policy by adding the old quads. If this succeeds, an internal server error with **status code 500** will indicate that nothing has been rewritten, and the old version is restored. - - When the rollback fails, we basically deleted the policy information whithin our reach. An internal server error with **status code 500** will indicate this. + - When the rollback fails, we basically deleted the policy information within our reach. An internal server error with **status code 500** will indicate this. Note that this endpoint uses the POST and DELETE functionality to implement the PUT. @@ -143,7 +143,7 @@ The PATCH process: - Performing DELETE queries on rules out of your scope will simply not work, since they are not part of the isolated store. - We can easily see exactly when the query goes out of scope by testing the resulting store, separating it in the 5 groups and performing the following checks: 1. If the resulting store has rules out of the clients' scope (indicated by groups **(2)** and **(5)**), we can abort the update and respond with **status code 400**. - 2. We can analyze the size of the resulting store. Substracting the amount of quads whithin reach should result in 0, since no other rules may be added. This test will fail when the client inserts any unrelated quads to its own policy. Upon failure, the server responds with **status code 400**. + 2. We can analyze the size of the resulting store. Substracting the amount of quads within reach should result in 0, since no other rules may be added. This test will fail when the client inserts any unrelated quads to its own policy. Upon failure, the server responds with **status code 400**. 4. The old definition will be replaced with the updated version. Since no real update function for our storage exists, we delete the old policy and add the resulting store from the query, together with the quads out of scope as collected in step 1. Note that any quads in the original policy that could not be collected by the procedure defined in [GET One Policy](#get-one-policy), will not be part of the newly defined policy. @@ -158,7 +158,7 @@ The DELETE process: * if there are other rules, we cannot delete the policy information as well. * if there are no other rules, we can delete the entire policy. -This method has one rather significant issue. When a client wishes to delete a policy, but other clients are still part of it, we will only remove the rules of the client whithin the policy. We will not remove the definitions of those rules in the policy itself, because there is currently no way to do this. A way to deal with this could be updating the store using dedicated DELETE sparql queries, or by introducing a variant of the storage.deleteRule. +This method has one rather significant issue. When a client wishes to delete a policy, but other clients are still part of it, we will only remove the rules of the client within the policy. We will not remove the definitions of those rules in the policy itself, because there is currently no way to do this. A way to deal with this could be updating the store using dedicated DELETE sparql queries, or by introducing a variant of the storage.deleteRule. ## Implementation details @@ -173,9 +173,10 @@ Some endpoints allow new policies to be created, or existing policies to be modi - Every assigner must match the authenticated client. Sanitization Limitations -- It is possible to `POST` a policy with an ID that already exists, or with rules that reuse already existing IDs. - There are currently no checks to verify whether a client is sufficiently authorized to create or modify a policy/rule for a specific target. * A client should not be in able to alter rights about a target it does not have access to. + + This issue is currently being solved in [a dedicated PR](https://github.com/SolidLabResearch/user-managed-access/pull/50) - There are plenty of other sanitization checks to be considered. #### URI encodig decision @@ -198,4 +199,7 @@ PATCH used to contain a safety hazard. When client A has a certain policy/rule, 3. The PATCH completes with code 200. Since the inserted quad does NOT belong to you, there must be another client that owns the quad. In this way, any policy can be discovered (exhaustively). An extra constraint, disabling clients to PATCH policies it has no rules in, would still enable the client to exploit policies that it has rules in. -This problem was solved by splitting the policy into the parts where the client has access to, and the parts where it does not. By executing the query only on the parts that the client has access to, it would be easier to analyse the resulting store of the query. If this store has rules that the client does not have access to, they must have been added by the client and the operation gets cancelled. This method is also protected from deleting rules out of our reach. \ No newline at end of file +This problem was solved by splitting the policy into the parts where the client has access to, and the parts where it does not. By executing the query only on the parts that the client has access to, it would be easier to analyse the resulting store of the query. If this store has rules that the client does not have access to, they must have been added by the client and the operation gets cancelled. This method is also protected from deleting rules out of our reach. + +#### POST checks +It is now impossible to POST an already existing policy or already existing rules. This means that a policy can nly be POSTED once. If a client wishes to be a part of a policy, it has to do it through a PUT request. If a client is already part of the policy, it can PATCH modifications. \ No newline at end of file diff --git a/packages/uma/src/routes/Policy.ts b/packages/uma/src/routes/Policy.ts index 2618233c..1872126e 100644 --- a/packages/uma/src/routes/Policy.ts +++ b/packages/uma/src/routes/Policy.ts @@ -49,7 +49,7 @@ export class PolicyRequestHandler extends HttpHandler { switch (request.method) { case 'GET': return getPolicies(request, store, client, this.baseUrl); - case 'POST': return addPolicies(request, this.storage, client); + case 'POST': return addPolicies(request, store, this.storage, client); case 'DELETE': return deletePolicy(request, store, this.storage, client, this.baseUrl); case 'PATCH': return editPolicy(request, store, this.storage, client, this.baseUrl); case 'PUT': return rewritePolicy(request, store, this.storage, client, this.baseUrl); diff --git a/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts b/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts index cc248ed4..00d77225 100644 --- a/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts @@ -1,12 +1,14 @@ -import { Store } from "n3"; +import { Quad, Store } from "n3"; import { HttpHandlerRequest, HttpHandlerResponse } from "../../http/models/HttpHandler"; import { odrlAssigner, parseBodyToStore, relations } from "./PolicyUtil"; import { BadRequestHttpError, InternalServerError } from "@solid/community-server"; import { UCRulesStorage } from "@solidlab/ucp"; +import { getPolicyInfo, OnePolicy } from "./GetPolicies"; -export function sanitizeRule(parsedPolicy: Store, clientId: string): void { +export function sanitizeRule(parsedPolicy: Store, clientId: string, strict: boolean = false): Set { // Check that every rule defined by the policy has exactly one assigner, every rule is unique and every assigner is the client const definedRules = new Set(); + const policyIds: Set = new Set(); for (const relation of relations) { // Every rule definition of every policy @@ -14,7 +16,7 @@ export function sanitizeRule(parsedPolicy: Store, clientId: string): void { for (const quad of policyRelationRules) { const rule = quad.object; - + policyIds.add(quad.subject.id); // The policy should not define multiple rules with the same ID, this check also // restricts the same rule to be defined twice (even if relation is equal) if (definedRules.has(rule)) throw new BadRequestHttpError(`Rule ambiguity in rule ${rule.id}`); @@ -32,15 +34,79 @@ export function sanitizeRule(parsedPolicy: Store, clientId: string): void { // Check if there is at least one permission/prohibition/duty // Check if every rule has a target // ... + + // Return the policies in the store for further use + return policyIds; } -export async function addPolicies(request: HttpHandlerRequest, storage: UCRulesStorage, clientId: string): Promise> { +export async function addPolicies(request: HttpHandlerRequest, store: Store, storage: UCRulesStorage, clientId: string): Promise> { // 1. Parse the requested policy const parsedPolicy = await parseBodyToStore(request); // 2. Sanitization checks (error is thrown when checks fail) - sanitizeRule(parsedPolicy, clientId); + const newPolicies = sanitizeRule(parsedPolicy, clientId); + + // Extra checks + const totalQuads: Quad[] = []; + if ([...newPolicies].some(id => { + const existingInfo = getPolicyInfo(id, store, clientId); + console.log(` + INITIAL POLICY ${id} + + POLICY ITSELF - ${existingInfo.policyDefinitions.length} + ${existingInfo.policyDefinitions} + ${existingInfo.ownedPolicyRules} + ${existingInfo.otherPolicyRules} + + OWNED RULES - ${existingInfo.ownedRules.length} + ${existingInfo.ownedRules.length} + + OTHER RULES - ${existingInfo.otherRules.length} + ${existingInfo.otherRules} + + TOTAL = ${parsedPolicy.getQuads(null, null, null, null).length} + `) + // None of these policies should already exist + if ([...existingInfo.policyDefinitions, ...existingInfo.ownedPolicyRules, ...existingInfo.otherPolicyRules, + ...existingInfo.ownedRules, ...existingInfo.otherRules].length > 0) { + console.log('TEST: ALREADY EXISTS') + return true; + } + + const { policyDefinitions, ownedPolicyRules, otherPolicyRules, ownedRules, otherRules } = getPolicyInfo(id, parsedPolicy, clientId); + + console.log(` + TEST FOR POLICY ${id} + + POLICY ITSELF - ${policyDefinitions.length} + ${policyDefinitions} + ${ownedPolicyRules} + ${otherPolicyRules} + + OWNED RULES - ${ownedRules.length} + ${ownedRules} + + OTHER RULES - ${otherRules.length} + ${otherRules} + + TOTAL = ${parsedPolicy.getQuads(null, null, null, null).length} + `) + // The policies may not declare rules out of scope + if (otherRules.length !== 0 || otherPolicyRules.length !== 0) { + console.log("TEST: out of scope") + return true; + } + + totalQuads.push(...policyDefinitions, ...ownedPolicyRules, ...ownedRules); + })) + throw new BadRequestHttpError("POST not allowed: improper request body"); + + // Extra check: No unrelated rules may be inserted + // The policies may not declare any quads unrelated to its own policy + const newQuads = parsedPolicy.getQuads(null, null, null, null); + if (newQuads.length - totalQuads.length !== 0) + throw new BadRequestHttpError("POST not allowed: inserted unrelated quads"); // 3 Add the policy to the rule storage diff --git a/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts b/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts index 75a821d3..ca8c174b 100644 --- a/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts @@ -4,7 +4,7 @@ import { UCRulesStorage } from "@solidlab/ucp"; import { checkBaseURL, parseBufferToString, quadsToText, retrieveID } from "./PolicyUtil"; import { QueryEngine } from '@comunica/query-sparql'; import { BadRequestHttpError, InternalServerError } from "@solid/community-server"; -import { getOnePolicyInfo } from "./GetPolicies"; +import { getPolicyInfo } from "./GetPolicies"; import { sanitizeRule } from "./CreatePolicies"; export async function editPolicy(request: HttpHandlerRequest, store: Store, storage: UCRulesStorage, clientId: string, baseUrl: string): Promise> { @@ -12,7 +12,7 @@ export async function editPolicy(request: HttpHandlerRequest, store: Store, stor const policyId = decodeURIComponent(retrieveID(checkBaseURL(request, baseUrl))); // 1. Retrieve the existing policy info - const { policyDefinitions, ownedPolicyRules, otherPolicyRules, ownedRules, otherRules } = getOnePolicyInfo(policyId, store, clientId); + const { policyDefinitions, ownedPolicyRules, otherPolicyRules, ownedRules, otherRules } = getPolicyInfo(policyId, store, clientId); // Cannot update a nonexistent policy if (policyDefinitions.length === 0) @@ -42,7 +42,7 @@ export async function editPolicy(request: HttpHandlerRequest, store: Store, stor sanitizeRule(policyStore, clientId); // 3.1 Check that the other rules are unchanged - const newState = getOnePolicyInfo(policyId, policyStore, clientId); + const newState = getPolicyInfo(policyId, policyStore, clientId); if (newState.otherRules.length !== 0 || newState.otherPolicyRules.length !== 0) throw new BadRequestHttpError("Update not allowed: attempted to modify rules not owned by client"); @@ -66,6 +66,6 @@ export async function editPolicy(request: HttpHandlerRequest, store: Store, stor } // 7. Print information within reach - const finalState = getOnePolicyInfo(policyId, policyStore, clientId); + const finalState = getPolicyInfo(policyId, policyStore, clientId); return quadsToText([...finalState.policyDefinitions, ...finalState.ownedPolicyRules, ...finalState.ownedRules]); } \ No newline at end of file diff --git a/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts b/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts index 3021075f..d13815ee 100644 --- a/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts @@ -38,7 +38,7 @@ export interface OnePolicy { } // Functional implementation to get one policy -export function getOnePolicyInfo(policyId: string, store: Store, clientId: string): OnePolicy { +export function getPolicyInfo(policyId: string, store: Store, clientId: string): OnePolicy { // 1. Find the rules that this policy defines const policyRules: Quad[] = relations.flatMap(relation => @@ -98,7 +98,7 @@ export function getOnePolicyInfo(policyId: string, store: Store, clientId: strin async function getOnePolicy(policyId: string, store: Store, clientId: string): Promise> { policyId = decodeURIComponent(policyId); - const { policyDefinitions, ownedPolicyRules, ownedRules } = getOnePolicyInfo(policyId, store, clientId); + const { policyDefinitions, ownedPolicyRules, ownedRules } = getPolicyInfo(policyId, store, clientId); if (ownedRules.length === 0) return { diff --git a/packages/uma/src/util/routeSpecific/policies/rewritePolicies.ts b/packages/uma/src/util/routeSpecific/policies/rewritePolicies.ts index 6ee9ef6e..1887db21 100644 --- a/packages/uma/src/util/routeSpecific/policies/rewritePolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/rewritePolicies.ts @@ -5,14 +5,14 @@ import { checkBaseURL, parseBodyToStore, quadsToText, relations, retrieveID } fr import { Quad, Store } from "n3"; import { sanitizeRule } from "./CreatePolicies"; import { deleteOnePolicy } from "./DeletePolicies"; -import { getOnePolicyInfo } from "./GetPolicies"; +import { getPolicyInfo } from "./GetPolicies"; export async function rewritePolicy(request: HttpHandlerRequest, store: Store, storage: UCRulesStorage, clientId: string, baseUrl: string): Promise> { // Retrieve Policy ID const policyId = decodeURIComponent(retrieveID(checkBaseURL(request, baseUrl))); // 1: Get all reachable policy information - const policyInfo = getOnePolicyInfo(policyId, store, clientId); + const policyInfo = getPolicyInfo(policyId, store, clientId); if (policyInfo.policyDefinitions.length === 0) throw new BadRequestHttpError("Patch not allowed: policy does not exist"); @@ -25,18 +25,18 @@ export async function rewritePolicy(request: HttpHandlerRequest, store: Store, s // Extra checks: this newly defined policy should not define other policies if (relations.some(relation => parsedPolicy.getQuads(null, relation, null, null).some(quad => quad.subject.id !== policyId))) - throw new BadRequestHttpError("Patch not allowed: the request went out of scope"); + throw new BadRequestHttpError("PUT not allowed: the request went out of scope"); // Extra checks: new policy should not contain out of scope rules - const newState = getOnePolicyInfo(policyId, parsedPolicy, clientId); + const newState = getPolicyInfo(policyId, parsedPolicy, clientId); if (newState.otherRules.length !== 0 || newState.otherPolicyRules.length !== 0) - throw new BadRequestHttpError("Patch not allowed: attempted to modify rules not owned by client"); + throw new BadRequestHttpError("PUT not allowed: attempted to modify rules not owned by client"); // Extra checks: only Policy/Rule changing quads are introduced and removed // The only modifications we allow are policy definitions, policy rules that define owned rules and owned rules themselves const newQuads = parsedPolicy.getQuads(null, null, null, null); if (newQuads.length - newState.ownedRules.length - newState.ownedPolicyRules.length - newState.policyDefinitions.length !== 0) - throw new BadRequestHttpError("Patch not allowed: this query introduces quads that have nothing to do with the policy/rules you own"); + throw new BadRequestHttpError("PUT not allowed: this query introduces quads that have nothing to do with the policy/rules you own"); // 3. Delete the old policy information and keep track of the old ones for possible rollback @@ -61,6 +61,6 @@ export async function rewritePolicy(request: HttpHandlerRequest, store: Store, s } // Delete only what is in your reach - const { policyDefinitions, ownedPolicyRules, ownedRules } = getOnePolicyInfo(policyId, parsedPolicy, clientId); + const { policyDefinitions, ownedPolicyRules, ownedRules } = getPolicyInfo(policyId, parsedPolicy, clientId); return quadsToText([...policyDefinitions, ...ownedPolicyRules, ...ownedRules]); } \ No newline at end of file diff --git a/scripts/test-uma-ODRL-policy.ts b/scripts/test-uma-ODRL-policy.ts index 3a73b70a..e9155b9b 100644 --- a/scripts/test-uma-ODRL-policy.ts +++ b/scripts/test-uma-ODRL-policy.ts @@ -4,7 +4,7 @@ * The purpose of this file is to test the /policies endpoint. */ -import { policyA, policyB, policyC, badPolicy1, changePolicy1, changePolicy95e, putPolicy95e } from "./util/policyExampels"; +import { policyA, policyB, policyC, badPolicy1, changePolicy1, changePolicy95e, putPolicy95e, putPolicyB } from "./util/policyExampels"; const endpoint = (extra: string = '') => 'http://localhost:4000/uma/policies' + extra; const client = (client: string = 'a') => `https://pod.${client}.com/profile/card#me`; @@ -36,7 +36,7 @@ async function putPolicies() { testCode(response.status); response = await fetch(endpoint(`/${encoded}`), { method: 'PUT', headers: { 'Authorization': client('a'), 'Content-Type': 'text/turtle' }, body: quickBuffer(putPolicy95e) }); - console.log(`expecting Policy header to mistakenly contain the old policy, and (correctly) the new policies: ${response.status}\n${await response.text()}`); + console.log(`expecting Policy header to mistakenly contain the new policies: ${response.status}\n${await response.text()}`); response = await fetch(endpoint(`/${encoded}`), { headers: { 'Authorization': client('b') } }); console.log(`expecting to stay the same as before ${policyId95e}`, await response.text()); @@ -156,6 +156,12 @@ async function testDelete() { testCode(resText.length, 0, false); } +async function furtherSeeding() { + // Due to new POST implementation, client B must PUT its own rules into existing policy `policy95e` + const response = await fetch(endpoint(`/${encodeURIComponent(policyId95e)}`), { method: 'PUT', headers: { 'Authorization': client('b'), 'Content-Type': 'text/turtle' }, body: quickBuffer(putPolicyB) }); + console.log(`expecting Policy header to mistakenly contain the new policies: ${response.status}\n${await response.text()}`); +} + async function deleteAll() { const obj = { 'a': ['http://example.org/usagePolicy1', 'http://example.org/usagePolicy1a', 'urn:uuid:95efe0e8-4fb7-496d-8f3c-4d78c97829bc'], @@ -178,6 +184,7 @@ async function main() { errorCounter = 0; console.log("Testing all implemented Policy Endpoints:\n\n\n"); await postPolicy(); + await furtherSeeding(); console.log("\n\n\n"); await getAllPolicies(); console.log("\n\n\n"); diff --git a/scripts/util/policyExampels.ts b/scripts/util/policyExampels.ts index c9107650..e9eb0c72 100644 --- a/scripts/util/policyExampels.ts +++ b/scripts/util/policyExampels.ts @@ -47,7 +47,9 @@ ex:permission2a odrl:action odrl:create . ex:permission2a odrl:target . ex:permission2a odrl:assignee . ex:permission2a odrl:assigner . +`; +export const putPolicyB = ` a odrl:Set; dct:description "ZENO is data owner of resource X. ALICE may READ resource X."; odrl:permission . @@ -56,8 +58,7 @@ ex:permission2a odrl:assigner . odrl:assignee ex:bob ; odrl:assigner ; odrl:action odrl:read ; - odrl:target ex:x . -`; + odrl:target ex:x .`; export const policyC = `@prefix ex: . @prefix odrl: . From bb08a836413b358f16940009c71853006c817058 Mon Sep 17 00:00:00 2001 From: lennertdr Date: Fri, 11 Jul 2025 16:58:45 +0200 Subject: [PATCH 46/62] DELETE idea, need to adjust tests --- .../ucp/src/storage/ContainerUCRulesStorage.ts | 3 +++ .../ucp/src/storage/DirectoryUCRulesStorage.ts | 5 +++++ .../ucp/src/storage/MemoryUCRulesStorage.ts | 11 ++++++++++- packages/ucp/src/storage/UCRulesStorage.ts | 3 +++ .../routeSpecific/policies/DeletePolicies.ts | 16 ++++++++-------- .../routeSpecific/policies/rewritePolicies.ts | 1 - scripts/test-uma-ODRL-policy.ts | 18 +++++------------- scripts/util/policyExampels.ts | 3 +++ 8 files changed, 37 insertions(+), 23 deletions(-) diff --git a/packages/ucp/src/storage/ContainerUCRulesStorage.ts b/packages/ucp/src/storage/ContainerUCRulesStorage.ts index 3866b512..b852dc1a 100644 --- a/packages/ucp/src/storage/ContainerUCRulesStorage.ts +++ b/packages/ucp/src/storage/ContainerUCRulesStorage.ts @@ -18,6 +18,9 @@ export class ContainerUCRulesStorage implements UCRulesStorage { console.log(`[${new Date().toISOString()}] - ContainerUCRulesStore: LDP Container that will be used as source for the Usage Control Rules`, this.containerURL); this.fetch = customFetch ?? fetch; } + async deleteRuleFromPolicy(ruleID: string, PolicyID: string){ + return new Promise(() => {}) + } public async getStore(): Promise { const store = new Store() diff --git a/packages/ucp/src/storage/DirectoryUCRulesStorage.ts b/packages/ucp/src/storage/DirectoryUCRulesStorage.ts index 4415a254..bf8b8320 100644 --- a/packages/ucp/src/storage/DirectoryUCRulesStorage.ts +++ b/packages/ucp/src/storage/DirectoryUCRulesStorage.ts @@ -33,6 +33,11 @@ export class DirectoryUCRulesStorage implements UCRulesStorage { return store; } + async deleteRuleFromPolicy(ruleID: string, PolicyID: string) { + return new Promise(() => { }) + } + + /** * TEST IMPLEMENTATION - This is just to test the POST :uma/policies endpoint diff --git a/packages/ucp/src/storage/MemoryUCRulesStorage.ts b/packages/ucp/src/storage/MemoryUCRulesStorage.ts index e0b8b62d..0110f666 100644 --- a/packages/ucp/src/storage/MemoryUCRulesStorage.ts +++ b/packages/ucp/src/storage/MemoryUCRulesStorage.ts @@ -1,7 +1,9 @@ -import { Store } from "n3"; +import { DataFactory, Store } from "n3"; import { extractQuadsRecursive } from "../util/Util"; import { UCRulesStorage } from "./UCRulesStorage"; +const { namedNode } = DataFactory; + export class MemoryUCRulesStorage implements UCRulesStorage { private store: Store; @@ -27,4 +29,11 @@ export class MemoryUCRulesStorage implements UCRulesStorage { const store = await this.getRule(identifier) this.store.removeQuads(store.getQuads(null, null, null, null)); } + + public async deleteRuleFromPolicy(ruleID: string, PolicyID: string) { + // Delete the rule and its definition in the policy + this.store.getQuads(namedNode(PolicyID), null, namedNode(ruleID), null).forEach(this.store.delete); + this.deleteRule(ruleID); + } + } \ No newline at end of file diff --git a/packages/ucp/src/storage/UCRulesStorage.ts b/packages/ucp/src/storage/UCRulesStorage.ts index e0a30c7f..14fb05b5 100644 --- a/packages/ucp/src/storage/UCRulesStorage.ts +++ b/packages/ucp/src/storage/UCRulesStorage.ts @@ -20,4 +20,7 @@ export interface UCRulesStorage { * @returns */ deleteRule: (identifier: string) => Promise; + + // Experimental endpoint + deleteRuleFromPolicy: (ruleID: string, PolicyID: string) => Promise; } \ No newline at end of file diff --git a/packages/uma/src/util/routeSpecific/policies/DeletePolicies.ts b/packages/uma/src/util/routeSpecific/policies/DeletePolicies.ts index 6c2416e7..edd0d699 100644 --- a/packages/uma/src/util/routeSpecific/policies/DeletePolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/DeletePolicies.ts @@ -45,15 +45,15 @@ export async function deleteOnePolicy(policyId: string, store: Store, storage: U }; } - // 2. If the policy contains only rules assigned by the client, we can remove the entire policy - // Otherwise, we only remove the rules within our reach - const idsToDelete = otherRules.length === 0 ? [policyId] : ownedRules; - - // 3. Remove the specified quads - // Note that the current implementation of the storages 'deleteRule' cannot delete the quads that define the deleted rules - // A way to deal with this could be updating the store using dedicated DELETE sparql queries, or by introducing a variant of the storage.deleteRule + // DELETE the quads try { - await Promise.all(idsToDelete.map(id => storage.deleteRule(id))); + if (otherRules.length === 0) { + // If the policy contains only rules assigned by the client, we can remove the entire policy + await storage.deleteRule(policyId); + } else { + // Otherwise, we only remove the rules within our reach + await Promise.all(ownedRules.map(id => storage.deleteRuleFromPolicy(id, policyId))); + } } catch (error) { throw new InternalServerError(`Failed to delete rules: ${error}`); diff --git a/packages/uma/src/util/routeSpecific/policies/rewritePolicies.ts b/packages/uma/src/util/routeSpecific/policies/rewritePolicies.ts index 1887db21..e8f1e4c7 100644 --- a/packages/uma/src/util/routeSpecific/policies/rewritePolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/rewritePolicies.ts @@ -42,7 +42,6 @@ export async function rewritePolicy(request: HttpHandlerRequest, store: Store, s // 3. Delete the old policy information and keep track of the old ones for possible rollback const oldQuads: Quad[] = [...policyInfo.policyDefinitions, ...policyInfo.ownedPolicyRules, ...policyInfo.otherPolicyRules, ...policyInfo.ownedRules, ...policyInfo.otherRules]; - // TODO: this deletion does not delete rule definitions in the Policy declaration when there are multiple clients in the Policy await deleteOnePolicy(policyId, store, storage, clientId); try { diff --git a/scripts/test-uma-ODRL-policy.ts b/scripts/test-uma-ODRL-policy.ts index e9155b9b..b2c72146 100644 --- a/scripts/test-uma-ODRL-policy.ts +++ b/scripts/test-uma-ODRL-policy.ts @@ -16,26 +16,18 @@ let errorCounter = 0; // Test if the first digit of the status code equals the second arg, or match the entire code when specific is false const testCode = (code: number, shouldbe: number = 2, trunc: boolean = true) => { - if ((trunc ? Math.trunc(Number(code) / 100) : code) !== shouldbe) errorCounter++; + if ((trunc ? Math.trunc(Number(code) / 100) : code) !== shouldbe) { errorCounter++; console.log("here") } } async function putPolicies() { const encoded = encodeURIComponent(policyId95e); console.log("test PUT policies"); - // Delete the policy to be sure - await fetch(endpoint(`/${encoded}`), { method: 'DELETE', headers: { 'Authorization': client('a') } }); - - // POST it again - let response = await fetch(endpoint(), { method: 'POST', headers: { 'Authorization': client('a'), 'Content-Type': 'text/turtle' }, body: quickBuffer(policyA) }); - console.log(`expecting a positive response: status code ${response.status}, ${await response.text()}`); - testCode(response.status); - - response = await fetch(endpoint(), { method: 'POST', headers: { 'Authorization': client('b'), 'Content-Type': 'text/turtle' }, body: quickBuffer(policyB) }); - console.log(`expecting a positive response: status code ${response.status}, ${await response.text()}`); - testCode(response.status); + // reset the policy to be sure + await deleteAll(); + await postPolicy(); - response = await fetch(endpoint(`/${encoded}`), { method: 'PUT', headers: { 'Authorization': client('a'), 'Content-Type': 'text/turtle' }, body: quickBuffer(putPolicy95e) }); + let response = await fetch(endpoint(`/${encoded}`), { method: 'PUT', headers: { 'Authorization': client('a'), 'Content-Type': 'text/turtle' }, body: quickBuffer(putPolicy95e) }); console.log(`expecting Policy header to mistakenly contain the new policies: ${response.status}\n${await response.text()}`); response = await fetch(endpoint(`/${encoded}`), { headers: { 'Authorization': client('b') } }); diff --git a/scripts/util/policyExampels.ts b/scripts/util/policyExampels.ts index e9eb0c72..4d8634da 100644 --- a/scripts/util/policyExampels.ts +++ b/scripts/util/policyExampels.ts @@ -50,6 +50,9 @@ ex:permission2a odrl:assigner . `; export const putPolicyB = ` +@prefix ex: . +@prefix odrl: . +@prefix dct: . a odrl:Set; dct:description "ZENO is data owner of resource X. ALICE may READ resource X."; odrl:permission . From 29b8eaaaf6ee1d37735e916840cabcfb713c9274 Mon Sep 17 00:00:00 2001 From: lennertdr Date: Mon, 14 Jul 2025 09:05:42 +0200 Subject: [PATCH 47/62] fixed small bug --- packages/ucp/src/storage/MemoryUCRulesStorage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ucp/src/storage/MemoryUCRulesStorage.ts b/packages/ucp/src/storage/MemoryUCRulesStorage.ts index 0110f666..e9d0af75 100644 --- a/packages/ucp/src/storage/MemoryUCRulesStorage.ts +++ b/packages/ucp/src/storage/MemoryUCRulesStorage.ts @@ -32,7 +32,7 @@ export class MemoryUCRulesStorage implements UCRulesStorage { public async deleteRuleFromPolicy(ruleID: string, PolicyID: string) { // Delete the rule and its definition in the policy - this.store.getQuads(namedNode(PolicyID), null, namedNode(ruleID), null).forEach(this.store.delete); + this.store.getQuads(namedNode(PolicyID), null, namedNode(ruleID), null).forEach(quad => this.store.delete(quad)); this.deleteRule(ruleID); } From 9c189ecf33a63446ffe0939ba77d4b5cc20bceef Mon Sep 17 00:00:00 2001 From: lennertdr Date: Mon, 14 Jul 2025 09:37:17 +0200 Subject: [PATCH 48/62] doc update --- docs/policy-management.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/policy-management.md b/docs/policy-management.md index 3ce21a72..a77a6f61 100644 --- a/docs/policy-management.md +++ b/docs/policy-management.md @@ -186,12 +186,20 @@ Some operations require the client to specify a policy ID in the URL. Since poli The current implementation is tested only by the script in `scripts\test-uma-ODRL-policy.ts`. This script tests every implemented endpoint with a designated flow. Since the script initiates with an empty storage, and there is no endpoint or other way to seed it, the first requests must test the POST endpoint. These tests are designed to ensure that the storage is filled. After the POST tests, the access endpoints can be tested. Every endpoint gets tested in this script, which makes sure that the added data is removed. The current testing will be replaced with proper unit tests in the near future. ## Problems -- When you have a policy with multiple rules that have different assigners, DELETE on every rule of one assigner will succesfully delete the rule itself, but not the definition of the rule within the policy. This is due to the fact that you can currently only DELETE based on the ID of the rule/policy you want to delete, and you cannot delete the entire policy since other assigners depend on it. Currently, the only problem with this is filling space, since the quads defining deleted rules will not be returned in GET requests. - The current [sanitization limitations](#sanitization-decisions) are to be considered ### Solved Problems +#### DELETE fix + +##### Problem +When you have a policy with multiple rules that have different assigners, DELETE on every rule of one assigner will succesfully delete the rule itself, but not the definition of the rule within the policy. This is due to the fact that you can currently only DELETE based on the ID of the rule/policy you want to delete, and you cannot delete the entire policy since other assigners depend on it. Currently, the only problem with this is filling space, since the quads defining deleted rules will not be returned in GET requests. + +##### Fix +We created a new RulesStorage function, made specifically to fix our problem entirely. The function is implemented to delete the rule AND its definition in the policy. + + #### PATCH fix PATCH used to contain a safety hazard. When client A has a certain policy/rule, or even just a certain quad, this could be discovered by an intrusive client B. Client B could simply PATCH an INSERT of a random quad that does NOT belong to its own rules/policies, which can have one of three outcomes: 1. The PATCH resolves in an error saying that you cannot change rules that do not belong to you. This means that the quad belongs to some other client, since it has been detected as a quad owned by someone else. From 7cbbedc6bfa22bb9287aa609ada69ef651b462e0 Mon Sep 17 00:00:00 2001 From: lennertdr Date: Mon, 14 Jul 2025 09:37:36 +0200 Subject: [PATCH 49/62] doc update --- docs/policy-management.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/policy-management.md b/docs/policy-management.md index a77a6f61..41b214e1 100644 --- a/docs/policy-management.md +++ b/docs/policy-management.md @@ -158,7 +158,7 @@ The DELETE process: * if there are other rules, we cannot delete the policy information as well. * if there are no other rules, we can delete the entire policy. -This method has one rather significant issue. When a client wishes to delete a policy, but other clients are still part of it, we will only remove the rules of the client within the policy. We will not remove the definitions of those rules in the policy itself, because there is currently no way to do this. A way to deal with this could be updating the store using dedicated DELETE sparql queries, or by introducing a variant of the storage.deleteRule. +This method used to have one rather significant issue, as discussed [later](#delete-fix). ## Implementation details @@ -197,7 +197,7 @@ The current implementation is tested only by the script in `scripts\test-uma-ODR When you have a policy with multiple rules that have different assigners, DELETE on every rule of one assigner will succesfully delete the rule itself, but not the definition of the rule within the policy. This is due to the fact that you can currently only DELETE based on the ID of the rule/policy you want to delete, and you cannot delete the entire policy since other assigners depend on it. Currently, the only problem with this is filling space, since the quads defining deleted rules will not be returned in GET requests. ##### Fix -We created a new RulesStorage function, made specifically to fix our problem entirely. The function is implemented to delete the rule AND its definition in the policy. +We created a new RulesStorage function, made specifically to fix our problem entirely. The function is implemented to delete the rule AND its definition in the policy. This solution is still a bit experimental. #### PATCH fix From 0ba8846a62eaee5b070d6e8c779b52fd85c4105f Mon Sep 17 00:00:00 2001 From: lennertdr Date: Mon, 14 Jul 2025 11:43:18 +0200 Subject: [PATCH 50/62] typos --- docs/policy-management.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/policy-management.md b/docs/policy-management.md index 41b214e1..a4133364 100644 --- a/docs/policy-management.md +++ b/docs/policy-management.md @@ -14,7 +14,7 @@ The current implementation supports the following requests: These requests comply with some restrictions: -- When the URL contains a policy ID, it must be [URI encoded](#uri-encodig-decision). +- When the URL contains a policy ID, it must be [URI encoded](#uri-encoding-decision). - The request must have its `'Authorization'` header set to the clients webID. More on that [later](#authorizationauthentication-decisions). @@ -57,7 +57,7 @@ ex:permission odrl:assigner .' ### Reading policies To read policies, two endpoints are implemented: - GET `/uma/policies`: get policy information you are authorized to see, for every policy. -- GET `/uma/policies/`: get policy information you are authorized to see, for the policy with the specified [URI encoded](#uri-encodig-decision) ID. +- GET `/uma/policies/`: get policy information you are authorized to see, for the policy with the specified [URI encoded](#uri-encoding-decision) ID. #### GET one policy The algorithm to GET a single policy will use a procedure to separate the policy into different parts: @@ -179,7 +179,7 @@ Sanitization Limitations This issue is currently being solved in [a dedicated PR](https://github.com/SolidLabResearch/user-managed-access/pull/50) - There are plenty of other sanitization checks to be considered. -#### URI encodig decision +#### URI encoding decision Some operations require the client to specify a policy ID in the URL. Since policy ID's might contain reserved characters (e.g. `/`, `:`, ...), we have chosen to encode them with the builtin [`encodeURIComponent()` function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent). Using this method, reserved characters will be converted to their respective UTF-8 encodings. ## Testing @@ -210,4 +210,4 @@ An extra constraint, disabling clients to PATCH policies it has no rules in, wou This problem was solved by splitting the policy into the parts where the client has access to, and the parts where it does not. By executing the query only on the parts that the client has access to, it would be easier to analyse the resulting store of the query. If this store has rules that the client does not have access to, they must have been added by the client and the operation gets cancelled. This method is also protected from deleting rules out of our reach. #### POST checks -It is now impossible to POST an already existing policy or already existing rules. This means that a policy can nly be POSTED once. If a client wishes to be a part of a policy, it has to do it through a PUT request. If a client is already part of the policy, it can PATCH modifications. \ No newline at end of file +It is now impossible to POST an already existing policy or already existing rules. This means that a policy can only be POSTED once. If a client wishes to be a part of a policy, it has to do it through a PUT request. If a client is already part of the policy, it can PATCH modifications. \ No newline at end of file From 36934e8ee2968367186115bb9e61fbb4a040ffd9 Mon Sep 17 00:00:00 2001 From: lennertdr Date: Wed, 16 Jul 2025 09:49:22 +0200 Subject: [PATCH 51/62] temporary header against CORS, not the right solution --- packages/uma/config/routes/policies.json | 3 ++- packages/uma/src/routes/Policy.ts | 11 ++++++++++- .../src/util/routeSpecific/policies/CreatePolicies.ts | 3 ++- .../src/util/routeSpecific/policies/DeletePolicies.ts | 3 ++- .../src/util/routeSpecific/policies/GetPolicies.ts | 3 +-- .../uma/src/util/routeSpecific/policies/PolicyUtil.ts | 2 +- 6 files changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/uma/config/routes/policies.json b/packages/uma/config/routes/policies.json index 3f751e45..f1197fb4 100644 --- a/packages/uma/config/routes/policies.json +++ b/packages/uma/config/routes/policies.json @@ -8,7 +8,8 @@ "@type": "HttpHandlerRoute", "methods": [ "GET", - "POST" + "POST", + "OPTIONS" ], "handler": { "@type": "PolicyRequestHandler", diff --git a/packages/uma/src/routes/Policy.ts b/packages/uma/src/routes/Policy.ts index 1872126e..45107817 100644 --- a/packages/uma/src/routes/Policy.ts +++ b/packages/uma/src/routes/Policy.ts @@ -31,7 +31,7 @@ export class PolicyRequestHandler extends HttpHandler { */ protected getCredentials(request: HttpHandlerRequest): string { const header = request.headers['authorization']; - if (typeof header !== 'string') { + if (typeof header !== 'string' && request.method !== "OPTIONS") { throw new BadRequestHttpError('Missing Authorization header'); } return header; @@ -53,6 +53,15 @@ export class PolicyRequestHandler extends HttpHandler { case 'DELETE': return deletePolicy(request, store, this.storage, client, this.baseUrl); case 'PATCH': return editPolicy(request, store, this.storage, client, this.baseUrl); case 'PUT': return rewritePolicy(request, store, this.storage, client, this.baseUrl); + case 'OPTIONS': return { + status: 204, + headers: { + // this is only for the url without + 'Access-Control-Allow-Origin': 'http://localhost:5173', + 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Authorization, Content-Type', + } + } default: throw new MethodNotAllowedHttpError(); } } diff --git a/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts b/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts index 00d77225..83a6269a 100644 --- a/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts @@ -118,6 +118,7 @@ export async function addPolicies(request: HttpHandlerRequest, store: Store, sto return { - status: 201 + status: 201, + headers: { 'access-control-allow-origin': 'http://localhost:5173' } } } \ No newline at end of file diff --git a/packages/uma/src/util/routeSpecific/policies/DeletePolicies.ts b/packages/uma/src/util/routeSpecific/policies/DeletePolicies.ts index edd0d699..7db61ef2 100644 --- a/packages/uma/src/util/routeSpecific/policies/DeletePolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/DeletePolicies.ts @@ -61,6 +61,7 @@ export async function deleteOnePolicy(policyId: string, store: Store, storage: U // Delete succesful return { - status: 200 + status: 200, + headers: { 'access-control-allow-origin': 'http://localhost:5173' } } } \ No newline at end of file diff --git a/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts b/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts index d13815ee..4c52f7e0 100644 --- a/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/GetPolicies.ts @@ -1,7 +1,6 @@ import { HttpHandlerRequest, HttpHandlerResponse } from "../../http/models/HttpHandler"; import { Quad, Store } from "n3"; import { odrlAssigner, relations, namedNode, quadsToText, checkBaseURL, retrieveID } from "./PolicyUtil"; -import { MethodNotAllowedHttpError } from "@solid/community-server"; /** @@ -104,7 +103,7 @@ async function getOnePolicy(policyId: string, store: Store, clientId: string): P return { status: 204, headers: { - 'content-type': 'text/turtle', + 'content-type': 'text/turtle', 'access-control-allow-origin': 'http://localhost:5173' }, body: '', } diff --git a/packages/uma/src/util/routeSpecific/policies/PolicyUtil.ts b/packages/uma/src/util/routeSpecific/policies/PolicyUtil.ts index 7a06d962..cc433695 100644 --- a/packages/uma/src/util/routeSpecific/policies/PolicyUtil.ts +++ b/packages/uma/src/util/routeSpecific/policies/PolicyUtil.ts @@ -54,7 +54,7 @@ export async function quadsToText(quads: Quad[]): Promise Date: Wed, 16 Jul 2025 17:30:54 +0200 Subject: [PATCH 52/62] script to seed for specific id --- package.json | 1 + scripts/seed-uma-ODRL-policy.ts | 50 +++++++++++++++++++ scripts/test-uma-ODRL-policy.ts | 2 +- .../{policyExampels.ts => policyExamples.ts} | 45 ++++++++++++++++- 4 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 scripts/seed-uma-ODRL-policy.ts rename scripts/util/{policyExampels.ts => policyExamples.ts} (80%) diff --git a/package.json b/package.json index f2fdb1a1..f123a78e 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "script:public": "yarn exec tsx ./scripts/test-public.ts", "script:private": "yarn exec tsx ./scripts/test-private.ts", "script:registration": "yarn exec tsx ./scripts/test-registration.ts", + "script:seed": "yarn exec tsx ./scripts/seed-uma-ODRL-policy.ts", "script:uma-ucp": "yarn exec tsx ./scripts/test-uma-ucp.ts", "script:uma-odrl": "yarn exec tsx ./scripts/test-uma-ODRL.ts", "script:uma-odrl-policy": "yarn exec tsx ./scripts/test-uma-ODRL-policy.ts", diff --git a/scripts/seed-uma-ODRL-policy.ts b/scripts/seed-uma-ODRL-policy.ts new file mode 100644 index 00000000..2cde8845 --- /dev/null +++ b/scripts/seed-uma-ODRL-policy.ts @@ -0,0 +1,50 @@ +import * as readline from 'readline'; +import { seedingPolicies } from './util/policyExamples'; + +async function seedForOneClient(id: string) { + await fetch("http://localhost:4000/uma/policies", { method: 'POST', headers: { 'Authorization': id, 'Content-Type': 'text/turtle' }, body: Buffer.from(seedingPolicies(id), 'utf-8') }); +} + +async function deleteForOneClient(id: string) { + for (const policyId of ['http://example.org/usagePolicy1', 'http://example.org/usagePolicy1a', 'http://example.org/usagePolicy3', 'urn:uuid:95efe0e8-4fb7-496d-8f3c-4d78c97829bc']) { + await fetch(`http://localhost:4000/uma/policies/${encodeURIComponent(policyId)}`, { method: 'DELETE', headers: { 'Authorization': id } }) + } +} + +async function main() { + let started: 'NO' | 'SEED' | 'DELETE' = 'NO'; + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + console.log("Do you want to seed or delete? (type 'seed' or 'delete')"); + rl.on('line', async (input) => { + if (started === 'NO') { + if (input === "seed") { + started = 'SEED'; + console.log("Type the webID that you wish to seed, or cancel:"); + } + else if (input === "delete") { + console.log("Type the webID of the client who's seeded data you want to delete, or cancel:") + started = 'DELETE'; + } + else + console.log("Type 'seed' or 'delete'") + } else { + if (input === 'cancel') + started = 'NO'; + else if (started === 'SEED') { + await seedForOneClient(input); + console.log('seeding completed') + } else { + await deleteForOneClient(input); + console.log('deleting complete') + } + console.log("Do you want to seed or delete? (type 'seed' or 'delete')"); + started = 'NO'; + } + + }); +} + +main(); \ No newline at end of file diff --git a/scripts/test-uma-ODRL-policy.ts b/scripts/test-uma-ODRL-policy.ts index b2c72146..5404cba5 100644 --- a/scripts/test-uma-ODRL-policy.ts +++ b/scripts/test-uma-ODRL-policy.ts @@ -4,7 +4,7 @@ * The purpose of this file is to test the /policies endpoint. */ -import { policyA, policyB, policyC, badPolicy1, changePolicy1, changePolicy95e, putPolicy95e, putPolicyB } from "./util/policyExampels"; +import { policyA, policyB, policyC, badPolicy1, changePolicy1, changePolicy95e, putPolicy95e, putPolicyB } from "./util/policyExamples"; const endpoint = (extra: string = '') => 'http://localhost:4000/uma/policies' + extra; const client = (client: string = 'a') => `https://pod.${client}.com/profile/card#me`; diff --git a/scripts/util/policyExampels.ts b/scripts/util/policyExamples.ts similarity index 80% rename from scripts/util/policyExampels.ts rename to scripts/util/policyExamples.ts index 4d8634da..56b8aeea 100644 --- a/scripts/util/policyExampels.ts +++ b/scripts/util/policyExamples.ts @@ -165,4 +165,47 @@ export const putPolicy95e = ` ; ; . -` \ No newline at end of file +` + +export const seedingPolicies = (id: string) => ` +@prefix ex: . +@prefix odrl: . +@prefix dct: . + +ex:usagePolicy1 a odrl:Agreement . +ex:usagePolicy1 odrl:permission ex:permission1 . +ex:permission1 a odrl:Permission . +ex:permission1 odrl:action odrl:read . +ex:permission1 odrl:action odrl:write . +ex:permission1 odrl:target . +ex:permission1 odrl:assignee . +ex:permission1 odrl:assigner <${id}> . + +ex:usagePolicy1a a odrl:Agreement . +ex:usagePolicy1a odrl:permission ex:permission1a . +ex:permission1a a odrl:Permission . +ex:permission1a odrl:action odrl:control . +ex:permission1 odrl:action odrl:append . +ex:permission1a odrl:target . +ex:permission1a odrl:assignee . +ex:permission1a odrl:assigner <${id}> . + + a odrl:Set; + dct:description "A is data owner of resource X. ALICE may READ resource X."; + odrl:permission . + a odrl:Permission; + odrl:action odrl:read; + odrl:action odrl:append; + odrl:action odrl:write; + odrl:target ex:x; + odrl:assignee ex:alice; + odrl:assigner <${id}>. + +ex:usagePolicy3 a odrl:Agreement . +ex:usagePolicy3 odrl:permission ex:permission3 . +ex:permission3 a odrl:Permission . +ex:permission3 odrl:action odrl:create . +ex:permission3 odrl:target . +ex:permission3 odrl:assignee . +ex:permission3 odrl:assigner <${id}> . +` From 155169e910057c7d5fadabf3921efc806e34579e Mon Sep 17 00:00:00 2001 From: lennertdr Date: Thu, 17 Jul 2025 15:03:51 +0200 Subject: [PATCH 53/62] options for other requests --- packages/uma/src/index.ts | 7 ++++- packages/uma/src/routes/Policy.ts | 13 ++------- .../routeSpecific/policies/PolicyOptions.ts | 29 +++++++++++++++++++ scripts/util/policyExamples.ts | 17 +++++++++-- 4 files changed, 52 insertions(+), 14 deletions(-) create mode 100644 packages/uma/src/util/routeSpecific/policies/PolicyOptions.ts diff --git a/packages/uma/src/index.ts b/packages/uma/src/index.ts index df3967a9..183a013c 100644 --- a/packages/uma/src/index.ts +++ b/packages/uma/src/index.ts @@ -74,5 +74,10 @@ export * from './util/ConvertUtil'; export * from './util/HttpMessageSignatures'; export * from './util/Result'; export * from './util/ReType'; -export * from './util/routeSpecific/policies/GetPolicies'; export * from './util/routeSpecific/policies/PolicyUtil'; +export * from './util/routeSpecific/policies/CreatePolicies'; +export * from './util/routeSpecific/policies/GetPolicies'; +export * from './util/routeSpecific/policies/EditPolicies'; +export * from './util/routeSpecific/policies/RewritePolicies'; +export * from './util/routeSpecific/policies/DeletePolicies'; +export * from './util/routeSpecific/policies/PolicyOptions'; diff --git a/packages/uma/src/routes/Policy.ts b/packages/uma/src/routes/Policy.ts index 45107817..3e70353e 100644 --- a/packages/uma/src/routes/Policy.ts +++ b/packages/uma/src/routes/Policy.ts @@ -5,7 +5,8 @@ import { getPolicies } from "../util/routeSpecific/policies/GetPolicies"; import { addPolicies } from "../util/routeSpecific/policies/CreatePolicies"; import { deletePolicy } from "../util/routeSpecific/policies/DeletePolicies"; import { editPolicy } from "../util/routeSpecific/policies/EditPolicies"; -import { rewritePolicy } from "../util/routeSpecific/policies/rewritePolicies"; +import { rewritePolicy } from "../util/routeSpecific/policies/RewritePolicies"; +import { policyOptions } from "../util/routeSpecific/policies/PolicyOptions"; /** * Endpoint to handle policies, this implementation gives all policies that have the @@ -53,15 +54,7 @@ export class PolicyRequestHandler extends HttpHandler { case 'DELETE': return deletePolicy(request, store, this.storage, client, this.baseUrl); case 'PATCH': return editPolicy(request, store, this.storage, client, this.baseUrl); case 'PUT': return rewritePolicy(request, store, this.storage, client, this.baseUrl); - case 'OPTIONS': return { - status: 204, - headers: { - // this is only for the url without - 'Access-Control-Allow-Origin': 'http://localhost:5173', - 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', - 'Access-Control-Allow-Headers': 'Authorization, Content-Type', - } - } + case 'OPTIONS': return policyOptions(request, this.baseUrl); default: throw new MethodNotAllowedHttpError(); } } diff --git a/packages/uma/src/util/routeSpecific/policies/PolicyOptions.ts b/packages/uma/src/util/routeSpecific/policies/PolicyOptions.ts new file mode 100644 index 00000000..b2b47b35 --- /dev/null +++ b/packages/uma/src/util/routeSpecific/policies/PolicyOptions.ts @@ -0,0 +1,29 @@ +import { HttpHandlerRequest } from "../../http/models/HttpHandler"; +import { checkBaseURL, retrieveID } from "./PolicyUtil"; + +export function policyOptions(request: HttpHandlerRequest, baseUrl: string) { + try { + retrieveID(checkBaseURL(request, baseUrl)) + return { + status: 204, + headers: { + // this is only for the url with + 'Access-Control-Allow-Origin': 'http://localhost:5173', + 'Access-Control-Allow-Methods': 'GET, PATCH, PUT, DELETE, OPTIONS', + 'Access-Control-Allow-Headers': 'Authorization, Content-Type', + } + } + + } catch (error) { + return { + status: 204, + headers: { + // this is only for the url without + 'Access-Control-Allow-Origin': 'http://localhost:5173', + 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Authorization, Content-Type', + } + } + + } +} \ No newline at end of file diff --git a/scripts/util/policyExamples.ts b/scripts/util/policyExamples.ts index 56b8aeea..2f2a79b2 100644 --- a/scripts/util/policyExamples.ts +++ b/scripts/util/policyExamples.ts @@ -178,7 +178,7 @@ ex:permission1 a odrl:Permission . ex:permission1 odrl:action odrl:read . ex:permission1 odrl:action odrl:write . ex:permission1 odrl:target . -ex:permission1 odrl:assignee . +ex:permission1 odrl:assignee . ex:permission1 odrl:assigner <${id}> . ex:usagePolicy1a a odrl:Agreement . @@ -187,7 +187,8 @@ ex:permission1a a odrl:Permission . ex:permission1a odrl:action odrl:control . ex:permission1 odrl:action odrl:append . ex:permission1a odrl:target . -ex:permission1a odrl:assignee . +ex:permission1a odrl:assignee . +ex:permission1a odrl:assignee . ex:permission1a odrl:assigner <${id}> . a odrl:Set; @@ -206,6 +207,16 @@ ex:usagePolicy3 odrl:permission ex:permission3 . ex:permission3 a odrl:Permission . ex:permission3 odrl:action odrl:create . ex:permission3 odrl:target . -ex:permission3 odrl:assignee . +ex:permission3 odrl:assignee . ex:permission3 odrl:assigner <${id}> . + +ex:usagePolicy3 odrl:permission ex:permission3b . +ex:permission3b a odrl:Permission . +ex:permission3b odrl:action odrl:create . +ex:permission3b odrl:action odrl:read . +ex:permission3b odrl:action odrl:write . +ex:permission3b odrl:action odrl:control . +ex:permission3b odrl:target . +ex:permission3b odrl:assigner <${id}> . + ` From 6eaf6f2edeeda7af44afb1b58d2c32839bc53b50 Mon Sep 17 00:00:00 2001 From: lennertdr Date: Fri, 25 Jul 2025 17:53:51 +0200 Subject: [PATCH 54/62] some requested changes --- docs/policy-management.md | 4 +- packages/ucp/src/storage/UCRulesStorage.ts | 6 +- packages/uma/config/routes/policies.json | 61 +++++++++- packages/uma/config/rules/odrl/addedRules.ttl | 7 -- .../routeSpecific/policies/EditPolicies.ts | 12 ++ .../routeSpecific/policies/rewritePolicies.ts | 4 +- scripts/seed-uma-ODRL-policy.ts | 28 ++++- scripts/util/policyExamples.ts | 109 +++++++++++++++++- 8 files changed, 211 insertions(+), 20 deletions(-) delete mode 100644 packages/uma/config/rules/odrl/addedRules.ttl diff --git a/docs/policy-management.md b/docs/policy-management.md index a4133364..e3264b2f 100644 --- a/docs/policy-management.md +++ b/docs/policy-management.md @@ -155,7 +155,7 @@ The DELETE process: 1. Find the rules defined in the policy. 2. Filter the rules that are assigned by the client, and delete them. 3. Find out if there are rules not assigned by the client. - * if there are other rules, we cannot delete the policy information as well. + * if there are other rules, we cannot delete the policy information as well. We delete the rule and its definition triple in the policy. * if there are no other rules, we can delete the entire policy. This method used to have one rather significant issue, as discussed [later](#delete-fix). @@ -183,7 +183,7 @@ Sanitization Limitations Some operations require the client to specify a policy ID in the URL. Since policy ID's might contain reserved characters (e.g. `/`, `:`, ...), we have chosen to encode them with the builtin [`encodeURIComponent()` function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent). Using this method, reserved characters will be converted to their respective UTF-8 encodings. ## Testing -The current implementation is tested only by the script in `scripts\test-uma-ODRL-policy.ts`. This script tests every implemented endpoint with a designated flow. Since the script initiates with an empty storage, and there is no endpoint or other way to seed it, the first requests must test the POST endpoint. These tests are designed to ensure that the storage is filled. After the POST tests, the access endpoints can be tested. Every endpoint gets tested in this script, which makes sure that the added data is removed. The current testing will be replaced with proper unit tests in the near future. +The current implementation is tested only by the script in `scripts\test-uma-ODRL-policy.ts`. This script tests every implemented endpoint with a designated flow. Since the script initiates with an empty storage, and there is no endpoint or other way to seed it, the first requests must test the POST endpoint. These tests are designed to ensure that the storage is filled. After the POST tests, the access endpoints can be tested. Every endpoint gets tested in this script, which makes sure that the added data is removed. The current testing will be replaced with proper unit tests and integration tests in the near future. ## Problems - The current [sanitization limitations](#sanitization-decisions) are to be considered diff --git a/packages/ucp/src/storage/UCRulesStorage.ts b/packages/ucp/src/storage/UCRulesStorage.ts index 14fb05b5..aa33748e 100644 --- a/packages/ucp/src/storage/UCRulesStorage.ts +++ b/packages/ucp/src/storage/UCRulesStorage.ts @@ -21,6 +21,10 @@ export interface UCRulesStorage { */ deleteRule: (identifier: string) => Promise; - // Experimental endpoint + /** + * Delete a Usage Control Rule with its reference from the storage + * @param identifier + * @returns + */ deleteRuleFromPolicy: (ruleID: string, PolicyID: string) => Promise; } \ No newline at end of file diff --git a/packages/uma/config/routes/policies.json b/packages/uma/config/routes/policies.json index f1197fb4..20d7e4f1 100644 --- a/packages/uma/config/routes/policies.json +++ b/packages/uma/config/routes/policies.json @@ -16,7 +16,34 @@ "storage": { "@id": "urn:uma:default:RulesStorage" }, - "baseUrl": { "@id": "urn:uma:variables:baseUrl" } + "baseUrl": { "@id": "urn:uma:variables:baseUrl" }, + "corsHandler": { + "comment": "Adds all the necessary CORS headers.", + "@id": "urn:uma:default:CorsHandler", + "@type": "CorsHandler", + "options_methods": [ + "GET", + "HEAD", + "OPTIONS", + "POST", + "PUT", + "PATCH", + "DELETE" + ], + "options_credentials": true, + "options_preflightContinue": false, + "options_exposedHeaders": [ + "Allow", + "ETag", + "Last-Modified", + "Link", + "Location", + "Updates-Via", + "Www-Authenticate", + "Authorization", + "Content-Type" + ] + } }, "path": "/uma/policies" }, @@ -27,14 +54,42 @@ "GET", "DELETE", "PATCH", - "PUT" + "PUT", + "OPTIONS" ], "handler": { "@type": "PolicyRequestHandler", "storage": { "@id": "urn:uma:default:RulesStorage" }, - "baseUrl": { "@id": "urn:uma:variables:baseUrl" } + "baseUrl": { "@id": "urn:uma:variables:baseUrl" }, + "corsHandler": { + "comment": "Adds all the necessary CORS headers.", + "@id": "urn:uma:default:CorsHandler", + "@type": "CorsHandler", + "options_methods": [ + "GET", + "HEAD", + "OPTIONS", + "POST", + "PUT", + "PATCH", + "DELETE" + ], + "options_credentials": true, + "options_preflightContinue": false, + "options_exposedHeaders": [ + "Allow", + "ETag", + "Last-Modified", + "Link", + "Location", + "Updates-Via", + "Www-Authenticate", + "Authorization", + "Content-Type" + ] + } }, "path": "/uma/policies/{id}" } diff --git a/packages/uma/config/rules/odrl/addedRules.ttl b/packages/uma/config/rules/odrl/addedRules.ttl deleted file mode 100644 index 9408efef..00000000 --- a/packages/uma/config/rules/odrl/addedRules.ttl +++ /dev/null @@ -1,7 +0,0 @@ - a ; - . - a ; - ; - ; - ; - . diff --git a/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts b/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts index ca8c174b..2c0281b8 100644 --- a/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts @@ -29,6 +29,18 @@ export async function editPolicy(request: HttpHandlerRequest, store: Store, stor } const query = parseBufferToString(request.body); + console.log(` + POLICY INFORMATION + ${ownedPolicyRules} + ${ownedRules} + ${policyDefinitions} + + QUERY: + ${query} + `) + + + // 3. Execute the query on the part of the policy that lays within reach const policyStore = new Store([...policyDefinitions, ...ownedPolicyRules, ...ownedRules]); const initialQuads = policyStore.getQuads(null, null, null, null); diff --git a/packages/uma/src/util/routeSpecific/policies/rewritePolicies.ts b/packages/uma/src/util/routeSpecific/policies/rewritePolicies.ts index e8f1e4c7..e2d08495 100644 --- a/packages/uma/src/util/routeSpecific/policies/rewritePolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/rewritePolicies.ts @@ -1,6 +1,6 @@ import { UCRulesStorage } from "@solidlab/ucp"; import { HttpHandlerRequest, HttpHandlerResponse } from "../../http/models/HttpHandler"; -import { BadRequestHttpError, InternalServerError } from "@solid/community-server"; +import { BadRequestHttpError, InternalServerError, NotFoundHttpError } from "@solid/community-server"; import { checkBaseURL, parseBodyToStore, quadsToText, relations, retrieveID } from "./PolicyUtil"; import { Quad, Store } from "n3"; import { sanitizeRule } from "./CreatePolicies"; @@ -14,7 +14,7 @@ export async function rewritePolicy(request: HttpHandlerRequest, store: Store, s // 1: Get all reachable policy information const policyInfo = getPolicyInfo(policyId, store, clientId); if (policyInfo.policyDefinitions.length === 0) - throw new BadRequestHttpError("Patch not allowed: policy does not exist"); + throw new NotFoundHttpError("Patch not allowed: policy does not exist"); // 2. Parse the requested policy, perform checks diff --git a/scripts/seed-uma-ODRL-policy.ts b/scripts/seed-uma-ODRL-policy.ts index 2cde8845..c372e9c1 100644 --- a/scripts/seed-uma-ODRL-policy.ts +++ b/scripts/seed-uma-ODRL-policy.ts @@ -1,16 +1,36 @@ import * as readline from 'readline'; -import { seedingPolicies } from './util/policyExamples'; +import { seedingPolicies, seedingPolicies2 } from './util/policyExamples'; async function seedForOneClient(id: string) { - await fetch("http://localhost:4000/uma/policies", { method: 'POST', headers: { 'Authorization': id, 'Content-Type': 'text/turtle' }, body: Buffer.from(seedingPolicies(id), 'utf-8') }); + await fetch("http://localhost:4000/uma/policies", { method: 'POST', headers: { 'Authorization': id, 'Content-Type': 'text/turtle' }, body: Buffer.from(seedingPolicies2(id), 'utf-8') }); } async function deleteForOneClient(id: string) { - for (const policyId of ['http://example.org/usagePolicy1', 'http://example.org/usagePolicy1a', 'http://example.org/usagePolicy3', 'urn:uuid:95efe0e8-4fb7-496d-8f3c-4d78c97829bc']) { - await fetch(`http://localhost:4000/uma/policies/${encodeURIComponent(policyId)}`, { method: 'DELETE', headers: { 'Authorization': id } }) + const policyIds = [ + 'http://example.org/usagePolicy1-read', + 'http://example.org/usagePolicy1-write', + 'http://example.org/usagePolicy1-append', + 'http://example.org/usagePolicy1a-control-1', + 'http://example.org/usagePolicy1a-control-2', + 'http://example.org/usagePolicy3-create', + 'http://example.org/usagePolicy3b-create', + 'http://example.org/usagePolicy3b-read', + 'http://example.org/usagePolicy3b-write', + 'http://example.org/usagePolicy3b-control', + 'urn:uuid:policy-read', + 'urn:uuid:policy-append', + 'urn:uuid:policy-write', + ]; + + for (const policyId of policyIds) { + await fetch(`http://localhost:4000/uma/policies/${encodeURIComponent(policyId)}`, { + method: 'DELETE', + headers: { 'Authorization': id } + }); } } + async function main() { let started: 'NO' | 'SEED' | 'DELETE' = 'NO'; const rl = readline.createInterface({ diff --git a/scripts/util/policyExamples.ts b/scripts/util/policyExamples.ts index 2f2a79b2..244ee37f 100644 --- a/scripts/util/policyExamples.ts +++ b/scripts/util/policyExamples.ts @@ -177,6 +177,7 @@ ex:usagePolicy1 odrl:permission ex:permission1 . ex:permission1 a odrl:Permission . ex:permission1 odrl:action odrl:read . ex:permission1 odrl:action odrl:write . +ex:permission1 odrl:action odrl:append . ex:permission1 odrl:target . ex:permission1 odrl:assignee . ex:permission1 odrl:assigner <${id}> . @@ -185,7 +186,6 @@ ex:usagePolicy1a a odrl:Agreement . ex:usagePolicy1a odrl:permission ex:permission1a . ex:permission1a a odrl:Permission . ex:permission1a odrl:action odrl:control . -ex:permission1 odrl:action odrl:append . ex:permission1a odrl:target . ex:permission1a odrl:assignee . ex:permission1a odrl:assignee . @@ -220,3 +220,110 @@ ex:permission3b odrl:target . ex:permission3b odrl:assigner <${id}> . ` + +export const seedingPolicies2 = (id: string) => ` +@prefix ex: . +@prefix odrl: . +@prefix dct: . + +ex:usagePolicy1-read a odrl:Agreement . +ex:usagePolicy1-read odrl:permission ex:permission1-read . +ex:permission1-read a odrl:Permission . +ex:permission1-read odrl:action odrl:read . +ex:permission1-read odrl:target . +ex:permission1-read odrl:assignee . +ex:permission1-read odrl:assigner <${id}> . + +ex:usagePolicy1-write a odrl:Agreement . +ex:usagePolicy1-write odrl:permission ex:permission1-write . +ex:permission1-write a odrl:Permission . +ex:permission1-write odrl:action odrl:write . +ex:permission1-write odrl:target . +ex:permission1-write odrl:assignee . +ex:permission1-write odrl:assigner <${id}> . + +ex:usagePolicy1-append a odrl:Agreement . +ex:usagePolicy1-append odrl:permission ex:permission1-append . +ex:permission1-append a odrl:Permission . +ex:permission1-append odrl:action odrl:append . +ex:permission1-append odrl:target . +ex:permission1-append odrl:assignee . +ex:permission1-append odrl:assigner <${id}> . + +ex:usagePolicy1a-control-1 a odrl:Agreement . +ex:usagePolicy1a-control-1 odrl:permission ex:permission1a-control-1 . +ex:permission1a-control-1 a odrl:Permission . +ex:permission1a-control-1 odrl:action odrl:control . +ex:permission1a-control-1 odrl:target . +ex:permission1a-control-1 odrl:assignee . +ex:permission1a-control-1 odrl:assigner <${id}> . + +ex:usagePolicy1a-control-2 a odrl:Agreement . +ex:usagePolicy1a-control-2 odrl:permission ex:permission1a-control-2 . +ex:permission1a-control-2 a odrl:Permission . +ex:permission1a-control-2 odrl:action odrl:control . +ex:permission1a-control-2 odrl:target . +ex:permission1a-control-2 odrl:assignee . +ex:permission1a-control-2 odrl:assigner <${id}> . + + a odrl:Set; + dct:description "A is data owner of resource X. ALICE may READ resource X."; + odrl:permission . + a odrl:Permission; + odrl:action odrl:read; + odrl:target ex:x; + odrl:assignee ex:alice; + odrl:assigner <${id}> . + + a odrl:Set; + odrl:permission . + a odrl:Permission; + odrl:action odrl:append; + odrl:target ex:x; + odrl:assignee ex:alice; + odrl:assigner <${id}> . + + a odrl:Set; + odrl:permission . + a odrl:Permission; + odrl:action odrl:write; + odrl:target ex:x; + odrl:assignee ex:alice; + odrl:assigner <${id}> . + +ex:usagePolicy3-create a odrl:Agreement . +ex:usagePolicy3-create odrl:permission ex:permission3-create . +ex:permission3-create a odrl:Permission . +ex:permission3-create odrl:action odrl:create . +ex:permission3-create odrl:target . +ex:permission3-create odrl:assignee . +ex:permission3-create odrl:assigner <${id}> . + +ex:usagePolicy3b-create a odrl:Agreement . +ex:usagePolicy3b-create odrl:permission ex:permission3b-create . +ex:permission3b-create a odrl:Permission . +ex:permission3b-create odrl:action odrl:create . +ex:permission3b-create odrl:target . +ex:permission3b-create odrl:assigner <${id}> . + +ex:usagePolicy3b-read a odrl:Agreement . +ex:usagePolicy3b-read odrl:permission ex:permission3b-read . +ex:permission3b-read a odrl:Permission . +ex:permission3b-read odrl:action odrl:read . +ex:permission3b-read odrl:target . +ex:permission3b-read odrl:assigner <${id}> . + +ex:usagePolicy3b-write a odrl:Agreement . +ex:usagePolicy3b-write odrl:permission ex:permission3b-write . +ex:permission3b-write a odrl:Permission . +ex:permission3b-write odrl:action odrl:write . +ex:permission3b-write odrl:target . +ex:permission3b-write odrl:assigner <${id}> . + +ex:usagePolicy3b-control a odrl:Agreement . +ex:usagePolicy3b-control odrl:permission ex:permission3b-control . +ex:permission3b-control a odrl:Permission . +ex:permission3b-control odrl:action odrl:control . +ex:permission3b-control odrl:target . +ex:permission3b-control odrl:assigner <${id}> . +` From b8de385c0b30385f3045f324c0bd886201d50525 Mon Sep 17 00:00:00 2001 From: lennertdr Date: Mon, 28 Jul 2025 14:34:05 +0200 Subject: [PATCH 55/62] undo wrong import --- packages/uma/config/routes/policies.json | 58 +----------------------- 1 file changed, 2 insertions(+), 56 deletions(-) diff --git a/packages/uma/config/routes/policies.json b/packages/uma/config/routes/policies.json index 20d7e4f1..823b8ea3 100644 --- a/packages/uma/config/routes/policies.json +++ b/packages/uma/config/routes/policies.json @@ -16,34 +16,7 @@ "storage": { "@id": "urn:uma:default:RulesStorage" }, - "baseUrl": { "@id": "urn:uma:variables:baseUrl" }, - "corsHandler": { - "comment": "Adds all the necessary CORS headers.", - "@id": "urn:uma:default:CorsHandler", - "@type": "CorsHandler", - "options_methods": [ - "GET", - "HEAD", - "OPTIONS", - "POST", - "PUT", - "PATCH", - "DELETE" - ], - "options_credentials": true, - "options_preflightContinue": false, - "options_exposedHeaders": [ - "Allow", - "ETag", - "Last-Modified", - "Link", - "Location", - "Updates-Via", - "Www-Authenticate", - "Authorization", - "Content-Type" - ] - } + "baseUrl": { "@id": "urn:uma:variables:baseUrl" } }, "path": "/uma/policies" }, @@ -62,34 +35,7 @@ "storage": { "@id": "urn:uma:default:RulesStorage" }, - "baseUrl": { "@id": "urn:uma:variables:baseUrl" }, - "corsHandler": { - "comment": "Adds all the necessary CORS headers.", - "@id": "urn:uma:default:CorsHandler", - "@type": "CorsHandler", - "options_methods": [ - "GET", - "HEAD", - "OPTIONS", - "POST", - "PUT", - "PATCH", - "DELETE" - ], - "options_credentials": true, - "options_preflightContinue": false, - "options_exposedHeaders": [ - "Allow", - "ETag", - "Last-Modified", - "Link", - "Location", - "Updates-Via", - "Www-Authenticate", - "Authorization", - "Content-Type" - ] - } + "baseUrl": { "@id": "urn:uma:variables:baseUrl" } }, "path": "/uma/policies/{id}" } From 10c72471b27fd65b876625a6d9e78c9b32508a0b Mon Sep 17 00:00:00 2001 From: lennertdr Date: Mon, 28 Jul 2025 17:09:35 +0200 Subject: [PATCH 56/62] quick workaround --- .../src/util/routeSpecific/policies/EditPolicies.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts b/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts index 2c0281b8..0fb3d0c6 100644 --- a/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts @@ -54,16 +54,20 @@ export async function editPolicy(request: HttpHandlerRequest, store: Store, stor sanitizeRule(policyStore, clientId); // 3.1 Check that the other rules are unchanged + const initialState = { policyDefinitions, ownedPolicyRules, otherPolicyRules, ownedRules, otherRules }; const newState = getPolicyInfo(policyId, policyStore, clientId); + + console.log(`\n--- POLICY STATE CHANGE ---\nInitial State:\n policyDefinitions: ${initialState.policyDefinitions.map(q => q.toString()).join("\n ")}\n ownedPolicyRules: ${initialState.ownedPolicyRules.map(q => q.toString()).join("\n ")}\n otherPolicyRules: ${initialState.otherPolicyRules.map(q => q.toString()).join("\n ")}\n ownedRules: ${initialState.ownedRules.map(q => q.toString()).join("\n ")}\n otherRules: ${initialState.otherRules.map(q => q.toString()).join("\n ")}\nNew State:\n policyDefinitions: ${newState.policyDefinitions.map(q => q.toString()).join("\n ")}\n ownedPolicyRules: ${newState.ownedPolicyRules.map(q => q.toString()).join("\n ")}\n otherPolicyRules: ${newState.otherPolicyRules.map(q => q.toString()).join("\n ")}\n ownedRules: ${newState.ownedRules.map(q => q.toString()).join("\n ")}\n otherRules: ${newState.otherRules.map(q => q.toString()).join("\n ")}\n`); + if (newState.otherRules.length !== 0 || newState.otherPolicyRules.length !== 0) throw new BadRequestHttpError("Update not allowed: attempted to modify rules not owned by client"); // 3.2 Check that only Policy/Rule changing quads are introduced and removed // The only modifications we allow are policy definitions, policy rules that define owned rules and owned rules themselves - const newQuads = policyStore.getQuads(null, null, null, null); - if (newQuads.length - newState.ownedRules.length - newState.ownedPolicyRules.length - newState.policyDefinitions.length - !== initialQuads.length - ownedPolicyRules.length - ownedRules.length - policyDefinitions.length) - throw new BadRequestHttpError("Update not allowed: this query introduces quads that have nothing to do with the policy/rules you own"); + // const newQuads = policyStore.getQuads(null, null, null, null); + // if (newQuads.length - newState.ownedRules.length - newState.ownedPolicyRules.length - newState.policyDefinitions.length + // !== initialQuads.length - ownedPolicyRules.length - ownedRules.length - policyDefinitions.length) + // throw new BadRequestHttpError("Update not allowed: this query introduces quads that have nothing to do with the policy/rules you own"); // 4 Modify the storage to the updated version try { From dc950dac96e20519ac764d385716cfb34c950057 Mon Sep 17 00:00:00 2001 From: lennertdr Date: Wed, 30 Jul 2025 13:49:04 +0200 Subject: [PATCH 57/62] demo script --- .../routeSpecific/policies/CreatePolicies.ts | 2 +- scripts/demo-uma-policy.ts | 74 ++++++++ scripts/seed-uma-ODRL-policy.ts | 4 +- scripts/util/policyExamples.ts | 161 +++++++++++++++++- 4 files changed, 235 insertions(+), 6 deletions(-) create mode 100644 scripts/demo-uma-policy.ts diff --git a/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts b/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts index 83a6269a..5bab9f38 100644 --- a/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts @@ -67,7 +67,7 @@ export async function addPolicies(request: HttpHandlerRequest, store: Store, sto TOTAL = ${parsedPolicy.getQuads(null, null, null, null).length} `) - // None of these policies should already exist + // None of the policies in the request should already exist if ([...existingInfo.policyDefinitions, ...existingInfo.ownedPolicyRules, ...existingInfo.otherPolicyRules, ...existingInfo.ownedRules, ...existingInfo.otherRules].length > 0) { console.log('TEST: ALREADY EXISTS') diff --git a/scripts/demo-uma-policy.ts b/scripts/demo-uma-policy.ts new file mode 100644 index 00000000..f00c6adf --- /dev/null +++ b/scripts/demo-uma-policy.ts @@ -0,0 +1,74 @@ +import { UserManagedAccessFetcher } from "./util/UMA-client"; +import * as readline from 'readline'; + +const claim_token_format = 'urn:solidlab:uma:claims:formats:webid'; + +const testCode = (code: number, shouldBe: number = 2, trunc: boolean = true): boolean => { + return (trunc ? Math.trunc(code / 100) : code) === shouldBe; +}; + +async function traverseFile(text: string, claim_token: string, resource: string) { + const fetcher = new UserManagedAccessFetcher({ + token: claim_token, + token_format: claim_token_format + }); + + console.log(`\nTest creation/modification rights with RPT`); + const response = await fetcher.fetch(resource, { + method: "PUT", + body: text + }); + + if (testCode(response.status)) { + console.log(`Document created. Server responded with status code ${response.status}`); + } else { + console.log(`Access denied. Creation responded with status code ${response.status}`); + } + + console.log(`\nTesting reading rights with RPT`); + const readingResponse = await fetcher.fetch(resource); + + if (testCode(readingResponse.status)) { + const contents = await readingResponse.text(); + console.log(`Reading successful (status ${readingResponse.status}). Contents:\n${contents}\n`); + } else { + console.log(`Access denied. Insufficient reading rights (status ${readingResponse.status})`); + } +} + +async function main() { + let state: 'NO' | 'ResourceSet' | 'AssigneeSet' = 'NO'; + let resourceId = ""; + let assigneeId = ""; + + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + console.log("What resource file do you want to write to?"); + + rl.on('line', async (input) => { + switch (state) { + case 'NO': + resourceId = input; + state = 'ResourceSet'; + console.log("What is your WebID?"); + break; + + case 'ResourceSet': + assigneeId = input; + state = 'AssigneeSet'; + console.log("What do you want to write?"); + break; + + case 'AssigneeSet': + await traverseFile(input, assigneeId, resourceId); + console.log("\n\n\n*************************************************\n\n\nWhat resource file do you want to write to?"); + state = 'NO'; + break; + } + }); +} + +main(); diff --git a/scripts/seed-uma-ODRL-policy.ts b/scripts/seed-uma-ODRL-policy.ts index c372e9c1..b1ac008a 100644 --- a/scripts/seed-uma-ODRL-policy.ts +++ b/scripts/seed-uma-ODRL-policy.ts @@ -1,8 +1,8 @@ import * as readline from 'readline'; -import { seedingPolicies, seedingPolicies2 } from './util/policyExamples'; +import { seedingPolicies, seedingPolicies2, seedingPolicies3 } from './util/policyExamples'; async function seedForOneClient(id: string) { - await fetch("http://localhost:4000/uma/policies", { method: 'POST', headers: { 'Authorization': id, 'Content-Type': 'text/turtle' }, body: Buffer.from(seedingPolicies2(id), 'utf-8') }); + await fetch("http://localhost:4000/uma/policies", { method: 'POST', headers: { 'Authorization': id, 'Content-Type': 'text/turtle' }, body: Buffer.from(seedingPolicies3(id), 'utf-8') }); } async function deleteForOneClient(id: string) { diff --git a/scripts/util/policyExamples.ts b/scripts/util/policyExamples.ts index 244ee37f..f7f7b216 100644 --- a/scripts/util/policyExamples.ts +++ b/scripts/util/policyExamples.ts @@ -14,7 +14,7 @@ ex:permission1 odrl:assigner . ex:usagePolicy1a a odrl:Agreement . ex:usagePolicy1a odrl:permission ex:permission1a . ex:permission1a a odrl:Permission . -ex:permission1a odrl:action odrl:create . +ex:permission1a odrl:action odrl:read . ex:permission1a odrl:target . ex:permission1a odrl:assignee . ex:permission1a odrl:assigner . @@ -43,7 +43,7 @@ ex:permission2 odrl:assigner . ex:usagePolicy2a a odrl:Agreement . ex:usagePolicy2a odrl:permission ex:permission2 . ex:permission2a a odrl:Permission . -ex:permission2a odrl:action odrl:create . +ex:permission2a odrl:action odrl:write . ex:permission2a odrl:target . ex:permission2a odrl:assignee . ex:permission2a odrl:assigner . @@ -205,7 +205,7 @@ ex:permission1a odrl:assigner <${id}> . ex:usagePolicy3 a odrl:Agreement . ex:usagePolicy3 odrl:permission ex:permission3 . ex:permission3 a odrl:Permission . -ex:permission3 odrl:action odrl:create . +ex:permission3 odrl:action odrl:control . ex:permission3 odrl:target . ex:permission3 odrl:assignee . ex:permission3 odrl:assigner <${id}> . @@ -327,3 +327,158 @@ ex:permission3b-control odrl:action odrl:control . ex:permission3b-control odrl:target . ex:permission3b-control odrl:assigner <${id}> . ` +export const seedingPolicies3 = (id: string) => ` +@prefix ex: . +@prefix odrl: . +@prefix dct: . + +ex:usagePolicy1 a odrl:Agreement . +ex:usagePolicy1 odrl:permission ex:permission1-read . +ex:permission1-read a odrl:Permission . +ex:permission1-read odrl:action odrl:read . +ex:permission1-read odrl:target . +ex:permission1-read odrl:assignee . +ex:permission1-read odrl:assigner <${id}> . + +ex:usagePolicy1 odrl:permission ex:permission1-write . +ex:permission1-write a odrl:Permission . +ex:permission1-write odrl:action odrl:write . +ex:permission1-write odrl:target . +ex:permission1-write odrl:assignee . +ex:permission1-write odrl:assigner <${id}> . + +ex:usagePolicy1 odrl:permission ex:permission1-append . +ex:permission1-append a odrl:Permission . +ex:permission1-append odrl:action odrl:append . +ex:permission1-append odrl:target . +ex:permission1-append odrl:assignee . +ex:permission1-append odrl:assigner <${id}> . + +ex:usagePolicy1a a odrl:Agreement . +ex:usagePolicy1a odrl:permission ex:permission1a-control-1 . +ex:permission1a-control-1 a odrl:Permission . +ex:permission1a-control-1 odrl:action odrl:control . +ex:permission1a-control-1 odrl:target . +ex:permission1a-control-1 odrl:assignee . +ex:permission1a-control-1 odrl:assigner <${id}> . + +ex:usagePolicy1a odrl:permission ex:permission1a-control-2 . +ex:permission1a-control-2 a odrl:Permission . +ex:permission1a-control-2 odrl:action odrl:control . +ex:permission1a-control-2 odrl:target . +ex:permission1a-control-2 odrl:assignee . +ex:permission1a-control-2 odrl:assigner <${id}> . + + a odrl:Set; + dct:description "A is data owner of resource X. ALICE may READ resource X."; + odrl:permission . + a odrl:Permission; + odrl:action odrl:read; + odrl:target ex:x; + odrl:assignee ex:alice; + odrl:assigner <${id}> . + + a odrl:Set; + odrl:permission . + a odrl:Permission; + odrl:action odrl:append; + odrl:target ex:x; + odrl:assignee ex:alice; + odrl:assigner <${id}> . + + a odrl:Set; + odrl:permission . + a odrl:Permission; + odrl:action odrl:write; + odrl:target ex:x; + odrl:assignee ex:alice; + odrl:assigner <${id}> . + +ex:usagePolicy3 a odrl:Agreement . +ex:usagePolicy3 odrl:permission ex:permission3-create . +ex:permission3-create a odrl:Permission . +ex:permission3-create odrl:action odrl:write . +ex:permission3-create odrl:target . +ex:permission3-create odrl:assignee . +ex:permission3-create odrl:assigner <${id}> . + +ex:usagePolicy3 odrl:permission ex:permission3b-create . +ex:permission3b-create a odrl:Permission . +ex:permission3b-create odrl:action odrl:read . +ex:permission3b-create odrl:target . +ex:permission3b-create odrl:assigner <${id}> . + +ex:usagePolicy3 odrl:permission ex:permission3b-read . +ex:permission3b-read a odrl:Permission . +ex:permission3b-read odrl:action odrl:read . +ex:permission3b-read odrl:target . +ex:permission3b-read odrl:assigner <${id}> . + +ex:usagePolicy3 odrl:permission ex:permission3b-read . +ex:permission3b-read a odrl:Permission . +ex:permission3b-read odrl:action odrl:read . +ex:permission3b-read odrl:target . +ex:permission3b-read odrl:assigner <${id}> . + + +ex:usagePolicy3 odrl:permission ex:permission3b-write . +ex:permission3b-write a odrl:Permission . +ex:permission3b-write odrl:action odrl:write . +ex:permission3b-write odrl:target . +ex:permission3b-write odrl:assigner <${id}> . +ex:permission3b-write odrl:assignee . + + +ex:usagePolicy3 odrl:permission ex:permission3b-control . +ex:permission3b-control a odrl:Permission . +ex:permission3b-control odrl:action odrl:control . +ex:permission3b-control odrl:target . +ex:permission3b-control odrl:assigner <${id}> . +` + +export const seedTestFetcher = ` +@prefix ex: . +@prefix odrl: . +@base . + + +ex:usagePolicy1 a odrl:Agreement . +ex:usagePolicy1 odrl:permission ex:permission1 . +ex:permission1 a odrl:Permission . +ex:permission1 odrl:action odrl:modify . +ex:permission1 odrl:target . +ex:permission1 odrl:assignee . +ex:permission1 odrl:assigner . + +ex:usagePolicy1a a odrl:Agreement . +ex:usagePolicy1a odrl:permission ex:permission1a . +ex:permission1a a odrl:Permission . +ex:permission1a odrl:action odrl:create . +ex:permission1a odrl:target . +ex:permission1a odrl:assignee . +ex:permission1a odrl:assigner . + +ex:usagePolicy2 a odrl:Agreement . +ex:usagePolicy2 odrl:permission ex:permission2a . +ex:permission2 a odrl:Permission . +ex:permission2 odrl:action odrl:modify . +ex:permission2 odrl:target . +ex:permission2 odrl:assignee . +ex:permission2 odrl:assigner . + +ex:usagePolicy2a a odrl:Agreement . +ex:usagePolicy2a odrl:permission ex:permission2 . +ex:permission2a a odrl:Permission . +ex:permission2a odrl:action odrl:create . +ex:permission2a odrl:target . +ex:permission2a odrl:assignee . +ex:permission2a odrl:assigner . + + +ex:usagePolicy3 a odrl:Agreement . +ex:usagePolicy3 odrl:permission ex:permission3 . +ex:permission3 a odrl:Permission . +ex:permission3 odrl:action odrl:read . +ex:permission3 odrl:target . +ex:permission3 odrl:assignee . +ex:permission3 odrl:assigner .` \ No newline at end of file From 80bf03b813c88ef526f85163c70ccf1cb1e9c358 Mon Sep 17 00:00:00 2001 From: lennertdr Date: Wed, 30 Jul 2025 13:49:27 +0200 Subject: [PATCH 58/62] script shortcut --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index f123a78e..01bd25a2 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "start:demo": "yarn workspaces foreach --include 'packages/*' -A -pi -j unlimited run demo", "script:demo": "yarn exec tsx ./demo/flow.ts", "script:demo-test": "yarn exec tsx ./demo/flow-test.ts", + "script:demo-uma-policy": "yarn exec tsx ./scripts/demo-uma-policy.ts", "script:public": "yarn exec tsx ./scripts/test-public.ts", "script:private": "yarn exec tsx ./scripts/test-private.ts", "script:registration": "yarn exec tsx ./scripts/test-registration.ts", From e6044f7e37dcffce2d23ffefb9b2f418661fcfd9 Mon Sep 17 00:00:00 2001 From: lennertdr Date: Thu, 31 Jul 2025 10:15:00 +0200 Subject: [PATCH 59/62] added test again --- .../src/util/routeSpecific/policies/EditPolicies.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts b/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts index 0fb3d0c6..e0d6eef2 100644 --- a/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts @@ -1,4 +1,4 @@ -import { Store } from "n3"; +import { Store, Writer } from "n3"; import { HttpHandlerRequest, HttpHandlerResponse } from "../../http/models/HttpHandler"; import { UCRulesStorage } from "@solidlab/ucp"; import { checkBaseURL, parseBufferToString, quadsToText, retrieveID } from "./PolicyUtil"; @@ -64,11 +64,9 @@ export async function editPolicy(request: HttpHandlerRequest, store: Store, stor // 3.2 Check that only Policy/Rule changing quads are introduced and removed // The only modifications we allow are policy definitions, policy rules that define owned rules and owned rules themselves - // const newQuads = policyStore.getQuads(null, null, null, null); - // if (newQuads.length - newState.ownedRules.length - newState.ownedPolicyRules.length - newState.policyDefinitions.length - // !== initialQuads.length - ownedPolicyRules.length - ownedRules.length - policyDefinitions.length) - // throw new BadRequestHttpError("Update not allowed: this query introduces quads that have nothing to do with the policy/rules you own"); - + const newQuads = policyStore.getQuads(null, null, null, null); + if (newQuads.length - newState.ownedRules.length - newState.ownedPolicyRules.length - newState.policyDefinitions.length !== 0) + throw new BadRequestHttpError("Update not allowed: this query changes quads that have nothing to do with the policy/rules you own"); // 4 Modify the storage to the updated version try { // Since no update function is available, we need to remove the old one and set the updated one From eb605dcf79f4accf2c79e16e2c48a654d92418d2 Mon Sep 17 00:00:00 2001 From: lennertdr Date: Thu, 31 Jul 2025 10:47:13 +0200 Subject: [PATCH 60/62] Removed logs and finetuned docs --- docs/policy-management.md | 8 ++--- .../routeSpecific/policies/CreatePolicies.ts | 34 ------------------- .../routeSpecific/policies/EditPolicies.ts | 13 ------- 3 files changed, 4 insertions(+), 51 deletions(-) diff --git a/docs/policy-management.md b/docs/policy-management.md index e3264b2f..1ea03739 100644 --- a/docs/policy-management.md +++ b/docs/policy-management.md @@ -118,7 +118,7 @@ A PUT completely replaces the policy within the scope of the client. The PUT works as a combination of DELETE and POST. It requires a body with the same content type as the [POST request](#creating-policies). This body will be interpreted as the requested policy with some rules. The PUT process: -1. Find information about the policy. If it does not exist, return with a **status code 400** to indicate that you cannot rewrite a nonexistent policy. +1. Find information about the policy. If it does not exist, return with a **status code 404** to indicate that you cannot rewrite a nonexistent policy. 2. Parse and validate the body, with the same procedure used in the POST endpoint. First, we perform the basic sanitization checks. Upon success, extra checks are performed to see if the new definition stays within the scope of the client: - Check that the newly defined policy does not define other policies - Check that the new policy does not contain any rules that do not belong to the client @@ -143,7 +143,7 @@ The PATCH process: - Performing DELETE queries on rules out of your scope will simply not work, since they are not part of the isolated store. - We can easily see exactly when the query goes out of scope by testing the resulting store, separating it in the 5 groups and performing the following checks: 1. If the resulting store has rules out of the clients' scope (indicated by groups **(2)** and **(5)**), we can abort the update and respond with **status code 400**. - 2. We can analyze the size of the resulting store. Substracting the amount of quads within reach should result in 0, since no other rules may be added. This test will fail when the client inserts any unrelated quads to its own policy. Upon failure, the server responds with **status code 400**. + 2. We can analyze the size of the resulting store. Substracting the amount of quads within reach should result in 0, since no other rules may be added. This test will fail when the client inserts/deletes any unrelated quads to its own policy. Upon failure, the server responds with **status code 400**. 4. The old definition will be replaced with the updated version. Since no real update function for our storage exists, we delete the old policy and add the resulting store from the query, together with the quads out of scope as collected in step 1. Note that any quads in the original policy that could not be collected by the procedure defined in [GET One Policy](#get-one-policy), will not be part of the newly defined policy. @@ -155,8 +155,8 @@ The DELETE process: 1. Find the rules defined in the policy. 2. Filter the rules that are assigned by the client, and delete them. 3. Find out if there are rules not assigned by the client. - * if there are other rules, we cannot delete the policy information as well. We delete the rule and its definition triple in the policy. - * if there are no other rules, we can delete the entire policy. + * If there are other rules, we cannot delete the policy information as well. We delete the rule and its definition triple in the policy. + * If there are no other rules, we can delete the entire policy. This method used to have one rather significant issue, as discussed [later](#delete-fix). diff --git a/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts b/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts index 5bab9f38..dad50bbf 100644 --- a/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/CreatePolicies.ts @@ -51,50 +51,16 @@ export async function addPolicies(request: HttpHandlerRequest, store: Store, sto const totalQuads: Quad[] = []; if ([...newPolicies].some(id => { const existingInfo = getPolicyInfo(id, store, clientId); - console.log(` - INITIAL POLICY ${id} - - POLICY ITSELF - ${existingInfo.policyDefinitions.length} - ${existingInfo.policyDefinitions} - ${existingInfo.ownedPolicyRules} - ${existingInfo.otherPolicyRules} - - OWNED RULES - ${existingInfo.ownedRules.length} - ${existingInfo.ownedRules.length} - - OTHER RULES - ${existingInfo.otherRules.length} - ${existingInfo.otherRules} - - TOTAL = ${parsedPolicy.getQuads(null, null, null, null).length} - `) // None of the policies in the request should already exist if ([...existingInfo.policyDefinitions, ...existingInfo.ownedPolicyRules, ...existingInfo.otherPolicyRules, ...existingInfo.ownedRules, ...existingInfo.otherRules].length > 0) { - console.log('TEST: ALREADY EXISTS') return true; } const { policyDefinitions, ownedPolicyRules, otherPolicyRules, ownedRules, otherRules } = getPolicyInfo(id, parsedPolicy, clientId); - console.log(` - TEST FOR POLICY ${id} - - POLICY ITSELF - ${policyDefinitions.length} - ${policyDefinitions} - ${ownedPolicyRules} - ${otherPolicyRules} - - OWNED RULES - ${ownedRules.length} - ${ownedRules} - - OTHER RULES - ${otherRules.length} - ${otherRules} - - TOTAL = ${parsedPolicy.getQuads(null, null, null, null).length} - `) // The policies may not declare rules out of scope if (otherRules.length !== 0 || otherPolicyRules.length !== 0) { - console.log("TEST: out of scope") return true; } diff --git a/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts b/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts index e0d6eef2..2a72953a 100644 --- a/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts +++ b/packages/uma/src/util/routeSpecific/policies/EditPolicies.ts @@ -29,17 +29,6 @@ export async function editPolicy(request: HttpHandlerRequest, store: Store, stor } const query = parseBufferToString(request.body); - console.log(` - POLICY INFORMATION - ${ownedPolicyRules} - ${ownedRules} - ${policyDefinitions} - - QUERY: - ${query} - `) - - // 3. Execute the query on the part of the policy that lays within reach const policyStore = new Store([...policyDefinitions, ...ownedPolicyRules, ...ownedRules]); @@ -57,8 +46,6 @@ export async function editPolicy(request: HttpHandlerRequest, store: Store, stor const initialState = { policyDefinitions, ownedPolicyRules, otherPolicyRules, ownedRules, otherRules }; const newState = getPolicyInfo(policyId, policyStore, clientId); - console.log(`\n--- POLICY STATE CHANGE ---\nInitial State:\n policyDefinitions: ${initialState.policyDefinitions.map(q => q.toString()).join("\n ")}\n ownedPolicyRules: ${initialState.ownedPolicyRules.map(q => q.toString()).join("\n ")}\n otherPolicyRules: ${initialState.otherPolicyRules.map(q => q.toString()).join("\n ")}\n ownedRules: ${initialState.ownedRules.map(q => q.toString()).join("\n ")}\n otherRules: ${initialState.otherRules.map(q => q.toString()).join("\n ")}\nNew State:\n policyDefinitions: ${newState.policyDefinitions.map(q => q.toString()).join("\n ")}\n ownedPolicyRules: ${newState.ownedPolicyRules.map(q => q.toString()).join("\n ")}\n otherPolicyRules: ${newState.otherPolicyRules.map(q => q.toString()).join("\n ")}\n ownedRules: ${newState.ownedRules.map(q => q.toString()).join("\n ")}\n otherRules: ${newState.otherRules.map(q => q.toString()).join("\n ")}\n`); - if (newState.otherRules.length !== 0 || newState.otherPolicyRules.length !== 0) throw new BadRequestHttpError("Update not allowed: attempted to modify rules not owned by client"); From 46c177f413f005fe03235016b8fa5abdb611aa86 Mon Sep 17 00:00:00 2001 From: lennertdr Date: Thu, 31 Jul 2025 16:46:06 +0200 Subject: [PATCH 61/62] docfix --- docs/policy-management.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/policy-management.md b/docs/policy-management.md index 1ea03739..7d27d693 100644 --- a/docs/policy-management.md +++ b/docs/policy-management.md @@ -186,7 +186,8 @@ Some operations require the client to specify a policy ID in the URL. Since poli The current implementation is tested only by the script in `scripts\test-uma-ODRL-policy.ts`. This script tests every implemented endpoint with a designated flow. Since the script initiates with an empty storage, and there is no endpoint or other way to seed it, the first requests must test the POST endpoint. These tests are designed to ensure that the storage is filled. After the POST tests, the access endpoints can be tested. Every endpoint gets tested in this script, which makes sure that the added data is removed. The current testing will be replaced with proper unit tests and integration tests in the near future. ## Problems -- The current [sanitization limitations](#sanitization-decisions) are to be considered +- The current [sanitization limitations](#sanitization-decisions) are to be considered. +- Fix CORS handling: the project configuration must be extended to the `/policies` endpoint. ### Solved Problems From 9a5f4e812fa79759b4f26748be58333a8f15c178 Mon Sep 17 00:00:00 2001 From: lennertdr Date: Thu, 31 Jul 2025 16:53:12 +0200 Subject: [PATCH 62/62] TODO's --- docs/policy-management.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/policy-management.md b/docs/policy-management.md index 7d27d693..d776db32 100644 --- a/docs/policy-management.md +++ b/docs/policy-management.md @@ -185,9 +185,11 @@ Some operations require the client to specify a policy ID in the URL. Since poli ## Testing The current implementation is tested only by the script in `scripts\test-uma-ODRL-policy.ts`. This script tests every implemented endpoint with a designated flow. Since the script initiates with an empty storage, and there is no endpoint or other way to seed it, the first requests must test the POST endpoint. These tests are designed to ensure that the storage is filled. After the POST tests, the access endpoints can be tested. Every endpoint gets tested in this script, which makes sure that the added data is removed. The current testing will be replaced with proper unit tests and integration tests in the near future. -## Problems +## TODO - The current [sanitization limitations](#sanitization-decisions) are to be considered. - Fix CORS handling: the project configuration must be extended to the `/policies` endpoint. +- Implement Unit Tests +- ... ### Solved Problems