mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Create AcpReader
This commit is contained in:
parent
b09bf66ad7
commit
a6409ad00d
11
package-lock.json
generated
11
package-lock.json
generated
@ -12,6 +12,7 @@
|
||||
"@comunica/context-entries": "^2.2.0",
|
||||
"@comunica/query-sparql": "^2.2.1",
|
||||
"@rdfjs/types": "^1.1.0",
|
||||
"@solid/access-control-policy": "^0.1.2",
|
||||
"@solid/access-token-verifier": "^2.0.3",
|
||||
"@types/async-lock": "^1.1.5",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
@ -3832,6 +3833,11 @@
|
||||
"@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": {
|
||||
"version": "2.0.3",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"@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": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@solid/access-token-verifier/-/access-token-verifier-2.0.3.tgz",
|
||||
|
@ -102,6 +102,7 @@
|
||||
"@comunica/context-entries": "^2.2.0",
|
||||
"@comunica/query-sparql": "^2.2.1",
|
||||
"@rdfjs/types": "^1.1.0",
|
||||
"@solid/access-control-policy": "^0.1.2",
|
||||
"@solid/access-token-verifier": "^2.0.3",
|
||||
"@types/async-lock": "^1.1.5",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
|
164
src/authorization/AcpReader.ts
Normal file
164
src/authorization/AcpReader.ts
Normal 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);
|
||||
}
|
||||
}
|
101
src/authorization/AcpUtil.ts
Normal file
101
src/authorization/AcpUtil.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
@ -24,6 +24,8 @@ export * from './authorization/permissions/Permissions';
|
||||
export * from './authorization/permissions/SparqlUpdateModesExtractor';
|
||||
|
||||
// Authorization
|
||||
export * from './authorization/AcpReader';
|
||||
export * from './authorization/AcpUtil';
|
||||
export * from './authorization/AllStaticReader';
|
||||
export * from './authorization/Authorizer';
|
||||
export * from './authorization/AuxiliaryReader';
|
||||
|
@ -117,6 +117,29 @@ export const ACL = createVocabulary('http://www.w3.org/ns/auth/acl#',
|
||||
'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#',
|
||||
'Create',
|
||||
'Delete',
|
||||
|
170
test/unit/authorization/AcpReader.test.ts
Normal file
170
test/unit/authorization/AcpReader.test.ts
Normal 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 }});
|
||||
});
|
||||
});
|
122
test/unit/authorization/AcpUtil.test.ts
Normal file
122
test/unit/authorization/AcpUtil.test.ts
Normal 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` }),
|
||||
}]);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user