-
Notifications
You must be signed in to change notification settings - Fork 125
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
14 changed files
with
524 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
const ACL_PREFIX = 'http://www.w3.org/ns/auth/acl#'; | ||
const FOAF_PREFIX = 'http://xmlns.com/foaf/0.1/'; | ||
|
||
export const ACL = { | ||
accessTo: `${ACL_PREFIX}accessTo`, | ||
agent: `${ACL_PREFIX}agent`, | ||
agentClass: `${ACL_PREFIX}agentClass`, | ||
default: `${ACL_PREFIX}default`, | ||
mode: `${ACL_PREFIX}mode`, | ||
|
||
Write: `${ACL_PREFIX}Write`, | ||
Read: `${ACL_PREFIX}Read`, | ||
Append: `${ACL_PREFIX}Append`, | ||
Control: `${ACL_PREFIX}Control`, | ||
}; | ||
|
||
export const FOAF = { | ||
Agent: `${FOAF_PREFIX}Agent`, | ||
AuthenticatedAgent: `${FOAF_PREFIX}AuthenticatedAgent`, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier'; | ||
|
||
/** | ||
* Handles where acl files are stored. | ||
*/ | ||
export interface AclManager { | ||
/** | ||
* Returns the identifier of the acl file corresponding to the given resource. | ||
* This does not guarantee that this acl file exists. | ||
* In the case the input is already an acl file that will also be the response. | ||
* @param id - The ResourceIdentifier of which we need the corresponding acl file. | ||
* | ||
* @returns The ResourceIdentifier of the corresponding acl file. | ||
*/ | ||
getAcl: (id: ResourceIdentifier) => Promise<ResourceIdentifier>; | ||
|
||
/** | ||
* Checks if the input identifier corresponds to an acl file. | ||
* This does not check if that acl file exists, | ||
* only if the identifier indicates that there could be an acl file there. | ||
* @param id - Identifier to check. | ||
* | ||
* @returns true if the input identifier points to an acl file. | ||
*/ | ||
isAcl: (id: ResourceIdentifier) => Promise<boolean>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
import { AclManager } from './AclManager'; | ||
import { ContainerManager } from '../storage/ContainerManager'; | ||
import { CONTENT_TYPE_QUADS } from '../util/ContentTypes'; | ||
import { Credentials } from '../authentication/Credentials'; | ||
import { ForbiddenHttpError } from '../util/errors/ForbiddenHttpError'; | ||
import { NotFoundHttpError } from '../util/errors/NotFoundHttpError'; | ||
import { PermissionSet } from '../ldp/permissions/PermissionSet'; | ||
import { Representation } from '../ldp/representation/Representation'; | ||
import { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier'; | ||
import { ResourceStore } from '../storage/ResourceStore'; | ||
import { UnauthorizedHttpError } from '../util/errors/UnauthorizedHttpError'; | ||
import { ACL, FOAF } from './AclConstants'; | ||
import { Authorizer, AuthorizerArgs } from './Authorizer'; | ||
import { Quad, Store, Term } from 'n3'; | ||
|
||
/** | ||
* 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. | ||
*/ | ||
export class SimpleAclAuthorizer extends Authorizer { | ||
private readonly aclManager: AclManager; | ||
private readonly containerManager: ContainerManager; | ||
private readonly resourceStore: ResourceStore; | ||
|
||
public constructor(aclManager: AclManager, containerManager: ContainerManager, resourceStore: ResourceStore) { | ||
super(); | ||
this.aclManager = aclManager; | ||
this.containerManager = containerManager; | ||
this.resourceStore = resourceStore; | ||
} | ||
|
||
public async canHandle(): Promise<void> { | ||
// Can handle everything | ||
} | ||
|
||
/** | ||
* Checks if an agent is allowed to execute the requested actions. | ||
* 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(input: AuthorizerArgs): Promise<void> { | ||
const store = await this.getAclRecursive(input.identifier); | ||
if (await this.aclManager.isAcl(input.identifier)) { | ||
this.checkPermission(input.credentials, store, 'control'); | ||
} else { | ||
(Object.keys(input.permissions) as (keyof PermissionSet)[]).forEach((key): void => { | ||
if (input.permissions[key]) { | ||
this.checkPermission(input.credentials, store, key); | ||
} | ||
}); | ||
} | ||
} | ||
|
||
/** | ||
* Checks if any of the triples in the store grant 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'). | ||
*/ | ||
private checkPermission(agent: Credentials, store: Store, mode: string): void { | ||
const modeString = ACL[this.capitalize(mode) as 'Write' | 'Read' | 'Append' | 'Control']; | ||
const auths = store.getQuads(null, ACL.mode, modeString, null).map((quad: Quad): Term => quad.subject); | ||
if (!auths.some((term): boolean => this.hasAccess(agent, term, store))) { | ||
throw typeof agent.webID === 'string' ? new ForbiddenHttpError() : new UnauthorizedHttpError(); | ||
} | ||
} | ||
|
||
/** | ||
* 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()}`; | ||
} | ||
|
||
/** | ||
* 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 store - A store containing the relevant triples of the authorization. | ||
* | ||
* @returns If the agent has access. | ||
*/ | ||
private hasAccess(agent: Credentials, auth: Term, store: Store): boolean { | ||
if (store.countQuads(auth, ACL.agentClass, FOAF.Agent, null) > 0) { | ||
return true; | ||
} | ||
if (typeof agent.webID !== 'string') { | ||
return false; | ||
} | ||
if (store.countQuads(auth, ACL.agentClass, FOAF.AuthenticatedAgent, null) > 0) { | ||
return true; | ||
} | ||
return store.countQuads(auth, ACL.agent, agent.webID, null) > 0; | ||
} | ||
|
||
/** | ||
* Returns the acl triples that are relevant for the given identifier. | ||
* These can either be from a corresponding acl file or an acl file higher up with defaults. | ||
* Rethrows any non-NotFoundHttpErrors thrown by the AclManager or ResourceStore. | ||
* @param id - ResourceIdentifier of which we need the acl triples. | ||
* @param recurse - Only used internally for recursion. | ||
* | ||
* @returns A store containing the relevant acl triples. | ||
*/ | ||
private async getAclRecursive(id: ResourceIdentifier, recurse?: boolean): Promise<Store> { | ||
try { | ||
const acl = await this.aclManager.getAcl(id); | ||
const data = await this.resourceStore.getRepresentation(acl, { type: [{ value: CONTENT_TYPE_QUADS, weight: 1 }]}); | ||
return this.filterData(data, recurse ? ACL.default : ACL.accessTo, id.path); | ||
} catch (error) { | ||
if (!(error instanceof NotFoundHttpError)) { | ||
throw error; | ||
} | ||
|
||
const parent = await this.containerManager.getContainer(id); | ||
return this.getAclRecursive(parent, true); | ||
} | ||
} | ||
|
||
/** | ||
* Finds all triples in the data stream of the given representation that use the given predicate and object. | ||
* Then extracts the unique subjects from those triples, | ||
* and returns a Store containing all triples from the data stream that have such a subject. | ||
* | ||
* This can be useful for finding the `acl:Authorization` objects corresponding to a specific URI | ||
* and returning all relevant information on them. | ||
* @param data - Representation with data stream of internal/quads. | ||
* @param predicate - Predicate to match. | ||
* @param object - Object to match. | ||
* | ||
* @returns A store containing the relevant triples. | ||
*/ | ||
private async filterData(data: Representation, predicate: string, object: string): Promise<Store> { | ||
const store = new Store(); | ||
const importEmitter = store.import(data.data); | ||
await new Promise((resolve, reject): void => { | ||
importEmitter.on('end', resolve); | ||
importEmitter.on('error', reject); | ||
}); | ||
|
||
const auths = store.getQuads(null, predicate, object, null).map((quad: Quad): Term => quad.subject); | ||
const newStore = new Store(); | ||
auths.forEach((subject): any => newStore.addQuads(store.getQuads(subject, null, null, null))); | ||
return newStore; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { AclManager } from './AclManager'; | ||
import { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier'; | ||
|
||
/** | ||
* Generates acl URIs by adding an .acl file extension. | ||
* | ||
* What actually should happen in getAcl: | ||
* 1. Return id if it isAcl | ||
* 2. Check store if id exists | ||
* 3a. (true) Close/destroy data stream! To prevent potential locking issues. | ||
* 4a. Check metadata if it is a container or a resource. | ||
* 3b. (false) Use input metadata/heuristic to check if container or resource. | ||
* 5. Generate the correct identifier (.acl right of / for containers, left for resources if there is a /) | ||
* | ||
* It is potentially possible that an agent wants to generate the acl file before generating the actual file. | ||
* (Unless this is not allowed by the spec, need to verify). | ||
*/ | ||
export class SimpleExtensionAclManager implements AclManager { | ||
public async getAcl(id: ResourceIdentifier): Promise<ResourceIdentifier> { | ||
return await this.isAcl(id) ? id : { path: `${id.path}.acl` }; | ||
} | ||
|
||
public async isAcl(id: ResourceIdentifier): Promise<boolean> { | ||
return /\.acl\/?/u.test(id.path); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,5 +5,4 @@ export interface PermissionSet { | |
read: boolean; | ||
append: boolean; | ||
write: boolean; | ||
delete: boolean; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier'; | ||
|
||
/** | ||
* Handles the identification of containers in which a resource is contained. | ||
*/ | ||
export interface ContainerManager { | ||
/** | ||
* Finds the corresponding container. | ||
* Should throw an error if there is no such container (in the case of root). | ||
* | ||
* @param id - Identifier to find container of. | ||
* | ||
* @returns The identifier of the container this resource is in. | ||
*/ | ||
getContainer: (id: ResourceIdentifier) => Promise<ResourceIdentifier>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import { ContainerManager } from './ContainerManager'; | ||
import { ensureTrailingSlash } from '../util/Util'; | ||
import { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier'; | ||
|
||
/** | ||
* Determines containers based on URL decomposition. | ||
*/ | ||
export class UrlContainerManager implements ContainerManager { | ||
private readonly root: string; | ||
|
||
public constructor(root: string) { | ||
this.root = this.canonicalUrl(root); | ||
} | ||
|
||
public async getContainer(id: ResourceIdentifier): Promise<ResourceIdentifier> { | ||
const path = this.canonicalUrl(id.path); | ||
if (this.root === path) { | ||
throw new Error('Root does not have a container.'); | ||
} | ||
|
||
const parentPath = new URL('..', path).toString(); | ||
|
||
// This probably means there is an issue with the root | ||
if (parentPath === path) { | ||
throw new Error('URL root reached.'); | ||
} | ||
|
||
return { path: parentPath }; | ||
} | ||
|
||
private canonicalUrl(path: string): string { | ||
return ensureTrailingSlash(new URL(path).toString()); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { HttpError } from './HttpError'; | ||
|
||
/** | ||
* An error thrown when an agent is not allowed to access data. | ||
*/ | ||
export class ForbiddenHttpError extends HttpError { | ||
public constructor(message?: string) { | ||
super(403, 'ForbiddenHttpError', message); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { HttpError } from './HttpError'; | ||
|
||
/** | ||
* An error thrown when an agent is not authorized. | ||
*/ | ||
export class UnauthorizedHttpError extends HttpError { | ||
public constructor(message?: string) { | ||
super(401, 'UnauthorizedHttpError', message); | ||
} | ||
} |
Oops, something went wrong.