mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00

The previous package was outdated, preventing us from updating TS. This one also lints YAML and JSON, and applies many more rules to the test files, explaining all the changes in this PR.
200 lines
6.2 KiB
TypeScript
200 lines
6.2 KiB
TypeScript
import type { IAccessControl, IAccessControlledResource, IMatcher, IPolicy } from '@solid/access-control-policy';
|
|
import { v4 } from 'uuid';
|
|
import { BasicRepresentation } from '../../src/http/representation/BasicRepresentation';
|
|
import type { ResourceIdentifier } from '../../src/http/representation/ResourceIdentifier';
|
|
import type { ResourceStore } from '../../src/storage/ResourceStore';
|
|
import { joinUrl } from '../../src/util/PathUtil';
|
|
|
|
export type CreateMatcherInput = { publicAgent: true } | { agent: string };
|
|
|
|
export type CreatePolicyInput = {
|
|
allow?: Iterable<'read' | 'append' | 'write' | 'control'>;
|
|
deny?: Iterable<'read' | 'append' | 'write' | 'control'>;
|
|
allOf?: Iterable<IMatcher>;
|
|
anyOf?: Iterable<IMatcher>;
|
|
noneOf?: Iterable<IMatcher>;
|
|
};
|
|
|
|
export type CreateAcrInput = {
|
|
resource: string | ResourceIdentifier;
|
|
policies?: Iterable<IPolicy>;
|
|
memberPolicies?: Iterable<IPolicy>;
|
|
};
|
|
|
|
const baseUrl = 'http://acp.example.com/';
|
|
|
|
/**
|
|
* Helper class for setting permissions through ACP.
|
|
*/
|
|
export class AcpHelper {
|
|
public readonly store: ResourceStore;
|
|
|
|
public constructor(store: ResourceStore) {
|
|
this.store = store;
|
|
}
|
|
|
|
public createMatcher(input: CreateMatcherInput): IMatcher {
|
|
return {
|
|
iri: joinUrl(baseUrl, v4()),
|
|
// Prefixed URI as this will be inserted into turtle below
|
|
agent: (input as any).publicAgent ? [ 'acp:PublicAgent' ] : [ (input as any).agent ],
|
|
client: [],
|
|
issuer: [],
|
|
vc: [],
|
|
};
|
|
}
|
|
|
|
public createPolicy({ allow, deny, allOf, anyOf, noneOf }: CreatePolicyInput): IPolicy {
|
|
return {
|
|
iri: joinUrl(baseUrl, v4()),
|
|
// Using the wrong identifiers so the turtle generated below uses the prefixed version
|
|
allow: new Set(this.convertModes(allow ?? []) as any),
|
|
deny: new Set(this.convertModes(deny ?? []) as any),
|
|
allOf: [ ...allOf ?? [] ],
|
|
anyOf: [ ...anyOf ?? [] ],
|
|
noneOf: [ ...noneOf ?? [] ],
|
|
};
|
|
}
|
|
|
|
private* convertModes<T extends string>(modes: Iterable<T>):
|
|
Iterable<`acl:${Capitalize<T>}`> {
|
|
for (const mode of modes) {
|
|
// Node.js typings aren't fancy enough yet to correctly type this
|
|
yield `acl:${mode.charAt(0).toUpperCase() + mode.slice(1)}` as any;
|
|
}
|
|
}
|
|
|
|
public createAcr({ resource, policies, memberPolicies }: CreateAcrInput): IAccessControlledResource {
|
|
return {
|
|
iri: (resource as ResourceIdentifier).path ?? resource,
|
|
accessControlResource: {
|
|
iri: joinUrl(baseUrl, v4()),
|
|
accessControl: policies ?
|
|
[{
|
|
iri: joinUrl(baseUrl, v4()),
|
|
policy: [ ...policies ],
|
|
}] :
|
|
[],
|
|
memberAccessControl: memberPolicies ?
|
|
[{
|
|
iri: joinUrl(baseUrl, v4()),
|
|
policy: [ ...memberPolicies ],
|
|
}] :
|
|
[],
|
|
},
|
|
};
|
|
}
|
|
|
|
public async setAcp(id: string | ResourceIdentifier,
|
|
resources: IAccessControlledResource[] | IAccessControlledResource): Promise<void> {
|
|
const turtle = this.toTurtle(resources);
|
|
await this.store.setRepresentation({ path: `${(id as ResourceIdentifier).path ?? id}.acr` },
|
|
new BasicRepresentation(turtle, 'text/turtle'));
|
|
}
|
|
|
|
public toTurtle(resources: IAccessControlledResource[] | IAccessControlledResource): string {
|
|
if (!Array.isArray(resources)) {
|
|
resources = [ resources ];
|
|
}
|
|
const result: string[] = [
|
|
'@prefix acp: <http://www.w3.org/ns/solid/acp#>.',
|
|
'@prefix acl: <http://www.w3.org/ns/auth/acl#>.',
|
|
];
|
|
|
|
const added = new Set<string>();
|
|
const acs: IAccessControl[] = [];
|
|
const policies: IPolicy[] = [];
|
|
const matchers: IMatcher[] = [];
|
|
|
|
for (const resource of resources) {
|
|
result.push(`<${resource.accessControlResource.iri}> a acp:AccessControlResource`);
|
|
result.push(` ; acp:resource <${resource.iri}>`);
|
|
for (const key of [ 'accessControl', 'memberAccessControl' ] as const) {
|
|
if (resource.accessControlResource[key].length > 0) {
|
|
result.push(` ; acp:${key} ${resource.accessControlResource[key].map((ac): string => {
|
|
acs.push(ac);
|
|
return `<${ac.iri}>`;
|
|
}).join(', ')}`);
|
|
}
|
|
}
|
|
result.push(' .');
|
|
}
|
|
|
|
for (const ac of acs) {
|
|
if (added.has(ac.iri)) {
|
|
continue;
|
|
}
|
|
|
|
result.push(`<${ac.iri}> a acp:AccessControl`);
|
|
result.push(` ; acp:apply ${ac.policy.map((policy): string => {
|
|
policies.push(policy);
|
|
return `<${policy.iri}>`;
|
|
}).join(', ')}`);
|
|
result.push(' .');
|
|
added.add(ac.iri);
|
|
}
|
|
|
|
for (const policy of policies) {
|
|
if (added.has(policy.iri)) {
|
|
continue;
|
|
}
|
|
|
|
const { policyString, requiredMatchers } = this.policyToTurtle(policy);
|
|
result.push(policyString);
|
|
matchers.push(...requiredMatchers);
|
|
added.add(policy.iri);
|
|
}
|
|
|
|
for (const matcher of matchers) {
|
|
if (added.has(matcher.iri)) {
|
|
continue;
|
|
}
|
|
|
|
result.push(this.matcherToTurtle(matcher));
|
|
added.add(matcher.iri);
|
|
}
|
|
|
|
return result.join('\n');
|
|
}
|
|
|
|
private policyToTurtle(policy: IPolicy): { policyString: string; requiredMatchers: IMatcher[] } {
|
|
const result: string[] = [];
|
|
|
|
result.push(`<${policy.iri}> a acp:Policy`);
|
|
|
|
for (const key of [ 'allow', 'deny' ] as const) {
|
|
if (policy[key].size > 0) {
|
|
result.push(` ; acp:${key} ${[ ...policy[key] ].join(', ')}`);
|
|
}
|
|
}
|
|
|
|
const requiredMatchers: IMatcher[] = [];
|
|
for (const key of [ 'allOf', 'anyOf', 'noneOf' ] as const) {
|
|
if (policy[key].length > 0) {
|
|
result.push(` ; acp:${key} ${policy[key].map((matcher): string => {
|
|
requiredMatchers.push(matcher);
|
|
return `<${matcher.iri}>`;
|
|
}).join(', ')}`);
|
|
}
|
|
}
|
|
|
|
result.push(' .');
|
|
|
|
return { policyString: result.join('\n'), requiredMatchers };
|
|
}
|
|
|
|
private matcherToTurtle(matcher: IMatcher): string {
|
|
const result: string[] = [];
|
|
|
|
result.push(`<${matcher.iri}> a acp:Matcher`);
|
|
for (const key of [ 'agent', 'client', 'issuer', 'vc' ] as const) {
|
|
if (matcher[key].length > 0) {
|
|
result.push(` ; acp:${key} ${matcher[key].join(', ')}`);
|
|
}
|
|
}
|
|
result.push(' .');
|
|
|
|
return result.join('\n');
|
|
}
|
|
}
|