diff --git a/src/authorization/AllowEverythingAuthorizer.ts b/src/authorization/AllowEverythingAuthorizer.ts index 4a1d5d4c7..7344d1a0c 100644 --- a/src/authorization/AllowEverythingAuthorizer.ts +++ b/src/authorization/AllowEverythingAuthorizer.ts @@ -1,10 +1,19 @@ +import type { PermissionSet } from '../ldp/permissions/PermissionSet'; import { Authorizer } from './Authorizer'; +import { WebAclAuthorization } from './WebAclAuthorization'; + +const allowEverything: PermissionSet = { + read: true, + write: true, + append: true, + control: true, +}; /** * Authorizer which allows all access independent of the identifier and requested permissions. */ export class AllowEverythingAuthorizer extends Authorizer { - public async handle(): Promise { - // Allows all actions + public async handle(): Promise { + return new WebAclAuthorization(allowEverything, allowEverything); } } diff --git a/src/authorization/Authorization.ts b/src/authorization/Authorization.ts new file mode 100644 index 000000000..f0d93a772 --- /dev/null +++ b/src/authorization/Authorization.ts @@ -0,0 +1,12 @@ +import type { RepresentationMetadata } from '../ldp/representation/RepresentationMetadata'; + +/** + * The output of an Authorizer + */ +export interface Authorization { + /** + * Add metadata relevant for this Authorization. + * @param metadata - Metadata to update. + */ + addMetadata: (metadata: RepresentationMetadata) => void; +} diff --git a/src/authorization/Authorizer.ts b/src/authorization/Authorizer.ts index 8e1dd8e05..bb4f2f3a9 100644 --- a/src/authorization/Authorizer.ts +++ b/src/authorization/Authorizer.ts @@ -2,12 +2,13 @@ import type { Credentials } from '../authentication/Credentials'; import type { PermissionSet } from '../ldp/permissions/PermissionSet'; import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier'; import { AsyncHandler } from '../util/handlers/AsyncHandler'; +import type { Authorization } from './Authorization'; /** * Verifies if the given credentials have access to the given permissions on the given resource. * An {@link Error} with the necessary explanation will be thrown when permissions are not granted. */ -export abstract class Authorizer extends AsyncHandler {} +export abstract class Authorizer extends AsyncHandler {} export interface AuthorizerArgs { /** diff --git a/src/authorization/AuxiliaryAuthorizer.ts b/src/authorization/AuxiliaryAuthorizer.ts index f385aff6d..6a97e6e00 100644 --- a/src/authorization/AuxiliaryAuthorizer.ts +++ b/src/authorization/AuxiliaryAuthorizer.ts @@ -1,6 +1,7 @@ import type { AuxiliaryIdentifierStrategy } from '../ldp/auxiliary/AuxiliaryIdentifierStrategy'; import { getLoggerFor } from '../logging/LogUtil'; import { NotImplementedHttpError } from '../util/errors/NotImplementedHttpError'; +import type { Authorization } from './Authorization'; import type { AuthorizerArgs } from './Authorizer'; import { Authorizer } from './Authorizer'; @@ -26,13 +27,13 @@ export class AuxiliaryAuthorizer extends Authorizer { return this.resourceAuthorizer.canHandle(resourceAuth); } - public async handle(auxiliaryAuth: AuthorizerArgs): Promise { + public async handle(auxiliaryAuth: AuthorizerArgs): Promise { const resourceAuth = this.getRequiredAuthorization(auxiliaryAuth); this.logger.debug(`Checking auth request for ${auxiliaryAuth.identifier.path} on ${resourceAuth.identifier.path}`); return this.resourceAuthorizer.handle(resourceAuth); } - public async handleSafe(auxiliaryAuth: AuthorizerArgs): Promise { + public async handleSafe(auxiliaryAuth: AuthorizerArgs): Promise { const resourceAuth = this.getRequiredAuthorization(auxiliaryAuth); this.logger.debug(`Checking auth request for ${auxiliaryAuth.identifier.path} to ${resourceAuth.identifier.path}`); return this.resourceAuthorizer.handleSafe(resourceAuth); diff --git a/src/authorization/WebAclAuthorization.ts b/src/authorization/WebAclAuthorization.ts new file mode 100644 index 000000000..6d8be837b --- /dev/null +++ b/src/authorization/WebAclAuthorization.ts @@ -0,0 +1,35 @@ +import type { PermissionSet } from '../ldp/permissions/PermissionSet'; +import type { RepresentationMetadata } from '../ldp/representation/RepresentationMetadata'; +import { ACL, AUTH } from '../util/Vocabularies'; +import type { Authorization } from './Authorization'; + +/** + * Indicates which permissions are available on the requested resource. + */ +export class WebAclAuthorization implements Authorization { + /** + * Permissions granted to the agent requesting the resource. + */ + public user: PermissionSet; + /** + * Permissions granted to the public. + */ + public everyone: PermissionSet; + + public constructor(user: PermissionSet, everyone: PermissionSet) { + this.user = user; + this.everyone = everyone; + } + + public addMetadata(metadata: RepresentationMetadata): void { + for (const mode of (Object.keys(this.user) as (keyof PermissionSet)[])) { + const capitalizedMode = mode.charAt(0).toUpperCase() + mode.slice(1) as 'Read' | 'Write' | 'Append' | 'Control'; + if (this.user[mode]) { + metadata.add(AUTH.terms.userMode, ACL.terms[capitalizedMode]); + } + if (this.everyone[mode]) { + metadata.add(AUTH.terms.publicMode, ACL.terms[capitalizedMode]); + } + } + } +} diff --git a/src/authorization/WebAclAuthorizer.ts b/src/authorization/WebAclAuthorizer.ts index 04329829b..74f8f0a5c 100644 --- a/src/authorization/WebAclAuthorizer.ts +++ b/src/authorization/WebAclAuthorizer.ts @@ -16,6 +16,7 @@ import type { IdentifierStrategy } from '../util/identifiers/IdentifierStrategy' import { ACL, FOAF } from '../util/Vocabularies'; import type { AuthorizerArgs } from './Authorizer'; import { Authorizer } from './Authorizer'; +import { WebAclAuthorization } from './WebAclAuthorization'; /** * Handles most web access control predicates such as @@ -48,36 +49,60 @@ export class WebAclAuthorizer extends Authorizer { * Will throw an error if this is not the case. * @param input - Relevant data needed to check if access can be granted. */ - public async handle({ identifier, permissions, credentials }: AuthorizerArgs): Promise { + public async handle({ identifier, permissions, credentials }: AuthorizerArgs): Promise { const modes = (Object.keys(permissions) as (keyof PermissionSet)[]).filter((key): boolean => permissions[key]); // Verify that all required modes are set for the given agent this.logger.debug(`Checking if ${credentials.webId} has ${modes.join()} permissions for ${identifier.path}`); const store = await this.getAclRecursive(identifier); + const authorization = this.createAuthorization(credentials, store); for (const mode of modes) { - this.checkPermission(credentials, store, mode); + this.checkPermission(credentials, authorization, mode); } this.logger.debug(`${credentials.webId} has ${modes.join()} permissions for ${identifier.path}`); + return authorization; } /** - * Checks if any of the triples in the store grant the agent permission to use the given mode. + * Creates an Authorization object based on the quads found in the store. + * @param agent - Agent who's credentials will be used for the `user` field. + * @param store - Store containing all relevant authorization triples. + */ + private createAuthorization(agent: Credentials, store: Store): WebAclAuthorization { + const publicPermissions = this.createPermissions({}, store); + const userPermissions = this.createPermissions(agent, store); + + return new WebAclAuthorization(userPermissions, publicPermissions); + } + + /** + * Creates the authorization permissions for the given credentials. + * @param credentials - Credentials to find the permissions for. + * @param store - Store containing all relevant authorization triples. + */ + private createPermissions(credentials: Credentials, store: Store): PermissionSet { + const permissions: PermissionSet = { + read: false, + write: false, + append: false, + control: false, + }; + for (const mode of (Object.keys(permissions) as (keyof PermissionSet)[])) { + permissions[mode] = this.hasPermission(credentials, store, mode); + } + return permissions; + } + + /** + * Checks if the authorization grants the agent permission to use the given mode. * Throws a {@link ForbiddenHttpError} or {@link UnauthorizedHttpError} depending on the credentials * if access is not allowed. * @param agent - Agent that wants access. - * @param store - A store containing the relevant triples for authorization. - * @param mode - Which mode is requested. Probable one of ('write' | 'read' | 'append' | 'control'). + * @param authorization - An Authorization containing the permissions the agent has on the resource. + * @param mode - Which mode is requested. */ - private checkPermission(agent: Credentials, store: Store, mode: string): void { - const modeString = ACL[this.capitalize(mode) as 'Write' | 'Read' | 'Append' | 'Control']; - const auths = this.getModePermissions(store, modeString); - - // Having write permissions implies having append permissions - if (modeString === ACL.Append) { - auths.push(...this.getModePermissions(store, ACL.Write)); - } - - if (!auths.some((term): boolean => this.hasAccess(agent, term, store))) { + private checkPermission(agent: Credentials, authorization: WebAclAuthorization, mode: keyof PermissionSet): void { + if (!authorization.user[mode]) { const isLoggedIn = typeof agent.webId === 'string'; if (isLoggedIn) { this.logger.warn(`Agent ${agent.webId} has no ${mode} permissions`); @@ -92,6 +117,24 @@ export class WebAclAuthorizer extends Authorizer { } } + /** + * Checks if the given agent has permission to execute the given mode based on the triples in the store. + * @param agent - Agent that wants access. + * @param store - A store containing the relevant triples for authorization. + * @param mode - Which mode is requested. + */ + private hasPermission(agent: Credentials, store: Store, mode: keyof PermissionSet): boolean { + const modeString = ACL[this.capitalize(mode) as 'Write' | 'Read' | 'Append' | 'Control']; + const auths = this.getModePermissions(store, modeString); + + // Having write permissions implies having append permissions + if (modeString === ACL.Append) { + auths.push(...this.getModePermissions(store, ACL.Write)); + } + + return auths.some((term): boolean => this.hasAccess(agent, term, store)); + } + /** * Capitalizes the input string. * @param mode - String to transform. diff --git a/src/index.ts b/src/index.ts index d68b27134..0f5d56f8d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,8 +9,10 @@ export * from './authentication/UnsecureWebIdExtractor'; // Authorization export * from './authorization/AllowEverythingAuthorizer'; +export * from './authorization/Authorization'; export * from './authorization/Authorizer'; export * from './authorization/AuxiliaryAuthorizer'; +export * from './authorization/WebAclAuthorization'; export * from './authorization/WebAclAuthorizer'; // Init diff --git a/src/util/Vocabularies.ts b/src/util/Vocabularies.ts index 6e1b979d8..d8c2d5027 100644 --- a/src/util/Vocabularies.ts +++ b/src/util/Vocabularies.ts @@ -68,6 +68,11 @@ export const ACL = createUriAndTermNamespace('http://www.w3.org/ns/auth/acl#', 'Control', ); +export const AUTH = createUriAndTermNamespace('urn:solid:auth:', + 'userMode', + 'publicMode', +); + export const DC = createUriAndTermNamespace('http://purl.org/dc/terms/', 'modified', ); diff --git a/test/unit/authorization/AllowEverythingAuthorizer.test.ts b/test/unit/authorization/AllowEverythingAuthorizer.test.ts index c87b0e1f2..3c0f8d4a0 100644 --- a/test/unit/authorization/AllowEverythingAuthorizer.test.ts +++ b/test/unit/authorization/AllowEverythingAuthorizer.test.ts @@ -1,13 +1,23 @@ import { AllowEverythingAuthorizer } from '../../../src/authorization/AllowEverythingAuthorizer'; +import type { PermissionSet } from '../../../src/ldp/permissions/PermissionSet'; describe('An AllowEverythingAuthorizer', (): void => { const authorizer = new AllowEverythingAuthorizer(); + const allowEverything: PermissionSet = { + read: true, + write: true, + append: true, + control: true, + }; it('can handle everything.', async(): Promise => { await expect(authorizer.canHandle({} as any)).resolves.toBeUndefined(); }); - it('always returns undefined.', async(): Promise => { - await expect(authorizer.handle()).resolves.toBeUndefined(); + it('always returns an empty Authorization.', async(): Promise => { + await expect(authorizer.handle()).resolves.toEqual({ + user: allowEverything, + everyone: allowEverything, + }); }); }); diff --git a/test/unit/authorization/WebAclAuthorization.test.ts b/test/unit/authorization/WebAclAuthorization.test.ts new file mode 100644 index 000000000..68eaf6058 --- /dev/null +++ b/test/unit/authorization/WebAclAuthorization.test.ts @@ -0,0 +1,43 @@ +import { WebAclAuthorization } from '../../../src/authorization/WebAclAuthorization'; +import { RepresentationMetadata } from '../../../src/ldp/representation/RepresentationMetadata'; +import { ACL, AUTH } from '../../../src/util/Vocabularies'; +import 'jest-rdf'; + +describe('A WebAclAuthorization', (): void => { + let authorization: WebAclAuthorization; + let metadata: RepresentationMetadata; + + beforeEach(async(): Promise => { + authorization = new WebAclAuthorization( + { + read: false, + append: false, + write: false, + control: false, + }, + { + read: false, + append: false, + write: false, + control: false, + }, + ); + + metadata = new RepresentationMetadata(); + }); + + it('adds no metadata if there are no permissions.', async(): Promise => { + expect(authorization.addMetadata(metadata)).toBeUndefined(); + expect(metadata.quads()).toHaveLength(0); + }); + + it('adds corresponding acl metadata for all permissions present.', async(): Promise => { + authorization.user.read = true; + authorization.user.write = true; + authorization.everyone.read = true; + expect(authorization.addMetadata(metadata)).toBeUndefined(); + expect(metadata.quads()).toHaveLength(3); + expect(metadata.getAll(AUTH.terms.userMode)).toEqualRdfTermArray([ ACL.terms.Read, ACL.terms.Write ]); + expect(metadata.get(AUTH.terms.publicMode)).toEqualRdfTerm(ACL.terms.Read); + }); +}); diff --git a/test/unit/authorization/WebAclAuthorizer.test.ts b/test/unit/authorization/WebAclAuthorizer.test.ts index 350496620..e9a599c43 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 { WebAclAuthorization } from '../../../src/authorization/WebAclAuthorization'; import { WebAclAuthorizer } from '../../../src/authorization/WebAclAuthorizer'; import type { AuxiliaryIdentifierStrategy } from '../../../src/ldp/auxiliary/AuxiliaryIdentifierStrategy'; import type { PermissionSet } from '../../../src/ldp/permissions/PermissionSet'; @@ -29,6 +30,7 @@ describe('A WebAclAuthorizer', (): void => { let permissions: PermissionSet; let credentials: Credentials; let identifier: ResourceIdentifier; + let authorization: WebAclAuthorization; beforeEach(async(): Promise => { permissions = { @@ -39,6 +41,20 @@ describe('A WebAclAuthorizer', (): void => { }; credentials = {}; identifier = { path: 'http://test.com/foo' }; + authorization = new WebAclAuthorization( + { + read: false, + append: false, + write: false, + control: false, + }, + { + read: false, + append: false, + write: false, + control: false, + }, + ); store = { getRepresentation: jest.fn(), @@ -60,7 +76,9 @@ describe('A WebAclAuthorizer', (): void => { 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 })).resolves.toBeUndefined(); + 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 if there is a parent acl file allowing all agents.', async(): Promise => { @@ -77,7 +95,9 @@ describe('A WebAclAuthorizer', (): void => { ]), } as Representation; }; - await expect(authorizer.handle({ identifier, permissions, credentials })).resolves.toBeUndefined(); + 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 => { @@ -88,7 +108,8 @@ describe('A WebAclAuthorizer', (): void => { quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Write`)), ]) } as Representation); credentials.webId = 'http://test.com/user'; - await expect(authorizer.handle({ identifier, permissions, credentials })).resolves.toBeUndefined(); + Object.assign(authorization.user, { read: true, write: true, append: true }); + await expect(authorizer.handle({ identifier, permissions, credentials })).resolves.toEqual(authorization); }); it('errors if authorization is required but the agent is not authorized.', async(): Promise => { @@ -109,7 +130,8 @@ describe('A WebAclAuthorizer', (): void => { 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 })).resolves.toBeUndefined(); + 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 => { @@ -153,6 +175,7 @@ describe('A WebAclAuthorizer', (): void => { quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)), quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Write`)), ]) } as Representation); - await expect(authorizer.handle({ identifier, permissions, credentials })).resolves.toBeUndefined(); + Object.assign(authorization.user, { write: true, append: true }); + await expect(authorizer.handle({ identifier, permissions, credentials })).resolves.toEqual(authorization); }); }); diff --git a/test/unit/ldp/AuthenticatedLdpHandler.test.ts b/test/unit/ldp/AuthenticatedLdpHandler.test.ts index 313b34343..c18e9768b 100644 --- a/test/unit/ldp/AuthenticatedLdpHandler.test.ts +++ b/test/unit/ldp/AuthenticatedLdpHandler.test.ts @@ -70,7 +70,7 @@ describe('An AuthenticatedLdpHandler', (): void => { }); it('errors an invalid object was thrown by a handler.', async(): Promise< void> => { - args.authorizer.handle = async(): Promise => { + args.authorizer.handle = async(): Promise => { throw 'apple'; }; const handler = new AuthenticatedLdpHandler(args);