feat: Remove agent/user permission differentiation

This was only used for the WAC-Allow header and greatly simplifies how we use permissions.
This commit is contained in:
Joachim Van Herwegen
2022-11-18 14:01:06 +01:00
parent 6ad5c0c797
commit c46d01d3d7
28 changed files with 220 additions and 276 deletions

View File

@@ -1,7 +1,7 @@
import 'jest-rdf';
import { fetch } from 'cross-fetch';
import { Parser } from 'n3';
import type { AclPermission } from '../../src/authorization/permissions/AclPermission';
import type { AclPermissionSet } from '../../src/authorization/permissions/AclPermissionSet';
import { BasicRepresentation } from '../../src/http/representation/BasicRepresentation';
import type { App } from '../../src/init/App';
import type { ResourceStore } from '../../src/storage/ResourceStore';
@@ -59,7 +59,7 @@ async function expectPatch(
}
// Creates/updates a resource with the given data and permissions
async function setResource(path: string, turtle: string, permissions: AclPermission): Promise<void> {
async function setResource(path: string, turtle: string, permissions: AclPermissionSet): Promise<void> {
const url = joinUrl(baseUrl, path);
await store.setRepresentation({ path: url }, new BasicRepresentation(turtle, 'text/turtle'));
await aclHelper.setSimpleAcl(url, { permissions, agentClass: 'agent', accessTo: true });

View File

@@ -1,6 +1,6 @@
import fetch from 'cross-fetch';
import { v4 } from 'uuid';
import type { AclPermission } from '../../src/authorization/permissions/AclPermission';
import type { AclPermissionSet } from '../../src/authorization/permissions/AclPermissionSet';
import { AccessMode as AM } from '../../src/authorization/permissions/Permissions';
import { BasicRepresentation } from '../../src/http/representation/BasicRepresentation';
import type { App } from '../../src/init/App';
@@ -120,12 +120,12 @@ const table: [string, string, AM[], AM[] | undefined, string, string, number, nu
];
/* eslint-enable no-multi-spaces */
function toPermission(modes: AM[]): AclPermission {
function toPermission(modes: AM[]): AclPermissionSet {
return Object.fromEntries(modes.map((mode): [AM, boolean] => [ mode, true ]));
}
async function setWebAclPermissions(store: ResourceStore, target: string, permissions: AclPermission,
childPermissions: AclPermission): Promise<void> {
async function setWebAclPermissions(store: ResourceStore, target: string, permissions: AclPermissionSet,
childPermissions: AclPermissionSet): Promise<void> {
const aclHelper = new AclHelper(store);
await aclHelper.setSimpleAcl(target, [
{ permissions, agentClass: 'agent', accessTo: true },
@@ -133,8 +133,8 @@ async function setWebAclPermissions(store: ResourceStore, target: string, permis
]);
}
async function setAcpPermissions(store: ResourceStore, target: string, permissions: AclPermission,
childPermissions: AclPermission): Promise<void> {
async function setAcpPermissions(store: ResourceStore, target: string, permissions: AclPermissionSet,
childPermissions: AclPermissionSet): Promise<void> {
const acpHelper = new AcpHelper(store);
const publicMatcher = acpHelper.createMatcher({ publicAgent: true });
const policies = [ acpHelper.createPolicy({
@@ -157,7 +157,7 @@ const port = getPort('PermissionTable');
const baseUrl = `http://localhost:${port}/`;
type AuthFunctionType = (store: ResourceStore, target: string,
permissions: AclPermission, childPermissions: AclPermission) => Promise<void>;
permissions: AclPermissionSet, childPermissions: AclPermissionSet) => Promise<void>;
const rootFilePath = getTestFolder('permissionTable');
const stores: [string, string, { configs: string[]; authFunction: AuthFunctionType; teardown: () => Promise<void> }][] =

View File

@@ -74,8 +74,8 @@ describe('An AcpReader', (): void => {
[{ path: baseUrl }, AccessMode.read ],
[ target, AccessMode.read ]]);
const expectedPermissions = new IdentifierMap([
[{ path: baseUrl }, { agent: { read: true }}],
[ target, { agent: {}}]]);
[{ path: baseUrl }, { read: true }],
[ target, {}]]);
compareMaps(await acpReader.handle({ credentials, requestedModes }), expectedPermissions);
});
@@ -100,8 +100,8 @@ describe('An AcpReader', (): void => {
[{ path: baseUrl }, AccessMode.read ],
[ target, AccessMode.read ]]);
const expectedPermissions = new IdentifierMap([
[{ path: baseUrl }, { agent: {}}],
[ target, { agent: { read: true }}]]);
[{ path: baseUrl }, {}],
[ target, { read: true }]]);
compareMaps(await acpReader.handle({ credentials, requestedModes }), expectedPermissions);
});
@@ -133,8 +133,8 @@ describe('An AcpReader', (): void => {
[{ path: baseUrl }, AccessMode.read ],
[ target, AccessMode.read ]]);
const expectedPermissions = new IdentifierMap([
[{ path: baseUrl }, { agent: { control: true }}],
[ target, { agent: { read: true, append: true }}]]);
[{ path: baseUrl }, { control: true }],
[ target, { read: true, append: true }]]);
compareMaps(await acpReader.handle({ credentials, requestedModes }), expectedPermissions);
});
@@ -155,9 +155,9 @@ describe('An AcpReader', (): void => {
[ target1, AccessMode.read ],
[ target2, AccessMode.read ]]);
const expectedPermissions = new IdentifierMap([
[{ path: baseUrl }, { agent: {}}],
[ target1, { agent: { read: true }}],
[ target2, { agent: { read: true }}]]);
[{ path: baseUrl }, {}],
[ target1, { read: true }],
[ target2, { read: true }]]);
compareMaps(await acpReader.handle({ credentials, requestedModes }), expectedPermissions);
expect(acrStore.getRepresentation).toHaveBeenCalledTimes(3);
expect(acrStore.getRepresentation)
@@ -182,7 +182,7 @@ describe('An AcpReader', (): void => {
acp:issuer <http://example.com/idp>.
`, baseUrl);
const requestedModes = new IdentifierSetMultiMap([[{ path: baseUrl }, AccessMode.read ]]);
let expectedPermissions = new IdentifierMap([[{ path: baseUrl }, { agent: {}}]]);
let expectedPermissions = new IdentifierMap([[{ path: baseUrl }, {}]]);
compareMaps(await acpReader.handle({ credentials, requestedModes }), expectedPermissions);
credentials = {
@@ -190,7 +190,7 @@ describe('An AcpReader', (): void => {
client: { clientId: 'http://client.example.com/#me' },
issuer: { url: 'http://example.com/idp' },
};
expectedPermissions = new IdentifierMap([[{ path: baseUrl }, { agent: { read: true }}]]);
expectedPermissions = new IdentifierMap([[{ path: baseUrl }, { read: true }]]);
compareMaps(await acpReader.handle({ credentials, requestedModes }), expectedPermissions);
});
});

View File

@@ -1,10 +1,10 @@
import { AllStaticReader } from '../../../src/authorization/AllStaticReader';
import type { Permission } from '../../../src/authorization/permissions/Permissions';
import type { PermissionSet } from '../../../src/authorization/permissions/Permissions';
import { AccessMode } from '../../../src/authorization/permissions/Permissions';
import { IdentifierMap, IdentifierSetMultiMap } from '../../../src/util/map/IdentifierMap';
import { compareMaps } from '../../util/Util';
function getPermissions(allow: boolean): Permission {
function getPermissions(allow: boolean): PermissionSet {
return {
read: allow,
write: allow,
@@ -27,13 +27,11 @@ describe('An AllStaticReader', (): void => {
let authorizer = new AllStaticReader(true);
const requestedModes = new IdentifierSetMultiMap<AccessMode>([[ identifier, AccessMode.read ]]);
let result = await authorizer.handle({ credentials, requestedModes });
compareMaps(result, new IdentifierMap([[ identifier, { public: getPermissions(true),
agent: getPermissions(true) }]]));
compareMaps(result, new IdentifierMap([[ identifier, getPermissions(true) ]]));
authorizer = new AllStaticReader(false);
result = await authorizer.handle({ credentials, requestedModes });
compareMaps(result, new IdentifierMap([[ identifier, { public: getPermissions(false),
agent: getPermissions(false) }]]));
compareMaps(result, new IdentifierMap([[ identifier, getPermissions(false) ]]));
});
});

View File

@@ -1,7 +1,7 @@
import type { Credentials } from '../../../src/authentication/Credentials';
import { AuthAuxiliaryReader } from '../../../src/authorization/AuthAuxiliaryReader';
import type { PermissionReader } from '../../../src/authorization/PermissionReader';
import { AclMode } from '../../../src/authorization/permissions/AclPermission';
import { AclMode } from '../../../src/authorization/permissions/AclPermissionSet';
import type { AccessMap, PermissionMap, PermissionSet } from '../../../src/authorization/permissions/Permissions';
import { AccessMode } from '../../../src/authorization/permissions/Permissions';
import type { AuxiliaryStrategy } from '../../../src/http/auxiliary/AuxiliaryStrategy';
@@ -40,10 +40,10 @@ describe('An AuthAuxiliaryReader', (): void => {
it('requires control permissions on the subject resource to do everything.', async(): Promise<void> => {
requestedModes.set(acl1, AccessMode.read);
requestedModes.set(acl2, AccessMode.read);
sourceResult.set(subject1, { public: { control: true }} as PermissionSet);
sourceResult.set(subject1, { control: true } as PermissionSet);
const result = await reader.handle({ requestedModes, credentials });
expect(result.get(acl1)).toEqual({ public: { read: true, append: true, write: true, control: true }});
expect(result.get(acl1)).toEqual({ read: true, append: true, write: true, control: true });
expect(result.get(acl2)).toEqual({ });
const updatedMap = new IdentifierMap();
@@ -59,10 +59,10 @@ describe('An AuthAuxiliaryReader', (): void => {
requestedModes.set(acl1, AccessMode.read);
requestedModes.set(subject1, AccessMode.write);
const resultSet = { public: { read: true, write: true, control: true }} as PermissionSet;
const resultSet = { read: true, write: true, control: true } as PermissionSet;
sourceResult.set(subject1, resultSet);
const resultMap: PermissionMap = new IdentifierMap([
[ acl1, { public: { read: true, write: true, control: true, append: true }} as PermissionSet ],
[ acl1, { read: true, write: true, control: true, append: true } as PermissionSet ],
[ subject1, resultSet ],
]);
compareMaps(await reader.handle({ credentials, requestedModes }), resultMap);

View File

@@ -15,7 +15,7 @@ describe('An AuxiliaryReader', (): void => {
const subjectIdentifier = { path: 'http://test.com/foo' };
const auxiliaryIdentifier1 = { path: 'http://test.com/foo.dummy1' };
const auxiliaryIdentifier2 = { path: 'http://test.com/foo.dummy2' };
const permissionSet: PermissionSet = { agent: { read: true }};
const permissionSet: PermissionSet = { read: true };
let source: jest.Mocked<PermissionReader>;
let strategy: jest.Mocked<AuxiliaryStrategy>;
let reader: AuxiliaryReader;
@@ -76,7 +76,7 @@ describe('An AuxiliaryReader', (): void => {
[ auxiliaryIdentifier2, AccessMode.read ],
[ subjectIdentifier, AccessMode.delete ],
]);
const resultSet = { agent: { read: true, write: true, delete: true }};
const resultSet = { read: true, write: true, delete: true };
source.handleSafe.mockResolvedValueOnce(new IdentifierMap([[ subjectIdentifier, resultSet ]]));
const permissionMap: PermissionMap = new IdentifierMap([
[ subjectIdentifier, resultSet ],

View File

@@ -1,6 +1,6 @@
import type { Credentials } from '../../../src/authentication/Credentials';
import { OwnerPermissionReader } from '../../../src/authorization/OwnerPermissionReader';
import { AclMode } from '../../../src/authorization/permissions/AclPermission';
import { AclMode } from '../../../src/authorization/permissions/AclPermissionSet';
import type { AccessMap } from '../../../src/authorization/permissions/Permissions';
import type { AuxiliaryIdentifierStrategy } from '../../../src/http/auxiliary/AuxiliaryIdentifierStrategy';
import type { ResourceIdentifier } from '../../../src/http/representation/ResourceIdentifier';
@@ -81,14 +81,14 @@ describe('An OwnerPermissionReader', (): void => {
it('returns full permissions if the owner is accessing an ACL resource in their pod.', async(): Promise<void> => {
compareMaps(await reader.handle({ credentials, requestedModes }), new IdentifierMap([[
identifier,
{ agent: {
{
read: true,
write: true,
append: true,
create: true,
delete: true,
control: true,
}},
},
]]));
});
});

View File

@@ -26,7 +26,7 @@ describe('A ParentContainerReader', (): void => {
beforeEach(async(): Promise<void> => {
requestedModes = new IdentifierSetMultiMap();
sourceResult = new IdentifierMap([[{ path: joinUrl(baseUrl, 'test') }, { public: { read: true }}]]);
sourceResult = new IdentifierMap([[{ path: joinUrl(baseUrl, 'test') }, { read: true }]]);
source = { handleSafe: jest.fn().mockResolvedValue(sourceResult) } as any;
reader = new ParentContainerReader(source, identifierStrategy);
@@ -35,10 +35,10 @@ describe('A ParentContainerReader', (): void => {
it('requires parent append permissions to create resources.', async(): Promise<void> => {
requestedModes.set(target1, new Set([ AccessMode.create ]));
requestedModes.set(target2, new Set([ AccessMode.create ]));
sourceResult.set(parent1, { public: { append: true }});
sourceResult.set(parent1, { append: true });
const result = await reader.handle({ requestedModes, credentials });
expect(result.get(target1)).toEqual({ public: { create: true }});
expect(result.get(target1)).toEqual({ create: true });
expect(result.get(target2)).toEqual({ });
const updatedMap = new IdentifierSetMultiMap(requestedModes);
@@ -54,15 +54,15 @@ describe('A ParentContainerReader', (): void => {
requestedModes.set(target1, new Set([ AccessMode.delete ]));
requestedModes.set(target2, new Set([ AccessMode.delete ]));
requestedModes.set(target3, new Set([ AccessMode.delete ]));
sourceResult.set(parent1, { public: { write: true }});
sourceResult.set(parent2, { public: { write: true }});
sourceResult.set(target1, { public: { write: true }});
sourceResult.set(target3, { public: { write: true }});
sourceResult.set(parent1, { write: true });
sourceResult.set(parent2, { write: true });
sourceResult.set(target1, { write: true });
sourceResult.set(target3, { write: true });
const result = await reader.handle({ requestedModes, credentials });
expect(result.get(target1)).toEqual({ public: { delete: true, write: true }});
expect(result.get(target2)).toEqual({ public: {}});
expect(result.get(target3)).toEqual({ public: { write: true }});
expect(result.get(target1)).toEqual({ delete: true, write: true });
expect(result.get(target2)).toEqual({ });
expect(result.get(target3)).toEqual({ write: true });
const updatedMap = new IdentifierSetMultiMap(requestedModes);
updatedMap.set(parent1, AccessMode.write);
@@ -76,14 +76,14 @@ describe('A ParentContainerReader', (): void => {
it('does not allow create/delete if the source explicitly forbids it.', async(): Promise<void> => {
requestedModes.set(target1, new Set([ AccessMode.create, AccessMode.delete ]));
requestedModes.set(target2, new Set([ AccessMode.create, AccessMode.delete ]));
sourceResult.set(parent1, { public: { write: true, append: true }});
sourceResult.set(parent2, { public: { write: true, append: true }});
sourceResult.set(target1, { public: { write: true }});
sourceResult.set(target2, { public: { write: true, create: false, delete: false }});
sourceResult.set(parent1, { write: true, append: true });
sourceResult.set(parent2, { write: true, append: true });
sourceResult.set(target1, { write: true });
sourceResult.set(target2, { write: true, create: false, delete: false });
const result = await reader.handle({ requestedModes, credentials });
expect(result.get(target1)).toEqual({ public: { write: true, create: true, delete: true }});
expect(result.get(target2)).toEqual({ public: { write: true, create: false, delete: false }});
expect(result.get(target1)).toEqual({ write: true, create: true, delete: true });
expect(result.get(target2)).toEqual({ write: true, create: false, delete: false });
const updatedMap = new IdentifierSetMultiMap(requestedModes);
updatedMap.set(parent1, new Set([ AccessMode.write, AccessMode.append ]));
@@ -96,12 +96,12 @@ describe('A ParentContainerReader', (): void => {
it('combines the modes with the parent resource if it is also being requested.', async(): Promise<void> => {
requestedModes.set(target1, AccessMode.create);
requestedModes.set(parent1, AccessMode.write);
sourceResult.set(parent1, { public: { write: true, append: true }});
sourceResult.set(target1, { public: { write: true }});
sourceResult.set(parent1, { write: true, append: true });
sourceResult.set(target1, { write: true });
const result = await reader.handle({ requestedModes, credentials });
expect(result.get(target1)).toEqual({ public: { write: true, create: true, delete: true }});
expect(result.get(parent1)).toEqual({ public: { write: true, append: true }});
expect(result.get(target1)).toEqual({ write: true, create: true, delete: true });
expect(result.get(parent1)).toEqual({ write: true, append: true });
const updatedMap = new IdentifierSetMultiMap(requestedModes);
updatedMap.set(parent1, new Set([ AccessMode.write, AccessMode.append ]));

View File

@@ -10,7 +10,7 @@ import { compareMaps } from '../../util/Util';
describe('A PathBasedReader', (): void => {
const baseUrl = 'http://test.com/foo/';
const permissionSet: PermissionSet = { agent: { read: true }};
const permissionSet: PermissionSet = { read: true };
let readers: jest.Mocked<PermissionReader>[];
let reader: PathBasedReader;

View File

@@ -34,10 +34,7 @@ describe('A PermissionBasedAuthorizer', (): void => {
input.requestedModes = new IdentifierSetMultiMap<AccessMode>(
[[ identifier, AccessMode.read ], [ identifier, AccessMode.write ]],
);
input.availablePermissions = new IdentifierMap([[ identifier, {
public: { read: true, write: false },
agent: { write: true },
}]]);
input.availablePermissions = new IdentifierMap([[ identifier, { read: true, write: true }]]);
await expect(authorizer.handle(input)).resolves.toBeUndefined();
});
@@ -45,9 +42,7 @@ describe('A PermissionBasedAuthorizer', (): void => {
input.requestedModes = new IdentifierSetMultiMap<AccessMode>(
[[ identifier, AccessMode.read ], [ identifier, AccessMode.write ]],
);
input.availablePermissions = new IdentifierMap([[ identifier, {
public: { read: true, write: false },
}]]);
input.availablePermissions = new IdentifierMap([[ identifier, { read: true, write: false }]]);
await expect(authorizer.handle(input)).rejects.toThrow(UnauthorizedHttpError);
});
@@ -56,9 +51,7 @@ describe('A PermissionBasedAuthorizer', (): void => {
input.requestedModes = new IdentifierSetMultiMap<AccessMode>(
[[ identifier, AccessMode.read ], [ identifier, AccessMode.write ]],
);
input.availablePermissions = new IdentifierMap([[ identifier, {
public: { read: true, write: false },
}]]);
input.availablePermissions = new IdentifierMap([[ identifier, { read: true, write: false }]]);
await expect(authorizer.handle(input)).rejects.toThrow(ForbiddenHttpError);
});
@@ -69,9 +62,7 @@ describe('A PermissionBasedAuthorizer', (): void => {
it('throws a 404 in case the target resource does not exist and would not be written to.', async(): Promise<void> => {
resourceSet.hasResource.mockResolvedValueOnce(false);
input.requestedModes = new IdentifierSetMultiMap<AccessMode>([[ identifier, AccessMode.delete ]]);
input.availablePermissions = new IdentifierMap([[ identifier, {
public: { read: true },
}]]);
input.availablePermissions = new IdentifierMap([[ identifier, { read: true }]]);
await expect(authorizer.handle(input)).rejects.toThrow(NotFoundHttpError);
});
@@ -84,14 +75,8 @@ describe('A PermissionBasedAuthorizer', (): void => {
[ identifier2, AccessMode.write ],
]);
input.availablePermissions = new IdentifierMap([
[ identifier, {
public: { read: true, write: false },
agent: { write: true },
}],
[ identifier2, {
public: { read: false },
agent: { write: true },
}],
[ identifier, { read: true, write: true }],
[ identifier2, { read: false, write: true }],
]);
await expect(authorizer.handle(input)).rejects.toThrow(UnauthorizedHttpError);
});
@@ -105,10 +90,7 @@ describe('A PermissionBasedAuthorizer', (): void => {
[ identifier2, AccessMode.write ],
]);
input.availablePermissions = new IdentifierMap([
[ identifier, {
public: { read: true, write: false },
agent: { write: true },
}],
[ identifier, { read: true, write: true }],
]);
await expect(authorizer.handle(input)).rejects.toThrow(UnauthorizedHttpError);
});

View File

@@ -30,45 +30,42 @@ describe('A UnionPermissionReader', (): void => {
it('only uses the results of readers that can handle the input.', async(): Promise<void> => {
readers[0].canHandle.mockRejectedValue(new Error('bad request'));
readers[0].handle.mockResolvedValue(
new IdentifierMap([[ identifier, { agent: { read: true }}]]),
new IdentifierMap([[ identifier, { read: true }]]),
);
readers[1].handle.mockResolvedValue(
new IdentifierMap([[ identifier, { agent: { write: true }}]]),
new IdentifierMap([[ identifier, { write: true }]]),
);
compareMaps(await unionReader.handle(input),
new IdentifierMap([[ identifier, { agent: { write: true }}]]));
new IdentifierMap([[ identifier, { write: true }]]));
});
it('combines results.', async(): Promise<void> => {
const identifier2 = { path: 'http://example.com/foo2' };
const identifier3 = { path: 'http://example.com/foo3' };
readers[0].handle.mockResolvedValue(new IdentifierMap([
[ identifier, { agent: { read: true }, public: undefined }],
[ identifier2, { agent: { write: true }}],
[ identifier3, { agent: { append: false }, public: { delete: true }}],
[ identifier, { read: true }],
[ identifier2, { write: true }],
[ identifier3, { append: false }],
]));
readers[1].handle.mockResolvedValue(new IdentifierMap<PermissionSet>([
[ identifier, { agent: { write: true }, public: { read: false }}],
[ identifier2, { public: { read: false }}],
[ identifier, { write: true }],
]));
compareMaps(await unionReader.handle(input), new IdentifierMap([
[ identifier, { agent: { read: true, write: true }, public: { read: false }}],
[ identifier2, { agent: { write: true }, public: { read: false }}],
[ identifier3, { agent: { append: false }, public: { delete: true }}],
[ identifier, { read: true, write: true }],
[ identifier2, { write: true }],
[ identifier3, { append: false }],
]));
});
it('merges same fields using false > true > undefined.', async(): Promise<void> => {
readers[0].handle.mockResolvedValue(new IdentifierMap(
[[ identifier,
{ agent: { read: true, write: false, append: undefined, create: true, delete: undefined }}]],
[[ identifier, { read: true, write: false, append: undefined, create: true, delete: undefined }]],
));
readers[1].handle.mockResolvedValue(new IdentifierMap(
[[ identifier, { agent:
{ read: false, write: true, append: true, create: true, delete: undefined }}]],
[[ identifier, { read: false, write: true, append: true, create: true, delete: undefined }]],
));
compareMaps(await unionReader.handle(input), new IdentifierMap(
[[ identifier, { read: false, write: false, append: true, create: true }]],
));
compareMaps(await unionReader.handle(input), new IdentifierMap([[ identifier, {
agent: { read: false, write: false, append: true, create: true },
}]]));
});
});

View File

@@ -2,7 +2,7 @@ import { DataFactory } from 'n3';
import type { Credentials } from '../../../src/authentication/Credentials';
import type { AccessChecker } from '../../../src/authorization/access/AccessChecker';
import type { PermissionReaderInput } from '../../../src/authorization/PermissionReader';
import { AclMode } from '../../../src/authorization/permissions/AclPermission';
import { AclMode } from '../../../src/authorization/permissions/AclPermissionSet';
import type { AccessMap, PermissionSet } from '../../../src/authorization/permissions/Permissions';
import { AccessMode } from '../../../src/authorization/permissions/Permissions';
import { WebAclReader } from '../../../src/authorization/WebAclReader';
@@ -77,10 +77,7 @@ describe('A WebAclReader', (): void => {
it('returns undefined permissions for undefined credentials.', async(): Promise<void> => {
input.credentials = {};
compareMaps(await reader.handle(input), new IdentifierMap([[ identifier, {
public: {},
agent: {},
}]]));
compareMaps(await reader.handle(input), new IdentifierMap([[ identifier, {}]]));
});
it('reads the accessTo value of the acl resource.', async(): Promise<void> => {
@@ -90,10 +87,7 @@ describe('A WebAclReader', (): void => {
quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)),
quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Read`)),
], INTERNAL_QUADS));
compareMaps(await reader.handle(input), new IdentifierMap([[ identifier, {
public: { read: true },
agent: { read: true },
}]]));
compareMaps(await reader.handle(input), new IdentifierMap([[ identifier, { read: true }]]));
});
it('ignores accessTo fields pointing to different resources.', async(): Promise<void> => {
@@ -103,10 +97,7 @@ describe('A WebAclReader', (): void => {
quad(nn('auth'), nn(`${acl}accessTo`), nn('somewhereElse')),
quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Read`)),
], INTERNAL_QUADS));
compareMaps(await reader.handle(input), new IdentifierMap([[ identifier, {
public: {},
agent: {},
}]]));
compareMaps(await reader.handle(input), new IdentifierMap([[ identifier, {}]]));
});
it('handles all valid modes and ignores other ones.', async(): Promise<void> => {
@@ -117,10 +108,7 @@ describe('A WebAclReader', (): void => {
quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Read`)),
quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}fakeMode1`)),
], INTERNAL_QUADS));
compareMaps(await reader.handle(input), new IdentifierMap([[ identifier, {
public: { read: true },
agent: { read: true },
}]]));
compareMaps(await reader.handle(input), new IdentifierMap([[ identifier, { read: true }]]));
});
it('reads the default value of a parent if there is no direct acl resource.', async(): Promise<void> => {
@@ -131,10 +119,7 @@ describe('A WebAclReader', (): void => {
quad(nn('auth'), nn(`${acl}default`), nn(identifierStrategy.getParentContainer(identifier).path)),
quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Read`)),
], INTERNAL_QUADS));
compareMaps(await reader.handle(input), new IdentifierMap([[ identifier, {
public: { read: true },
agent: { read: true },
}]]));
compareMaps(await reader.handle(input), new IdentifierMap([[ identifier, { read: true }]]));
});
it('does not use default authorizations for the resource itself.', async(): Promise<void> => {
@@ -148,10 +133,7 @@ describe('A WebAclReader', (): void => {
quad(nn('auth2'), nn(`${acl}accessTo`), nn(identifier.path)),
quad(nn('auth2'), nn(`${acl}mode`), nn(`${acl}Append`)),
], INTERNAL_QUADS));
compareMaps(await reader.handle(input), new IdentifierMap([[ identifier, {
public: { append: true },
agent: { append: true },
}]]));
compareMaps(await reader.handle(input), new IdentifierMap([[ identifier, { append: true }]]));
});
it('re-throws ResourceStore errors as internal errors.', async(): Promise<void> => {
@@ -170,9 +152,7 @@ describe('A WebAclReader', (): void => {
it('ignores rules where no access is granted.', async(): Promise<void> => {
credentials.agent = { webId: 'http://test.com/user' };
// CredentialGroup.public gets true on auth1, CredentialGroup.agent on auth2
accessChecker.handleSafe.mockImplementation(async({ rule, credentials: cred }): Promise<boolean> =>
(rule.value === 'auth1') === !cred.agent?.webId);
accessChecker.handleSafe.mockImplementation(async({ rule }): Promise<boolean> => rule.value !== 'auth1');
store.getRepresentation.mockResolvedValue(new BasicRepresentation([
quad(nn('auth1'), nn(`${rdf}type`), nn(`${acl}Authorization`)),
@@ -183,10 +163,7 @@ describe('A WebAclReader', (): void => {
quad(nn('auth2'), nn(`${acl}mode`), nn(`${acl}Append`)),
], INTERNAL_QUADS));
compareMaps(await reader.handle(input), new IdentifierMap<PermissionSet>([[ identifier, {
public: { read: true },
agent: { append: true },
}]]));
compareMaps(await reader.handle(input), new IdentifierMap<PermissionSet>([[ identifier, { append: true }]]));
});
it('combines ACL representation requests for resources when possible.', async(): Promise<void> => {
@@ -224,9 +201,9 @@ describe('A WebAclReader', (): void => {
input.requestedModes.set(identifier3, new Set([ AccessMode.append ]));
compareMaps(await reader.handle(input), new IdentifierMap([
[ identifier, { public: { read: true }, agent: { read: true }}],
[ identifier2, { public: { read: true }, agent: { read: true }}],
[ identifier3, { public: { append: true }, agent: { append: true }}],
[ identifier, { read: true }],
[ identifier2, { read: true }],
[ identifier3, { append: true }],
]));
// http://example.com/.acl and http://example.com/bar/.acl
expect(store.getRepresentation).toHaveBeenCalledTimes(2);

View File

@@ -18,7 +18,7 @@ describe('An AuthorizingHttpHandler', (): void => {
const target = { path: 'http://example.com/foo' };
const requestedModes: AccessMap = new IdentifierSetMultiMap<AccessMode>([[ target, AccessMode.read ]]);
const availablePermissions: PermissionMap = new IdentifierMap(
[[ target, { public: { read: true }}]],
[[ target, { read: true }]],
);
const request: HttpRequest = {} as any;
const response: HttpResponse = {} as any;

View File

@@ -1,7 +1,6 @@
import 'jest-rdf';
import type { CredentialsExtractor } from '../../../src/authentication/CredentialsExtractor';
import type { PermissionReader } from '../../../src/authorization/PermissionReader';
import type { AclPermission } from '../../../src/authorization/permissions/AclPermission';
import type { ModesExtractor } from '../../../src/authorization/permissions/ModesExtractor';
import type { Operation } from '../../../src/http/Operation';
import { OkResponseDescription } from '../../../src/http/output/response/OkResponseDescription';
@@ -56,15 +55,14 @@ describe('A WacAllowHttpHandler', (): void => {
});
it('adds permission metadata.', async(): Promise<void> => {
permissionReader.handleSafe.mockResolvedValueOnce(new IdentifierMap([[ target, {
agent: { read: true, write: true, control: false } as AclPermission,
public: { read: true, write: false },
}]]));
permissionReader.handleSafe.mockResolvedValueOnce(new IdentifierMap(
[[ target, { read: true, write: true, append: false }]],
));
await expect(handler.handle({ operation, request, response })).resolves.toEqual(output);
expect(output.metadata!.quads()).toHaveLength(3);
expect(output.metadata!.quads()).toHaveLength(4);
expect(output.metadata!.getAll(AUTH.terms.userMode)).toEqualRdfTermArray([ ACL.terms.Read, ACL.terms.Write ]);
expect(output.metadata!.get(AUTH.terms.publicMode)).toEqualRdfTerm(ACL.terms.Read);
expect(output.metadata!.getAll(AUTH.terms.publicMode)).toEqualRdfTermArray([ ACL.terms.Read, ACL.terms.Write ]);
expect(source.handleSafe).toHaveBeenCalledTimes(1);
expect(source.handleSafe).toHaveBeenLastCalledWith({ operation, request, response });
@@ -79,21 +77,59 @@ describe('A WacAllowHttpHandler', (): void => {
});
});
it('adds no permissions for credential groups that are not defined.', async(): Promise<void> => {
permissionReader.handleSafe.mockResolvedValueOnce(new IdentifierMap([[ target, {}]]));
it('determines public permissions separately in case of an authenticated request.', async(): Promise<void> => {
credentialsExtractor.handleSafe.mockResolvedValue({ agent: { webId: 'http://example.com/#me' }});
permissionReader.handleSafe.mockResolvedValueOnce(new IdentifierMap(
[[ target, { read: true, write: true, append: false }]],
));
permissionReader.handleSafe.mockResolvedValueOnce(new IdentifierMap(
[[ target, { read: true, write: false, append: true }]],
));
await expect(handler.handle({ operation, request, response })).resolves.toEqual(output);
expect(output.metadata!.quads()).toHaveLength(4);
expect(output.metadata!.getAll(AUTH.terms.userMode)).toEqualRdfTermArray([ ACL.terms.Read, ACL.terms.Write ]);
expect(output.metadata!.getAll(AUTH.terms.publicMode)).toEqualRdfTermArray([ ACL.terms.Read, ACL.terms.Append ]);
expect(source.handleSafe).toHaveBeenCalledTimes(1);
expect(source.handleSafe).toHaveBeenLastCalledWith({ operation, request, response });
expect(credentialsExtractor.handleSafe).toHaveBeenCalledTimes(1);
expect(credentialsExtractor.handleSafe).toHaveBeenLastCalledWith(request);
expect(modesExtractor.handleSafe).toHaveBeenCalledTimes(1);
expect(modesExtractor.handleSafe).toHaveBeenLastCalledWith(operation);
expect(permissionReader.handleSafe).toHaveBeenCalledTimes(2);
expect(permissionReader.handleSafe).toHaveBeenNthCalledWith(1, {
credentials: { agent: { webId: 'http://example.com/#me' }},
requestedModes: await modesExtractor.handleSafe.mock.results[0].value,
});
expect(permissionReader.handleSafe).toHaveBeenNthCalledWith(2, {
credentials: {},
requestedModes: await modesExtractor.handleSafe.mock.results[0].value,
});
});
it('adds no permissions if none of them are on the target.', async(): Promise<void> => {
permissionReader.handleSafe.mockResolvedValueOnce(new IdentifierMap(
[[{ path: 'http://example/other' }, { read: true, write: false }]],
));
await expect(handler.handle({ operation, request, response })).resolves.toEqual(output);
expect(output.metadata!.quads()).toHaveLength(0);
});
it('adds no permissions if none of them are on the target.', async(): Promise<void> => {
permissionReader.handleSafe.mockResolvedValueOnce(new IdentifierMap([[{ path: 'http://example/other' }, {
agent: { read: true, write: true, control: false } as AclPermission,
public: { read: true, write: false },
}]]));
it('adds no public permissions if the second call has no results for the target.', async(): Promise<void> => {
credentialsExtractor.handleSafe.mockResolvedValue({ agent: { webId: 'http://example.com/#me' }});
permissionReader.handleSafe.mockResolvedValueOnce(new IdentifierMap(
[[ target, { read: true, write: false }]],
));
permissionReader.handleSafe.mockResolvedValueOnce(new IdentifierMap(
[[{ path: 'http://example/other' }, { read: false, write: true }]],
));
await expect(handler.handle({ operation, request, response })).resolves.toEqual(output);
expect(output.metadata!.quads()).toHaveLength(0);
expect(output.metadata!.quads()).toHaveLength(1);
expect(output.metadata!.getAll(AUTH.terms.userMode)).toEqualRdfTermArray([ ACL.terms.Read ]);
expect(output.metadata!.getAll(AUTH.terms.publicMode)).toEqualRdfTermArray([]);
});
it('immediately returns the source output if the operation method is not GET or HEAD.', async(): Promise<void> => {

View File

@@ -1,9 +1,9 @@
import type { ResourceStore } from '../../src/';
import { BasicRepresentation } from '../../src/';
import type { AclPermission } from '../../src/authorization/permissions/AclPermission';
import type { AclPermissionSet } from '../../src/authorization/permissions/AclPermissionSet';
export type AclHelperInput = {
permissions: AclPermission;
permissions: AclPermissionSet;
agentClass?: 'agent' | 'authenticated';
agent?: string;
accessTo?: boolean;
@@ -39,7 +39,7 @@ export class AclHelper {
}
for (const perm of [ 'Read', 'Append', 'Write', 'Control' ]) {
if (option.permissions[perm.toLowerCase() as keyof AclPermission]) {
if (option.permissions[perm.toLowerCase() as keyof AclPermissionSet]) {
acl.push(`;\n acl:mode acl:${perm}`);
}
}