From b1bcc2aa2f5317ccc9b5199b0297a12a2dc50040 Mon Sep 17 00:00:00 2001 From: Vincent Date: Mon, 5 Oct 2020 16:13:01 +0200 Subject: [PATCH] Add Access Control management functions --- src/acp/control.test.ts | 165 +++++++++++++++++++++++++++++++++++++++- src/acp/control.ts | 111 ++++++++++++++++++++++++++- src/acp/mock.test.ts | 31 ++++++++ src/acp/mock.ts | 48 ++++++++++++ src/constants.ts | 1 + src/thing/thing.ts | 1 + 6 files changed, 353 insertions(+), 4 deletions(-) create mode 100644 src/acp/mock.test.ts create mode 100644 src/acp/mock.ts diff --git a/src/acp/control.test.ts b/src/acp/control.test.ts index 91c4c7256c..b335b47368 100644 --- a/src/acp/control.test.ts +++ b/src/acp/control.test.ts @@ -21,9 +21,22 @@ import { describe, it, expect } from "@jest/globals"; -import { hasLinkedAcr, WithLinkedAcpAccessControl } from "./control"; -import { acp } from "../constants"; +import { + createAccessControl, + getAccessControl, + getAccessControlAll, + hasLinkedAcr, + removeAccessControl, + setAccessControl, + WithLinkedAcpAccessControl, +} from "./control"; +import { acp, rdf } from "../constants"; import { WithAccessibleAcl, WithResourceInfo } from "../interfaces"; +import { getIri } from "../thing/get"; +import { createSolidDataset } from "../resource/solidDataset"; +import { createThing, getThing, setThing } from "../thing/thing"; +import { mockAcrFor } from "./mock"; +import { setUrl } from "../thing/set"; describe("hasLinkedAcr", () => { it("returns true if a Resource exposes a URL to an Access Control Resource", () => { @@ -67,3 +80,151 @@ describe("hasLinkedAcr", () => { expect(hasLinkedAcr(withLinkedAcr)).toBe(false); }); }); + +describe("createAccessControl", () => { + it("sets the type of the new Access Control to acp:AccessControl", () => { + const newAccessControl = createAccessControl(); + + expect(getIri(newAccessControl, rdf.type)).toBe(acp.AccessControl); + }); +}); + +describe("getAccessControl", () => { + it("returns the Access Control if found", () => { + const accessControlUrl = + "https://some.pod/access-control-resource.ttl#access-control"; + const accessControl = setUrl( + createThing({ url: accessControlUrl }), + rdf.type, + acp.AccessControl + ); + const accessControlResource = setThing( + mockAcrFor("https://some.pod/resource"), + accessControl + ); + + const foundAccessControl = getAccessControl( + accessControlResource, + accessControlUrl + ); + + expect(foundAccessControl).toEqual(accessControl); + }); + + it("returns null if the specified Thing is not an Access Control", () => { + const accessControlUrl = + "https://some.pod/access-control-resource.ttl#access-control"; + const accessControl = createThing({ url: accessControlUrl }); + const accessControlResource = setThing( + mockAcrFor("https://some.pod/resource"), + accessControl + ); + + const foundAccessControl = getAccessControl( + accessControlResource, + accessControlUrl + ); + + expect(foundAccessControl).toBeNull(); + }); + + it("returns null if the Access Control could not be found", () => { + const accessControlUrl = + "https://some.pod/access-control-resource.ttl#access-control"; + const accessControl = createThing({ url: accessControlUrl }); + const accessControlResource = setThing( + mockAcrFor("https://some.pod/resource"), + accessControl + ); + + const foundAccessControl = getAccessControl( + accessControlResource, + "https://some-other.pod/access-control-resource.ttl#access-control" + ); + + expect(foundAccessControl).toBeNull(); + }); +}); + +describe("getAccessControlAll", () => { + it("returns all included Access Controls", () => { + const accessControl = setUrl(createThing(), rdf.type, acp.AccessControl); + const accessControlResource = setThing( + mockAcrFor("https://some.pod/resource"), + accessControl + ); + + const foundAccessControls = getAccessControlAll(accessControlResource); + + expect(foundAccessControls).toEqual([accessControl]); + }); + + it("ignores Things that are not Access Controls", () => { + const accessControl = setUrl(createThing(), rdf.type, acp.AccessControl); + const notAnAccessControl = setUrl( + createThing(), + rdf.type, + "https://some.vocab/not-access-control" + ); + let accessControlResource = mockAcrFor("https://some.pod/resource"); + accessControlResource = setThing(accessControlResource, accessControl); + accessControlResource = setThing(accessControlResource, notAnAccessControl); + + const foundAccessControls = getAccessControlAll(accessControlResource); + + expect(foundAccessControls).toEqual([accessControl]); + }); + + it("returns an empty array if no Access Controls could be found", () => { + const accessControlResource = mockAcrFor("https://some.pod/resource"); + + const foundAccessControl = getAccessControlAll(accessControlResource); + + expect(foundAccessControl).toEqual([]); + }); +}); + +describe("setAccessControl", () => { + it("adds the given Access Control to the given Access Control Resource", () => { + const accessControlUrl = + "https://some.pod/access-control-resource.ttl#access-control"; + const accessControl = setUrl( + createThing({ url: accessControlUrl }), + rdf.type, + acp.AccessControl + ); + const accessControlResource = mockAcrFor("https://some.pod/resource"); + + const newAccessControlResource = setAccessControl( + accessControlResource, + accessControl + ); + + expect(getThing(newAccessControlResource, accessControlUrl)).toEqual( + accessControl + ); + }); +}); + +describe("removeAccessControl", () => { + it("removes the given Access Control from the given Access Control Resource", () => { + const accessControlUrl = + "https://some.pod/access-control-resource.ttl#access-control"; + const accessControl = setUrl( + createThing({ url: accessControlUrl }), + rdf.type, + acp.AccessControl + ); + const accessControlResource = setThing( + mockAcrFor("https://some.pod/resource"), + accessControl + ); + + const newAccessControlResource = removeAccessControl( + accessControlResource, + accessControl + ); + + expect(getThing(newAccessControlResource, accessControlUrl)).toBeNull(); + }); +}); diff --git a/src/acp/control.ts b/src/acp/control.ts index 9b324305a5..c66e407664 100644 --- a/src/acp/control.ts +++ b/src/acp/control.ts @@ -19,8 +19,25 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { acp } from "../constants"; -import { hasResourceInfo, WithResourceInfo } from "../interfaces"; +import { acp, rdf } from "../constants"; +import { + hasResourceInfo, + SolidDataset, + Thing, + Url, + UrlString, + WithResourceInfo, +} from "../interfaces"; +import { getIriAll } from "../thing/get"; +import { setIri } from "../thing/set"; +import { + createThing, + CreateThingOptions, + getThing, + getThingAll, + removeThing, + setThing, +} from "../thing/thing"; /** * ```{note} The Web Access Control specification is not yet finalised. As such, this @@ -48,6 +65,26 @@ export function hasLinkedAcr( ); } +/** + * ```{note} The Web Access Control specification is not yet finalised. As such, this + * function is still experimental and subject to change, even in a non-major release. + * ``` + * + * An Access Control Resource, containing [[AccessControl]]s specifying which [[AccessPolicy]]'s + * apply to the Resource this Access Control Resource is linked to. + */ +export type AccessControlResource = SolidDataset & { accessTo: UrlString }; + +/** + * ```{note} The Web Access Control specification is not yet finalised. As such, this + * function is still experimental and subject to change, even in a non-major release. + * ``` + * + * An Access Control, usually contained in an [[AccessControlResource]]. It describes which + * [[AccessPolicy]]'s apply to a Resource. + */ +export type AccessControl = Thing; + /** * ```{note} The Web Access Control specification is not yet finalised. As such, this * function is still experimental and subject to change, even in a non-major release. @@ -67,3 +104,73 @@ export type WithLinkedAcpAccessControl< }; }; }; + +/** + * Initialise a new [[AccessControl]]. + */ +export function createAccessControl( + options?: Parameters[0] +): AccessControl { + let accessControl = createThing(options); + accessControl = setIri(accessControl, rdf.type, acp.AccessControl); + return accessControl; +} +/** + * Find an [[AccessControl]] with a given URL in a given Access Control Resource. + * + * @returns The requested Access Control, or `null` if it could not be found. + */ +export function getAccessControl( + accessControlResource: AccessControlResource, + url: Parameters[1], + options?: Parameters[2] +): AccessControl | null { + const foundThing = getThing(accessControlResource, url, options); + if ( + foundThing === null || + !getIriAll(foundThing, rdf.type).includes(acp.AccessControl) + ) { + return null; + } + + return foundThing; +} +/** + * Get all [[AccessControl]]s in a given Access Control Resource. + */ +export function getAccessControlAll( + accessControlResource: AccessControlResource, + options?: Parameters[1] +): AccessControl[] { + const foundThings = getThingAll(accessControlResource, options); + + return foundThings.filter((foundThing) => + getIriAll(foundThing, rdf.type).includes(acp.AccessControl) + ); +} +/** + * Insert an [[AccessControl]] into an [[AccessControlResource]], replacing previous instances of that Access Control. + * + * @param accessControlResource The Access Control Resource to insert an Access Control into. + * @param accessControl The Access Control to insert into the given Access Control Resource. + * @returns A new Access Control Resource equal to the given Access Control Resource, but with the given Access Control. + */ +export function setAccessControl( + accessControlResource: AccessControlResource, + accessControl: AccessControl +): AccessControlResource { + return setThing(accessControlResource, accessControl); +} +/** + * Remove an [[AccessControl]] from an [[AccessControlResource]]. + * + * @param accessControlResource The Access Control Resource to remove an Access Control from. + * @param accessControl The Access Control to remove from the given Access Control Resource. + * @returns A new Access Control Resource equal to the given Access Control Resource, excluding the given Access Control. + */ +export function removeAccessControl( + accessControlResource: AccessControlResource, + accessControl: AccessControl +): AccessControlResource { + return removeThing(accessControlResource, accessControl); +} diff --git a/src/acp/mock.test.ts b/src/acp/mock.test.ts new file mode 100644 index 0000000000..18a4905993 --- /dev/null +++ b/src/acp/mock.test.ts @@ -0,0 +1,31 @@ +/** + * 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 { mockAcrFor } from "./mock"; + +describe("mockAcrFor", () => { + it("should attach the URL of the Resource it applies to", () => { + const mockedAcr = mockAcrFor("https://some.pod/resource"); + + expect(mockedAcr.accessTo).toBe("https://some.pod/resource"); + }); +}); diff --git a/src/acp/mock.ts b/src/acp/mock.ts new file mode 100644 index 0000000000..d387fb3151 --- /dev/null +++ b/src/acp/mock.ts @@ -0,0 +1,48 @@ +/** + * 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 { UrlString, WithResourceInfo } from "../interfaces"; +import { mockSolidDatasetFrom } from "../resource/mock"; +import { getSourceUrl } from "../resource/resource"; +import { AccessControlResource } from "./control"; + +/** + * + * ```{warning} + * Do not use this function in production code. For use in **unit tests** that require a + * [[AccessControlResource]]. + * ``` + * + * Initialises a new empty Access Control Resource for a given Resource for use + * in **unit tests**. + * + * @param resourceUrl The URL of the Resource to which the mocked ACR should apply. + * @returns The mocked empty Access Control Resource for the given Resource. + */ +export function mockAcrFor(resourceUrl: UrlString): AccessControlResource { + const acrUrl = new URL("access-control-resource", resourceUrl).href; + const acr: AccessControlResource = Object.assign( + mockSolidDatasetFrom(acrUrl), + { accessTo: resourceUrl } + ); + + return acr; +} diff --git a/src/constants.ts b/src/constants.ts index c52831ed1b..7cf845671b 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -49,5 +49,6 @@ export const foaf = { export const acp = { AccessPolicyResource: "http://www.w3.org/ns/solid/acp#AccessPolicyResource", AccessPolicy: "http://www.w3.org/ns/solid/acp#AccessPolicy", + AccessControl: "http://www.w3.org/ns/solid/acp#AccessControl", accessControl: "http://www.w3.org/ns/solid/acp#accessControl", } as const; diff --git a/src/thing/thing.ts b/src/thing/thing.ts index 9237ee5152..fbfe44c9a5 100644 --- a/src/thing/thing.ts +++ b/src/thing/thing.ts @@ -274,6 +274,7 @@ export function createThing( * @param options Optional parameters that affect the final URL of this [[Thing]] when saved. */ export function createThing(options?: CreateThingLocalOptions): ThingLocal; +export function createThing(options?: CreateThingOptions): Thing; export function createThing(options: CreateThingOptions = {}): Thing { if (typeof (options as CreateThingPersistedOptions).url !== "undefined") { const url = (options as CreateThingPersistedOptions).url;