feat: Let Authorizers output an Authorization

This commit is contained in:
Joachim Van Herwegen 2021-02-16 15:59:41 +01:00
parent 8ccc68d29c
commit f2f265c586
12 changed files with 212 additions and 28 deletions

View File

@ -1,10 +1,19 @@
import type { PermissionSet } from '../ldp/permissions/PermissionSet';
import { Authorizer } from './Authorizer'; 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. * Authorizer which allows all access independent of the identifier and requested permissions.
*/ */
export class AllowEverythingAuthorizer extends Authorizer { export class AllowEverythingAuthorizer extends Authorizer {
public async handle(): Promise<void> { public async handle(): Promise<WebAclAuthorization> {
// Allows all actions return new WebAclAuthorization(allowEverything, allowEverything);
} }
} }

View File

@ -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;
}

View File

@ -2,12 +2,13 @@ import type { Credentials } from '../authentication/Credentials';
import type { PermissionSet } from '../ldp/permissions/PermissionSet'; import type { PermissionSet } from '../ldp/permissions/PermissionSet';
import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier'; import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
import { AsyncHandler } from '../util/handlers/AsyncHandler'; 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. * 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. * An {@link Error} with the necessary explanation will be thrown when permissions are not granted.
*/ */
export abstract class Authorizer extends AsyncHandler<AuthorizerArgs> {} export abstract class Authorizer extends AsyncHandler<AuthorizerArgs, Authorization> {}
export interface AuthorizerArgs { export interface AuthorizerArgs {
/** /**

View File

@ -1,6 +1,7 @@
import type { AuxiliaryIdentifierStrategy } from '../ldp/auxiliary/AuxiliaryIdentifierStrategy'; import type { AuxiliaryIdentifierStrategy } from '../ldp/auxiliary/AuxiliaryIdentifierStrategy';
import { getLoggerFor } from '../logging/LogUtil'; import { getLoggerFor } from '../logging/LogUtil';
import { NotImplementedHttpError } from '../util/errors/NotImplementedHttpError'; import { NotImplementedHttpError } from '../util/errors/NotImplementedHttpError';
import type { Authorization } from './Authorization';
import type { AuthorizerArgs } from './Authorizer'; import type { AuthorizerArgs } from './Authorizer';
import { Authorizer } from './Authorizer'; import { Authorizer } from './Authorizer';
@ -26,13 +27,13 @@ export class AuxiliaryAuthorizer extends Authorizer {
return this.resourceAuthorizer.canHandle(resourceAuth); return this.resourceAuthorizer.canHandle(resourceAuth);
} }
public async handle(auxiliaryAuth: AuthorizerArgs): Promise<void> { public async handle(auxiliaryAuth: AuthorizerArgs): Promise<Authorization> {
const resourceAuth = this.getRequiredAuthorization(auxiliaryAuth); const resourceAuth = this.getRequiredAuthorization(auxiliaryAuth);
this.logger.debug(`Checking auth request for ${auxiliaryAuth.identifier.path} on ${resourceAuth.identifier.path}`); this.logger.debug(`Checking auth request for ${auxiliaryAuth.identifier.path} on ${resourceAuth.identifier.path}`);
return this.resourceAuthorizer.handle(resourceAuth); return this.resourceAuthorizer.handle(resourceAuth);
} }
public async handleSafe(auxiliaryAuth: AuthorizerArgs): Promise<void> { public async handleSafe(auxiliaryAuth: AuthorizerArgs): Promise<Authorization> {
const resourceAuth = this.getRequiredAuthorization(auxiliaryAuth); const resourceAuth = this.getRequiredAuthorization(auxiliaryAuth);
this.logger.debug(`Checking auth request for ${auxiliaryAuth.identifier.path} to ${resourceAuth.identifier.path}`); this.logger.debug(`Checking auth request for ${auxiliaryAuth.identifier.path} to ${resourceAuth.identifier.path}`);
return this.resourceAuthorizer.handleSafe(resourceAuth); return this.resourceAuthorizer.handleSafe(resourceAuth);

View File

@ -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]);
}
}
}
}

View File

@ -16,6 +16,7 @@ import type { IdentifierStrategy } from '../util/identifiers/IdentifierStrategy'
import { ACL, FOAF } from '../util/Vocabularies'; import { ACL, FOAF } from '../util/Vocabularies';
import type { AuthorizerArgs } from './Authorizer'; import type { AuthorizerArgs } from './Authorizer';
import { Authorizer } from './Authorizer'; import { Authorizer } from './Authorizer';
import { WebAclAuthorization } from './WebAclAuthorization';
/** /**
* Handles most web access control predicates such as * 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. * Will throw an error if this is not the case.
* @param input - Relevant data needed to check if access can be granted. * @param input - Relevant data needed to check if access can be granted.
*/ */
public async handle({ identifier, permissions, credentials }: AuthorizerArgs): Promise<void> { public async handle({ identifier, permissions, credentials }: AuthorizerArgs): Promise<WebAclAuthorization> {
const modes = (Object.keys(permissions) as (keyof PermissionSet)[]).filter((key): boolean => permissions[key]); const modes = (Object.keys(permissions) as (keyof PermissionSet)[]).filter((key): boolean => permissions[key]);
// Verify that all required modes are set for the given agent // 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}`); this.logger.debug(`Checking if ${credentials.webId} has ${modes.join()} permissions for ${identifier.path}`);
const store = await this.getAclRecursive(identifier); const store = await this.getAclRecursive(identifier);
const authorization = this.createAuthorization(credentials, store);
for (const mode of modes) { 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}`); 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 * Throws a {@link ForbiddenHttpError} or {@link UnauthorizedHttpError} depending on the credentials
* if access is not allowed. * if access is not allowed.
* @param agent - Agent that wants access. * @param agent - Agent that wants access.
* @param store - A store containing the relevant triples for authorization. * @param authorization - An Authorization containing the permissions the agent has on the resource.
* @param mode - Which mode is requested. Probable one of ('write' | 'read' | 'append' | 'control'). * @param mode - Which mode is requested.
*/ */
private checkPermission(agent: Credentials, store: Store, mode: string): void { private checkPermission(agent: Credentials, authorization: WebAclAuthorization, mode: keyof PermissionSet): void {
const modeString = ACL[this.capitalize(mode) as 'Write' | 'Read' | 'Append' | 'Control']; if (!authorization.user[mode]) {
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))) {
const isLoggedIn = typeof agent.webId === 'string'; const isLoggedIn = typeof agent.webId === 'string';
if (isLoggedIn) { if (isLoggedIn) {
this.logger.warn(`Agent ${agent.webId} has no ${mode} permissions`); 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. * Capitalizes the input string.
* @param mode - String to transform. * @param mode - String to transform.

View File

@ -9,8 +9,10 @@ export * from './authentication/UnsecureWebIdExtractor';
// Authorization // Authorization
export * from './authorization/AllowEverythingAuthorizer'; export * from './authorization/AllowEverythingAuthorizer';
export * from './authorization/Authorization';
export * from './authorization/Authorizer'; export * from './authorization/Authorizer';
export * from './authorization/AuxiliaryAuthorizer'; export * from './authorization/AuxiliaryAuthorizer';
export * from './authorization/WebAclAuthorization';
export * from './authorization/WebAclAuthorizer'; export * from './authorization/WebAclAuthorizer';
// Init // Init

View File

@ -68,6 +68,11 @@ export const ACL = createUriAndTermNamespace('http://www.w3.org/ns/auth/acl#',
'Control', 'Control',
); );
export const AUTH = createUriAndTermNamespace('urn:solid:auth:',
'userMode',
'publicMode',
);
export const DC = createUriAndTermNamespace('http://purl.org/dc/terms/', export const DC = createUriAndTermNamespace('http://purl.org/dc/terms/',
'modified', 'modified',
); );

View File

@ -1,13 +1,23 @@
import { AllowEverythingAuthorizer } from '../../../src/authorization/AllowEverythingAuthorizer'; import { AllowEverythingAuthorizer } from '../../../src/authorization/AllowEverythingAuthorizer';
import type { PermissionSet } from '../../../src/ldp/permissions/PermissionSet';
describe('An AllowEverythingAuthorizer', (): void => { describe('An AllowEverythingAuthorizer', (): void => {
const authorizer = new AllowEverythingAuthorizer(); const authorizer = new AllowEverythingAuthorizer();
const allowEverything: PermissionSet = {
read: true,
write: true,
append: true,
control: true,
};
it('can handle everything.', async(): Promise<void> => { it('can handle everything.', async(): Promise<void> => {
await expect(authorizer.canHandle({} as any)).resolves.toBeUndefined(); await expect(authorizer.canHandle({} as any)).resolves.toBeUndefined();
}); });
it('always returns undefined.', async(): Promise<void> => { it('always returns an empty Authorization.', async(): Promise<void> => {
await expect(authorizer.handle()).resolves.toBeUndefined(); await expect(authorizer.handle()).resolves.toEqual({
user: allowEverything,
everyone: allowEverything,
});
}); });
}); });

View File

@ -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<void> => {
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<void> => {
expect(authorization.addMetadata(metadata)).toBeUndefined();
expect(metadata.quads()).toHaveLength(0);
});
it('adds corresponding acl metadata for all permissions present.', async(): Promise<void> => {
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);
});
});

View File

@ -1,5 +1,6 @@
import { namedNode, quad } from '@rdfjs/data-model'; import { namedNode, quad } from '@rdfjs/data-model';
import type { Credentials } from '../../../src/authentication/Credentials'; import type { Credentials } from '../../../src/authentication/Credentials';
import { WebAclAuthorization } from '../../../src/authorization/WebAclAuthorization';
import { WebAclAuthorizer } from '../../../src/authorization/WebAclAuthorizer'; import { WebAclAuthorizer } from '../../../src/authorization/WebAclAuthorizer';
import type { AuxiliaryIdentifierStrategy } from '../../../src/ldp/auxiliary/AuxiliaryIdentifierStrategy'; import type { AuxiliaryIdentifierStrategy } from '../../../src/ldp/auxiliary/AuxiliaryIdentifierStrategy';
import type { PermissionSet } from '../../../src/ldp/permissions/PermissionSet'; import type { PermissionSet } from '../../../src/ldp/permissions/PermissionSet';
@ -29,6 +30,7 @@ describe('A WebAclAuthorizer', (): void => {
let permissions: PermissionSet; let permissions: PermissionSet;
let credentials: Credentials; let credentials: Credentials;
let identifier: ResourceIdentifier; let identifier: ResourceIdentifier;
let authorization: WebAclAuthorization;
beforeEach(async(): Promise<void> => { beforeEach(async(): Promise<void> => {
permissions = { permissions = {
@ -39,6 +41,20 @@ describe('A WebAclAuthorizer', (): void => {
}; };
credentials = {}; credentials = {};
identifier = { path: 'http://test.com/foo' }; 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 = { store = {
getRepresentation: jest.fn(), 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}Read`)),
quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Write`)), quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Write`)),
]) } as Representation); ]) } 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<void> => { it('allows access if there is a parent acl file allowing all agents.', async(): Promise<void> => {
@ -77,7 +95,9 @@ describe('A WebAclAuthorizer', (): void => {
]), ]),
} as Representation; } 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<void> => { it('allows access to authorized agents if the acl files allows all authorized users.', async(): Promise<void> => {
@ -88,7 +108,8 @@ describe('A WebAclAuthorizer', (): void => {
quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Write`)), quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Write`)),
]) } as Representation); ]) } as Representation);
credentials.webId = 'http://test.com/user'; 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<void> => { it('errors if authorization is required but the agent is not authorized.', async(): Promise<void> => {
@ -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}Read`)),
quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Write`)), quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Write`)),
]) } as Representation); ]) } 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<void> => { it('errors if a specific agents wants to access files not assigned to them.', async(): Promise<void> => {
@ -153,6 +175,7 @@ describe('A WebAclAuthorizer', (): void => {
quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)), quad(nn('auth'), nn(`${acl}accessTo`), nn(identifier.path)),
quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Write`)), quad(nn('auth'), nn(`${acl}mode`), nn(`${acl}Write`)),
]) } as Representation); ]) } 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);
}); });
}); });

View File

@ -70,7 +70,7 @@ describe('An AuthenticatedLdpHandler', (): void => {
}); });
it('errors an invalid object was thrown by a handler.', async(): Promise< void> => { it('errors an invalid object was thrown by a handler.', async(): Promise< void> => {
args.authorizer.handle = async(): Promise<void> => { args.authorizer.handle = async(): Promise<any> => {
throw 'apple'; throw 'apple';
}; };
const handler = new AuthenticatedLdpHandler(args); const handler = new AuthenticatedLdpHandler(args);