UNPKG

@inrupt/solid-client

Version:
1,590 lines (1,457 loc) • 78.8 kB
/** * Copyright 2020 Inrupt Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal in * the Software without restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the * Software, and to permit persons to whom the Software is furnished to do so, * subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { describe, it, expect } from "@jest/globals"; import { dataset } from "@rdfjs/dataset"; import { DataFactory } from "n3"; import { getAgentResourceAccess, getAgentResourceAccessAll, getAgentDefaultAccess, getAgentDefaultAccessAll, setAgentResourceAccess, getAgentAccess, getAgentAccessAll, setAgentDefaultAccess, } from "./agent"; import { SolidDataset, IriString, WithServerResourceInfo } from "../interfaces"; import { getThingAll } from "../thing/thing"; import { getIri, getIriAll } from "../thing/get"; import { getLocalNode } from "../datatypes"; import { AclDataset, WithAcl } from "./acl"; import { addMockAclRuleQuads } from "./mock.internal"; import { internal_setAcl } from "./acl.internal"; import { getMatchingQuads } from "../rdfjs.test"; function addAclDatasetToSolidDataset( solidDataset: SolidDataset & WithServerResourceInfo, aclDataset: AclDataset, type: "resource" | "fallback" ): SolidDataset & WithServerResourceInfo & WithAcl { const acl: WithAcl["internal_acl"] = { fallbackAcl: null, resourceAcl: null, ...(((solidDataset as any) as WithAcl).internal_acl ?? {}), }; if (type === "resource") { solidDataset.internal_resourceInfo.aclUrl = aclDataset.internal_resourceInfo.sourceIri; aclDataset.internal_accessTo = solidDataset.internal_resourceInfo.sourceIri; acl.resourceAcl = aclDataset; } else if (type === "fallback") { acl.fallbackAcl = aclDataset; } return internal_setAcl(solidDataset, acl); } function getMockDataset( sourceIri: IriString ): SolidDataset & WithServerResourceInfo { return Object.assign(dataset(), { internal_resourceInfo: { sourceIri: sourceIri, isRawData: false, linkedResources: {}, }, }); } describe("getGroupAccess", () => { it("returns the Resource's own applicable ACL rules", () => { const solidDataset = getMockDataset("https://some.pod/container/resource"); const resourceAcl = addMockAclRuleQuads( getMockDataset("https://some.pod/container/resource.acl"), "https://some.pod/profileDoc#webId", "https://some.pod/container/resource", { read: false, append: false, write: false, control: true }, "resource" ); const solidDatasetWithAcl = addAclDatasetToSolidDataset( solidDataset, resourceAcl, "resource" ); const access = getAgentAccess( solidDatasetWithAcl, "https://some.pod/profileDoc#webId" ); expect(access).toEqual({ read: false, append: false, write: false, control: true, }); }); it("returns the fallback ACL rules if no Resource ACL SolidDataset is available", () => { const solidDataset = getMockDataset("https://some.pod/container/resource"); const fallbackAcl = addMockAclRuleQuads( getMockDataset("https://some.pod/container/.acl"), "https://some.pod/profileDoc#webId", "https://some.pod/container/", { read: false, append: false, write: false, control: true }, "default" ); const solidDatasetWithAcl = addAclDatasetToSolidDataset( solidDataset, fallbackAcl, "fallback" ); const access = getAgentAccess( solidDatasetWithAcl, "https://some.pod/profileDoc#webId" ); expect(access).toEqual({ read: false, append: false, write: false, control: true, }); }); it("returns null if neither the Resource's own nor a fallback ACL was accessible", () => { const solidDataset = getMockDataset("https://some.pod/container/resource"); const solidDatasetWithInaccessibleAcl = internal_setAcl(solidDataset, { fallbackAcl: null, resourceAcl: null, }); expect( getAgentAccess( solidDatasetWithInaccessibleAcl, "https://arbitrary.pod/profileDoc#webId" ) ).toBeNull(); }); it("ignores the fallback ACL rules if a Resource ACL SolidDataset is available", () => { const solidDataset = getMockDataset("https://some.pod/container/resource"); const resourceAcl = addMockAclRuleQuads( getMockDataset("https://some.pod/container/resource.acl"), "https://some.pod/profileDoc#webId", "https://some.pod/container/resource", { read: true, append: false, write: false, control: false }, "resource" ); const fallbackAcl = addMockAclRuleQuads( getMockDataset("https://some.pod/container/.acl"), "https://some.pod/profileDoc#webId", "https://some.pod/container/", { read: false, append: false, write: false, control: true }, "default" ); const solidDatasetWithJustResourceAcl = addAclDatasetToSolidDataset( solidDataset, resourceAcl, "resource" ); const solidDatasetWithAcl = addAclDatasetToSolidDataset( solidDatasetWithJustResourceAcl, fallbackAcl, "fallback" ); const access = getAgentAccess( solidDatasetWithAcl, "https://some.pod/profileDoc#webId" ); expect(access).toEqual({ read: true, append: false, write: false, control: false, }); }); it("ignores default ACL rules from the Resource's own ACL SolidDataset", () => { const solidDataset = getMockDataset("https://some.pod/container/"); const resourceAcl = addMockAclRuleQuads( getMockDataset("https://some.pod/container/.acl"), "https://some.pod/profileDoc#webId", "https://some.pod/container/", { read: true, append: false, write: false, control: false }, "resource" ); const resourceAclWithDefaultRules = addMockAclRuleQuads( resourceAcl, "https://some.pod/profileDoc#webId", "https://some.pod/container/", { read: false, append: false, write: false, control: true }, "default" ); const solidDatasetWithAcl = addAclDatasetToSolidDataset( solidDataset, resourceAclWithDefaultRules, "resource" ); const access = getAgentAccess( solidDatasetWithAcl, "https://some.pod/profileDoc#webId" ); expect(access).toEqual({ read: true, append: false, write: false, control: false, }); }); it("ignores Resource ACL rules from the fallback ACL SolidDataset", () => { const solidDataset = getMockDataset("https://some.pod/container/resource"); const fallbackAcl = addMockAclRuleQuads( getMockDataset("https://some.pod/container/.acl"), "https://some.pod/profileDoc#webId", "https://some.pod/container/", { read: true, append: false, write: false, control: false }, "resource" ); const fallbackAclWithDefaultRules = addMockAclRuleQuads( fallbackAcl, "https://some.pod/profileDoc#webId", "https://some.pod/container/", { read: false, append: false, write: false, control: true }, "default" ); const solidDatasetWithAcl = addAclDatasetToSolidDataset( solidDataset, fallbackAclWithDefaultRules, "fallback" ); const access = getAgentAccess( solidDatasetWithAcl, "https://some.pod/profileDoc#webId" ); expect(access).toEqual({ read: false, append: false, write: false, control: true, }); }); }); describe("getAgentAccessAll", () => { it("returns the Resource's own applicable ACL rules, grouped by Agent", () => { const solidDataset = getMockDataset("https://some.pod/container/resource"); const resourceAcl = addMockAclRuleQuads( getMockDataset("https://some.pod/container/resource.acl"), "https://some.pod/profileDoc#webId", "https://some.pod/container/resource", { read: false, append: false, write: false, control: true }, "resource" ); const solidDatasetWithAcl = addAclDatasetToSolidDataset( solidDataset, resourceAcl, "resource" ); const access = getAgentAccessAll(solidDatasetWithAcl); expect(access).toEqual({ "https://some.pod/profileDoc#webId": { read: false, append: false, write: false, control: true, }, }); }); it("returns the fallback ACL rules if no Resource ACL SolidDataset is available", () => { const solidDataset = getMockDataset("https://some.pod/container/resource"); const fallbackAcl = addMockAclRuleQuads( getMockDataset("https://some.pod/container/.acl"), "https://some.pod/profileDoc#webId", "https://some.pod/container/", { read: false, append: false, write: false, control: true }, "default" ); const solidDatasetWithAcl = addAclDatasetToSolidDataset( solidDataset, fallbackAcl, "fallback" ); const access = getAgentAccessAll(solidDatasetWithAcl); expect(access).toEqual({ "https://some.pod/profileDoc#webId": { read: false, append: false, write: false, control: true, }, }); }); it("returns null if neither the Resource's own nor a fallback ACL was accessible", () => { const solidDataset = getMockDataset("https://some.pod/container/resource"); const solidDatasetWithInaccessibleAcl = internal_setAcl(solidDataset, { fallbackAcl: null, resourceAcl: null, }); expect(getAgentAccessAll(solidDatasetWithInaccessibleAcl)).toBeNull(); }); it("ignores the fallback ACL rules if a Resource ACL SolidDataset is available", () => { const solidDataset = getMockDataset("https://some.pod/container/resource"); const resourceAcl = addMockAclRuleQuads( getMockDataset("https://some.pod/container/resource.acl"), "https://some.pod/profileDoc#webId", "https://some.pod/container/resource", { read: true, append: false, write: false, control: false }, "resource" ); const fallbackAcl = addMockAclRuleQuads( getMockDataset("https://some.pod/container/.acl"), "https://some.pod/profileDoc#webId", "https://some.pod/container/", { read: false, append: false, write: false, control: true }, "default" ); const solidDatasetWithJustResourceAcl = addAclDatasetToSolidDataset( solidDataset, resourceAcl, "resource" ); const solidDatasetWithAcl = addAclDatasetToSolidDataset( solidDatasetWithJustResourceAcl, fallbackAcl, "fallback" ); const access = getAgentAccessAll(solidDatasetWithAcl); expect(access).toEqual({ "https://some.pod/profileDoc#webId": { read: true, append: false, write: false, control: false, }, }); }); it("does not merge fallback ACL rules with a Resource's own ACL rules, if available", () => { const solidDataset = getMockDataset("https://some.pod/container/resource"); const resourceAcl = addMockAclRuleQuads( getMockDataset("https://some.pod/container/resource.acl"), "https://some.pod/profileDoc#webId", "https://some.pod/container/resource", { read: true, append: false, write: false, control: false }, "resource" ); const fallbackAcl = addMockAclRuleQuads( getMockDataset("https://some.pod/container/.acl"), "https://some-other.pod/profileDoc#webId", "https://some.pod/container/", { read: false, append: false, write: false, control: true }, "default" ); const solidDatasetWithJustResourceAcl = addAclDatasetToSolidDataset( solidDataset, resourceAcl, "resource" ); const solidDatasetWithAcl = addAclDatasetToSolidDataset( solidDatasetWithJustResourceAcl, fallbackAcl, "fallback" ); const access = getAgentAccessAll(solidDatasetWithAcl); // It only includes rules for agent "https://some.pod/profileDoc#webId", // not for "https://some-other.pod/profileDoc#webId" expect(access).toEqual({ "https://some.pod/profileDoc#webId": { read: true, append: false, write: false, control: false, }, }); }); it("ignores default ACL rules from the Resource's own ACL SolidDataset", () => { const solidDataset = getMockDataset("https://some.pod/container/"); const resourceAcl = addMockAclRuleQuads( getMockDataset("https://some.pod/container/.acl"), "https://some.pod/profileDoc#webId", "https://some.pod/container/", { read: true, append: false, write: false, control: false }, "resource" ); const resourceAclWithDefaultRules = addMockAclRuleQuads( resourceAcl, "https://some.pod/profileDoc#webId", "https://some.pod/container/", { read: false, append: false, write: false, control: true }, "default" ); const solidDatasetWithAcl = addAclDatasetToSolidDataset( solidDataset, resourceAclWithDefaultRules, "resource" ); const access = getAgentAccessAll(solidDatasetWithAcl); expect(access).toEqual({ "https://some.pod/profileDoc#webId": { read: true, append: false, write: false, control: false, }, }); }); it("ignores Resource ACL rules from the fallback ACL SolidDataset", () => { const solidDataset = getMockDataset("https://some.pod/container/resource"); const fallbackAcl = addMockAclRuleQuads( getMockDataset("https://some.pod/container/.acl"), "https://some.pod/profileDoc#webId", "https://some.pod/container/", { read: true, append: false, write: false, control: false }, "resource" ); const fallbackAclWithDefaultRules = addMockAclRuleQuads( fallbackAcl, "https://some.pod/profileDoc#webId", "https://some.pod/container/", { read: false, append: false, write: false, control: true }, "default" ); const solidDatasetWithAcl = addAclDatasetToSolidDataset( solidDataset, fallbackAclWithDefaultRules, "fallback" ); const access = getAgentAccessAll(solidDatasetWithAcl); expect(access).toEqual({ "https://some.pod/profileDoc#webId": { read: false, append: false, write: false, control: true, }, }); }); }); describe("getAgentResourceAccess", () => { it("returns the applicable Access Modes for a single Agent", () => { const resourceAcl = addMockAclRuleQuads( getMockDataset("https://arbitrary.pod/resource.acl"), "https://some.pod/profileDoc#webId", "https://arbitrary.pod/resource", { read: true, append: false, write: false, control: true }, "resource" ); const agentAccess = getAgentResourceAccess( resourceAcl, "https://some.pod/profileDoc#webId" ); expect(agentAccess).toEqual({ read: true, append: false, write: false, control: true, }); }); it("combines Access Modes defined for a given Agent in separate rules", () => { let resourceAcl = addMockAclRuleQuads( getMockDataset("https://arbitrary.pod/resource.acl"), "https://some.pod/profileDoc#webId", "https://arbitrary.pod/resource", { read: true, append: false, write: false, control: false }, "resource" ); resourceAcl = addMockAclRuleQuads( resourceAcl, "https://some.pod/profileDoc#webId", "https://arbitrary.pod/resource", { read: false, append: true, write: false, control: false }, "resource" ); const agentAccess = getAgentResourceAccess( resourceAcl, "https://some.pod/profileDoc#webId" ); expect(agentAccess).toEqual({ read: true, append: true, write: false, control: false, }); }); it("returns false for all Access Modes if there are no ACL rules for the given Agent", () => { const resourceAcl = addMockAclRuleQuads( getMockDataset("https://arbitrary.pod/resource.acl"), "https://some.pod/profileDoc#webId", "https://arbitrary.pod/resource", { read: true, append: false, write: false, control: false }, "resource" ); const agentAccess = getAgentResourceAccess( resourceAcl, "https://some-other.pod/profileDoc#webId" ); expect(agentAccess).toEqual({ read: false, append: false, write: false, control: false, }); }); it("ignores ACL rules that apply to a different Agent", () => { let resourceAcl = addMockAclRuleQuads( getMockDataset("https://arbitrary.pod/resource.acl"), "https://some-other.pod/profileDoc#webId", "https://arbitrary.pod/resource", { read: true, append: false, write: false, control: false }, "resource" ); resourceAcl = addMockAclRuleQuads( resourceAcl, "https://some.pod/profileDoc#webId", "https://arbitrary.pod/resource", { read: false, append: true, write: false, control: false }, "resource" ); const agentAccess = getAgentResourceAccess( resourceAcl, "https://some.pod/profileDoc#webId" ); expect(agentAccess).toEqual({ read: false, append: true, write: false, control: false, }); }); it("ignores ACL rules that apply to a different Resource", () => { let resourceAcl = addMockAclRuleQuads( getMockDataset("https://some.pod/resource.acl"), "https://arbitrary.pod/profileDoc#webId", "https://some-other.pod/resource", { read: true, append: false, write: false, control: false }, "resource" ); resourceAcl = addMockAclRuleQuads( resourceAcl, "https://arbitrary.pod/profileDoc#webId", "https://some.pod/resource", { read: false, append: true, write: false, control: false }, "resource" ); const agentAccess = getAgentResourceAccess( resourceAcl, "https://arbitrary.pod/profileDoc#webId" ); expect(agentAccess).toEqual({ read: false, append: true, write: false, control: false, }); }); }); describe("getAgentResourceAccessAll", () => { it("returns the applicable Access Modes for all Agents for whom Access Modes have been defined", () => { let resourceAcl = addMockAclRuleQuads( getMockDataset("https://arbitrary.pod/resource.acl"), "https://some-other.pod/profileDoc#webId", "https://arbitrary.pod/resource", { read: true, append: false, write: false, control: false }, "resource" ); resourceAcl = addMockAclRuleQuads( resourceAcl, "https://some.pod/profileDoc#webId", "https://arbitrary.pod/resource", { read: false, append: true, write: false, control: false }, "resource" ); const agentAccess = getAgentResourceAccessAll(resourceAcl); expect(agentAccess).toEqual({ "https://some.pod/profileDoc#webId": { read: false, append: true, write: false, control: false, }, "https://some-other.pod/profileDoc#webId": { read: true, append: false, write: false, control: false, }, }); }); it("combines Access Modes defined for the same Agent in different Rules", () => { let resourceAcl = addMockAclRuleQuads( getMockDataset("https://arbitrary.pod/resource.acl"), "https://some.pod/profileDoc#webId", "https://arbitrary.pod/resource", { read: true, append: false, write: false, control: false }, "resource" ); resourceAcl = addMockAclRuleQuads( resourceAcl, "https://some.pod/profileDoc#webId", "https://arbitrary.pod/resource", { read: false, append: true, write: false, control: false }, "resource" ); const agentAccess = getAgentResourceAccessAll(resourceAcl); expect(agentAccess).toEqual({ "https://some.pod/profileDoc#webId": { read: true, append: true, write: false, control: false, }, }); }); it("returns Access Modes for all Agents even if they are assigned in the same Rule", () => { const resourceAcl = addMockAclRuleQuads( getMockDataset("https://arbitrary.pod/resource.acl"), "https://some.pod/profileDoc#webId", "https://arbitrary.pod/resource", { read: true, append: false, write: false, control: false }, "resource" ); const oneQuad = Array.from(resourceAcl)[0]; resourceAcl.add( DataFactory.quad( oneQuad.subject, DataFactory.namedNode("http://www.w3.org/ns/auth/acl#agent"), DataFactory.namedNode("https://some-other.pod/profileDoc#webId") ) ); const agentAccess = getAgentResourceAccessAll(resourceAcl); expect(agentAccess).toEqual({ "https://some.pod/profileDoc#webId": { read: true, append: false, write: false, control: false, }, "https://some-other.pod/profileDoc#webId": { read: true, append: false, write: false, control: false, }, }); }); it("ignores ACL rules that do not apply to an Agent", () => { const resourceAcl = addMockAclRuleQuads( getMockDataset("https://arbitrary.pod/resource.acl"), "https://some.pod/profileDoc#webId", "https://arbitrary.pod/resource", { read: true, append: false, write: false, control: false }, "resource" ); const agentClassRuleSubjectIri = "#arbitrary-agent-class-rule"; resourceAcl.add( DataFactory.quad( getLocalNode(agentClassRuleSubjectIri), DataFactory.namedNode( "http://www.w3.org/1999/02/22-rdf-syntax-ns#type" ), DataFactory.namedNode("http://www.w3.org/ns/auth/acl#Authorization") ) ); resourceAcl.add( DataFactory.quad( getLocalNode(agentClassRuleSubjectIri), DataFactory.namedNode("http://www.w3.org/ns/auth/acl#accessTo"), DataFactory.namedNode("https://arbitrary.pod/resource") ) ); resourceAcl.add( DataFactory.quad( getLocalNode(agentClassRuleSubjectIri), DataFactory.namedNode("http://www.w3.org/ns/auth/acl#agentClass"), DataFactory.namedNode("http://xmlns.com/foaf/0.1/Agent") ) ); const agentAccess = getAgentResourceAccessAll(resourceAcl); expect(agentAccess).toEqual({ "https://some.pod/profileDoc#webId": { read: true, append: false, write: false, control: false, }, }); }); it("ignores ACL rules that apply to a different Resource", () => { let resourceAcl = addMockAclRuleQuads( getMockDataset("https://some.pod/resource.acl"), "https://arbitrary.pod/profileDoc#webId", "https://some-other.pod/resource", { read: true, append: false, write: false, control: false }, "resource" ); resourceAcl = addMockAclRuleQuads( resourceAcl, "https://some.pod/profileDoc#webId", "https://some.pod/resource", { read: false, append: true, write: false, control: false }, "resource" ); const agentAccess = getAgentResourceAccessAll(resourceAcl); expect(agentAccess).toEqual({ "https://some.pod/profileDoc#webId": { read: false, append: true, write: false, control: false, }, }); }); }); describe("setAgentResourceAccess", () => { it("adds Quads for the appropriate Access Modes", () => { const sourceDataset = Object.assign( getMockDataset("https://arbitrary.pod/resource.acl"), { internal_accessTo: "https://arbitrary.pod/resource" } ); const updatedDataset = setAgentResourceAccess( sourceDataset, "https://some.pod/profileDoc#webId", { read: true, append: true, write: true, control: true, } ); expect(updatedDataset.size).toBe(6); expect( getMatchingQuads(updatedDataset, { predicate: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type", object: "http://www.w3.org/ns/auth/acl#Authorization", }) ).toHaveLength(1); expect( getMatchingQuads(updatedDataset, { predicate: "http://www.w3.org/ns/auth/acl#mode", object: "http://www.w3.org/ns/auth/acl#Read", }) ).toHaveLength(1); expect( getMatchingQuads(updatedDataset, { predicate: "http://www.w3.org/ns/auth/acl#mode", object: "http://www.w3.org/ns/auth/acl#Write", }) ).toHaveLength(1); expect( getMatchingQuads(updatedDataset, { predicate: "http://www.w3.org/ns/auth/acl#mode", object: "http://www.w3.org/ns/auth/acl#Control", }) ).toHaveLength(1); expect( getMatchingQuads(updatedDataset, { predicate: "http://www.w3.org/ns/auth/acl#accessTo", object: "https://arbitrary.pod/resource", }) ).toHaveLength(1); expect( getMatchingQuads(updatedDataset, { predicate: "http://www.w3.org/ns/auth/acl#agent", object: "https://some.pod/profileDoc#webId", }) ).toHaveLength(1); }); it("does not copy over access for an unrelated Agent", async () => { let sourceDataset = addMockAclRuleQuads( getMockDataset("https://arbitrary.pod/resource/?ext=acl"), "https://arbitrary.pod/profileDoc#webId", "https://arbitrary.pod/resource", { read: true, append: true, write: true, control: true }, "resource", "https://arbitrary.pod/resource/?ext=acl#owner" ); sourceDataset = addMockAclRuleQuads( sourceDataset, "https://arbitrary.pod/profileDoc#webId", "https://arbitrary.pod/resource", { read: true, append: true, write: true, control: true }, "default", "https://arbitrary.pod/resource/?ext=acl#owner" ); const updatedDataset = setAgentResourceAccess( sourceDataset, "https://some.pod/profileDoc#webId", { read: true, append: true, write: false, control: false, } ); // Explicitly check that the agent given resource access doesn't get additional privilege getThingAll(updatedDataset).forEach((thing) => { // The agent given resource access should not have default access const expectedNrOfDefaultRules = getIriAll( thing, "http://www.w3.org/ns/auth/acl#agent" ).includes("https://some.pod/profileDoc#webId") ? 0 : 1; expect( getIriAll(thing, "http://www.w3.org/ns/auth/acl#default") ).toHaveLength(expectedNrOfDefaultRules); }); // Roughly check that the ACL dataset is as we expect it expect(updatedDataset.size).toBe(13); }); it("does not copy over access for an unrelated Group, Agent Class or Origin", async () => { let sourceDataset = addMockAclRuleQuads( getMockDataset("https://arbitrary.pod/resource/?ext=acl"), "https://arbitrary.pod/profileDoc#someGroup", "https://arbitrary.pod/resource", { read: true, append: false, write: false, control: false }, "default", "https://arbitrary.pod/resource/?ext=acl#owner", "http://www.w3.org/ns/auth/acl#agentGroup" ); sourceDataset = addMockAclRuleQuads( sourceDataset, "http://xmlns.com/foaf/0.1/Agent", "https://arbitrary.pod/resource", { read: true, append: true, write: false, control: false }, "default", "https://arbitrary.pod/resource/?ext=acl#owner", "http://www.w3.org/ns/auth/acl#agentClass" ); sourceDataset = addMockAclRuleQuads( sourceDataset, "https://arbitrary.pod/profileDoc#webId", "https://arbitrary.pod/resource", { read: true, append: true, write: false, control: false }, "default", "https://arbitrary.pod/resource/?ext=acl#owner", "http://www.w3.org/ns/auth/acl#agent" ); sourceDataset = addMockAclRuleQuads( sourceDataset, "https://arbitrary.app.origin/", "https://arbitrary.pod/resource", { read: true, append: true, write: false, control: false }, "default", "https://arbitrary.pod/resource/?ext=acl#owner", "http://www.w3.org/ns/auth/acl#origin" ); const updatedDataset = setAgentResourceAccess( sourceDataset, "https://arbitrary.pod/profileDoc#webId", { read: true, append: true, write: true, control: true, } ); // Explicitly check that the group ACL is separate from the modified agent ACL getThingAll(updatedDataset).forEach((thing) => { const isAgentGroupRule = getIri(thing, "http://www.w3.org/ns/auth/acl#agentGroup") !== null; const isAgentClassRule = getIri(thing, "http://www.w3.org/ns/auth/acl#agentClass") !== null; const isOriginRule = getIri(thing, "http://www.w3.org/ns/auth/acl#origin") !== null; if (!isAgentGroupRule && !isAgentClassRule && !isOriginRule) { return; } // Any actors other than the specified agent should not have been given resource access: expect( getIri(thing, "http://www.w3.org/ns/auth/acl#accessTo") ).toBeNull(); }); // Roughly check that the ACL dataset is as we expect it expect(updatedDataset.size).toBe(18); }); it("does not alter the input SolidDataset", () => { const sourceDataset = Object.assign( getMockDataset("https://arbitrary.pod/resource.acl"), { internal_accessTo: "https://arbitrary.pod/resource" } ); setAgentResourceAccess(sourceDataset, "https://some.pod/profileDoc#webId", { read: true, append: false, write: false, control: false, }); expect(sourceDataset.size).toBe(0); }); it("keeps a log of changes made to the ACL", () => { const sourceDataset = Object.assign( getMockDataset("https://arbitrary.pod/resource.acl"), { internal_accessTo: "https://arbitrary.pod/resource" } ); const updatedDataset = setAgentResourceAccess( sourceDataset, "https://some.pod/profileDoc#webId", { read: true, append: false, write: false, control: false, } ); const deletedQuads = updatedDataset.internal_changeLog.deletions; expect(deletedQuads).toEqual([]); const addedQuads = updatedDataset.internal_changeLog.additions; expect(addedQuads).toHaveLength(4); expect(addedQuads[0].predicate.value).toBe( "http://www.w3.org/1999/02/22-rdf-syntax-ns#type" ); expect(addedQuads[0].object.value).toBe( "http://www.w3.org/ns/auth/acl#Authorization" ); expect(addedQuads[1].predicate.value).toBe( "http://www.w3.org/ns/auth/acl#mode" ); expect(addedQuads[1].object.value).toBe( "http://www.w3.org/ns/auth/acl#Read" ); expect(addedQuads[2].predicate.value).toBe( "http://www.w3.org/ns/auth/acl#accessTo" ); expect(addedQuads[2].object.value).toBe("https://arbitrary.pod/resource"); expect(addedQuads[3].predicate.value).toBe( "http://www.w3.org/ns/auth/acl#agent" ); expect(addedQuads[3].object.value).toBe( "https://some.pod/profileDoc#webId" ); }); it("does not forget to add a Quad for Append access if Write access is not given", () => { // This test is basically there to test for regressions // if we ever try to be clever about inferring Append access // (but we should be able to leave that to the server). const sourceDataset = Object.assign( getMockDataset("https://arbitrary.pod/resource.acl"), { internal_accessTo: "https://arbitrary.pod/resource" } ); const updatedDataset = setAgentResourceAccess( sourceDataset, "https://some.pod/profileDoc#webId", { read: false, append: true, write: false, control: false, } ); expect(updatedDataset.size).toBe(4); expect( getMatchingQuads(updatedDataset, { predicate: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type", object: "http://www.w3.org/ns/auth/acl#Authorization", }) ).toHaveLength(1); expect( getMatchingQuads(updatedDataset, { predicate: "http://www.w3.org/ns/auth/acl#mode", object: "http://www.w3.org/ns/auth/acl#Append", }) ).toHaveLength(1); expect( getMatchingQuads(updatedDataset, { predicate: "http://www.w3.org/ns/auth/acl#accessTo", object: "https://arbitrary.pod/resource", }) ).toHaveLength(1); expect( getMatchingQuads(updatedDataset, { predicate: "http://www.w3.org/ns/auth/acl#agent", object: "https://some.pod/profileDoc#webId", }) ).toHaveLength(1); }); it("replaces existing Quads defining Access Modes for this agent", () => { const sourceDataset = addMockAclRuleQuads( getMockDataset("https://arbitrary.pod/resource.acl"), "https://some.pod/profileDoc#webId", "https://arbitrary.pod/resource", { read: false, append: false, write: false, control: true }, "resource" ); const updatedDataset = setAgentResourceAccess( sourceDataset, "https://some.pod/profileDoc#webId", { read: true, append: false, write: false, control: false, } ); expect(updatedDataset.size).toBe(4); expect( getMatchingQuads(updatedDataset, { predicate: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type", object: "http://www.w3.org/ns/auth/acl#Authorization", }) ).toHaveLength(1); expect( getMatchingQuads(updatedDataset, { predicate: "http://www.w3.org/ns/auth/acl#mode", object: "http://www.w3.org/ns/auth/acl#Read", }) ).toHaveLength(1); expect( getMatchingQuads(updatedDataset, { predicate: "http://www.w3.org/ns/auth/acl#accessTo", object: "https://arbitrary.pod/resource", }) ).toHaveLength(1); expect( getMatchingQuads(updatedDataset, { predicate: "http://www.w3.org/ns/auth/acl#agent", object: "https://some.pod/profileDoc#webId", }) ).toHaveLength(1); }); it("removes all Quads for an ACL rule if it no longer applies to anything", () => { const sourceDataset = addMockAclRuleQuads( getMockDataset("https://arbitrary.pod/resource.acl"), "https://some.pod/profileDoc#webId", "https://arbitrary.pod/resource", { read: true, append: false, write: false, control: false }, "resource" ); const updatedDataset = setAgentResourceAccess( sourceDataset, "https://some.pod/profileDoc#webId", { read: false, append: false, write: false, control: false, } ); expect(updatedDataset.size).toBe(0); }); it("does not remove ACL rules that apply to the Agent but also act as default rules", () => { const sourceDataset = addMockAclRuleQuads( getMockDataset("https://arbitrary.pod/container/.acl"), "https://some.pod/profileDoc#webId", "https://arbitrary.pod/container/", { read: true, append: false, write: false, control: false }, "resource" ); const aclRuleSubject = Array.from(sourceDataset)[0].subject; sourceDataset.add( DataFactory.quad( aclRuleSubject, DataFactory.namedNode("http://www.w3.org/ns/auth/acl#default"), DataFactory.namedNode("https://arbitrary.pod/container/") ) ); const updatedDataset = setAgentResourceAccess( sourceDataset, "https://some.pod/profileDoc#webId", { read: false, append: false, write: false, control: false, } ); expect(updatedDataset.size).toBe(4); expect( getMatchingQuads(updatedDataset, { predicate: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type", object: "http://www.w3.org/ns/auth/acl#Authorization", }) ).toHaveLength(1); expect( getMatchingQuads(updatedDataset, { predicate: "http://www.w3.org/ns/auth/acl#default", object: "https://arbitrary.pod/container/", }) ).toHaveLength(1); expect( getMatchingQuads(updatedDataset, { predicate: "http://www.w3.org/ns/auth/acl#mode", object: "http://www.w3.org/ns/auth/acl#Read", }) ).toHaveLength(1); expect( getMatchingQuads(updatedDataset, { predicate: "http://www.w3.org/ns/auth/acl#agent", object: "https://some.pod/profileDoc#webId", }) ).toHaveLength(1); }); it("does not remove ACL rules that apply to the Agent but also apply to a different Resource", () => { const sourceDataset = addMockAclRuleQuads( getMockDataset("https://arbitrary.pod/resource.acl"), "https://some.pod/profileDoc#webId", "https://arbitrary.pod/resource", { read: true, append: false, write: false, control: false }, "resource" ); const aclRuleSubject = Array.from(sourceDataset)[0].subject; sourceDataset.add( DataFactory.quad( aclRuleSubject, DataFactory.namedNode("http://www.w3.org/ns/auth/acl#accessTo"), DataFactory.namedNode("https://arbitrary.pod/other-resource") ) ); const updatedDataset = setAgentResourceAccess( sourceDataset, "https://some.pod/profileDoc#webId", { read: false, append: false, write: false, control: false, } ); expect(updatedDataset.size).toBe(4); expect( getMatchingQuads(updatedDataset, { predicate: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type", object: "http://www.w3.org/ns/auth/acl#Authorization", }) ).toHaveLength(1); expect( getMatchingQuads(updatedDataset, { predicate: "http://www.w3.org/ns/auth/acl#accessTo", object: "https://arbitrary.pod/other-resource", }) ).toHaveLength(1); expect( getMatchingQuads(updatedDataset, { predicate: "http://www.w3.org/ns/auth/acl#mode", object: "http://www.w3.org/ns/auth/acl#Read", }) ).toHaveLength(1); expect( getMatchingQuads(updatedDataset, { predicate: "http://www.w3.org/ns/auth/acl#agent", object: "https://some.pod/profileDoc#webId", }) ).toHaveLength(1); }); it("does not remove ACL rules that no longer apply to the given Agent, but still apply to others", () => { const sourceDataset = addMockAclRuleQuads( getMockDataset("https://arbitrary.pod/resource.acl"), "https://some.pod/profileDoc#webId", "https://arbitrary.pod/resource", { read: true, append: false, write: false, control: false }, "resource" ); const aclRuleSubject = Array.from(sourceDataset)[0].subject; sourceDataset.add( DataFactory.quad( aclRuleSubject, DataFactory.namedNode("http://www.w3.org/ns/auth/acl#agent"), DataFactory.namedNode("https://some-other.pod/profileDoc#webId") ) ); const updatedDataset = setAgentResourceAccess( sourceDataset, "https://some.pod/profileDoc#webId", { read: false, append: false, write: false, control: false, } ); expect(updatedDataset.size).toBe(4); expect( getMatchingQuads(updatedDataset, { predicate: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type", object: "http://www.w3.org/ns/auth/acl#Authorization", }) ).toHaveLength(1); expect( getMatchingQuads(updatedDataset, { predicate: "http://www.w3.org/ns/auth/acl#accessTo", object: "https://arbitrary.pod/resource", }) ).toHaveLength(1); expect( getMatchingQuads(updatedDataset, { predicate: "http://www.w3.org/ns/auth/acl#mode", object: "http://www.w3.org/ns/auth/acl#Read", }) ).toHaveLength(1); expect( getMatchingQuads(updatedDataset, { predicate: "http://www.w3.org/ns/auth/acl#agent", object: "https://some-other.pod/profileDoc#webId", }) ).toHaveLength(1); }); it("does not remove ACL rules that no longer apply to the given Agent, but still apply to non-Agents", () => { const sourceDataset = addMockAclRuleQuads( getMockDataset("https://arbitrary.pod/resource.acl"), "https://some.pod/profileDoc#webId", "https://arbitrary.pod/resource", { read: true, append: false, write: false, control: false }, "resource" ); const aclRuleSubject = Array.from(sourceDataset)[0].subject; sourceDataset.add( DataFactory.quad( aclRuleSubject, DataFactory.namedNode("http://www.w3.org/ns/auth/acl#agentClass"), DataFactory.namedNode("http://xmlns.com/foaf/0.1/Agent") ) ); const updatedDataset = setAgentResourceAccess( sourceDataset, "https://some.pod/profileDoc#webId", { read: false, append: false, write: false, control: false, } ); expect(updatedDataset.size).toBe(4); expect( getMatchingQuads(updatedDataset, { predicate: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type", object: "http://www.w3.org/ns/auth/acl#Authorization", }) ).toHaveLength(1); expect( getMatchingQuads(updatedDataset, { predicate: "http://www.w3.org/ns/auth/acl#accessTo", object: "https://arbitrary.pod/resource", }) ).toHaveLength(1); expect( getMatchingQuads(updatedDataset, { predicate: "http://www.w3.org/ns/auth/acl#mode", object: "http://www.w3.org/ns/auth/acl#Read", }) ).toHaveLength(1); expect( getMatchingQuads(updatedDataset, { predicate: "http://www.w3.org/ns/auth/acl#agentClass", object: "http://xmlns.com/foaf/0.1/Agent", }) ).toHaveLength(1); }); it("does not change ACL rules that also apply to other Agents", () => { const sourceDataset = addMockAclRuleQuads( getMockDataset("https://arbitrary.pod/resource.acl"), "https://some.pod/profileDoc#webId", "https://arbitrary.pod/resource", { read: true, append: false, write: false, control: false }, "resource" ); const aclRuleSubject = Array.from(sourceDataset)[0].subject; sourceDataset.add( DataFactory.quad( aclRuleSubject, DataFactory.namedNode("http://www.w3.org/ns/auth/acl#agent"), DataFactory.namedNode("https://some-other.pod/profileDoc#webId") ) ); const updatedDataset = setAgentResourceAccess( sourceDataset, "https://some.pod/profileDoc#webId", { read: false, append: true, write: false, control: false, } ); expect(updatedDataset.size).toBe(8); expect( getMatchingQuads(updatedDataset, { predicate: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type", object: "http://www.w3.org/ns/auth/acl#Authorization", }) ).toHaveLength(2); expect( getMatchingQuads(updatedDataset, { predicate: "http://www.w3.org/ns/auth/acl#accessTo", object: "https://arbitrary.pod/resource", }) ).toHaveLength(2); const matchedQuads1 = getMatchingQuads(updatedDataset, { predicate: "http://www.w3.org/ns/auth/acl#mode", object: "http://www.w3.org/ns/auth/acl#Read", }); expect( getMatchingQuads(updatedDataset, { predicate: "http://www.w3.org/ns/auth/acl#agent", object: "https://some-other.pod/profileDoc#webId", }) ).toHaveLength(1); const matchedQuads2 = getMatchingQuads(updatedDataset, { predicate: "http://www.w3.org/ns/auth/acl#mode", object: "http://www.w3.org/ns/auth/acl#Append", }); expect( getMatchingQuads(updatedDataset, { predicate: "http://www.w3.org/ns/auth/acl#agent", object: "https://some.pod/profileDoc#webId", }) ).toHaveLength(1); // Make sure the Access Modes granted are in separate ACL Rules: expect(matchedQuads1.length).toBeGreaterThan(0); expect(matchedQuads2.length).toBeGreaterThan(0); expect(matchedQuads1[0].subject.value).not.toBe( matchedQuads2[0].subject.value ); }); }); describe("getAgentDefaultAccess", () => { it("returns the applicable Access Modes for a single Agent", () => { const containerAcl = addMockAclRuleQuads( getMockDataset("https://arbitrary.pod/container/.acl"), "https://some.pod/profileDoc#webId", "https://arbitrary.pod/container/", { read: true, append: false, write: false, control: true }, "default" ); const agentAccess = getAgentDefaultAccess( containerAcl, "https://some.pod/profileDoc#webId" ); expect(agentAccess).toEqual({ read: true, append: false, write: false, control: true, }); }); it("combines Access Modes defined for a given Agent in separate rules", () => { let containerAcl = addMockAclRuleQuads( getMockDataset("https://arbitrary.pod/container/.acl"), "https://some.pod/profileDoc#webId", "https://arbitrary.pod/container/", { read: true, append: false, write: false, control: false }, "default" ); containerAcl = addMockAclRuleQuads( containerAcl, "https://some.pod/profileDoc#webId", "https://arbitrary.pod/container/", { read: false, append: true, write: false, control: false }, "default" ); const agentAccess = getAgentDefaultAccess( containerAcl, "https://some.pod/profileDoc#webId" ); expect(agentAccess).toEqual({ read: true, append: true, write: false, control: false, }); }); it("returns false for all Access Modes if there are no ACL rules for the given Agent", () => { const containerAcl = addMockAclRuleQuads( getMockDataset("https://arbitrary.pod/container/.acl"), "https://some.pod/profileDoc#webId", "https://arbitrary.pod/container/", { read: true, append: false, write: false, control: false }, "default" ); const agentAccess = getAgentDefaultAccess( containerAcl, "https://some-other.pod/profileDoc#webId" ); expect(agentAccess).toEqual({ read: false, append: false, write: false, control: false, }); }); it("ignores ACL rules that apply to a different Agent", () => { let containerAcl = addMockAclRuleQuads( getMockDataset("https://arbitrary.pod/container/.acl"), "https://some-other.pod/profileDoc#webId", "https://arbitrary.pod/container/", { read: true, append: false, write: false, control: false }, "default" ); containerAcl = addMockAclRuleQuads( containerAcl, "https://some.pod/profileDoc#webId", "https://arbitrary.pod/container/", { read: false, append: true, write: false, control: false }, "default" ); const agentAccess = getAgentDefaultAccess( containerAcl, "https://some.pod/profileDoc#webId" ); expect(agentAccess).toEqual({ read: false, append: true, write: false, control: false, }); }); it("ignores ACL rules that apply to a different Resource", () => { let containerAcl = addMockAclRuleQuads( getMockDataset("https://some.pod/container/.acl"), "https://arbitrary.pod/profileDoc#webId", "https://some-other.pod/container/", { read: true, append: false, write: false, control: false }, "default" ); containerAcl = addMockAclRuleQuads( containerAcl, "https://arbitrary.pod/profileDoc#webId", "https://some.pod/container/", { read: false, append: true, write: false, control: false }, "default" ); const agentAccess = getAgentDefaultAccess( containerAcl, "https://arbitrary.pod/profileDoc#webId" ); expect(agentAccess).toEqual({ read: false, append: true, write: false, control: false, }); }); }); describe("getAgentDefaultAccessAll", () => { it("returns the applicable Access Modes for all Agents for whom Access Modes have been defined", () => { let containerAcl = addMockAclRuleQuads( getMockDataset("https://arbitrary.pod/container/.acl"), "https://some-other.pod/profileDoc#webId", "https://arbitrary.pod/container/", { read: true, append: false, write: false, control: false }, "default" ); containerAcl = addMockAclRuleQuads( containerAcl, "https://some.pod/profileDoc#webId", "https://arbitrary.pod/container/", { read: false, append: true, write: false, control: false }, "default" ); const agentAccess = getAgentDefaultAccessAll(containerAcl); expect(agentAccess).toEqual({ "https://some.pod/profileDoc#webId": { read: false, append: true, write: false, control: false, }, "https://some-other.pod/profileDoc#webId": { read: true, append: false, write: false, control: false, }, });