From ec4c833fea2b5cc9958ffc3c22867a5fbb68924a Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Thu, 30 Jul 2026 14:42:18 +0200 Subject: [PATCH] fix: Correctly override policies on PUT requests --- .../src/controller/PolicyRequestController.ts | 11 +++- packages/uma/src/util/routeSpecific/delete.ts | 59 ------------------- packages/uma/src/util/routeSpecific/index.ts | 1 - test/integration/Policies.test.ts | 53 +++++++++++++++++ 4 files changed, 62 insertions(+), 62 deletions(-) delete mode 100644 packages/uma/src/util/routeSpecific/delete.ts diff --git a/packages/uma/src/controller/PolicyRequestController.ts b/packages/uma/src/controller/PolicyRequestController.ts index aea8dd21..04895a61 100644 --- a/packages/uma/src/controller/PolicyRequestController.ts +++ b/packages/uma/src/controller/PolicyRequestController.ts @@ -1,7 +1,7 @@ +import { Store } from 'n3'; import { UCRulesStorage } from "../ucp/storage/UCRulesStorage"; import { BaseController } from "./BaseController"; import { - deletePolicy, getPolicies, getPolicy, patchPolicy, @@ -18,10 +18,17 @@ export class PolicyController extends BaseController { super( store, postPolicy, - deletePolicy, + null as any, getPolicies, getPolicy, patchPolicy, ); + this.sanitizeDelete = this.deletePolicy.bind(this); + } + + // TODO: quick ugly workaround for delete not removing constraints + protected async deletePolicy(store: Store, policyID: string, resourceOwner: string): Promise { + const policy = await getPolicy(store, policyID, resourceOwner); + store.removeQuads([ ...policy ]); } } diff --git a/packages/uma/src/util/routeSpecific/delete.ts b/packages/uma/src/util/routeSpecific/delete.ts deleted file mode 100644 index ec78f13b..00000000 --- a/packages/uma/src/util/routeSpecific/delete.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Store } from "n3" -import {queryEngine} from './index'; - -/** - * Executes a DELETE query against the given store. - * - * @param store store containing the data to be modified - * @param query DELETE query string to be executed - * @returns a promise resolving when the deletion is completed - */ -const executeDelete = async ( - store: Store, - query: string, -): Promise => { - await queryEngine.queryVoid(query, { sources: [ store ] }); -} - -/** - * Build a query that deletes a policy and its associated permissions. - * - * The deletion only occurs if the policy has the given `policyID` and is assigned - * to the provided `clientID`. - * - * @param policyID ID of the policy to delete - * @param resourceOwner ID of the resource owner (assigner) who owns the policy - * @returns a DELETE query string - */ -const buildPolicyDeletionQuery = (policyID: string, resourceOwner: string) => ` - PREFIX odrl: - - DELETE { - ?policy ?policyPredicate ?policyObject . - ?permission ?permissionPredicate ?permissionObject . - } WHERE { - ?policy odrl:permission ?permission ; - odrl:uid <${policyID}> . - ?permission odrl:assigner <${resourceOwner}> . - - { - ?policy a odrl:Agreement . - } UNION { - ?policy a odrl:Set . - } - - ?policy ?policyPredicate ?policyObject . - ?permission ?permissionPredicate ?permissionObject . - } -`; - -/** - * Delete a policy (including its associated permissions) from the store. - * - * @param store store containing the policies - * @param policyID ID of the policy to delete - * @param resourceOwner ID of the resource owner (assigner) responsible for the policy - * @returns a promise resolving when deletion is completed - */ -export const deletePolicy = (store: Store, policyID: string, resourceOwner: string) => - executeDelete(store, buildPolicyDeletionQuery(policyID, resourceOwner)); diff --git a/packages/uma/src/util/routeSpecific/index.ts b/packages/uma/src/util/routeSpecific/index.ts index c998bedb..c800e1f7 100644 --- a/packages/uma/src/util/routeSpecific/index.ts +++ b/packages/uma/src/util/routeSpecific/index.ts @@ -1,5 +1,4 @@ export * from './post'; -export * from './delete'; export * from './get'; export * from './patch'; diff --git a/test/integration/Policies.test.ts b/test/integration/Policies.test.ts index 4a045f36..05c4076d 100644 --- a/test/integration/Policies.test.ts +++ b/test/integration/Policies.test.ts @@ -1,3 +1,4 @@ +import 'jest-rdf'; import { App } from '@solid/community-server'; import { ODRL } from '@solidlab/uma'; import { setGlobalLoggerFactory, WinstonLoggerFactory } from 'global-logger-factory'; @@ -151,6 +152,57 @@ describe('A policy server setup', (): void => { expect(response.status).toBe(204); }); + it('can replace policies using PUT.', async(): Promise => { + const policyA = ` + @prefix ex: . + @prefix odrl: . + + ex:purposePolicy a odrl:Agreement ; + odrl:uid ex:purposePolicy ; + odrl:permission ex:purposePermission . + ex:purposePermission a odrl:Permission ; + odrl:action odrl:read ; + odrl:target ex:target ; + odrl:assignee ex:assignee ; + odrl:assigner <${webIds.a}> ; + odrl:constraint ex:constraint1 . + ex:constraint1 a odrl:Constraint ; + odrl:leftOperand odrl:purpose ; + odrl:operator odrl:eq ; + odrl:rightOperand ex:purpose1 . + `; + // There was an issue where constraints were not fully removed when replacing policies with PUT + let response = await fetchPolicy('PUT', webIds.a, 'http://example.org/purposePolicy', policyA); + expect(response.status).toBe(204); + + const policyB = ` + @prefix ex: . + @prefix odrl: . + + ex:purposePolicy a odrl:Agreement ; + odrl:uid ex:purposePolicy ; + odrl:permission ex:purposePermission . + ex:purposePermission a odrl:Permission ; + odrl:action odrl:read ; + odrl:target ex:target ; + odrl:assignee ex:assignee ; + odrl:assigner <${webIds.a}> ; + odrl:constraint ex:constraint1 . + ex:constraint1 a odrl:Constraint ; + odrl:leftOperand odrl:purpose ; + odrl:operator odrl:eq ; + odrl:rightOperand ex:purpose3 . + `; + response = await fetchPolicy('PUT', webIds.a, 'http://example.org/purposePolicy', policyB); + expect(response.status).toBe(204); + + response = await fetchPolicy('GET', webIds.a, 'http://example.org/purposePolicy'); + expect(response.status).toBe(200); + const getResponse = await response.text(); + const parser = new Parser(); + expect(parser.parse(policyB)).toBeRdfIsomorphic(parser.parse(getResponse)); + }); + it('can show a user their policies.', async(): Promise => { let response = await fetch(policyEndpoint); expect(response.status).toBe(401); @@ -160,6 +212,7 @@ describe('A policy server setup', (): void => { let store = new Store(new Parser().parse(await response.text())); let policies = store.getSubjects(ODRL.terms.uid, null, null); expect(policies.map((term) => term.value).sort()).toEqual([ + 'http://example.org/purposePolicy', 'http://example.org/usagePolicy1', 'http://example.org/usagePolicy1a', 'urn:uuid:95efe0e8-4fb7-496d-8f3c-4d78c97829bc',