@inrupt/solid-client
Version:
Make your web apps work with Solid Pods.
671 lines (635 loc) • 23.4 kB
text/typescript
// Copyright 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 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 {
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, getTermAll } from "../thing/get";
import { removeAll } from "../thing/remove";
import { setUrl } from "../thing/set";
import {
asUrl,
createThing,
getThing,
getThingAll,
isThingLocal,
removeThing,
setThing,
} from "../thing/thing";
import type { WithAccessibleAcr } from "./acp";
import {
addAcrPolicyUrl,
addPolicyUrl,
getAcrPolicyUrlAll,
getPolicyUrlAll,
removeAcrPolicyUrl,
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.
* @since 1.6.0
*/
export type Policy = 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 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
*/
export type AccessModes = {
read: boolean;
append: boolean;
write: boolean;
};
/**
* @param thing the [[Thing]] to check to see if it's an ACP Policy or not
*/
function isPolicy(thing: ThingPersisted): thing is Policy {
return getIriAll(thing, rdf.type).includes(acp.Policy);
}
/**
* ```{note} There is no Access Control Policies specification yet. As such, this
* function is still experimental and subject to change, even in a non-major release.
* ```
*
* Initialise a new, empty [[Policy]].
*
* @param url URL that identifies this Policy.
* @since 1.6.0
*/
export function createPolicy(url: Url | UrlString): Policy {
const stringUrl = internal_toIriString(url);
let policyThing = createThing({ url: stringUrl });
policyThing = setUrl(policyThing, rdf.type, acp.Policy);
return policyThing;
}
/**
* ```{note} There is no Access Control Policies specification yet. As such, this
* function is still experimental and subject to change, even in a non-major release.
* ```
*
* Get the [[Policy]] with the given URL from an [[SolidDataset]].
*
* @param policyResource The Resource that contains the given Policy.
* @param url URL that identifies this Policy.
* @returns The requested Policy, if it exists, or `null` if it does not.
* @since 1.6.0
*/
export function getPolicy(
policyResource: SolidDataset,
url: Url | UrlString,
): Policy | null {
const foundThing = getThing(policyResource, url);
if (foundThing === null || !isPolicy(foundThing)) {
return null;
}
return foundThing;
}
/**
* ```{note} There is no Access Control Policies specification yet. As such, this
* function is still experimental and subject to change, even in a non-major release.
* ```
*
* Get all [[Policy]]'s in a given [[SolidDataset]].
*
* @param policyResource The Resource that contains Access Policies.
* @since 1.6.0
*/
export function getPolicyAll(policyResource: SolidDataset): Policy[] {
const foundThings = getThingAll(policyResource);
const foundPolicies = foundThings.filter(
(thing) => !isThingLocal(thing) && isPolicy(thing),
) as Policy[];
return foundPolicies;
}
/**
* ```{note} There is no Access Control Policies specification yet. As such, this
* function is still experimental and subject to change, even in a non-major release.
* ```
*
* Remove the given [[Policy]] from the given [[SolidDataset]].
*
* @param policyResource The Resource that contains Access Policies.
* @param policy The Policy to remove from the resource.
* @since 1.6.0
*/
export function removePolicy<Dataset extends SolidDataset>(
policyResource: Dataset,
policy: Url | UrlString | Policy,
): Dataset {
return removeThing(policyResource, policy);
}
/**
* ```{note} There is no Access Control Policies specification yet. As such, this
* function is still experimental and subject to change, even in a non-major release.
* ```
*
* Insert the given [[Policy]] into the given [[SolidDataset]], replacing previous instances of that Policy.
*
* @param policyResource The Resource that contains Access Policies.
* @param policy The Policy to insert into the Resource.
* @returns A new dataset equal to the given resource, but with the given Policy.
* @since 1.6.0
*/
export function setPolicy<Dataset extends SolidDataset>(
policyResource: Dataset,
policy: Policy,
): Dataset {
return setThing(policyResource, policy);
}
/**
* ```{note} There is no Access Control Policies specification yet. As such, this
* function is still experimental and subject to change, even in a non-major release.
* ```
*
* Given a [[Policy]] and a set of [[AccessModes]], return a new Policy based on the given
* Policy, but with the given Access Modes allowed on it.
*
* @param policy The Policy on which to set the modes to allow.
* @param modes Modes to allow for this Policy.
* @since Not released yet.
*/
export function setAllowModesV2<P extends Policy | ResourcePolicy>(
policy: P,
modes: AccessModes,
): P {
let newPolicy = removeAll(policy, acp.allow);
if (modes.read === true) {
newPolicy = addIri(
newPolicy,
acp.allow,
internal_accessModeIriStrings.read,
);
}
if (modes.append === true) {
newPolicy = addIri(
newPolicy,
acp.allow,
internal_accessModeIriStrings.append,
);
}
if (modes.write === true) {
newPolicy = addIri(
newPolicy,
acp.allow,
internal_accessModeIriStrings.write,
);
}
return newPolicy;
}
/**
* See [[setAllowModesV2]].
*/
export const setAllowModes = setAllowModesV2;
/**
* ```{note} There is no Access Control Policies specification yet. As such, this
* function is still experimental and subject to change, even in a non-major release.
* ```
*
* Given a [[Policy]], return which [[AccessModes]] it allows.
*
* @param policy The Policy for which you want to know the Access Modes it allows.
* @since Not released yet.
*/
export function getAllowModesV2<P extends Policy | ResourcePolicy>(
policy: P,
): AccessModes {
const allowedModes = getIriAll(policy, acp.allow);
return {
read: allowedModes.includes(internal_accessModeIriStrings.read),
append: allowedModes.includes(internal_accessModeIriStrings.append),
write: allowedModes.includes(internal_accessModeIriStrings.write),
};
}
/**
* See [[getAllowModesV2]].
*/
export const getAllowModes = getAllowModesV2;
/**
* ```{note} There is no Access Control Policies specification yet. As such, this
* function is still experimental and subject to change, even in a non-major release.
* ```
*
* Given a [[Policy]] and a set of [[AccessModes]], return a new Policy based on the given
* Policy, but with the given Access Modes disallowed on it.
*
* @param policy The Policy on which to set the modes to disallow.
* @param modes Modes to disallow for this Policy.
* @since Not released yet.
*/
export function setDenyModesV2<P extends Policy | ResourcePolicy>(
policy: P,
modes: AccessModes,
): P {
let newPolicy = removeAll(policy, acp.deny);
if (modes.read === true) {
newPolicy = addIri(newPolicy, acp.deny, internal_accessModeIriStrings.read);
}
if (modes.append === true) {
newPolicy = addIri(
newPolicy,
acp.deny,
internal_accessModeIriStrings.append,
);
}
if (modes.write === true) {
newPolicy = addIri(
newPolicy,
acp.deny,
internal_accessModeIriStrings.write,
);
}
return newPolicy;
}
/**
* See [[setDenyModesV2]].
*/
export const setDenyModes = setDenyModesV2;
/**
* ```{note} There is no Access Control Policies specification yet. As such, this
* function is still experimental and subject to change, even in a non-major release.
* ```
*
* Given a [[Policy]], return which [[AccessModes]] it disallows.
*
* @param policy The Policy on which you want to know the Access Modes it disallows.
* @since Not released yet.
*/
export function getDenyModesV2<P extends Policy | ResourcePolicy>(
policy: P,
): AccessModes {
const deniedModes = getIriAll(policy, acp.deny);
return {
read: deniedModes.includes(internal_accessModeIriStrings.read),
append: deniedModes.includes(internal_accessModeIriStrings.append),
write: deniedModes.includes(internal_accessModeIriStrings.write),
};
}
/**
* See [[getDenyModesV2]].
*/
export const getDenyModes = getDenyModesV2;
/**
* ```{note} There is no Access Control Policies specification yet. As such, this
* function is still experimental and subject to change, even in a non-major release.
* ```
*
* Initialise a new, empty [[ResourcePolicy]] for the given Resource.
*
* @param resourceWithAcr The Resource to which the Policy is to apply.
* @param name The name that identifies this Policy.
* @since 1.6.0
*/
export function createResourcePolicyFor(
resourceWithAcr: WithAccessibleAcr,
name: string,
): ResourcePolicy {
const acr = internal_getAcr(resourceWithAcr);
const url = new URL(getSourceUrl(acr));
url.hash = `#${name}`;
let policyThing = createThing({ url: url.href });
policyThing = setUrl(policyThing, rdf.type, acp.Policy);
return policyThing;
}
/**
* ```{note} There is no Access Control Policies specification yet. As such, this
* function is still experimental and subject to change, even in a non-major release.
* ```
*
* Get the [[ResourcePolicy]] with the given name that applies to a Resource
* from its Access Control Resource.
*
* @param resourceWithAcr The Resource whose ACR contains the given Policy.
* @param name The name that identifies this Policy.
* @returns The requested Policy, if it exists and applies to the given Resource, or `null` if it does not.
* @since 1.6.0
*/
export function getResourcePolicy(
resourceWithAcr: WithAccessibleAcr,
name: string,
): ResourcePolicy | null {
const acr = internal_getAcr(resourceWithAcr);
const acrUrl = getSourceUrl(acr);
const url = new URL(acrUrl);
url.hash = `#${name}`;
const foundThing = getThing(acr, url.href);
if (
!getPolicyUrlAll(resourceWithAcr).includes(url.href) ||
foundThing === null ||
!isPolicy(foundThing)
) {
return null;
}
return foundThing;
}
/**
* ```{note} There is no Access Control Policies specification yet. As such, this
* function is still experimental and subject to change, even in a non-major release.
* ```
*
* Get the [[ResourcePolicy]] with the given name that applies to a Resource's
* Access Control Resource from that Access Control Resource.
*
* @param resourceWithAcr The Resource whose ACR contains the given Policy.
* @param name The name that identifies this Policy.
* @returns The requested Policy, if it exists and applies to the Resource's ACR, or `null` if it does not.
* @since 1.6.0
*/
export function getResourceAcrPolicy(
resourceWithAcr: WithAccessibleAcr,
name: string,
): ResourcePolicy | null {
const acr = internal_getAcr(resourceWithAcr);
const acrUrl = getSourceUrl(acr);
const url = new URL(acrUrl);
url.hash = `#${name}`;
const foundThing = getThing(acr, url.href);
if (
!getAcrPolicyUrlAll(resourceWithAcr).includes(url.href) ||
foundThing === null ||
!isPolicy(foundThing)
) {
return null;
}
return foundThing;
}
/**
* ```{note} There is no Access Control Policies specification yet. As such, this
* function is still experimental and subject to change, even in a non-major release.
* ```
*
* Get all [[ResourcePolicy]]'s that apply to a Resource in its Access Control
* Resource.
*
* @param resourceWithAcr The Resource whose Access Control Resource contains Access Policies applying to it.
* @since 1.6.0
*/
export function getResourcePolicyAll(
resourceWithAcr: WithAccessibleAcr,
): 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 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)
);
}
/**
* ```{note} There is no Access Control Policies specification yet. As such, this
* function is still experimental and subject to change, even in a non-major release.
* ```
*
* Get all [[ResourcePolicy]]'s that apply to a given Resource's Access Control
* Resource from that Access Control Resource.
*
* @param resourceWithAcr The Resource whose Access Control Resource contains Access Policies.
* @since 1.6.0
*/
export function getResourceAcrPolicyAll(
resourceWithAcr: WithAccessibleAcr,
): ResourcePolicy[] {
const acr = internal_getAcr(resourceWithAcr);
const policyUrls = getAcrPolicyUrlAll(resourceWithAcr);
const foundThings = policyUrls.map((policyUrl) => getThing(acr, policyUrl));
const foundPolicies = foundThings.filter(
(thing) => thing !== null && isPolicy(thing),
) as ResourcePolicy[];
return foundPolicies;
}
/**
* ```{note} There is no Access Control Policies specification yet. As such, this
* function is still experimental and subject to change, even in a non-major release.
* ```
*
* Remove the given [[ResourcePolicy]] from the given Resource's Access Control
* Resource.
*
* @param resourceWithAcr The Resource whose Access Control Resource contains Access Policies.
* @param policy The Policy to remove from the Resource's Access Control Resource.
* @since 1.6.0
*/
export function removeResourcePolicy<ResourceExt extends WithAccessibleAcr>(
resourceWithAcr: ResourceExt,
policy: string | Url | UrlString | ResourcePolicy,
): ResourceExt {
const acr = internal_getAcr(resourceWithAcr);
let policyToRemove = policy;
if (typeof policyToRemove === "string") {
if (internal_isValidUrl(policyToRemove) === false) {
// If the given Policy to remove is the name of the Policy,
// resolve it to its full URL — developers usually refer to either the
// Policy itself, or by its name, as they do not have access to the ACR
// directly.
const policyUrl = new URL(getSourceUrl(acr));
policyUrl.hash = `#${policy}`;
policyToRemove = policyUrl.href;
}
}
let policyUrlString: UrlString;
if (typeof policyToRemove === "string") {
policyUrlString = policyToRemove;
} else if (isNamedNode(policyToRemove)) {
policyUrlString = internal_toIriString(policyToRemove);
} else {
policyUrlString = asUrl(policyToRemove, getSourceUrl(acr));
}
// Check whether the actual Policy (i.e. with the Policy type) exists:
const matchingRule = getResourcePolicy(
resourceWithAcr,
new URL(policyUrlString).hash.substring(1),
);
if (matchingRule === null) {
// No such Policy exists yet, so return the Resource+ACR unchanged:
return resourceWithAcr;
}
const updatedAcr = removeThing(acr, policyToRemove);
const updatedResource = internal_setAcr(resourceWithAcr, updatedAcr);
return removePolicyUrl(updatedResource, policyUrlString);
}
/**
* ```{note} There is no Access Control Policies specification yet. As such, this
* function is still experimental and subject to change, even in a non-major release.
* ```
*
* Remove the given [[ResourcePolicy]] that applies to a given Resource's Access
* Control Resource from that Access Control Resource.
*
* @param resourceWithAcr The Resource whose Access Control Resource contains Access Policies.
* @param policy The ACR Policy to remove from the Resource's Access Control Resource.
* @since 1.6.0
*/
export function removeResourceAcrPolicy<ResourceExt extends WithAccessibleAcr>(
resourceWithAcr: ResourceExt,
policy: string | Url | UrlString | ResourcePolicy,
): ResourceExt {
const acr = internal_getAcr(resourceWithAcr);
let policyToRemove = policy;
if (typeof policyToRemove === "string") {
if (internal_isValidUrl(policyToRemove) === false) {
// If the given Policy to remove is the name of the Policy,
// resolve it to its full URL — developers usually refer to either the
// Policy itself, or by its name, as they do not have access to the ACR
// directly.
const policyUrl = new URL(getSourceUrl(acr));
policyUrl.hash = `#${policy}`;
policyToRemove = policyUrl.href;
}
}
let policyUrlString: string;
if (typeof policyToRemove === "string") {
policyUrlString = policyToRemove;
} else if (isNamedNode(policyToRemove)) {
policyUrlString = internal_toIriString(policyToRemove);
} else {
policyUrlString = asUrl(policyToRemove, getSourceUrl(acr));
}
// Check whether the actual Policy (i.e. with the Policy type) exists:
const matchingRule = getResourceAcrPolicy(
resourceWithAcr,
new URL(policyUrlString).hash.substring(1),
);
if (matchingRule === null) {
// No such Policy exists yet, so return the Resource+ACR unchanged:
return resourceWithAcr;
}
const updatedAcr = removeThing(acr, policyToRemove);
const updatedResource = internal_setAcr(resourceWithAcr, updatedAcr);
return removeAcrPolicyUrl(updatedResource, policyUrlString);
}
/**
* ```{note} There is no Access Control Policies specification yet. As such, this
* function is still experimental and subject to change, even in a non-major release.
* ```
*
* Insert the given [[ResourcePolicy]] into the given Resource's Acccess Control
* Resource, replacing previous instances of that Policy.
*
* @param resourceWithAcr The Resource whose Access Control Resource contains Access Policies.
* @param policy The Policy to insert into the Resource's Access Control Resource.
* @returns A new Resource equal to the given Resource, but with the given Policy in its Access Control Resource.
* @since 1.6.0
*/
export function setResourcePolicy<ResourceExt extends WithAccessibleAcr>(
resourceWithAcr: ResourceExt,
policy: ResourcePolicy,
): ResourceExt {
const acr = internal_getAcr(resourceWithAcr);
const updatedAcr = setThing(acr, policy);
const updatedResource = internal_setAcr(resourceWithAcr, updatedAcr);
const policyUrl = asUrl(policy, getSourceUrl(acr));
return addPolicyUrl(updatedResource, policyUrl);
}
/**
* ```{note} There is no Access Control Policies specification yet. As such, this
* function is still experimental and subject to change, even in a non-major release.
* ```
*
* Insert the given [[ResourcePolicy]] into the given Resource's Acccess Control
* Resource, replacing previous instances of that Policy, to apply to the Access
* Control Resource itself.
*
* @param resourceWithAcr The Resource whose Access Control Resource contains Access Policies.
* @param policy The Policy to insert into the Resource's Access Control Resource.
* @returns A new Resource equal to the given Resource, but with the given Policy in its Access Control Resource, applying to that Access Control Resource.
* @since 1.6.0
*/
export function setResourceAcrPolicy<ResourceExt extends WithAccessibleAcr>(
resourceWithAcr: ResourceExt,
policy: ResourcePolicy,
): ResourceExt {
const acr = internal_getAcr(resourceWithAcr);
const updatedAcr = setThing(acr, policy);
const updatedResource = internal_setAcr(resourceWithAcr, updatedAcr);
const policyUrl = asUrl(policy, getSourceUrl(acr));
return addAcrPolicyUrl(updatedResource, policyUrl);
}