mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Let Authorizers output an Authorization
This commit is contained in:
@@ -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<void> {
|
||||
// Allows all actions
|
||||
public async handle(): Promise<WebAclAuthorization> {
|
||||
return new WebAclAuthorization(allowEverything, allowEverything);
|
||||
}
|
||||
}
|
||||
|
||||
12
src/authorization/Authorization.ts
Normal file
12
src/authorization/Authorization.ts
Normal 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;
|
||||
}
|
||||
@@ -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<AuthorizerArgs> {}
|
||||
export abstract class Authorizer extends AsyncHandler<AuthorizerArgs, Authorization> {}
|
||||
|
||||
export interface AuthorizerArgs {
|
||||
/**
|
||||
|
||||
@@ -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<void> {
|
||||
public async handle(auxiliaryAuth: AuthorizerArgs): Promise<Authorization> {
|
||||
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<void> {
|
||||
public async handleSafe(auxiliaryAuth: AuthorizerArgs): Promise<Authorization> {
|
||||
const resourceAuth = this.getRequiredAuthorization(auxiliaryAuth);
|
||||
this.logger.debug(`Checking auth request for ${auxiliaryAuth.identifier.path} to ${resourceAuth.identifier.path}`);
|
||||
return this.resourceAuthorizer.handleSafe(resourceAuth);
|
||||
|
||||
35
src/authorization/WebAclAuthorization.ts
Normal file
35
src/authorization/WebAclAuthorization.ts
Normal 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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<void> {
|
||||
public async handle({ identifier, permissions, credentials }: AuthorizerArgs): Promise<WebAclAuthorization> {
|
||||
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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user