feat: Replace acl specific permissions with generic permissions

This required AuxiliaryStrategy to have a new function
indicating if the auxiliary resource just used its associated resource authorization
or its own.
This commit is contained in:
Joachim Van Herwegen
2021-09-21 11:56:05 +02:00
parent 5104cd56e8
commit 7f8b923399
46 changed files with 221 additions and 152 deletions

View File

@@ -177,7 +177,8 @@ describe.each(stores)('An LDP handler with auth using %s', (name, { storeConfig,
const response = await fetch(`${baseUrl}.acl`);
expect(response.status).toBe(200);
expect(response.headers.get('wac-allow')).toBe('user="control",public="control"');
expect(response.headers.get('wac-allow'))
.toBe('user="append control read write",public="append control read write"');
// Close response
await response.text();

View File

@@ -13,7 +13,7 @@
"files-scs:config/ldp/handler/default.json",
"files-scs:config/ldp/metadata-parser/default.json",
"files-scs:config/ldp/metadata-writer/default.json",
"files-scs:config/ldp/modes/acl.json",
"files-scs:config/ldp/modes/default.json",
"files-scs:config/storage/key-value/memory.json",
"files-scs:config/storage/middleware/default.json",
"files-scs:config/util/auxiliary/acl.json",

View File

@@ -13,7 +13,7 @@
"files-scs:config/ldp/handler/default.json",
"files-scs:config/ldp/metadata-parser/default.json",
"files-scs:config/ldp/metadata-writer/default.json",
"files-scs:config/ldp/modes/acl.json",
"files-scs:config/ldp/modes/default.json",
"files-scs:config/storage/backend/memory.json",
"files-scs:config/storage/key-value/memory.json",
"files-scs:config/storage/middleware/default.json",

View File

@@ -18,7 +18,7 @@
"files-scs:config/ldp/handler/default.json",
"files-scs:config/ldp/metadata-parser/default.json",
"files-scs:config/ldp/metadata-writer/default.json",
"files-scs:config/ldp/modes/acl.json",
"files-scs:config/ldp/modes/default.json",
"files-scs:config/storage/backend/dynamic.json",
"files-scs:config/storage/key-value/memory.json",
"files-scs:config/storage/middleware/default.json",

View File

@@ -17,7 +17,7 @@
"files-scs:config/ldp/handler/default.json",
"files-scs:config/ldp/metadata-parser/default.json",
"files-scs:config/ldp/metadata-writer/default.json",
"files-scs:config/ldp/modes/acl.json",
"files-scs:config/ldp/modes/default.json",
"files-scs:config/storage/backend/memory.json",
"files-scs:config/storage/key-value/resource-store.json",
"files-scs:config/storage/middleware/default.json",

View File

@@ -18,7 +18,7 @@
"files-scs:config/ldp/handler/default.json",
"files-scs:config/ldp/metadata-parser/default.json",
"files-scs:config/ldp/metadata-writer/default.json",
"files-scs:config/ldp/modes/acl.json",
"files-scs:config/ldp/modes/default.json",
"files-scs:config/storage/key-value/memory.json",
"files-scs:config/storage/middleware/default.json",
"files-scs:config/util/auxiliary/acl.json",

View File

@@ -13,7 +13,7 @@
"files-scs:config/ldp/handler/default.json",
"files-scs:config/ldp/metadata-parser/default.json",
"files-scs:config/ldp/metadata-writer/default.json",
"files-scs:config/ldp/modes/acl.json",
"files-scs:config/ldp/modes/default.json",
"files-scs:config/storage/backend/memory.json",
"files-scs:config/storage/key-value/memory.json",
"files-scs:config/storage/middleware/default.json",

View File

@@ -18,7 +18,7 @@
"files-scs:config/ldp/handler/default.json",
"files-scs:config/ldp/metadata-parser/default.json",
"files-scs:config/ldp/metadata-writer/default.json",
"files-scs:config/ldp/modes/acl.json",
"files-scs:config/ldp/modes/default.json",
"files-scs:config/storage/backend/memory.json",
"files-scs:config/storage/key-value/resource-store.json",
"files-scs:config/storage/middleware/default.json",

View File

@@ -7,7 +7,8 @@ function getPermissions(allow: boolean): Permission {
read: allow,
write: allow,
append: allow,
control: allow,
create: allow,
delete: allow,
};
}

View File

@@ -1,7 +1,7 @@
import { CredentialGroup } from '../../../src/authentication/Credentials';
import { AuxiliaryReader } from '../../../src/authorization/AuxiliaryReader';
import type { PermissionReader } from '../../../src/authorization/PermissionReader';
import type { AuxiliaryIdentifierStrategy } from '../../../src/ldp/auxiliary/AuxiliaryIdentifierStrategy';
import type { AuxiliaryStrategy } from '../../../src/ldp/auxiliary/AuxiliaryStrategy';
import type { PermissionSet } from '../../../src/ldp/permissions/Permissions';
import type { ResourceIdentifier } from '../../../src/ldp/representation/ResourceIdentifier';
import { NotImplementedHttpError } from '../../../src/util/errors/NotImplementedHttpError';
@@ -12,8 +12,8 @@ describe('An AuxiliaryReader', (): void => {
const associatedIdentifier = { path: 'http://test.com/foo' };
const auxiliaryIdentifier = { path: 'http://test.com/foo.dummy' };
const permissionSet: PermissionSet = { [CredentialGroup.agent]: { read: true }};
let source: PermissionReader;
let strategy: AuxiliaryIdentifierStrategy;
let source: jest.Mocked<PermissionReader>;
let strategy: jest.Mocked<AuxiliaryStrategy>;
let reader: AuxiliaryReader;
beforeEach(async(): Promise<void> => {
@@ -27,6 +27,7 @@ describe('An AuxiliaryReader', (): void => {
isAuxiliaryIdentifier: jest.fn((identifier: ResourceIdentifier): boolean => identifier.path.endsWith(suffix)),
getAssociatedIdentifier: jest.fn((identifier: ResourceIdentifier): ResourceIdentifier =>
({ path: identifier.path.slice(0, -suffix.length) })),
usesOwnAuthorization: jest.fn().mockReturnValue(false),
} as any;
reader = new AuxiliaryReader(source, strategy);
});
@@ -39,7 +40,12 @@ describe('An AuxiliaryReader', (): void => {
);
await expect(reader.canHandle({ identifier: associatedIdentifier, credentials }))
.rejects.toThrow(NotImplementedHttpError);
source.canHandle = jest.fn().mockRejectedValue(new Error('no source support'));
strategy.usesOwnAuthorization.mockReturnValueOnce(true);
await expect(reader.canHandle({ identifier: auxiliaryIdentifier, credentials }))
.rejects.toThrow(NotImplementedHttpError);
source.canHandle.mockRejectedValue(new Error('no source support'));
await expect(reader.canHandle({ identifier: auxiliaryIdentifier, credentials }))
.rejects.toThrow('no source support');
});
@@ -61,9 +67,15 @@ describe('An AuxiliaryReader', (): void => {
expect(source.handleSafe).toHaveBeenLastCalledWith(
{ identifier: associatedIdentifier, credentials },
);
await expect(reader.handleSafe({ identifier: associatedIdentifier, credentials }))
.rejects.toThrow(NotImplementedHttpError);
source.handleSafe = jest.fn().mockRejectedValue(new Error('no source support'));
strategy.usesOwnAuthorization.mockReturnValueOnce(true);
await expect(reader.canHandle({ identifier: auxiliaryIdentifier, credentials }))
.rejects.toThrow(NotImplementedHttpError);
source.handleSafe.mockRejectedValue(new Error('no source support'));
await expect(reader.handleSafe({ identifier: auxiliaryIdentifier, credentials }))
.rejects.toThrow('no source support');
});

View File

@@ -44,13 +44,13 @@ describe('A UnionPermissionReader', (): void => {
it('merges same fields using false > true > undefined.', async(): Promise<void> => {
readers[0].handle.mockResolvedValue(
{ [CredentialGroup.agent]: { read: true, write: false, append: undefined, control: true }},
{ [CredentialGroup.agent]: { read: true, write: false, append: undefined, create: true, delete: undefined }},
);
readers[1].handle.mockResolvedValue(
{ [CredentialGroup.agent]: { read: false, write: true, append: true, control: true }},
{ [CredentialGroup.agent]: { read: false, write: true, append: true, create: true, delete: undefined }},
);
await expect(unionReader.handle(input)).resolves.toEqual({
[CredentialGroup.agent]: { read: false, write: false, append: true, control: true },
[CredentialGroup.agent]: { read: false, write: false, append: true, create: true },
});
});
});

View File

@@ -12,7 +12,6 @@ import { INTERNAL_QUADS } from '../../../src/util/ContentTypes';
import { ForbiddenHttpError } from '../../../src/util/errors/ForbiddenHttpError';
import { InternalServerError } from '../../../src/util/errors/InternalServerError';
import { NotFoundHttpError } from '../../../src/util/errors/NotFoundHttpError';
import { NotImplementedHttpError } from '../../../src/util/errors/NotImplementedHttpError';
import { SingleRootIdentifierStrategy } from '../../../src/util/identifiers/SingleRootIdentifierStrategy';
import { guardedStreamFrom } from '../../../src/util/StreamUtil';
@@ -51,10 +50,8 @@ describe('A WebAclReader', (): void => {
reader = new WebAclReader(aclStrategy, store, identifierStrategy, accessChecker);
});
it('handles all non-acl inputs.', async(): Promise<void> => {
await expect(reader.canHandle({ identifier, credentials })).resolves.toBeUndefined();
await expect(reader.canHandle({ identifier: aclStrategy.getAuxiliaryIdentifier(identifier) } as any))
.rejects.toThrow(NotImplementedHttpError);
it('handles all input.', async(): Promise<void> => {
await expect(reader.canHandle({ } as any)).resolves.toBeUndefined();
});
it('returns undefined permissions for undefined credentials.', async(): Promise<void> => {
@@ -137,15 +134,39 @@ describe('A WebAclReader', (): void => {
await expect(promise).rejects.toThrow(ForbiddenHttpError);
});
it('allows an agent to append if they have write access.', async(): Promise<void> => {
it('allows an agent to append/create/delete if they have write access.', async(): Promise<void> => {
store.getRepresentation.mockResolvedValue({ data: guardedStreamFrom([
quad(nn('auth'), nn(`${rdf}type`), nn(`${acl}Authorization`)),
quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)),
quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Write`)),
]) } as Representation);
await expect(reader.handle({ identifier, credentials })).resolves.toEqual({
[CredentialGroup.public]: { write: true, append: true },
[CredentialGroup.agent]: { write: true, append: true },
[CredentialGroup.public]: { write: true, append: true, create: true, delete: true },
[CredentialGroup.agent]: { write: true, append: true, create: true, delete: true },
});
});
it('allows everything on an acl resource if control permissions are granted.', async(): Promise<void> => {
store.getRepresentation.mockResolvedValue({ data: guardedStreamFrom([
quad(nn('auth'), nn(`${rdf}type`), nn(`${acl}Authorization`)),
quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)),
quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Control`)),
]) } as Representation);
await expect(reader.handle({ identifier: { path: `${identifier.path}.acl` }, credentials })).resolves.toEqual({
[CredentialGroup.public]: { read: true, write: true, append: true, create: true, delete: true, control: true },
[CredentialGroup.agent]: { read: true, write: true, append: true, create: true, delete: true, control: true },
});
});
it('rejects everything on an acl resource if there are no control permissions.', async(): Promise<void> => {
store.getRepresentation.mockResolvedValue({ data: guardedStreamFrom([
quad(nn('auth'), nn(`${rdf}type`), nn(`${acl}Authorization`)),
quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)),
quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Read`)),
]) } as Representation);
await expect(reader.handle({ identifier: { path: `${identifier.path}.acl` }, credentials })).resolves.toEqual({
[CredentialGroup.public]: {},
[CredentialGroup.agent]: {},
});
});

View File

@@ -24,7 +24,7 @@ describe('A ComposedAuxiliaryStrategy', (): void => {
validator = {
handleSafe: jest.fn(),
} as any;
strategy = new ComposedAuxiliaryStrategy(identifierStrategy, metadataGenerator, validator, true);
strategy = new ComposedAuxiliaryStrategy(identifierStrategy, metadataGenerator, validator, false, true);
});
it('calls the AuxiliaryIdentifierStrategy for related calls.', async(): Promise<void> => {
@@ -45,6 +45,10 @@ describe('A ComposedAuxiliaryStrategy', (): void => {
expect(identifierStrategy.isAuxiliaryIdentifier).toHaveBeenLastCalledWith(identifier);
});
it('returns the injected value for usesOwnAuthorization.', async(): Promise<void> => {
expect(strategy.usesOwnAuthorization()).toBe(false);
});
it('returns the injected value for isRequiredInRoot.', async(): Promise<void> => {
expect(strategy.isRequiredInRoot()).toBe(true);
});

View File

@@ -11,6 +11,10 @@ class SimpleSuffixStrategy implements AuxiliaryStrategy {
this.suffix = suffix;
}
public usesOwnAuthorization(): boolean {
return true;
}
public getAuxiliaryIdentifier(identifier: ResourceIdentifier): ResourceIdentifier {
return { path: `${identifier.path}${this.suffix}` };
}
@@ -77,6 +81,15 @@ describe('A RoutingAuxiliaryStrategy', (): void => {
expect(sources[1].addMetadata).toHaveBeenLastCalledWith(metadata);
});
it('#usesOwnAuthorization returns the result of the correct source.', async(): Promise<void> => {
sources[0].usesOwnAuthorization = jest.fn();
sources[1].usesOwnAuthorization = jest.fn();
strategy.usesOwnAuthorization(dummy2Id);
expect(sources[0].usesOwnAuthorization).toHaveBeenCalledTimes(0);
expect(sources[1].usesOwnAuthorization).toHaveBeenCalledTimes(1);
expect(sources[1].usesOwnAuthorization).toHaveBeenLastCalledWith(dummy2Id);
});
it('#isRequiredInRoot returns the result of the correct source.', async(): Promise<void> => {
sources[0].isRequiredInRoot = jest.fn();
sources[1].isRequiredInRoot = jest.fn();

View File

@@ -2,6 +2,7 @@ import 'jest-rdf';
import { CredentialGroup } from '../../../../../src/authentication/Credentials';
import { WebAclMetadataCollector } from '../../../../../src/ldp/operations/metadata/WebAclMetadataCollector';
import type { Operation } from '../../../../../src/ldp/operations/Operation';
import type { AclPermission } from '../../../../../src/ldp/permissions/AclPermission';
import { RepresentationMetadata } from '../../../../../src/ldp/representation/RepresentationMetadata';
import { ACL, AUTH } from '../../../../../src/util/Vocabularies';
@@ -38,7 +39,7 @@ describe('A WebAclMetadataCollector', (): void => {
it('adds corresponding metadata for all permissions present.', async(): Promise<void> => {
operation.permissionSet = {
[CredentialGroup.agent]: { read: true, write: true, control: false },
[CredentialGroup.agent]: { read: true, write: true, control: false } as AclPermission,
[CredentialGroup.public]: { read: true, write: false },
};
await expect(writer.handle({ metadata, operation })).resolves.toBeUndefined();
@@ -46,4 +47,15 @@ describe('A WebAclMetadataCollector', (): void => {
expect(metadata.getAll(AUTH.terms.userMode)).toEqualRdfTermArray([ ACL.terms.Read, ACL.terms.Write ]);
expect(metadata.get(AUTH.terms.publicMode)).toEqualRdfTerm(ACL.terms.Read);
});
it('ignores unknown modes.', async(): Promise<void> => {
operation.permissionSet = {
[CredentialGroup.agent]: { read: true, create: true },
[CredentialGroup.public]: { read: true },
};
await expect(writer.handle({ metadata, operation })).resolves.toBeUndefined();
expect(metadata.quads()).toHaveLength(2);
expect(metadata.getAll(AUTH.terms.userMode)).toEqualRdfTermArray([ ACL.terms.Read ]);
expect(metadata.get(AUTH.terms.publicMode)).toEqualRdfTerm(ACL.terms.Read);
});
});

View File

@@ -1,26 +0,0 @@
import type { AuxiliaryIdentifierStrategy } from '../../../../src/ldp/auxiliary/AuxiliaryIdentifierStrategy';
import { AclModesExtractor } from '../../../../src/ldp/permissions/AclModesExtractor';
import { AccessMode } from '../../../../src/ldp/permissions/Permissions';
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
describe('An AclModesExtractor', (): void => {
let extractor: AclModesExtractor;
beforeEach(async(): Promise<void> => {
const aclStrategy = {
isAuxiliaryIdentifier: (id): boolean => id.path.endsWith('.acl'),
} as AuxiliaryIdentifierStrategy;
extractor = new AclModesExtractor(aclStrategy);
});
it('can only handle acl files.', async(): Promise<void> => {
await expect(extractor.canHandle({ target: { path: 'http://test.com/foo' }} as any))
.rejects.toThrow(NotImplementedHttpError);
await expect(extractor.canHandle({ target: { path: 'http://test.com/foo.acl' }} as any))
.resolves.toBeUndefined();
});
it('returns control permissions.', async(): Promise<void> => {
await expect(extractor.handle()).resolves.toEqual(new Set([ AccessMode.control ]));
});
});

View File

@@ -29,11 +29,11 @@ describe('A MethodModesExtractor', (): void => {
it('requires write for PUT operations.', async(): Promise<void> => {
await expect(extractor.handle({ method: 'PUT' } as Operation))
.resolves.toEqual(new Set([ AccessMode.append, AccessMode.write ]));
.resolves.toEqual(new Set([ AccessMode.append, AccessMode.write, AccessMode.create, AccessMode.delete ]));
});
it('requires write for DELETE operations.', async(): Promise<void> => {
await expect(extractor.handle({ method: 'DELETE' } as Operation))
.resolves.toEqual(new Set([ AccessMode.append, AccessMode.write ]));
.resolves.toEqual(new Set([ AccessMode.append, AccessMode.write, AccessMode.create, AccessMode.delete ]));
});
});

View File

@@ -58,7 +58,8 @@ describe('A SparqlPatchModesExtractor', (): void => {
factory.createPattern(factory.createTerm('<s>'), factory.createTerm('<p>'), factory.createTerm('<o>')),
]) },
} as unknown as Operation;
await expect(extractor.handle(operation)).resolves.toEqual(new Set([ AccessMode.append, AccessMode.write ]));
await expect(extractor.handle(operation))
.resolves.toEqual(new Set([ AccessMode.append, AccessMode.write, AccessMode.create, AccessMode.delete ]));
});
it('requires append for composite operations with an insert.', async(): Promise<void> => {
@@ -81,6 +82,7 @@ describe('A SparqlPatchModesExtractor', (): void => {
factory.createPattern(factory.createTerm('<s>'), factory.createTerm('<p>'), factory.createTerm('<o>')),
]) ]) },
} as unknown as Operation;
await expect(extractor.handle(operation)).resolves.toEqual(new Set([ AccessMode.append, AccessMode.write ]));
await expect(extractor.handle(operation))
.resolves.toEqual(new Set([ AccessMode.append, AccessMode.write, AccessMode.create, AccessMode.delete ]));
});
});

View File

@@ -91,6 +91,10 @@ class SimpleSuffixStrategy implements AuxiliaryStrategy {
this.suffix = suffix;
}
public usesOwnAuthorization(): boolean {
return true;
}
public getAuxiliaryIdentifier(identifier: ResourceIdentifier): ResourceIdentifier {
return { path: `${identifier.path}${this.suffix}` };
}

View File

@@ -1,5 +1,6 @@
import type { ResourceStore, Permission } from '../../src/';
import type { ResourceStore } from '../../src/';
import { BasicRepresentation } from '../../src/';
import type { AclPermission } from '../../src/ldp/permissions/AclPermission';
export class AclHelper {
public readonly store: ResourceStore;
@@ -11,7 +12,7 @@ export class AclHelper {
public async setSimpleAcl(
resource: string,
options: {
permissions: Partial<Permission>;
permissions: AclPermission;
agentClass?: 'agent' | 'authenticated';
agent?: string;
accessTo?: boolean;
@@ -32,7 +33,7 @@ export class AclHelper {
];
for (const perm of [ 'Read', 'Append', 'Write', 'Control' ]) {
if (options.permissions[perm.toLowerCase() as keyof Permission]) {
if (options.permissions[perm.toLowerCase() as keyof AclPermission]) {
acl.push(`;\n acl:mode acl:${perm}`);
}
}