diff --git a/CHANGELOG.md b/CHANGELOG.md index 9856cc893c..d009921491 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ The following changes are pending, and will be applied on the next major release ### Patch changes +- Fixed #2339: Unnamed policies are now returned by `getResourcePolicyAll` if an optional argument + `{ acceptBlankNodes: true }` is specified. This additional argument makes this a non-breaking change, + as the current type signature isn't changed. - `getThing` now supports Blank Node identifiers in addition to IRIs and skolems to refer to a subject. - `getThingAll(dataset, { allowacceptBlankNodes: true })` now returns all Blank Nodes subjects in the Dataset, in particular including those part of a single chain of diff --git a/src/acp/control.internal.ts b/src/acp/control.internal.ts index 8ff9f5b266..7ffa24df7c 100644 --- a/src/acp/control.internal.ts +++ b/src/acp/control.internal.ts @@ -24,10 +24,16 @@ import type { ThingPersisted, Url, UrlString } from "../interfaces"; import { getSourceUrl } from "../resource/resource"; import { internal_cloneResource } from "../resource/resource.internal"; import { addIri } from "../thing/add"; -import { getIriAll } from "../thing/get"; +import { getIriAll, getUrlAll } from "../thing/get"; import { removeAll, removeIri } from "../thing/remove"; import { setIri } from "../thing/set"; -import { createThing, getThing, getThingAll, setThing } from "../thing/thing"; +import { + asIri, + createThing, + getThing, + getThingAll, + setThing, +} from "../thing/thing"; import type { WithAccessibleAcr, WithAcp } from "./acp"; import { hasAccessibleAcr } from "./acp"; import type { AccessControlResource, Control } from "./control"; @@ -156,7 +162,20 @@ export function internal_setControl( control: Control, ): ResourceExt { const acr = internal_getAcr(withAccessControlResource); - const updatedAcr = setThing(acr, control); + let updatedAcr = setThing(acr, control); + const acrSubj = getThing(updatedAcr, getSourceUrl(acr)); + // If the ACR has an anchor node, link the Access Control. + if ( + acrSubj !== null && + getUrlAll(acrSubj, acp.accessControl).every( + (object) => object.toString() !== asIri(control, getSourceUrl(acr)), + ) + ) { + updatedAcr = setThing( + updatedAcr, + addIri(acrSubj, acp.accessControl, asIri(control, getSourceUrl(acr))), + ); + } const updatedResource = internal_setAcr( withAccessControlResource, updatedAcr, diff --git a/src/acp/control.test.ts b/src/acp/control.test.ts index 3f55701c3c..7c2abaceab 100644 --- a/src/acp/control.test.ts +++ b/src/acp/control.test.ts @@ -43,8 +43,10 @@ import { } from "./control"; import { internal_createControl, + internal_getAcr, internal_getControl, internal_getControlAll, + internal_setAcr, internal_setControl, } from "./control.internal"; import { acp, rdf } from "../constants"; @@ -52,9 +54,11 @@ import type { WithServerResourceInfo } from "../interfaces"; import { getIri, getUrl, getUrlAll } from "../thing/get"; import { asIri, + asUrl, createThing, getThing, getThingAll, + removeThing, setThing, } from "../thing/thing"; import { addMockAcrTo, mockAcrFor } from "./mock"; @@ -407,10 +411,31 @@ describe("addAcrPolicyUrl", () => { mockSolidDatasetFrom("https://some.pod/resource"), accessControlResource, ); + const inputControlsBefore = getThingAll(accessControlResource); addAcrPolicyUrl(resourceWithAcr, "https://some.pod/policy-resource#policy"); + const inputControlsAfter = getThingAll(accessControlResource); - expect(getThingAll(accessControlResource)).toHaveLength(0); + expect(inputControlsAfter).toEqual(inputControlsBefore); + }); + + it("creates the acr subject", () => { + const accessControlResource = mockAcrFor("https://some.pod/resource"); + const inputControlsBefore = getThingAll(accessControlResource); + const resourceWithAcr = addMockAcrTo( + mockSolidDatasetFrom("https://some.pod/resource"), + accessControlResource, + ); + + const acr = internal_getAcr(resourceWithAcr); + const acrUrl = getSourceUrl(acr); + const updatedAcr = removeThing(acr, acrUrl); + const updatedResource = internal_setAcr(resourceWithAcr, updatedAcr); + + addAcrPolicyUrl(updatedResource, "https://some.pod/policy-resource#policy"); + + const inputControlsAfter = getThingAll(accessControlResource); + expect(inputControlsAfter).toEqual(inputControlsBefore); }); }); @@ -463,6 +488,7 @@ describe("addMemberAcrPolicyUrl", () => { it("does not modify the input ACR", () => { const accessControlResource = mockAcrFor("https://some.pod/resource"); + const inputControlsBefore = getThingAll(accessControlResource); const resourceWithAcr = addMockAcrTo( mockSolidDatasetFrom("https://some.pod/resource"), accessControlResource, @@ -473,7 +499,30 @@ describe("addMemberAcrPolicyUrl", () => { "https://some.pod/policy-resource#policy", ); - expect(getThingAll(accessControlResource)).toHaveLength(0); + const inputControlsAfter = getThingAll(accessControlResource); + expect(inputControlsAfter).toEqual(inputControlsBefore); + }); + + it("creates the acr subject", () => { + const accessControlResource = mockAcrFor("https://some.pod/resource"); + const inputControlsBefore = getThingAll(accessControlResource); + const resourceWithAcr = addMockAcrTo( + mockSolidDatasetFrom("https://some.pod/resource"), + accessControlResource, + ); + + const acr = internal_getAcr(resourceWithAcr); + const acrUrl = getSourceUrl(acr); + const updatedAcr = removeThing(acr, acrUrl); + const updatedResource = internal_setAcr(resourceWithAcr, updatedAcr); + + addMemberAcrPolicyUrl( + updatedResource, + "https://some.pod/policy-resource#policy", + ); + + const inputControlsAfter = getThingAll(accessControlResource); + expect(inputControlsAfter).toEqual(inputControlsBefore); }); }); @@ -523,6 +572,24 @@ describe("getAcrPolicyUrlAll", () => { expect(policyUrls).toEqual([]); }); + + it("does not return policies if acr does not have an anchor node", () => { + const accessControlResource = mockAcrFor("https://some.pod/resource"); + const existingControl = addUrl( + createThing({ url: getSourceUrl(accessControlResource) }), + acp.accessMembers, + "https://some.pod/policy-resource#policy", + ); + const resourceWithAcr = addMockAcrTo( + mockSolidDatasetFrom("https://some.pod/resource"), + setThing(accessControlResource, existingControl), + ); + const acr = internal_getAcr(resourceWithAcr); + const acrUrl = getSourceUrl(acr); + const updatedAcr = removeThing(acr, acrUrl); + const updatedResource = internal_setAcr(resourceWithAcr, updatedAcr); + expect(getAcrPolicyUrlAll(updatedResource)).toHaveLength(0); + }); }); describe("getMemberAcrPolicyUrlAll", () => { @@ -571,6 +638,24 @@ describe("getMemberAcrPolicyUrlAll", () => { expect(policyUrls).toEqual([]); }); + + it("does not return policies if acr does not have an anchor node", () => { + const accessControlResource = mockAcrFor("https://some.pod/resource"); + const existingControl = addUrl( + createThing({ url: getSourceUrl(accessControlResource) }), + acp.accessMembers, + "https://some.pod/policy-resource#policy", + ); + const resourceWithAcr = addMockAcrTo( + mockSolidDatasetFrom("https://some.pod/resource"), + setThing(accessControlResource, existingControl), + ); + const acr = internal_getAcr(resourceWithAcr); + const acrUrl = getSourceUrl(acr); + const updatedAcr = removeThing(acr, acrUrl); + const updatedResource = internal_setAcr(resourceWithAcr, updatedAcr); + expect(getMemberAcrPolicyUrlAll(updatedResource)).toHaveLength(0); + }); }); describe("removeAcrPolicyUrl", () => { @@ -701,6 +786,29 @@ describe("removeAcrPolicyUrl", () => { "https://some.pod/policy-resource#policy", ); }); + + it("returns the resource unchanged if acr does not have an anchor node", () => { + const accessControlResource = mockAcrFor("https://some.pod/resource"); + const existingControl = addUrl( + createThing({ url: getSourceUrl(accessControlResource) }), + acp.accessMembers, + "https://some.pod/policy-resource#policy", + ); + const resourceWithAcr = addMockAcrTo( + mockSolidDatasetFrom("https://some.pod/resource"), + setThing(accessControlResource, existingControl), + ); + const acr = internal_getAcr(resourceWithAcr); + const acrUrl = getSourceUrl(acr); + const updatedAcr = removeThing(acr, acrUrl); + const updatedResource = internal_setAcr(resourceWithAcr, updatedAcr); + expect( + removeAcrPolicyUrl( + updatedResource, + "https://some.pod/policy-resource#policy", + ), + ).toEqual(updatedResource); + }); }); describe("removeMemberAcrPolicyUrl", () => { @@ -831,6 +939,33 @@ describe("removeMemberAcrPolicyUrl", () => { "https://some.pod/policy-resource#policy", ); }); + + it("returns the resource unchanged if acr does not have an anchor node", () => { + let accessControlResource = mockAcrFor("https://some.pod/resource"); + let existingControl = createThing({ + url: getSourceUrl(accessControlResource), + }); + existingControl = addUrl( + existingControl, + acp.accessMembers, + "https://some.pod/policy-resource#policy", + ); + accessControlResource = setThing(accessControlResource, existingControl); + const resourceWithAcr = addMockAcrTo( + mockSolidDatasetFrom("https://some.pod/resource"), + accessControlResource, + ); + const acr = internal_getAcr(resourceWithAcr); + const acrUrl = getSourceUrl(acr); + const updatedAcr = removeThing(acr, acrUrl); + const updatedResource = internal_setAcr(resourceWithAcr, updatedAcr); + expect( + removeMemberAcrPolicyUrl( + updatedResource, + "https://some.pod/policy-resource#policy", + ), + ).toEqual(updatedResource); + }); }); describe("removeAcrPolicyUrlAll", () => { @@ -925,6 +1060,24 @@ describe("removeAcrPolicyUrlAll", () => { "https://some.pod/policy-resource#policy", ); }); + + it("returns the resource unchanged if acr does not have an anchor node", () => { + const accessControlResource = mockAcrFor("https://some.pod/resource"); + const existingControl = addUrl( + createThing({ url: getSourceUrl(accessControlResource) }), + acp.accessMembers, + "https://some.pod/policy-resource#policy", + ); + const resourceWithAcr = addMockAcrTo( + mockSolidDatasetFrom("https://some.pod/resource"), + setThing(accessControlResource, existingControl), + ); + const acr = internal_getAcr(resourceWithAcr); + const acrUrl = getSourceUrl(acr); + const updatedAcr = removeThing(acr, acrUrl); + const updatedResource = internal_setAcr(resourceWithAcr, updatedAcr); + expect(removeAcrPolicyUrlAll(updatedResource)).toEqual(updatedResource); + }); }); describe("removeMemberAcrPolicyUrlAll", () => { @@ -1019,6 +1172,26 @@ describe("removeMemberAcrPolicyUrlAll", () => { "https://some.pod/policy-resource#policy", ); }); + + it("returns the resource unchanged if acr does not have an anchor node", () => { + const accessControlResource = mockAcrFor("https://some.pod/resource"); + const existingControl = addUrl( + createThing({ url: getSourceUrl(accessControlResource) }), + acp.accessMembers, + "https://some.pod/policy-resource#policy", + ); + const resourceWithAcr = addMockAcrTo( + mockSolidDatasetFrom("https://some.pod/resource"), + setThing(accessControlResource, existingControl), + ); + const acr = internal_getAcr(resourceWithAcr); + const acrUrl = getSourceUrl(acr); + const updatedAcr = removeThing(acr, acrUrl); + const updatedResource = internal_setAcr(resourceWithAcr, updatedAcr); + expect(removeMemberAcrPolicyUrlAll(updatedResource)).toEqual( + updatedResource, + ); + }); }); describe("addPolicyUrl", () => { @@ -1028,15 +1201,24 @@ describe("addPolicyUrl", () => { mockSolidDatasetFrom("https://some.pod/resource"), accessControlResource, ); - + const oldControls = getThingAll(resourceWithAcr.internal_acp.acr); const updatedResourceWithAcr = addPolicyUrl( resourceWithAcr, "https://some.pod/policy-resource#policy", ); - const controls = getThingAll(updatedResourceWithAcr.internal_acp.acr); - expect(controls).toHaveLength(1); - expect(getUrl(controls[0], acp.apply)).toBe( + const updatedControls = getThingAll( + updatedResourceWithAcr.internal_acp.acr, + ); + + const difference = updatedControls.filter((control) => { + return !oldControls.some((oldControl) => { + return asUrl(control) === asUrl(oldControl); + }); + })[0]; + + expect(updatedControls).toHaveLength(oldControls.length + 1); + expect(getUrl(difference, acp.apply)).toBe( "https://some.pod/policy-resource#policy", ); }); @@ -1074,15 +1256,16 @@ describe("addPolicyUrl", () => { it("does not modify the input ACR", () => { const accessControlResource = mockAcrFor("https://some.pod/resource"); + const inputControlsBefore = getThingAll(accessControlResource); + const resourceWithAcr = addMockAcrTo( mockSolidDatasetFrom("https://some.pod/resource"), accessControlResource, ); - addPolicyUrl(resourceWithAcr, "https://some.pod/policy-resource#policy"); - const oldControls = getThingAll(accessControlResource); - expect(oldControls).toHaveLength(0); + const inputControlsAfter = getThingAll(accessControlResource); + expect(inputControlsAfter).toEqual(inputControlsBefore); }); }); @@ -1093,15 +1276,24 @@ describe("addMemberPolicyUrl", () => { mockSolidDatasetFrom("https://some.pod/resource"), accessControlResource, ); - + const oldControls = getThingAll(resourceWithAcr.internal_acp.acr); const updatedResourceWithAcr = addMemberPolicyUrl( resourceWithAcr, "https://some.pod/policy-resource#policy", ); - const controls = getThingAll(updatedResourceWithAcr.internal_acp.acr); - expect(controls).toHaveLength(1); - expect(getUrl(controls[0], acp.applyMembers)).toBe( + const updatedControls = getThingAll( + updatedResourceWithAcr.internal_acp.acr, + ); + + const difference = updatedControls.filter((control) => { + return !oldControls.some((oldControl) => { + return asUrl(control) === asUrl(oldControl); + }); + })[0]; + + expect(updatedControls).toHaveLength(oldControls.length + 1); + expect(getUrl(difference, acp.applyMembers)).toBe( "https://some.pod/policy-resource#policy", ); }); @@ -1139,6 +1331,8 @@ describe("addMemberPolicyUrl", () => { it("does not modify the input ACR", () => { const accessControlResource = mockAcrFor("https://some.pod/resource"); + const inputControlsBefore = getThingAll(accessControlResource); + const resourceWithAcr = addMockAcrTo( mockSolidDatasetFrom("https://some.pod/resource"), accessControlResource, @@ -1149,8 +1343,8 @@ describe("addMemberPolicyUrl", () => { "https://some.pod/policy-resource#policy", ); - const oldControls = getThingAll(accessControlResource); - expect(oldControls).toEqual([]); + const inputControlsAfter = getThingAll(accessControlResource); + expect(inputControlsAfter).toEqual(inputControlsBefore); }); }); @@ -1264,6 +1458,24 @@ describe("getMemberPolicyUrlAll", () => { expect(policyUrls).toEqual([]); }); + + it("returns the resource unchanged if acr does not have an anchor node", () => { + const accessControlResource = mockAcrFor("https://some.pod/resource"); + const existingControl = addUrl( + createThing({ url: getSourceUrl(accessControlResource) }), + acp.applyMembers, + "https://some.pod/policy-resource#policy", + ); + const resourceWithAcr = addMockAcrTo( + mockSolidDatasetFrom("https://some.pod/resource"), + setThing(accessControlResource, existingControl), + ); + const acr = internal_getAcr(resourceWithAcr); + const acrUrl = getSourceUrl(acr); + const updatedAcr = removeThing(acr, acrUrl); + const updatedResource = internal_setAcr(resourceWithAcr, updatedAcr); + expect(getMemberPolicyUrlAll(updatedResource)).toHaveLength(0); + }); }); describe("removePolicyUrl", () => { diff --git a/src/acp/control.ts b/src/acp/control.ts index 4d06945541..e82bd45991 100644 --- a/src/acp/control.ts +++ b/src/acp/control.ts @@ -32,7 +32,7 @@ import type { import { hasServerResourceInfo } from "../interfaces"; import { getSourceUrl } from "../resource/resource"; import { addIri } from "../thing/add"; -import { getIriAll } from "../thing/get"; +import { getIriAll, getUrlAll } from "../thing/get"; import { removeAll, removeIri } from "../thing/remove"; import { createThing, getThing, setThing } from "../thing/thing"; import type { WithAccessibleAcr } from "./acp"; @@ -252,6 +252,9 @@ export function removeAcrPolicyUrl( if (acrThing === null) { return resourceWithAcr; } + if (!getIriAll(acrThing, acp.access).includes(policyUrl.toString())) { + return resourceWithAcr; + } const updatedAcrThing = removeIri(acrThing, acp.access, policyUrl); const updatedAcr = setThing(acr, updatedAcrThing); @@ -282,6 +285,9 @@ export function removeMemberAcrPolicyUrl( if (acrThing === null) { return resourceWithAcr; } + if (!getIriAll(acrThing, acp.accessMembers).includes(policyUrl.toString())) { + return resourceWithAcr; + } const updatedAcrThing = removeIri(acrThing, acp.accessMembers, policyUrl); const updatedAcr = setThing(acr, updatedAcrThing); @@ -455,14 +461,10 @@ export function removePolicyUrl( policyUrl: Url | UrlString | ThingPersisted, ): ResourceExt { const controls = internal_getControlAll(resourceWithAcr); - const updatedControls = controls.map((control) => - internal_removePolicyUrl(control, policyUrl), - ); - const updatedResource = updatedControls.reduce( - internal_setControl, - resourceWithAcr, - ); - return updatedResource; + return controls + .filter((control) => getUrlAll(control, acp.apply).length > 0) + .map((control) => internal_removePolicyUrl(control, policyUrl)) + .reduce(internal_setControl, resourceWithAcr); } /** diff --git a/src/acp/mock.ts b/src/acp/mock.ts index a8fe26b9f5..5723bd98d8 100644 --- a/src/acp/mock.ts +++ b/src/acp/mock.ts @@ -23,6 +23,7 @@ import type { UrlString, WithResourceInfo } from "../interfaces"; import { mockSolidDatasetFrom } from "../resource/mock"; import { getSourceUrl } from "../resource/resource"; import { internal_cloneResource } from "../resource/resource.internal"; +import { createThing, setThing } from "../thing/thing"; import type { WithAccessibleAcr } from "./acp"; import type { AccessControlResource } from "./control"; @@ -40,10 +41,17 @@ import type { AccessControlResource } from "./control"; * @returns The mocked empty Access Control Resource for the given Resource. * @since 1.6.0 */ -export function mockAcrFor(resourceUrl: UrlString): AccessControlResource { - const acrUrl = new URL("access-control-resource", resourceUrl).href; +export function mockAcrFor( + resourceUrl: UrlString, + acrUrl?: UrlString, +): AccessControlResource { + const finalAcrUrl = + acrUrl ?? new URL("access-control-resource", resourceUrl).href; const acr: AccessControlResource = { - ...mockSolidDatasetFrom(acrUrl), + ...setThing( + mockSolidDatasetFrom(finalAcrUrl), + createThing({ url: finalAcrUrl }), + ), accessTo: resourceUrl, }; diff --git a/src/acp/policy.test.ts b/src/acp/policy.test.ts index f4482ebb08..0501cb7777 100644 --- a/src/acp/policy.test.ts +++ b/src/acp/policy.test.ts @@ -20,7 +20,8 @@ // import { jest, describe, it, expect } from "@jest/globals"; -import { DataFactory } from "n3"; +import { DataFactory, Parser, Store } from "n3"; +import type DatasetCore from "@rdfjs/dataset/DatasetCore"; import { internal_accessModeIriStrings } from "../acl/acl.internal"; import { rdf, acp } from "../constants"; import { mockSolidDatasetFrom } from "../resource/mock"; @@ -36,6 +37,7 @@ import { createThing, getThing, getThingAll, + removeThing, setThing, } from "../thing/thing"; import { @@ -44,7 +46,7 @@ import { getAcrPolicyUrlAll, getPolicyUrlAll, } from "./control"; -import { internal_getAcr } from "./control.internal"; +import { internal_getAcr, internal_setAcr } from "./control.internal"; import { addMockAcrTo, mockAcrFor } from "./mock"; import { createPolicy, @@ -66,6 +68,8 @@ import { setResourceAcrPolicy, setResourcePolicy, } from "./policy"; +import { fromRdfJsDataset } from "../rdfjs"; +import { SolidClientError } from "../interfaces"; jest.spyOn(globalThis, "fetch").mockImplementation( async () => @@ -449,36 +453,74 @@ describe("getResourcePolicyAll", () => { expect(getResourcePolicyAll(mockedResourceWithAcr)).toHaveLength(1); }); - it("returns only those Things whose type is of acp:AccessPolicy", () => { - let mockedAcr = mockAcrFor("https://some.pod/resource"); - let mockedPolicy = createThing({ - url: `${getSourceUrl(mockedAcr)}#policy`, - }); - mockedPolicy = setUrl(mockedPolicy, rdf.type, acp.Policy); - let notAPolicy = createThing({ - url: "https://some.pod/policy-resource#not-a-policy", - }); - notAPolicy = setUrl( - notAPolicy, - rdf.type, - "https://arbitrary.vocab/not-a-policy", - ); - mockedAcr = setThing(mockedAcr, mockedPolicy); - mockedAcr = setThing(mockedAcr, notAPolicy); - let mockedResourceWithAcr = addMockAcrTo( + // Initially reported in https://github.com/inrupt/solid-client-js/issues/2339 + it("supports Blank Nodes policies if the option is set", async () => { + const acrWithBlankNodes = ` + @prefix acl: . + @prefix acp: . + @base . + + a acp:AccessControlResource; + acp:resource ; + acp:accessControl <#fullOwnerAccess>, <#publicReadAccess>, <#defaultAccessControl>. + <#fullOwnerAccess> a acp:AccessControl; + acp:apply [ + a acp:Policy; + acp:allow acl:Read, acl:Write, acl:Control; + acp:anyOf [ + a acp:Matcher; + acp:agent + ] + ]. + <#publicReadAccess> a acp:AccessControl; + acp:apply [ + a acp:Policy; + acp:allow acl:Read; + acp:anyOf [ + a acp:Matcher; + acp:agent acp:PublicAgent + ] + ]. + <#match-app-friends> a acp:Matcher; + acp:agent , ; + acp:client . + <#defaultAccessControl> acp:apply <#app-friends-policy>. + <#app-friends-policy> a acp:Policy; + acp:allow acl:Read, acl:Write; + acp:allOf <#match-app-friends>. + `; + const mockedAcr = await new Promise((resolve, reject) => { + const parser = new Parser(); + const store = new Store(); + parser.parse(acrWithBlankNodes, (error, quad) => { + if (error) { + reject(error); + } + if (quad) { + store.add(quad); + } else { + resolve(store); + } + }); + }) + .then(fromRdfJsDataset) + .then((dataset) => + getThingAll(dataset, { acceptBlankNodes: true }).reduce( + setThing, + mockAcrFor( + "https://some.pod/resource", + "https://MY-SITE/test/test-acr-1/res1.ttl.acr", + ), + ), + ); + + const mockedResourceWithAcr = addMockAcrTo( mockSolidDatasetFrom("https://some.pod/resource"), mockedAcr, ); - mockedResourceWithAcr = addPolicyUrl( - mockedResourceWithAcr, - `${getSourceUrl(mockedAcr)}#policy`, - ); - mockedResourceWithAcr = addPolicyUrl( - mockedResourceWithAcr, - "https://some.pod/policy-resource#not-a-policy", - ); - - expect(getResourcePolicyAll(mockedResourceWithAcr)).toHaveLength(1); + expect( + getResourcePolicyAll(mockedResourceWithAcr, { acceptBlankNodes: true }), + ).toHaveLength(3); }); it("returns only those Things that apply to the given Resource", () => { @@ -1101,6 +1143,26 @@ describe("removeResourceAcrPolicy", () => { expect(getResourceAcrPolicyAll(updatedPolicyDataset)).toHaveLength(1); }); + + it("errors if the acr does not have an anchor node matching its url", () => { + let mockedAcr = mockAcrFor("https://some.pod/resource"); + let mockedPolicy1 = createThing({ + url: `${getSourceUrl(mockedAcr)}#policy1`, + }); + mockedPolicy1 = setUrl(mockedPolicy1, rdf.type, acp.Policy); + mockedAcr = setThing(mockedAcr, mockedPolicy1); + const mockedResourceWithAcr = addMockAcrTo( + mockSolidDatasetFrom("https://some.pod/resource"), + mockedAcr, + ); + const acr = internal_getAcr(mockedResourceWithAcr); + const acrUrl = getSourceUrl(acr); + const updatedAcr = removeThing(acr, acrUrl); + const updatedResource = internal_setAcr(mockedResourceWithAcr, updatedAcr); + expect(() => getResourcePolicyAll(updatedResource)).toThrow( + SolidClientError, + ); + }); }); describe("setAllowModes", () => { diff --git a/src/acp/policy.ts b/src/acp/policy.ts index 952cf5c3fb..0072fc2a9e 100644 --- a/src/acp/policy.ts +++ b/src/acp/policy.ts @@ -19,19 +19,22 @@ // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // +import type { Quad_Object } from "@rdfjs/types"; import { internal_accessModeIriStrings } from "../acl/acl.internal"; import { acp, rdf } from "../constants"; import { internal_isValidUrl, isNamedNode } from "../datatypes"; -import type { - SolidDataset, - ThingPersisted, - Url, - UrlString, +import { + SolidClientError, + type SolidDataset, + type Thing, + type ThingPersisted, + type Url, + type UrlString, } from "../interfaces"; import { internal_toIriString } from "../interfaces.internal"; import { getSourceUrl } from "../resource/resource"; import { addIri } from "../thing/add"; -import { getIriAll } from "../thing/get"; +import { getIriAll, getTermAll } from "../thing/get"; import { removeAll } from "../thing/remove"; import { setUrl } from "../thing/set"; import { @@ -53,6 +56,7 @@ import { removePolicyUrl, } from "./control"; import { internal_getAcr, internal_setAcr } from "./control.internal"; +import type { BlankNodeId } from "../rdf.internal"; /** * A Policy can be applied to Resources to grant or deny [[AccessModes]] to users who match the Policy's [[Rule]]s. @@ -64,6 +68,11 @@ export type Policy = ThingPersisted; * @since 1.6.0 */ export type ResourcePolicy = ThingPersisted; +/** + * A Resource Policy is like a regular [[Policy]], but rather than being re-used for different Resources, it is used for a single Resource and is stored in that Resource's Access Control Resource. + * @since 1.6.0 + */ +export type AnonymousResourcePolicy = Thing & { url: BlankNodeId }; /** * The different Access Modes that a [[Policy]] can allow or deny for a Resource. * @since 1.6.0 @@ -421,14 +430,65 @@ export function getResourceAcrPolicy( */ export function getResourcePolicyAll( resourceWithAcr: WithAccessibleAcr, -): ResourcePolicy[] { +): ResourcePolicy[]; +export function getResourcePolicyAll( + resourceWithAcr: WithAccessibleAcr, + options: { acceptBlankNodes: true }, +): (ResourcePolicy | AnonymousResourcePolicy)[]; +export function getResourcePolicyAll( + resourceWithAcr: WithAccessibleAcr, + options: { acceptBlankNodes: false }, +): ResourcePolicy[]; +export function getResourcePolicyAll( + resourceWithAcr: WithAccessibleAcr, + options: { acceptBlankNodes: boolean }, +): (ResourcePolicy | AnonymousResourcePolicy)[]; +export function getResourcePolicyAll( + resourceWithAcr: WithAccessibleAcr, + options: { acceptBlankNodes: boolean } = { acceptBlankNodes: false }, +): (ResourcePolicy | AnonymousResourcePolicy)[] { const acr = internal_getAcr(resourceWithAcr); - const policyUrls = getPolicyUrlAll(resourceWithAcr); - const foundThings = policyUrls.map((policyUrl) => getThing(acr, policyUrl)); - const foundPolicies = foundThings.filter( - (thing) => thing !== null && isPolicy(thing), - ) as ResourcePolicy[]; - return foundPolicies; + const acrUrl = getSourceUrl(acr); + const acrSubj = getThing(acr, acrUrl); + if (acrSubj === null) { + throw new SolidClientError( + `The provided ACR graph does not have an anchor node matching its URL ${acrUrl}`, + ); + } + // Follow the links from acr -acp:accessControl> Acccess Control -acp:apply> Policy. + return ( + // List all candidate Access Controls + getTermAll(acrSubj, acp.accessControl) + // For each candidate, check whether it is a subject with associated triples. + .map((accessControlId) => getThing(acr, accessControlId.value)) + // Eliminate non-subject Access Control Candidates + .filter( + (accessControlSubject): accessControlSubject is ThingPersisted => + accessControlSubject !== null, + ) + // For all subject Access Control candidates, list all the policies they apply. + .reduce((policies, accessControlSubj) => { + const accessControlPolicies = getTermAll( + accessControlSubj, + acp.apply, + ).filter( + // If the option is set, all candidate policies are acceptable. + (policy) => + options.acceptBlankNodes ? true : policy.termType === "NamedNode", + ); + return [...policies, ...accessControlPolicies]; + }, [] as Quad_Object[]) + // Get all the triples for the found policies subjects. + .map((policyId) => { + if (policyId.termType === "BlankNode") { + // policyId.value removes the _: prefix, + // which we rely on for matching. + return getThing(acr, `_:${policyId.value}`); + } + return getThing(acr, policyId.value); + }) + .filter((thing): thing is Thing => thing !== null) + ); } /**