Skip to content

Commit 7085252

Browse files
committed
feat: Update ModesExtractors to support new permission interface
1 parent 23f0b37 commit 7085252

8 files changed

+147
-94
lines changed

src/authorization/permissions/MethodModesExtractor.ts

+11-9
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import type { Operation } from '../../http/Operation';
22
import type { ResourceSet } from '../../storage/ResourceSet';
33
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
4+
import { IdentifierSetMultiMap } from '../../util/map/IdentifierMap';
45
import { isContainerIdentifier } from '../../util/PathUtil';
56
import { ModesExtractor } from './ModesExtractor';
7+
import type { AccessMap } from './Permissions';
68
import { AccessMode } from './Permissions';
79

810
const READ_METHODS = new Set([ 'OPTIONS', 'GET', 'HEAD' ]);
@@ -31,33 +33,33 @@ export class MethodModesExtractor extends ModesExtractor {
3133
}
3234
}
3335

34-
public async handle({ method, target }: Operation): Promise<Set<AccessMode>> {
35-
const modes = new Set<AccessMode>();
36+
public async handle({ method, target }: Operation): Promise<AccessMap> {
37+
const requiredModes: AccessMap = new IdentifierSetMultiMap();
3638
// Reading requires Read permissions on the resource
3739
if (READ_METHODS.has(method)) {
38-
modes.add(AccessMode.read);
40+
requiredModes.add(target, AccessMode.read);
3941
}
4042
// Setting a resource's representation requires Write permissions
4143
if (method === 'PUT') {
42-
modes.add(AccessMode.write);
44+
requiredModes.add(target, AccessMode.write);
4345
// …and, if the resource does not exist yet, Create permissions are required as well
4446
if (!await this.resourceSet.hasResource(target)) {
45-
modes.add(AccessMode.create);
47+
requiredModes.add(target, AccessMode.create);
4648
}
4749
}
4850
// Creating a new resource in a container requires Append access to that container
4951
if (method === 'POST') {
50-
modes.add(AccessMode.append);
52+
requiredModes.add(target, AccessMode.append);
5153
}
5254
// Deleting a resource requires Delete access
5355
if (method === 'DELETE') {
54-
modes.add(AccessMode.delete);
56+
requiredModes.add(target, AccessMode.delete);
5557
// …and, if the target is a container, Read permissions are required as well
5658
// as this exposes if a container is empty or not
5759
if (isContainerIdentifier(target)) {
58-
modes.add(AccessMode.read);
60+
requiredModes.add(target, AccessMode.read);
5961
}
6062
}
61-
return modes;
63+
return requiredModes;
6264
}
6365
}

src/authorization/permissions/N3PatchModesExtractor.ts

+10-8
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import type { N3Patch } from '../../http/representation/N3Patch';
33
import { isN3Patch } from '../../http/representation/N3Patch';
44
import type { ResourceSet } from '../../storage/ResourceSet';
55
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
6+
import { IdentifierSetMultiMap } from '../../util/map/IdentifierMap';
67
import { ModesExtractor } from './ModesExtractor';
8+
import type { AccessMap } from './Permissions';
79
import { AccessMode } from './Permissions';
810

