feat: Create AcpReader

This commit is contained in:
Joachim Van Herwegen 2022-08-10 13:40:50 +02:00
parent b09bf66ad7
commit a6409ad00d
8 changed files with 594 additions and 0 deletions

11
package-lock.json generated
View File

@ -12,6 +12,7 @@
"@comunica/context-entries": "^2.2.0", "@comunica/context-entries": "^2.2.0",
"@comunica/query-sparql": "^2.2.1", "@comunica/query-sparql": "^2.2.1",
"@rdfjs/types": "^1.1.0", "@rdfjs/types": "^1.1.0",
"@solid/access-control-policy": "^0.1.2",
"@solid/access-token-verifier": "^2.0.3", "@solid/access-token-verifier": "^2.0.3",
"@types/async-lock": "^1.1.5", "@types/async-lock": "^1.1.5",
"@types/bcryptjs": "^2.4.2", "@types/bcryptjs": "^2.4.2",
@ -3832,6 +3833,11 @@
"@sinonjs/commons": "^1.7.0" "@sinonjs/commons": "^1.7.0"
} }
}, },
"node_modules/@solid/access-control-policy": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/@solid/access-control-policy/-/access-control-policy-0.1.2.tgz",
"integrity": "sha512-zviquBk05id837Ff3dJTGwlt0y+ocWtHuLEuZenramh7qVXUoJuFQ6BnxiMDxUJY/rCpNEmjyE8rn+dT/NpMqA=="
},
"node_modules/@solid/access-token-verifier": { "node_modules/@solid/access-token-verifier": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/@solid/access-token-verifier/-/access-token-verifier-2.0.3.tgz", "resolved": "https://registry.npmjs.org/@solid/access-token-verifier/-/access-token-verifier-2.0.3.tgz",
@ -18582,6 +18588,11 @@
"@sinonjs/commons": "^1.7.0" "@sinonjs/commons": "^1.7.0"
} }
}, },
"@solid/access-control-policy": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/@solid/access-control-policy/-/access-control-policy-0.1.2.tgz",
"integrity": "sha512-zviquBk05id837Ff3dJTGwlt0y+ocWtHuLEuZenramh7qVXUoJuFQ6BnxiMDxUJY/rCpNEmjyE8rn+dT/NpMqA=="
},
"@solid/access-token-verifier": { "@solid/access-token-verifier": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/@solid/access-token-verifier/-/access-token-verifier-2.0.3.tgz", "resolved": "https://registry.npmjs.org/@solid/access-token-verifier/-/access-token-verifier-2.0.3.tgz",

View File

@ -102,6 +102,7 @@
"@comunica/context-entries": "^2.2.0", "@comunica/context-entries": "^2.2.0",
"@comunica/query-sparql": "^2.2.1", "@comunica/query-sparql": "^2.2.1",
"@rdfjs/types": "^1.1.0", "@rdfjs/types": "^1.1.0",
"@solid/access-control-policy": "^0.1.2",
"@solid/access-token-verifier": "^2.0.3", "@solid/access-token-verifier": "^2.0.3",
"@types/async-lock": "^1.1.5", "@types/async-lock": "^1.1.5",
"@types/bcryptjs": "^2.4.2", "@types/bcryptjs": "^2.4.2",

View File

@ -0,0 +1,164 @@
import { Readable } from 'stream';
import { allowAccessModes } from '@solid/access-control-policy/dist/algorithm/allow_access_modes';
import type { IAccessControlledResource } from '@solid/access-control-policy/dist/type/i_access_controlled_resource';
import type { IContext } from '@solid/access-control-policy/dist/type/i_context';
import type { IPolicy } from '@solid/access-control-policy/dist/type/i_policy';
import type { Store } from 'n3';
import type { CredentialSet } from '../authentication/Credentials';
import type { AuxiliaryStrategy } from '../http/auxiliary/AuxiliaryStrategy';
import type { ResourceIdentifier } from '../http/representation/ResourceIdentifier';
import { getLoggerFor } from '../logging/LogUtil';
import type { ResourceStore } from '../storage/ResourceStore';
import { INTERNAL_QUADS } from '../util/ContentTypes';
import { createErrorMessage } from '../util/errors/ErrorUtil';
import { InternalServerError } from '../util/errors/InternalServerError';
import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
import type { IdentifierStrategy } from '../util/identifiers/IdentifierStrategy';
import { IdentifierMap } from '../util/map/IdentifierMap';
import { readableToQuads } from '../util/StreamUtil';
import { ACL } from '../util/Vocabularies';
import { getAccessControlledResources } from './AcpUtil';
import type { PermissionReaderInput } from './PermissionReader';
import { PermissionReader } from './PermissionReader';
import type { AclPermission } from './permissions/AclPermission';
import { AclMode } from './permissions/AclPermission';
import { AccessMode } from './permissions/Permissions';
import type { PermissionMap, PermissionSet } from './permissions/Permissions';
const modesMap: Record<string, Readonly<(keyof AclPermission)[]>> = {
[ACL.Read]: [ AccessMode.read ],
[ACL.Write]: [ AccessMode.append, AccessMode.write ],
[ACL.Append]: [ AccessMode.append ],
[ACL.Control]: [ AclMode.control ],
} as const;
/**
* Finds the permissions of a resource as defined in the corresponding ACRs.
* Implementation based on https://solid.github.io/authorization-panel/acp-specification/.
*
* Caches data so no duplicate calls are made to the {@link ResourceStore} for a single request.
*/
export class AcpReader extends PermissionReader {
protected readonly logger = getLoggerFor(this);
private readonly acrStrategy: AuxiliaryStrategy;
private readonly acrStore: ResourceStore;
private readonly identifierStrategy: IdentifierStrategy;
public constructor(acrStrategy: AuxiliaryStrategy, acrStore: ResourceStore, identifierStrategy: IdentifierStrategy) {
super();
this.acrStrategy = acrStrategy;
this.acrStore = acrStore;
this.identifierStrategy = identifierStrategy;
}
public async handle({ credentials, requestedModes }: PermissionReaderInput): Promise<PermissionMap> {
this.logger.debug(`Retrieving permissions of ${JSON.stringify(credentials)}`);
const resourceCache = new IdentifierMap<IAccessControlledResource[]>();
const permissionMap: PermissionMap = new IdentifierMap();
// Resolves the targets sequentially so the `resourceCache` can be filled and reused
for (const target of requestedModes.keys()) {
permissionMap.set(target, await this.extractPermissions(target, credentials, resourceCache));
}
return permissionMap;
}
/**
* Generates the allowed permissions.
* @param target - Target to generate permissions for.
* @param credentials - Credentials that are trying to access the resource.
* @param resourceCache - Cache used to store ACR data.
*/
private async extractPermissions(target: ResourceIdentifier, credentials: CredentialSet,
resourceCache: IdentifierMap<IAccessControlledResource[]>): Promise<PermissionSet> {
const context = this.createContext(target, credentials);
const policies: IPolicy[] = [];
// Extract all the policies relevant for the target
const identifiers = this.getAncestorIdentifiers(target);
for (const identifier of identifiers) {
let acrs = resourceCache.get(identifier);
if (!acrs) {
const data = await this.readAcrData(identifier);
acrs = [ ...getAccessControlledResources(data) ];
resourceCache.set(identifier, acrs);
}
const size = policies.length;
policies.push(...this.getEffectivePolicies(target, acrs));
this.logger.debug(`Found ${policies.length - size} policies relevant for ${target.path} in ${identifier.path}`);
}
const modes = allowAccessModes(policies, context);
// We don't do a separate ACP run for public and agent credentials
// as that is only relevant for the WAC-Allow header.
// All permissions are put in the `agent` field of the PermissionSet,
// as the actual field used does not matter for authorization.
const permissionSet: PermissionSet = { agent: {}};
for (const mode of modes) {
if (mode in modesMap) {
for (const permission of modesMap[mode]) {
permissionSet.agent![permission as AccessMode] = true;
}
}
}
return permissionSet;
}
/**
* Creates an ACP context targeting the given identifier with the provided credentials.
*/
private createContext(target: ResourceIdentifier, credentials: CredentialSet): IContext {
return {
target: target.path,
agent: credentials.agent?.webId,
};
}
/**
* Returns all {@link IPolicy} found in `resources` that apply to the target identifier.
* https://solidproject.org/TR/2022/acp-20220518#effective-policies
*/
private* getEffectivePolicies(target: ResourceIdentifier, resources: Iterable<IAccessControlledResource>):
Iterable<IPolicy> {
for (const { iri, accessControlResource } of resources) {
// Use the `accessControl` entries if the `target` corresponds to the `iri` used in the ACR.
// If not, this means this is an ACR of a parent resource, and we need to use the `memberAccessControl` field.
const accessControlField = iri === target.path ? 'accessControl' : 'memberAccessControl';
yield* accessControlResource[accessControlField].flatMap((ac): IPolicy[] => ac.policy);
}
}
/**
* Returns the given identifier and all its ancestors.
* These are all the identifiers that are relevant for determining the effective policies.
*/
private* getAncestorIdentifiers(identifier: ResourceIdentifier): Iterable<ResourceIdentifier> {
yield identifier;
while (!this.identifierStrategy.isRootContainer(identifier)) {
identifier = this.identifierStrategy.getParentContainer(identifier);
yield identifier;
}
}
/**
* Returns the data found in the ACR corresponding to the given identifier.
*/
private async readAcrData(identifier: ResourceIdentifier): Promise<Store> {
const acrIdentifier = this.acrStrategy.getAuxiliaryIdentifier(identifier);
let data: Readable;
try {
this.logger.debug(`Reading ACR document ${acrIdentifier.path}`);
({ data } = await this.acrStore.getRepresentation(acrIdentifier, { type: { [INTERNAL_QUADS]: 1 }}));
} catch (error: unknown) {
if (!NotFoundHttpError.isInstance(error)) {
const message = `Error reading ACR ${acrIdentifier.path}: ${createErrorMessage(error)}`;
this.logger.error(message);
throw new InternalServerError(message, { cause: error });
}
this.logger.debug(`No direct ACR document found for ${identifier.path}`);
data = Readable.from([]);
}
return readableToQuads(data);
}
}

View File

@ -0,0 +1,101 @@
import type { IAccessControl } from '@solid/access-control-policy/dist/type/i_access_control';
import type { IAccessControlResource } from '@solid/access-control-policy/dist/type/i_access_control_resource';
import type { IAccessControlledResource } from '@solid/access-control-policy/dist/type/i_access_controlled_resource';
import type { IAccessMode } from '@solid/access-control-policy/dist/type/i_access_mode';
import type { IMatcher } from '@solid/access-control-policy/dist/type/i_matcher';
import type { IPolicy } from '@solid/access-control-policy/dist/type/i_policy';
import type { Store } from 'n3';
import type { NamedNode, Term } from 'rdf-js';
import { ACP } from '../util/Vocabularies';
/**
* Returns all objects found using the given subject and predicate, mapped with the given function.
*/
function mapObjects<T>(data: Store, subject: Term, predicate: Term, fn: (data: Store, term: Term) => T): T[] {
return data.getObjects(subject, predicate, null)
.map((term): T => fn(data, term));
}
/**
* Returns the string values of all objects found using the given subject and predicate.
*/
function getObjectValues(data: Store, subject: Term, predicate: NamedNode): string[] {
return mapObjects(data, subject, predicate, (unused, term): string => term.value);
}
/**
* Finds the {@link IMatcher} with the given identifier in the given dataset.
* @param data - Dataset to look in.
* @param matcher - Identifier of the matcher.
*/
export function getMatcher(data: Store, matcher: Term): IMatcher {
return {
iri: matcher.value,
agent: getObjectValues(data, matcher, ACP.terms.agent),
client: getObjectValues(data, matcher, ACP.terms.client),
issuer: getObjectValues(data, matcher, ACP.terms.issuer),
vc: getObjectValues(data, matcher, ACP.terms.vc),
};
}
/**
* Finds the {@link IPolicy} with the given identifier in the given dataset.
* @param data - Dataset to look in.
* @param policy - Identifier of the policy.
*/
export function getPolicy(data: Store, policy: Term): IPolicy {
return {
iri: policy.value,
allow: new Set(getObjectValues(data, policy, ACP.terms.allow) as IAccessMode[]),
deny: new Set(getObjectValues(data, policy, ACP.terms.deny) as IAccessMode[]),
allOf: mapObjects(data, policy, ACP.terms.allOf, getMatcher),
anyOf: mapObjects(data, policy, ACP.terms.anyOf, getMatcher),
noneOf: mapObjects(data, policy, ACP.terms.noneOf, getMatcher),
};
}
/**
* Finds the {@link IAccessControl} with the given identifier in the given dataset.
* @param data - Dataset to look in.
* @param accessControl - Identifier of the access control.
*/
export function getAccessControl(data: Store, accessControl: Term): IAccessControl {
const policy = mapObjects(data, accessControl, ACP.terms.apply, getPolicy);
return {
iri: accessControl.value,
policy,
};
}
/**
* Finds the {@link IAccessControlResource} with the given identifier in the given dataset.
* @param data - Dataset to look in.
* @param acr - Identifier of the access control resource.
*/
export function getAccessControlResource(data: Store, acr: Term): IAccessControlResource {
const accessControl = data.getObjects(acr, ACP.terms.accessControl, null)
.map((term): IAccessControl => getAccessControl(data, term));
const memberAccessControl = data.getObjects(acr, ACP.terms.memberAccessControl, null)
.map((term): IAccessControl => getAccessControl(data, term));
return {
iri: acr.value,
accessControl,
memberAccessControl,
};
}
/**
* Finds all {@link IAccessControlledResource} in the given dataset.
* @param data - Dataset to look in.
*/
export function* getAccessControlledResources(data: Store): Iterable<IAccessControlledResource> {
const acrQuads = data.getQuads(null, ACP.terms.resource, null, null);
for (const quad of acrQuads) {
const accessControlResource = getAccessControlResource(data, quad.subject);
yield {
iri: quad.object.value,
accessControlResource,
};
}
}

View File

@ -24,6 +24,8 @@ export * from './authorization/permissions/Permissions';
export * from './authorization/permissions/SparqlUpdateModesExtractor'; export * from './authorization/permissions/SparqlUpdateModesExtractor';
// Authorization // Authorization
export * from './authorization/AcpReader';
export * from './authorization/AcpUtil';
export * from './authorization/AllStaticReader'; export * from './authorization/AllStaticReader';
export * from './authorization/Authorizer'; export * from './authorization/Authorizer';
export * from './authorization/AuxiliaryReader'; export * from './authorization/AuxiliaryReader';

View File

@ -117,6 +117,29 @@ export const ACL = createVocabulary('http://www.w3.org/ns/auth/acl#',
'Control', 'Control',
); );
export const ACP = createVocabulary('http://www.w3.org/ns/solid/acp#',
// Access Control Resource
'resource',
'accessControl',
'memberAccessControl',
// Access Control,
'apply',
// Policy
'allow',
'deny',
'allOf',
'anyOf',
'noneOf',
// Matcher
'agent',
'client',
'issuer',
'vc',
);
export const AS = createVocabulary('https://www.w3.org/ns/activitystreams#', export const AS = createVocabulary('https://www.w3.org/ns/activitystreams#',
'Create', 'Create',
'Delete', 'Delete',

View File

@ -0,0 +1,170 @@
import { Parser } from 'n3';
import type { Quad } from 'rdf-js';
import type { CredentialSet } from '../../../src/authentication/Credentials';
import { AcpReader } from '../../../src/authorization/AcpReader';
import { AccessMode } from '../../../src/authorization/permissions/Permissions';
import type { AuxiliaryStrategy } from '../../../src/http/auxiliary/AuxiliaryStrategy';
import { BasicRepresentation } from '../../../src/http/representation/BasicRepresentation';
import type { Representation } from '../../../src/http/representation/Representation';
import type { ResourceStore } from '../../../src/storage/ResourceStore';
import { INTERNAL_QUADS } from '../../../src/util/ContentTypes';
import { NotFoundHttpError } from '../../../src/util/errors/NotFoundHttpError';
import type { IdentifierStrategy } from '../../../src/util/identifiers/IdentifierStrategy';
import { SingleRootIdentifierStrategy } from '../../../src/util/identifiers/SingleRootIdentifierStrategy';
import { IdentifierMap, IdentifierSetMultiMap } from '../../../src/util/map/IdentifierMap';
import { joinUrl } from '../../../src/util/PathUtil';
import { SimpleSuffixStrategy } from '../../util/SimpleSuffixStrategy';
import { compareMaps } from '../../util/Util';
const acrSuffix = '.acr';
function toQuads(turtle: string, baseIRI: string): Quad[] {
baseIRI = `${baseIRI}${acrSuffix}`;
turtle = `
@prefix acp: <http://www.w3.org/ns/solid/acp#>.
@prefix acl: <http://www.w3.org/ns/auth/acl#>.
${turtle}
`;
return new Parser({ format: 'Turtle', baseIRI }).parse(turtle);
}
describe('An AcpReader', (): void => {
const baseUrl = 'http://example.com/';
let credentials: CredentialSet;
// Subject identifiers are used as keys, values are the output of their corresponding ACR resource
let dataMap: Record<string, Quad[]>;
let acrStrategy: AuxiliaryStrategy;
let acrStore: jest.Mocked<ResourceStore>;
let identifierStrategy: IdentifierStrategy;
let acpReader: AcpReader;
beforeEach(async(): Promise<void> => {
credentials = { public: {}};
dataMap = {};
acrStrategy = new SimpleSuffixStrategy(acrSuffix);
acrStore = {
getRepresentation: jest.fn((identifier): Representation => {
const subjectIdentifier = acrStrategy.getSubjectIdentifier(identifier);
if (!dataMap[subjectIdentifier.path]) {
throw new NotFoundHttpError();
}
return new BasicRepresentation(dataMap[subjectIdentifier.path], subjectIdentifier, INTERNAL_QUADS, false);
}),
} as any;
identifierStrategy = new SingleRootIdentifierStrategy(baseUrl);
acpReader = new AcpReader(acrStrategy, acrStore, identifierStrategy);
});
it('can check permissions on the root container.', async(): Promise<void> => {
const target = { path: joinUrl(baseUrl, 'foo') };
dataMap[baseUrl] = toQuads(`
[]
acp:resource <./> ;
acp:accessControl [ acp:apply _:policy ].
_:policy
acp:allow acl:Read;
acp:allOf _:matcher.
_:matcher acp:agent acp:PublicAgent.
`, baseUrl);
const requestedModes = new IdentifierSetMultiMap([
[{ path: baseUrl }, AccessMode.read ],
[ target, AccessMode.read ]]);
const expectedPermissions = new IdentifierMap([
[{ path: baseUrl }, { agent: { read: true }}],
[ target, { agent: {}}]]);
compareMaps(await acpReader.handle({ credentials, requestedModes }), expectedPermissions);
});
it('throws an error if something goes wrong reading data.', async(): Promise<void> => {
acrStore.getRepresentation.mockRejectedValueOnce(new Error('bad request'));
const requestedModes = new IdentifierSetMultiMap([[{ path: baseUrl }, AccessMode.read ]]);
await expect(acpReader.handle({ credentials, requestedModes })).rejects.toThrow('bad request');
});
it('allows for permission inheritance.', async(): Promise<void> => {
const target = { path: joinUrl(baseUrl, 'foo') };
dataMap[baseUrl] = toQuads(`
[]
acp:resource <./> ;
acp:memberAccessControl [ acp:apply _:policy ].
_:policy
acp:allow acl:Read;
acp:allOf _:matcher.
_:matcher acp:agent acp:PublicAgent.
`, baseUrl);
const requestedModes = new IdentifierSetMultiMap([
[{ path: baseUrl }, AccessMode.read ],
[ target, AccessMode.read ]]);
const expectedPermissions = new IdentifierMap([
[{ path: baseUrl }, { agent: {}}],
[ target, { agent: { read: true }}]]);
compareMaps(await acpReader.handle({ credentials, requestedModes }), expectedPermissions);
});
it('combines all relevant ACRs.', async(): Promise<void> => {
const target = { path: joinUrl(baseUrl, 'foo') };
dataMap[baseUrl] = toQuads(`
[]
acp:resource <./> ;
acp:accessControl [ acp:apply _:controlPolicy ];
acp:memberAccessControl [ acp:apply _:readPolicy ].
_:readPolicy
acp:allow acl:Read;
acp:allOf _:matcher.
_:controlPolicy
acp:allow acl:Control;
acp:allOf _:matcher.
_:matcher acp:agent acp:PublicAgent.
`, baseUrl);
dataMap[target.path] = toQuads(`
[]
acp:resource <./foo> ;
acp:accessControl [ acp:apply _:appendPolicy ].
_:appendPolicy
acp:allow acl:Append;
acp:allOf _:matcher.
_:matcher acp:agent acp:PublicAgent.
`, target.path);
const requestedModes = new IdentifierSetMultiMap([
[{ path: baseUrl }, AccessMode.read ],
[ target, AccessMode.read ]]);
const expectedPermissions = new IdentifierMap([
[{ path: baseUrl }, { agent: { control: true }}],
[ target, { agent: { read: true, append: true }}]]);
compareMaps(await acpReader.handle({ credentials, requestedModes }), expectedPermissions);
});
it('caches data to prevent duplicate ResourceStore calls.', async(): Promise<void> => {
const target1 = { path: joinUrl(baseUrl, 'foo/') };
const target2 = { path: joinUrl(baseUrl, 'foo/bar') };
dataMap[baseUrl] = toQuads(`
[]
acp:resource <./> ;
acp:memberAccessControl [ acp:apply _:policy ].
_:policy
acp:allow acl:Read;
acp:allOf _:matcher.
_:matcher acp:agent acp:PublicAgent.
`, baseUrl);
const requestedModes = new IdentifierSetMultiMap([
[{ path: baseUrl }, AccessMode.read ],
[ target1, AccessMode.read ],
[ target2, AccessMode.read ]]);
const expectedPermissions = new IdentifierMap([
[{ path: baseUrl }, { agent: {}}],
[ target1, { agent: { read: true }}],
[ target2, { agent: { read: true }}]]);
compareMaps(await acpReader.handle({ credentials, requestedModes }), expectedPermissions);
expect(acrStore.getRepresentation).toHaveBeenCalledTimes(3);
expect(acrStore.getRepresentation)
.toHaveBeenCalledWith(acrStrategy.getAuxiliaryIdentifier(target1), { type: { [INTERNAL_QUADS]: 1 }});
expect(acrStore.getRepresentation)
.toHaveBeenCalledWith(acrStrategy.getAuxiliaryIdentifier(target2), { type: { [INTERNAL_QUADS]: 1 }});
expect(acrStore.getRepresentation)
.toHaveBeenCalledWith(acrStrategy.getAuxiliaryIdentifier({ path: baseUrl }), { type: { [INTERNAL_QUADS]: 1 }});
});
});

View File

@ -0,0 +1,122 @@
import { ACL } from '@solid/access-control-policy/dist/constant/acl';
import { DataFactory, Parser, Store } from 'n3';
import {
getAccessControl,
getAccessControlledResources,
getAccessControlResource,
getMatcher,
getPolicy,
} from '../../../src/authorization/AcpUtil';
import { joinUrl } from '../../../src/util/PathUtil';
import { ACP } from '../../../src/util/Vocabularies';
import namedNode = DataFactory.namedNode;
describe('AcpUtil', (): void => {
const baseUrl = 'http://example.com/';
const data = new Store(new Parser({ format: 'Turtle', baseIRI: baseUrl }).parse(`
@prefix acp: <http://www.w3.org/ns/solid/acp#>.
@prefix acl: <http://www.w3.org/ns/auth/acl#>.
@prefix ex: <http://example.com/>.
ex:acr
acp:resource <./foo>;
acp:accessControl ex:ac;
acp:memberAccessControl ex:ac.
ex:ac acp:apply ex:policy.
ex:policy
acp:allow acl:Read, acl:Append;
acp:deny acl:Write;
acp:allOf ex:matcher;
acp:anyOf ex:matcher;
acp:noneOf ex:matcher.
ex:matcher acp:agent acp:PublicAgent, ex:agent;
acp:client ex:client;
acp:issuer ex:issuer;
acp:vc ex:vc.
`));
describe('#getMatcher', (): void => {
it('returns the relevant matcher.', async(): Promise<void> => {
expect(getMatcher(data, namedNode(`${baseUrl}matcher`))).toEqual({
iri: joinUrl(baseUrl, 'matcher'),
agent: [ `${ACP.namespace}PublicAgent`, `${baseUrl}agent` ],
client: [ `${baseUrl}client` ],
issuer: [ `${baseUrl}issuer` ],
vc: [ `${baseUrl}vc` ],
});
});
it('returns an empty matcher if no data is found.', async(): Promise<void> => {
expect(getMatcher(data, namedNode(`${baseUrl}unknown`))).toEqual({
iri: `${baseUrl}unknown`,
agent: [],
client: [],
issuer: [],
vc: [],
});
});
});
describe('#getPolicy', (): void => {
it('returns the relevant policy.', async(): Promise<void> => {
expect(getPolicy(data, namedNode(`${baseUrl}policy`))).toEqual({
iri: `${baseUrl}policy`,
allow: new Set([ ACL.Read, ACL.Append ]),
deny: new Set([ ACL.Write ]),
allOf: [ expect.objectContaining({ iri: `${baseUrl}matcher` }) ],
anyOf: [ expect.objectContaining({ iri: `${baseUrl}matcher` }) ],
noneOf: [ expect.objectContaining({ iri: `${baseUrl}matcher` }) ],
});
});
it('returns an empty policy if no data is found.', async(): Promise<void> => {
expect(getPolicy(data, namedNode(`${baseUrl}unknown`))).toEqual({
iri: `${baseUrl}unknown`,
allow: new Set(),
deny: new Set(),
allOf: [],
anyOf: [],
noneOf: [],
});
});
});
describe('#getAccessControl', (): void => {
it('returns the relevant access control.', async(): Promise<void> => {
expect(getAccessControl(data, namedNode(`${baseUrl}ac`))).toEqual({
iri: `${baseUrl}ac`,
policy: [ expect.objectContaining({ iri: `${baseUrl}policy` }) ],
});
});
it('returns an empty access control if no data is found.', async(): Promise<void> => {
expect(getAccessControl(data, namedNode(`${baseUrl}unknown`))).toEqual({
iri: `${baseUrl}unknown`,
policy: [],
});
});
});
describe('#getAccessControlResource', (): void => {
it('returns the relevant access control resource.', async(): Promise<void> => {
expect(getAccessControlResource(data, namedNode(`${baseUrl}acr`))).toEqual({
iri: `${baseUrl}acr`,
accessControl: [ expect.objectContaining({ iri: `${baseUrl}ac` }) ],
memberAccessControl: [ expect.objectContaining({ iri: `${baseUrl}ac` }) ],
});
});
it('returns an empty access control resource if no data is found.', async(): Promise<void> => {
expect(getAccessControlResource(data, namedNode(`${baseUrl}unknown`))).toEqual({
iri: `${baseUrl}unknown`,
accessControl: [],
memberAccessControl: [],
});
});
});
describe('#getAccessControlledResources', (): void => {
it('returns all access controlled resources found in the dataset.', async(): Promise<void> => {
expect([ ...getAccessControlledResources(data) ]).toEqual([{
iri: `${baseUrl}foo`,
accessControlResource: expect.objectContaining({ iri: `${baseUrl}acr` }),
}]);
});
});
});