From 16ebfb329f273f155b9d2c361ba3dfb68a5b279e Mon Sep 17 00:00:00 2001 From: Simone Persiani Date: Tue, 17 Aug 2021 16:51:50 +0200 Subject: [PATCH] refactor: Refactor WebAclAuthorizer Co-Authored-By: Ludovico Granata --- .../access-checkers/agent-class.json | 10 ++ .../authorizers/access-checkers/agent.json | 10 ++ config/ldp/authorization/authorizers/acl.json | 11 ++ src/authorization/WebAclAuthorizer.ts | 124 ++++++------------ .../access-checkers/AccessChecker.ts | 25 ++++ .../access-checkers/AgentAccessChecker.ts | 15 +++ .../AgentClassAccessChecker.ts | 20 +++ src/index.ts | 5 + src/util/Vocabularies.ts | 1 + .../authorization/WebAclAuthorizer.test.ts | 97 +++++++------- .../AgentAccessChecker.test.ts | 32 +++++ .../AgentClassAccessChecker.test.ts | 37 ++++++ 12 files changed, 252 insertions(+), 135 deletions(-) create mode 100644 config/ldp/authorization/authorizers/access-checkers/agent-class.json create mode 100644 config/ldp/authorization/authorizers/access-checkers/agent.json create mode 100644 src/authorization/access-checkers/AccessChecker.ts create mode 100644 src/authorization/access-checkers/AgentAccessChecker.ts create mode 100644 src/authorization/access-checkers/AgentClassAccessChecker.ts create mode 100644 test/unit/authorization/access-checkers/AgentAccessChecker.test.ts create mode 100644 test/unit/authorization/access-checkers/AgentClassAccessChecker.test.ts diff --git a/config/ldp/authorization/authorizers/access-checkers/agent-class.json b/config/ldp/authorization/authorizers/access-checkers/agent-class.json new file mode 100644 index 000000000..c0d88c228 --- /dev/null +++ b/config/ldp/authorization/authorizers/access-checkers/agent-class.json @@ -0,0 +1,10 @@ +{ + "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld", + "@graph": [ + { + "comment": "Checks access based on the agent being authenticated or not.", + "@id": "urn:solid-server:default:AgentClassAccessChecker", + "@type": "AgentClassAccessChecker" + } + ] +} diff --git a/config/ldp/authorization/authorizers/access-checkers/agent.json b/config/ldp/authorization/authorizers/access-checkers/agent.json new file mode 100644 index 000000000..f5f585095 --- /dev/null +++ b/config/ldp/authorization/authorizers/access-checkers/agent.json @@ -0,0 +1,10 @@ +{ + "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld", + "@graph": [ + { + "comment": "Checks if a specific WebID has access.", + "@id": "urn:solid-server:default:AgentAccessChecker", + "@type": "AgentAccessChecker" + } + ] +} diff --git a/config/ldp/authorization/authorizers/acl.json b/config/ldp/authorization/authorizers/acl.json index 4ede94e85..35a9c3170 100644 --- a/config/ldp/authorization/authorizers/acl.json +++ b/config/ldp/authorization/authorizers/acl.json @@ -1,5 +1,9 @@ { "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld", + "import": [ + "files-scs:config/ldp/authorization/authorizers/access-checkers/agent.json", + "files-scs:config/ldp/authorization/authorizers/access-checkers/agent-class.json" + ], "@graph": [ { "@id": "urn:solid-server:default:WebAclAuthorizer", @@ -12,6 +16,13 @@ }, "identifierStrategy": { "@id": "urn:solid-server:default:IdentifierStrategy" + }, + "accessChecker": { + "@type": "BooleanHandler", + "handlers": [ + { "@id": "urn:solid-server:default:AgentAccessChecker" }, + { "@id": "urn:solid-server:default:AgentClassAccessChecker" } + ] } } ] diff --git a/src/authorization/WebAclAuthorizer.ts b/src/authorization/WebAclAuthorizer.ts index 97650b078..27cc935d8 100644 --- a/src/authorization/WebAclAuthorizer.ts +++ b/src/authorization/WebAclAuthorizer.ts @@ -16,15 +16,22 @@ import { NotImplementedHttpError } from '../util/errors/NotImplementedHttpError' import { UnauthorizedHttpError } from '../util/errors/UnauthorizedHttpError'; import type { IdentifierStrategy } from '../util/identifiers/IdentifierStrategy'; import { readableToQuads } from '../util/StreamUtil'; -import { ACL, FOAF } from '../util/Vocabularies'; +import { ACL, RDF } from '../util/Vocabularies'; +import type { AccessChecker } from './access-checkers/AccessChecker'; import type { AuthorizerArgs } from './Authorizer'; import { Authorizer } from './Authorizer'; import { WebAclAuthorization } from './WebAclAuthorization'; +const modesMap: Record = { + [ACL.Read]: 'read', + [ACL.Write]: 'write', + [ACL.Append]: 'append', + [ACL.Control]: 'control', +} as const; + /** - * Handles most web access control predicates such as - * `acl:mode`, `acl:agentClass`, `acl:agent`, `acl:default` and `acl:accessTo`. - * Does not support `acl:agentGroup`, `acl:origin` and `acl:trustedApp` yet. + * Handles authorization according to the WAC specification. + * Specific access checks are done by the provided {@link AccessChecker}. */ export class WebAclAuthorizer extends Authorizer { protected readonly logger = getLoggerFor(this); @@ -32,13 +39,15 @@ export class WebAclAuthorizer extends Authorizer { private readonly aclStrategy: AuxiliaryIdentifierStrategy; private readonly resourceStore: ResourceStore; private readonly identifierStrategy: IdentifierStrategy; + private readonly accessChecker: AccessChecker; public constructor(aclStrategy: AuxiliaryIdentifierStrategy, resourceStore: ResourceStore, - identifierStrategy: IdentifierStrategy) { + identifierStrategy: IdentifierStrategy, accessChecker: AccessChecker) { super(); this.aclStrategy = aclStrategy; this.resourceStore = resourceStore; this.identifierStrategy = identifierStrategy; + this.accessChecker = accessChecker; } public async canHandle({ identifier }: AuthorizerArgs): Promise { @@ -59,7 +68,7 @@ export class WebAclAuthorizer extends Authorizer { // Determine the full authorization for the agent granted by the applicable ACL const acl = await this.getAclRecursive(identifier); - const authorization = this.createAuthorization(credentials, acl); + const authorization = await this.createAuthorization(credentials, acl); // Verify that the authorization allows all required modes for (const mode of modes) { @@ -82,11 +91,11 @@ export class WebAclAuthorizer extends Authorizer { * @param agent - Agent whose credentials will be used for the `user` field. * @param acl - Store containing all relevant authorization triples. */ - private createAuthorization(agent: Credentials, acl: Store): WebAclAuthorization { - const publicPermissions = this.determinePermissions({}, acl); - const userPermissions = this.determinePermissions(agent, acl); + private async createAuthorization(agent: Credentials, acl: Store): Promise { + const publicPermissions = await this.determinePermissions({}, acl); + const agentPermissions = await this.determinePermissions(agent, acl); - return new WebAclAuthorization(userPermissions, publicPermissions); + return new WebAclAuthorization(agentPermissions, publicPermissions); } /** @@ -94,16 +103,34 @@ export class WebAclAuthorizer extends Authorizer { * @param credentials - Credentials to find the permissions for. * @param acl - Store containing all relevant authorization triples. */ - private determinePermissions(credentials: Credentials, acl: Store): PermissionSet { - const permissions: PermissionSet = { + private async determinePermissions(credentials: Credentials, acl: Store): Promise { + const permissions = { read: false, write: false, append: false, control: false, }; - for (const mode of (Object.keys(permissions) as (keyof PermissionSet)[])) { - permissions[mode] = this.hasPermission(credentials, acl, mode); + + // Apply all ACL rules + const aclRules = acl.getSubjects(RDF.type, ACL.Authorization, null); + for (const rule of aclRules) { + const hasAccess = await this.accessChecker.handleSafe({ acl, rule, credentials }); + if (hasAccess) { + // Set all allowed modes to true + const modes = acl.getObjects(rule, ACL.mode, null); + for (const { value: mode } of modes) { + if (mode in modesMap) { + permissions[modesMap[mode]] = true; + } + } + } } + + if (permissions.write) { + // Write permission implies Append permission + permissions.append = true; + } + return permissions; } @@ -130,75 +157,6 @@ export class WebAclAuthorizer extends Authorizer { } } - /** - * Checks if the given agent has permission to execute the given mode based on the triples in the ACL. - * @param agent - Agent that wants access. - * @param acl - A store containing the relevant triples for authorization. - * @param mode - Which mode is requested. - */ - private hasPermission(agent: Credentials, acl: Store, mode: keyof PermissionSet): boolean { - // Collect all authorization blocks for this specific mode - const modeString = ACL[this.capitalize(mode) as 'Write' | 'Read' | 'Append' | 'Control']; - const auths = this.getModePermissions(acl, modeString); - - // Append permissions are implied by Write permissions - if (modeString === ACL.Append) { - auths.push(...this.getModePermissions(acl, ACL.Write)); - } - - // Check if any collected authorization block allows the specific agent - return auths.some((term): boolean => this.hasAccess(agent, term, acl)); - } - - /** - * Capitalizes the input string. - * @param mode - String to transform. - * - * @returns The capitalized string. - */ - private capitalize(mode: string): string { - return `${mode[0].toUpperCase()}${mode.slice(1).toLowerCase()}`; - } - - /** - * Returns the identifiers of all authorizations that grant the given mode access for a resource. - * @param acl - The store containing the quads of the ACL resource. - * @param aclMode - A valid acl mode (ACL.Write/Read/...) - */ - private getModePermissions(acl: Store, aclMode: string): Term[] { - return acl.getQuads(null, ACL.mode, aclMode, null).map((quad: Quad): Term => quad.subject); - } - - /** - * Checks if the given agent has access to the modes specified by the given authorization. - * @param agent - Credentials of agent that needs access. - * @param auth - acl:Authorization that needs to be checked. - * @param acl - A store containing the relevant triples of the authorization. - * - * @returns If the agent has access. - */ - private hasAccess(agent: Credentials, auth: Term, acl: Store): boolean { - // Check if public access is allowed - if (acl.countQuads(auth, ACL.agentClass, FOAF.Agent, null) !== 0) { - return true; - } - - // Check if authenticated access is allowed - if (this.isAuthenticated(agent)) { - // Check if any authenticated agent is allowed - if (acl.countQuads(auth, ACL.agentClass, ACL.AuthenticatedAgent, null) !== 0) { - return true; - } - // Check if this specific agent is allowed - if (acl.countQuads(auth, ACL.agent, agent.webId, null) !== 0) { - return true; - } - } - - // Neither unauthenticated nor authenticated access are allowed - return false; - } - /** * Returns the ACL triples that are relevant for the given identifier. * These can either be from a corresponding ACL document or an ACL document higher up with defaults. diff --git a/src/authorization/access-checkers/AccessChecker.ts b/src/authorization/access-checkers/AccessChecker.ts new file mode 100644 index 000000000..30186b785 --- /dev/null +++ b/src/authorization/access-checkers/AccessChecker.ts @@ -0,0 +1,25 @@ +import type { Store, Term } from 'n3'; +import type { Credentials } from '../../authentication/Credentials'; +import { AsyncHandler } from '../../util/handlers/AsyncHandler'; + +/** + * Performs an authorization check against the given acl resource. + */ +export abstract class AccessChecker extends AsyncHandler {} + +export interface AccessCheckerArgs { + /** + * A store containing the relevant triples of the authorization. + */ + acl: Store; + + /** + * Authorization rule to be processed. + */ + rule: Term; + + /** + * Credentials of the entity that wants to use the resource. + */ + credentials: Credentials; +} diff --git a/src/authorization/access-checkers/AgentAccessChecker.ts b/src/authorization/access-checkers/AgentAccessChecker.ts new file mode 100644 index 000000000..c5d9a6c43 --- /dev/null +++ b/src/authorization/access-checkers/AgentAccessChecker.ts @@ -0,0 +1,15 @@ +import { ACL } from '../../util/Vocabularies'; +import type { AccessCheckerArgs } from './AccessChecker'; +import { AccessChecker } from './AccessChecker'; + +/** + * Checks if the given WebID has been given access. + */ +export class AgentAccessChecker extends AccessChecker { + public async handle({ acl, rule, credentials }: AccessCheckerArgs): Promise { + if (typeof credentials.webId === 'string') { + return acl.countQuads(rule, ACL.terms.agent, credentials.webId, null) !== 0; + } + return false; + } +} diff --git a/src/authorization/access-checkers/AgentClassAccessChecker.ts b/src/authorization/access-checkers/AgentClassAccessChecker.ts new file mode 100644 index 000000000..2866b663d --- /dev/null +++ b/src/authorization/access-checkers/AgentClassAccessChecker.ts @@ -0,0 +1,20 @@ +import { ACL, FOAF } from '../../util/Vocabularies'; +import type { AccessCheckerArgs } from './AccessChecker'; +import { AccessChecker } from './AccessChecker'; + +/** + * Checks access based on the agent class. + */ +export class AgentClassAccessChecker extends AccessChecker { + public async handle({ acl, rule, credentials }: AccessCheckerArgs): Promise { + // Check if unauthenticated agents have access + if (acl.countQuads(rule, ACL.terms.agentClass, FOAF.terms.Agent, null) !== 0) { + return true; + } + // Check if the agent is authenticated and if authenticated agents have access + if (typeof credentials.webId === 'string') { + return acl.countQuads(rule, ACL.terms.agentClass, ACL.terms.AuthenticatedAgent, null) !== 0; + } + return false; + } +} diff --git a/src/index.ts b/src/index.ts index acb8d16f7..6a9b7ce8f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,6 +17,11 @@ export * from './authorization/PathBasedAuthorizer'; export * from './authorization/WebAclAuthorization'; export * from './authorization/WebAclAuthorizer'; +// Authorization/access-checkers +export * from './authorization/access-checkers/AccessChecker'; +export * from './authorization/access-checkers/AgentAccessChecker'; +export * from './authorization/access-checkers/AgentClassAccessChecker'; + // Identity/Configuration export * from './identity/configuration/IdentityProviderFactory'; export * from './identity/configuration/ProviderFactory'; diff --git a/src/util/Vocabularies.ts b/src/util/Vocabularies.ts index 740567b10..a27bde91e 100644 --- a/src/util/Vocabularies.ts +++ b/src/util/Vocabularies.ts @@ -60,6 +60,7 @@ export const ACL = createUriAndTermNamespace('http://www.w3.org/ns/auth/acl#', 'agent', 'agentClass', 'AuthenticatedAgent', + 'Authorization', 'default', 'mode', diff --git a/test/unit/authorization/WebAclAuthorizer.test.ts b/test/unit/authorization/WebAclAuthorizer.test.ts index 09590c409..c72468aca 100644 --- a/test/unit/authorization/WebAclAuthorizer.test.ts +++ b/test/unit/authorization/WebAclAuthorizer.test.ts @@ -1,5 +1,6 @@ import { namedNode, quad } from '@rdfjs/data-model'; import type { Credentials } from '../../../src/authentication/Credentials'; +import type { AccessChecker } from '../../../src/authorization/access-checkers/AccessChecker'; import { WebAclAuthorization } from '../../../src/authorization/WebAclAuthorization'; import { WebAclAuthorizer } from '../../../src/authorization/WebAclAuthorizer'; import type { AuxiliaryIdentifierStrategy } from '../../../src/ldp/auxiliary/AuxiliaryIdentifierStrategy'; @@ -18,6 +19,7 @@ import { guardedStreamFrom } from '../../../src/util/StreamUtil'; const nn = namedNode; const acl = 'http://www.w3.org/ns/auth/acl#'; +const rdf = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'; describe('A WebAclAuthorizer', (): void => { let authorizer: WebAclAuthorizer; @@ -26,12 +28,13 @@ describe('A WebAclAuthorizer', (): void => { isAuxiliaryIdentifier: (id: ResourceIdentifier): boolean => id.path.endsWith('.acl'), getAssociatedIdentifier: (id: ResourceIdentifier): ResourceIdentifier => ({ path: id.path.slice(0, -4) }), } as any; - let store: ResourceStore; + let store: jest.Mocked; const identifierStrategy = new SingleRootIdentifierStrategy('http://test.com/'); let permissions: PermissionSet; let credentials: Credentials; let identifier: ResourceIdentifier; let authorization: WebAclAuthorization; + let accessChecker: jest.Mocked; beforeEach(async(): Promise => { permissions = { @@ -60,18 +63,38 @@ describe('A WebAclAuthorizer', (): void => { store = { getRepresentation: jest.fn(), } as any; - authorizer = new WebAclAuthorizer(aclStrategy, store, identifierStrategy); + + accessChecker = { + handleSafe: jest.fn().mockResolvedValue(true), + } as any; + + authorizer = new WebAclAuthorizer(aclStrategy, store, identifierStrategy, accessChecker); }); it('handles all non-acl inputs.', async(): Promise => { - authorizer = new WebAclAuthorizer(aclStrategy, null as any, identifierStrategy); + authorizer = new WebAclAuthorizer(aclStrategy, null as any, identifierStrategy, accessChecker); await expect(authorizer.canHandle({ identifier } as any)).resolves.toBeUndefined(); await expect(authorizer.canHandle({ identifier: aclStrategy.getAuxiliaryIdentifier(identifier) } as any)) .rejects.toThrow(NotImplementedHttpError); }); + it('handles all valid modes and ignores other ones.', async(): Promise => { + credentials.webId = 'http://test.com/user'; + 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`)), + quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Write`)), + quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}fakeMode1`)), + ]) } as Representation); + Object.assign(authorization.everyone, { read: true, write: true, append: true, control: false }); + Object.assign(authorization.user, { read: true, write: true, append: true, control: false }); + await expect(authorizer.handle({ identifier, permissions, credentials })).resolves.toEqual(authorization); + }); + it('allows access if the acl file allows all agents.', async(): Promise => { - store.getRepresentation = async(): Promise => ({ data: guardedStreamFrom([ + store.getRepresentation.mockResolvedValue({ data: guardedStreamFrom([ + quad(nn('auth'), nn(`${rdf}type`), nn(`${acl}Authorization`)), quad(nn('auth'), nn(`${acl}agentClass`), nn('http://xmlns.com/foaf/0.1/Agent')), quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)), quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Read`)), @@ -83,101 +106,71 @@ describe('A WebAclAuthorizer', (): void => { }); it('allows access if there is a parent acl file allowing all agents.', async(): Promise => { - store.getRepresentation = async(id: ResourceIdentifier): Promise => { + store.getRepresentation.mockImplementation(async(id: ResourceIdentifier): Promise => { if (id.path.endsWith('foo.acl')) { throw new NotFoundHttpError(); } return { data: guardedStreamFrom([ + quad(nn('auth'), nn(`${rdf}type`), nn(`${acl}Authorization`)), quad(nn('auth'), nn(`${acl}agentClass`), nn('http://xmlns.com/foaf/0.1/Agent')), quad(nn('auth'), nn(`${acl}default`), nn(identifierStrategy.getParentContainer(identifier).path)), quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Read`)), quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Write`)), ]), } as Representation; - }; + }); Object.assign(authorization.everyone, { read: true, write: true, append: true }); Object.assign(authorization.user, { read: true, write: true, append: true }); await expect(authorizer.handle({ identifier, permissions, credentials })).resolves.toEqual(authorization); }); - it('allows access to authorized agents if the acl files allows all authorized users.', async(): Promise => { - store.getRepresentation = async(): Promise => ({ data: guardedStreamFrom([ - quad(nn('auth'), nn(`${acl}agentClass`), nn(`${acl}AuthenticatedAgent`)), + it('throws a ForbiddenHttpError if access is not granted and credentials have a WebID.', async(): Promise => { + accessChecker.handleSafe.mockResolvedValue(false); + 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`)), - quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Write`)), ]) } as Representation); - credentials.webId = 'http://test.com/user'; - Object.assign(authorization.user, { read: true, write: true, append: true }); - await expect(authorizer.handle({ identifier, permissions, credentials })).resolves.toEqual(authorization); + credentials.webId = 'http://test.com/alice/profile/card#me'; + await expect(authorizer.handle({ identifier, permissions, credentials })).rejects.toThrow(ForbiddenHttpError); }); - it('errors if authorization is required but the agent is not authorized.', async(): Promise => { - store.getRepresentation = async(): Promise => ({ data: guardedStreamFrom([ - quad(nn('auth'), nn(`${acl}agentClass`), nn(`${acl}AuthenticatedAgent`)), + it('throws an UnauthorizedHttpError if access is not granted there are no credentials.', async(): Promise => { + accessChecker.handleSafe.mockResolvedValue(false); + 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`)), - quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Write`)), ]) } as Representation); await expect(authorizer.handle({ identifier, permissions, credentials })).rejects.toThrow(UnauthorizedHttpError); }); - it('allows access to specific agents if the acl files identifies them.', async(): Promise => { - credentials.webId = 'http://test.com/user'; - store.getRepresentation = async(): Promise => ({ data: guardedStreamFrom([ - quad(nn('auth'), nn(`${acl}agent`), nn(credentials.webId!)), - quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)), - quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Read`)), - quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Write`)), - ]) } as Representation); - Object.assign(authorization.user, { read: true, write: true, append: true }); - await expect(authorizer.handle({ identifier, permissions, credentials })).resolves.toEqual(authorization); - }); - - it('errors if a specific agents wants to access files not assigned to them.', async(): Promise => { - credentials.webId = 'http://test.com/user'; - store.getRepresentation = async(): Promise => ({ data: guardedStreamFrom([ - quad(nn('auth'), nn(`${acl}agent`), nn('http://test.com/differentUser')), - quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)), - quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Read`)), - quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Write`)), - ]) } as Representation); - await expect(authorizer.handle({ identifier, permissions, credentials })).rejects.toThrow(ForbiddenHttpError); - }); - it('re-throws ResourceStore errors as internal errors.', async(): Promise => { - store.getRepresentation = async(): Promise => { - throw new Error('TEST!'); - }; + store.getRepresentation.mockRejectedValue(new Error('TEST!')); const promise = authorizer.handle({ identifier, permissions, credentials }); await expect(promise).rejects.toThrow(`Error reading ACL for ${identifier.path}: TEST!`); await expect(promise).rejects.toThrow(InternalServerError); }); it('errors if the root container has no corresponding acl document.', async(): Promise => { - store.getRepresentation = async(): Promise => { - throw new NotFoundHttpError(); - }; + store.getRepresentation.mockRejectedValue(new NotFoundHttpError()); const promise = authorizer.handle({ identifier, permissions, credentials }); await expect(promise).rejects.toThrow('No ACL document found for root container'); await expect(promise).rejects.toThrow(ForbiddenHttpError); }); it('allows an agent to append if they have write access.', async(): Promise => { - credentials.webId = 'http://test.com/user'; - identifier.path = 'http://test.com/foo'; permissions = { read: false, write: false, append: true, control: false, }; - store.getRepresentation = async(): Promise => ({ data: guardedStreamFrom([ - quad(nn('auth'), nn(`${acl}agent`), nn(credentials.webId!)), + 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); + Object.assign(authorization.everyone, { write: true, append: true }); Object.assign(authorization.user, { write: true, append: true }); await expect(authorizer.handle({ identifier, permissions, credentials })).resolves.toEqual(authorization); }); diff --git a/test/unit/authorization/access-checkers/AgentAccessChecker.test.ts b/test/unit/authorization/access-checkers/AgentAccessChecker.test.ts new file mode 100644 index 000000000..a1b02ad8c --- /dev/null +++ b/test/unit/authorization/access-checkers/AgentAccessChecker.test.ts @@ -0,0 +1,32 @@ +import { DataFactory, Store } from 'n3'; +import type { AccessCheckerArgs } from '../../../../src/authorization/access-checkers/AccessChecker'; +import { AgentAccessChecker } from '../../../../src/authorization/access-checkers/AgentAccessChecker'; +import { ACL } from '../../../../src/util/Vocabularies'; +import namedNode = DataFactory.namedNode; + +describe('A AgentAccessChecker', (): void => { + const webId = 'http://test.com/alice/profile/card#me'; + const acl = new Store(); + acl.addQuad(namedNode('match'), ACL.terms.agent, namedNode(webId)); + acl.addQuad(namedNode('noMatch'), ACL.terms.agent, namedNode('http://test.com/bob')); + const checker = new AgentAccessChecker(); + + it('can handle all requests.', async(): Promise => { + await expect(checker.canHandle(null as any)).resolves.toBeUndefined(); + }); + + it('returns true if a match is found for the given WebID.', async(): Promise => { + const input: AccessCheckerArgs = { acl, rule: namedNode('match'), credentials: { webId }}; + await expect(checker.handle(input)).resolves.toBe(true); + }); + + it('returns false if no match is found.', async(): Promise => { + const input: AccessCheckerArgs = { acl, rule: namedNode('noMatch'), credentials: { webId }}; + await expect(checker.handle(input)).resolves.toBe(false); + }); + + it('returns false if the credentials contain no WebID.', async(): Promise => { + const input: AccessCheckerArgs = { acl, rule: namedNode('match'), credentials: {}}; + await expect(checker.handle(input)).resolves.toBe(false); + }); +}); diff --git a/test/unit/authorization/access-checkers/AgentClassAccessChecker.test.ts b/test/unit/authorization/access-checkers/AgentClassAccessChecker.test.ts new file mode 100644 index 000000000..b9e6ef285 --- /dev/null +++ b/test/unit/authorization/access-checkers/AgentClassAccessChecker.test.ts @@ -0,0 +1,37 @@ +import { DataFactory, Store } from 'n3'; +import type { AccessCheckerArgs } from '../../../../src/authorization/access-checkers/AccessChecker'; +import { AgentClassAccessChecker } from '../../../../src/authorization/access-checkers/AgentClassAccessChecker'; +import { ACL, FOAF } from '../../../../src/util/Vocabularies'; +import namedNode = DataFactory.namedNode; + +describe('An AgentClassAccessChecker', (): void => { + const webId = 'http://test.com/alice/profile/card#me'; + const acl = new Store(); + acl.addQuad(namedNode('agentMatch'), ACL.terms.agentClass, FOAF.terms.Agent); + acl.addQuad(namedNode('authenticatedMatch'), ACL.terms.agentClass, ACL.terms.AuthenticatedAgent); + const checker = new AgentClassAccessChecker(); + + it('can handle all requests.', async(): Promise => { + await expect(checker.canHandle(null as any)).resolves.toBeUndefined(); + }); + + it('returns true if the rule contains foaf:agent as supported class.', async(): Promise => { + const input: AccessCheckerArgs = { acl, rule: namedNode('agentMatch'), credentials: {}}; + await expect(checker.handle(input)).resolves.toBe(true); + }); + + it('returns true for authenticated users with an acl:AuthenticatedAgent rule.', async(): Promise => { + const input: AccessCheckerArgs = { acl, rule: namedNode('authenticatedMatch'), credentials: { webId }}; + await expect(checker.handle(input)).resolves.toBe(true); + }); + + it('returns false for unauthenticated users with an acl:AuthenticatedAgent rule.', async(): Promise => { + const input: AccessCheckerArgs = { acl, rule: namedNode('authenticatedMatch'), credentials: {}}; + await expect(checker.handle(input)).resolves.toBe(false); + }); + + it('returns false if no class rule is found.', async(): Promise => { + const input: AccessCheckerArgs = { acl, rule: namedNode('noMatch'), credentials: {}}; + await expect(checker.handle(input)).resolves.toBe(false); + }); +});