911
/**
@@ -33,28 +35,28 @@ export class N3PatchModesExtractor extends ModesExtractor {
3335
}
3436
}
3537

36-
public async handle({ body, target }: Operation): Promise<Set<AccessMode>> {
38+
public async handle({ body, target }: Operation): Promise<AccessMap> {
3739
const { deletes, inserts, conditions } = body as N3Patch;
3840

39-
const accessModes = new Set<AccessMode>();
41+
const requiredModes: AccessMap = new IdentifierSetMultiMap();
4042

4143
// When ?conditions is non-empty, servers MUST treat the request as a Read operation.
4244
if (conditions.length > 0) {
43-
accessModes.add(AccessMode.read);
45+
requiredModes.add(target, AccessMode.read);
4446
}
4547
// When ?insertions is non-empty, servers MUST (also) treat the request as an Append operation.
4648
if (inserts.length > 0) {
47-
accessModes.add(AccessMode.append);
49+
requiredModes.add(target, AccessMode.append);
4850
if (!await this.resourceSet.hasResource(target)) {
49-
accessModes.add(AccessMode.create);
51+
requiredModes.add(target, AccessMode.create);
5052
}
5153
}
5254
// When ?deletions is non-empty, servers MUST treat the request as a Read and Write operation.
5355
if (deletes.length > 0) {
54-
accessModes.add(AccessMode.read);
55-
accessModes.add(AccessMode.write);
56+
requiredModes.add(target, AccessMode.read);
57+
requiredModes.add(target, AccessMode.write);
5658
}
5759

58-
return accessModes;
60+
return requiredModes;
5961
}
6062
}

src/authorization/permissions/SparqlUpdateModesExtractor.ts

+11-9
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import type { Representation } from '../../http/representation/Representation';
44
import type { SparqlUpdatePatch } from '../../http/representation/SparqlUpdatePatch';
55
import type { ResourceSet } from '../../storage/ResourceSet';
66
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
7+
import { IdentifierSetMultiMap } from '../../util/map/IdentifierMap';
78
import { ModesExtractor } from './ModesExtractor';
9+
import type { AccessMap } from './Permissions';
810
import { AccessMode } from './Permissions';
911

1012
/**
@@ -34,31 +36,31 @@ export class SparqlUpdateModesExtractor extends ModesExtractor {
3436
}
3537
}
3638

37-
public async handle({ body, target }: Operation): Promise<Set<AccessMode>> {
39+
public async handle({ body, target }: Operation): Promise<AccessMap> {
3840
// Verified in `canHandle` call
3941
const update = (body as SparqlUpdatePatch).algebra as Algebra.DeleteInsert;
40-
const modes = new Set<AccessMode>();
42+
const requiredModes: AccessMap = new IdentifierSetMultiMap();
4143

4244
if (this.isNop(update)) {
43-
return modes;
45+
return requiredModes;
4446
}
4547

4648
// Access modes inspired by the requirements on N3 Patch requests
4749
if (this.hasConditions(update)) {
48-
modes.add(AccessMode.read);
50+
requiredModes.add(target, AccessMode.read);
4951
}
5052
if (this.hasInserts(update)) {
51-
modes.add(AccessMode.append);
53+
requiredModes.add(target, AccessMode.append);
5254
if (!await this.resourceSet.hasResource(target)) {
53-
modes.add(AccessMode.create);
55+
requiredModes.add(target, AccessMode.create);
5456
}
5557
}
5658
if (this.hasDeletes(update)) {
57-
modes.add(AccessMode.read);
58-
modes.add(AccessMode.write);
59+
requiredModes.add(target, AccessMode.read);
60+
requiredModes.add(target, AccessMode.write);
5961
}
6062

61-
return modes;
63+
return requiredModes;
6264
}
6365

6466
private isSparql(data: Representation): data is SparqlUpdatePatch {

src/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@ export * from './authorization/access/AgentClassAccessChecker';
1515
export * from './authorization/access/AgentGroupAccessChecker';
1616

1717
// Authorization/Permissions
18-
export * from './authorization/permissions/Permissions';
18+
export * from './authorization/permissions/AclPermission';
1919
export * from './authorization/permissions/ModesExtractor';
2020
export * from './authorization/permissions/MethodModesExtractor';
2121
export * from './authorization/permissions/N3PatchModesExtractor';
22+
export * from './authorization/permissions/Permissions';
2223
export * from './authorization/permissions/SparqlUpdateModesExtractor';
2324

2425
// Authorization
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,31 @@
11
import { MethodModesExtractor } from '../../../../src/authorization/permissions/MethodModesExtractor';
2+
import type { AccessMap } from '../../../../src/authorization/permissions/Permissions';
23
import { AccessMode } from '../../../../src/authorization/permissions/Permissions';
34
import type { Operation } from '../../../../src/http/Operation';
5+
import { BasicRepresentation } from '../../../../src/http/representation/BasicRepresentation';
6+
import type { ResourceIdentifier } from '../../../../src/http/representation/ResourceIdentifier';
47
import type { ResourceSet } from '../../../../src/storage/ResourceSet';
58
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
9+
import { IdentifierSetMultiMap } from '../../../../src/util/map/IdentifierMap';
10+
import { compareMaps } from '../../../util/Util';
611

712
describe('A MethodModesExtractor', (): void => {
13+
const target: ResourceIdentifier = { path: 'http://example.com/foo' };
14+
const operation: Operation = {
15+
method: 'GET',
16+
target,
17+
preferences: {},
18+
body: new BasicRepresentation(),
19+
};
820
let resourceSet: jest.Mocked<ResourceSet>;
921
let extractor: MethodModesExtractor;
1022

23+
function getMap(modes: AccessMode[], identifier?: ResourceIdentifier): AccessMap {
24+
return new IdentifierSetMultiMap(
25+
modes.map((mode): [ResourceIdentifier, AccessMode] => [ identifier ?? target, mode ]),
26+
);
27+
}
28+
1129
beforeEach(async(): Promise<void> => {
1230
resourceSet = {
1331
hasResource: jest.fn().mockResolvedValue(true),
@@ -16,44 +34,43 @@ describe('A MethodModesExtractor', (): void => {
1634
});
1735

1836
it('can handle HEAD/GET/POST/PUT/DELETE.', async(): Promise<void> => {
19-
await expect(extractor.canHandle({ method: 'HEAD' } as Operation)).resolves.toBeUndefined();
20-
await expect(extractor.canHandle({ method: 'GET' } as Operation)).resolves.toBeUndefined();
21-
await expect(extractor.canHandle({ method: 'POST' } as Operation)).resolves.toBeUndefined();
22-
await expect(extractor.canHandle({ method: 'PUT' } as Operation)).resolves.toBeUndefined();
23-
await expect(extractor.canHandle({ method: 'DELETE' } as Operation)).resolves.toBeUndefined();
24-
await expect(extractor.canHandle({ method: 'PATCH' } as Operation)).rejects.toThrow(NotImplementedHttpError);
37+
await expect(extractor.canHandle({ ...operation, method: 'HEAD' })).resolves.toBeUndefined();
38+
await expect(extractor.canHandle({ ...operation, method: 'GET' })).resolves.toBeUndefined();
39+
await expect(extractor.canHandle({ ...operation, method: 'POST' })).resolves.toBeUndefined();
40+
await expect(extractor.canHandle({ ...operation, method: 'PUT' })).resolves.toBeUndefined();
41+
await expect(extractor.canHandle({ ...operation, method: 'DELETE' })).resolves.toBeUndefined();
42+
await expect(extractor.canHandle({ ...operation, method: 'PATCH' })).rejects.toThrow(NotImplementedHttpError);
2543
});
2644

2745
it('requires read for HEAD operations.', async(): Promise<void> => {
28-
await expect(extractor.handle({ method: 'HEAD' } as Operation)).resolves.toEqual(new Set([ AccessMode.read ]));
46+
compareMaps(await extractor.handle({ ...operation, method: 'HEAD' }), getMap([ AccessMode.read ]));
2947
});
3048

3149
it('requires read for GET operations.', async(): Promise<void> => {
32-
await expect(extractor.handle({ method: 'GET' } as Operation)).resolves.toEqual(new Set([ AccessMode.read ]));
50+
compareMaps(await extractor.handle({ ...operation, method: 'GET' }), getMap([ AccessMode.read ]));
3351
});
3452

3553
it('requires append for POST operations.', async(): Promise<void> => {
36-
await expect(extractor.handle({ method: 'POST' } as Operation)).resolves.toEqual(new Set([ AccessMode.append ]));
54+
compareMaps(await extractor.handle({ ...operation, method: 'POST' }), getMap([ AccessMode.append ]));
3755
});
3856

3957
it('requires write for PUT operations.', async(): Promise<void> => {
40-
await expect(extractor.handle({ method: 'PUT' } as Operation))
41-
.resolves.toEqual(new Set([ AccessMode.write ]));
58+
compareMaps(await extractor.handle({ ...operation, method: 'PUT' }), getMap([ AccessMode.write ]));
4259
});
4360

4461
it('requires create for PUT operations if the target does not exist.', async(): Promise<void> => {
4562
resourceSet.hasResource.mockResolvedValueOnce(false);
46-
await expect(extractor.handle({ method: 'PUT' } as Operation))
47-
.resolves.toEqual(new Set([ AccessMode.write, AccessMode.create ]));
63+
compareMaps(await extractor.handle({ ...operation, method: 'PUT' }),
64+
getMap([ AccessMode.write, AccessMode.create ]));
4865
});
4966

5067
it('requires delete for DELETE operations.', async(): Promise<void> => {
51-
await expect(extractor.handle({ method: 'DELETE', target: { path: 'http://example.com/foo' }} as Operation))
52-
.resolves.toEqual(new Set([ AccessMode.delete ]));
68+
compareMaps(await extractor.handle({ ...operation, method: 'DELETE' }), getMap([ AccessMode.delete ]));
5369
});
5470

5571
it('also requires read for DELETE operations on containers.', async(): Promise<void> => {
56-
await expect(extractor.handle({ method: 'DELETE', target: { path: 'http://example.com/foo/' }} as Operation))
57-
.resolves.toEqual(new Set([ AccessMode.delete, AccessMode.read ]));
72+
const identifier = { path: 'http://example.com/foo/' };
73+
compareMaps(await extractor.handle({ ...operation, method: 'DELETE', target: identifier }),
74+
getMap([ AccessMode.delete, AccessMode.read ], identifier));
5875
});
5976
});

test/unit/authorization/permissions/N3PatchModesExtractor.test.ts

+17-7
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,33 @@
11
import { DataFactory } from 'n3';
22
import type { Quad } from 'rdf-js';
33
import { N3PatchModesExtractor } from '../../../../src/authorization/permissions/N3PatchModesExtractor';
4+
import type { AccessMap } from '../../../../src/authorization/permissions/Permissions';
45
import { AccessMode } from '../../../../src/authorization/permissions/Permissions';
56
import type { Operation } from '../../../../src/http/Operation';
67
import { BasicRepresentation } from '../../../../src/http/representation/BasicRepresentation';
78
import type { N3Patch } from '../../../../src/http/representation/N3Patch';
9+
import type { ResourceIdentifier } from '../../../../src/http/representation/ResourceIdentifier';
810
import type { ResourceSet } from '../../../../src/storage/ResourceSet';
911
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
12+
import { IdentifierSetMultiMap } from '../../../../src/util/map/IdentifierMap';
13+
import { compareMaps } from '../../../util/Util';
1014

1115
const { quad, namedNode } = DataFactory;
1216

1317
describe('An N3PatchModesExtractor', (): void => {
18+
const target: ResourceIdentifier = { path: 'http://example.com/foo' };
1419
const triple: Quad = quad(namedNode('a'), namedNode('b'), namedNode('c'));
1520
let patch: N3Patch;
1621
let operation: Operation;
1722
let resourceSet: jest.Mocked<ResourceSet>;
1823
let extractor: N3PatchModesExtractor;
1924

25+
function getMap(modes: AccessMode[], identifier?: ResourceIdentifier): AccessMap {
26+
return new IdentifierSetMultiMap(
27+
modes.map((mode): [ResourceIdentifier, AccessMode] => [ identifier ?? target, mode ]),
28+
);
29+
}
30+
2031
beforeEach(async(): Promise<void> => {
2132
patch = new BasicRepresentation() as N3Patch;
2233
patch.deletes = [];
@@ -27,7 +38,7 @@ describe('An N3PatchModesExtractor', (): void => {
2738
method: 'PATCH',
2839
body: patch,
2940
preferences: {},
30-
target: { path: 'http://example.com/foo' },
41+
target,
3142
};
3243

3344
resourceSet = {
@@ -47,30 +58,29 @@ describe('An N3PatchModesExtractor', (): void => {
4758

4859
it('requires read access when there are conditions.', async(): Promise<void> => {
4960
patch.conditions = [ triple ];
50-
await expect(extractor.handle(operation)).resolves.toEqual(new Set([ AccessMode.read ]));
61+
compareMaps(await extractor.handle(operation), getMap([ AccessMode.read ]));
5162
});
5263

5364
it('requires append access when there are inserts.', async(): Promise<void> => {
5465
patch.inserts = [ triple ];
55-
await expect(extractor.handle(operation)).resolves.toEqual(new Set([ AccessMode.append ]));
66+
compareMaps(await extractor.handle(operation), getMap([ AccessMode.append ]));
5667
});
5768

5869
it('requires create access when there are inserts and the resource does not exist.', async(): Promise<void> => {
5970
resourceSet.hasResource.mockResolvedValueOnce(false);
6071
patch.inserts = [ triple ];
61-
await expect(extractor.handle(operation)).resolves.toEqual(new Set([ AccessMode.append, AccessMode.create ]));
72+
compareMaps(await extractor.handle(operation), getMap([ AccessMode.append, AccessMode.create ]));
6273
});
6374

6475
it('requires read and write access when there are inserts.', async(): Promise<void> => {
6576
patch.deletes = [ triple ];
66-
await expect(extractor.handle(operation)).resolves.toEqual(new Set([ AccessMode.read, AccessMode.write ]));
77+
compareMaps(await extractor.handle(operation), getMap([ AccessMode.read, AccessMode.write ]));
6778
});
6879

6980
it('combines required access modes when required.', async(): Promise<void> => {
7081
patch.conditions = [ triple ];
7182
patch.inserts = [ triple ];
7283
patch.deletes = [ triple ];
73-
await expect(extractor.handle(operation)).resolves
74-
.toEqual(new Set([ AccessMode.read, AccessMode.append, AccessMode.write ]));
84+
compareMaps(await extractor.handle(operation), getMap([ AccessMode.read, AccessMode.append, AccessMode.write ]));
7585
});
7686
});

0 commit comments

Comments
 (0)