Skip to content

Commit

Permalink
feat: Use PermissionReaders to determine available permissions
Browse files Browse the repository at this point in the history
These readers will determine which permissions
are available for the incoming credentials.
Their results then get combined in a UnionReader
and authorized in a PermissionBasedAuthorizer
  • Loading branch information
joachimvh committed Sep 28, 2021
1 parent e8dedf5 commit bf28c83
Show file tree
Hide file tree
Showing 50 changed files with 708 additions and 439 deletions.
2 changes: 1 addition & 1 deletion .componentsignore
Expand Up @@ -5,7 +5,7 @@
"Error",
"EventEmitter",
"HttpErrorOptions",
"PermissionSet",
"Permission",
"Template",
"TemplateEngine",
"ValuePreferencesArg",
Expand Down
6 changes: 5 additions & 1 deletion config/ldp/authorization/allow-all.json
Expand Up @@ -7,7 +7,11 @@
"Always allows all operations."
],
"@id": "urn:solid-server:default:Authorizer",
"@type": "AllowAllAuthorizer"
"@type": "PermissionBasedAuthorizer",
"reader": {
"@type": "AllStaticReader",
"allow": true
}
}
]
}
@@ -1,14 +1,14 @@
{
"@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",
"files-scs:config/ldp/authorization/authorizers/access-checkers/agent-group.json"
"files-scs:config/ldp/authorization/readers/access-checkers/agent.json",
"files-scs:config/ldp/authorization/readers/access-checkers/agent-class.json",
"files-scs:config/ldp/authorization/readers/access-checkers/agent-group.json"
],
"@graph": [
{
"@id": "urn:solid-server:default:WebAclAuthorizer",
"@type": "WebAclAuthorizer",
"@id": "urn:solid-server:default:WebAclReader",
"@type": "WebAclReader",
"aclStrategy": {
"@id": "urn:solid-server:default:AclStrategy"
},
Expand Down
37 changes: 20 additions & 17 deletions config/ldp/authorization/webacl.json
@@ -1,28 +1,31 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
"import": [
"files-scs:config/ldp/authorization/authorizers/acl.json"
"files-scs:config/ldp/authorization/readers/acl.json"
],
"@graph": [
{
"comment": "Uses Web Access Control for authorization.",
"@id": "urn:solid-server:default:Authorizer",
"@type": "WaterfallHandler",
"handlers": [
{
"comment": "This authorizer will be used to prevent external access to containers used for internal storage.",
"@id": "urn:solid-server:default:PathBasedAuthorizer",
"@type": "PathBasedAuthorizer",
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }
},
{
"comment": "This authorizer makes sure that for auxiliary resources, the main authorizer gets called with the associated identifier.",
"@type": "AuxiliaryAuthorizer",
"resourceAuthorizer": { "@id": "urn:solid-server:default:WebAclAuthorizer" },
"auxiliaryStrategy": { "@id": "urn:solid-server:default:AuxiliaryStrategy" }
},
{ "@id": "urn:solid-server:default:WebAclAuthorizer" }
]
"@type": "PermissionBasedAuthorizer",
"reader": {
"@type": "UnionPermissionReader",
"readers": [
{
"comment": "This PermissionReader will be used to prevent external access to containers used for internal storage.",
"@id": "urn:solid-server:default:PathBasedReader",
"@type": "PathBasedReader",
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }
},
{
"comment": "This PermissionReader makes sure that for auxiliary resources, the main reader gets called with the associated identifier.",
"@type": "AuxiliaryReader",
"resourceReader": { "@id": "urn:solid-server:default:WebAclReader" },
"auxiliaryStrategy": { "@id": "urn:solid-server:default:AuxiliaryStrategy" }
},
{ "@id": "urn:solid-server:default:WebAclReader" }
]
}
}
]
}
11 changes: 7 additions & 4 deletions config/storage/key-value/resource-store.json
Expand Up @@ -57,11 +57,14 @@
},
{
"comment": "Block external access to the storage containers to avoid exposing internal data.",
"@id": "urn:solid-server:default:PathBasedAuthorizer",
"PathBasedAuthorizer:_paths": [
"@id": "urn:solid-server:default:PathBasedReader",
"PathBasedReader:_paths": [
{
"PathBasedAuthorizer:_paths_key": "^/.internal(/.*)?$",
"PathBasedAuthorizer:_paths_value": { "@type": "DenyAllAuthorizer" }
"PathBasedReader:_paths_key": "^/.internal(/.*)?$",
"PathBasedReader:_paths_value": {
"@type": "AllStaticReader",
"allow": false
}
}
]
},
Expand Down
32 changes: 32 additions & 0 deletions src/authorization/AllStaticReader.ts
@@ -0,0 +1,32 @@
import type { CredentialGroup } from '../authentication/Credentials';
import type { Permission, PermissionSet } from '../ldp/permissions/Permissions';
import type { PermissionReaderInput } from './PermissionReader';
import { PermissionReader } from './PermissionReader';

/**
* PermissionReader which sets all permissions to true or false
* independently of the identifier and requested permissions.
*/
export class AllStaticReader extends PermissionReader {
private readonly permissions: Permission;

public constructor(allow: boolean) {
super();
this.permissions = Object.freeze({
read: allow,
write: allow,
append: allow,
control: allow,
});
}

public async handle({ credentials }: PermissionReaderInput): Promise<PermissionSet> {
const result: PermissionSet = {};
for (const [ key, value ] of Object.entries(credentials) as [CredentialGroup, Permission][]) {
if (value) {
result[key] = this.permissions;
}
}
return result;
}
}
19 changes: 0 additions & 19 deletions src/authorization/AllowAllAuthorizer.ts

This file was deleted.

4 changes: 2 additions & 2 deletions src/authorization/Authorizer.ts
@@ -1,5 +1,5 @@
import type { CredentialSet } from '../authentication/Credentials';
import type { AccessMode } from '../ldp/permissions/PermissionSet';
import type { AccessMode } from '../ldp/permissions/Permissions';
import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
import { AsyncHandler } from '../util/handlers/AsyncHandler';
import type { Authorization } from './Authorization';
Expand All @@ -20,7 +20,7 @@ export interface AuthorizerInput {
}

/**
* Verifies if the given credentials have access to the given permissions on the given resource.
* Verifies if the credentials provide access with the given permissions on the resource.
* An {@link Error} with the necessary explanation will be thrown when permissions are not granted.
*/
export abstract class Authorizer extends AsyncHandler<AuthorizerInput, Authorization> {}
@@ -1,45 +1,46 @@
import type { AuxiliaryIdentifierStrategy } from '../ldp/auxiliary/AuxiliaryIdentifierStrategy';
import type { PermissionSet } from '../ldp/permissions/Permissions';
import { getLoggerFor } from '../logging/LogUtil';
import { NotImplementedHttpError } from '../util/errors/NotImplementedHttpError';
import type { Authorization } from './Authorization';
import type { AuthorizerInput } from './Authorizer';
import { Authorizer } from './Authorizer';

import type { PermissionReaderInput } from './PermissionReader';
import { PermissionReader } from './PermissionReader';

/**
* An authorizer for auxiliary resources such as acl or shape resources.
* A PermissionReader for auxiliary resources such as acl or shape resources.
* The access permissions of an auxiliary resource depend on those of the resource it is associated with.
* This authorizer calls the source authorizer with the identifier of the associated resource.
*/
export class AuxiliaryAuthorizer extends Authorizer {
export class AuxiliaryReader extends PermissionReader {
protected readonly logger = getLoggerFor(this);

private readonly resourceAuthorizer: Authorizer;
private readonly resourceReader: PermissionReader;
private readonly auxiliaryStrategy: AuxiliaryIdentifierStrategy;

public constructor(resourceAuthorizer: Authorizer, auxiliaryStrategy: AuxiliaryIdentifierStrategy) {
public constructor(resourceReader: PermissionReader, auxiliaryStrategy: AuxiliaryIdentifierStrategy) {
super();
this.resourceAuthorizer = resourceAuthorizer;
this.resourceReader = resourceReader;
this.auxiliaryStrategy = auxiliaryStrategy;
}

public async canHandle(auxiliaryAuth: AuthorizerInput): Promise<void> {
public async canHandle(auxiliaryAuth: PermissionReaderInput): Promise<void> {
const resourceAuth = this.getRequiredAuthorization(auxiliaryAuth);
return this.resourceAuthorizer.canHandle(resourceAuth);
return this.resourceReader.canHandle(resourceAuth);
}

public async handle(auxiliaryAuth: AuthorizerInput): Promise<Authorization> {
public async handle(auxiliaryAuth: PermissionReaderInput): Promise<PermissionSet> {
const resourceAuth = this.getRequiredAuthorization(auxiliaryAuth);
this.logger.debug(`Checking auth request for ${auxiliaryAuth.identifier.path} on ${resourceAuth.identifier.path}`);
return this.resourceAuthorizer.handle(resourceAuth);
return this.resourceReader.handle(resourceAuth);
}

public async handleSafe(auxiliaryAuth: AuthorizerInput): Promise<Authorization> {
public async handleSafe(auxiliaryAuth: PermissionReaderInput): Promise<PermissionSet> {
const resourceAuth = this.getRequiredAuthorization(auxiliaryAuth);
this.logger.debug(`Checking auth request for ${auxiliaryAuth.identifier.path} to ${resourceAuth.identifier.path}`);
return this.resourceAuthorizer.handleSafe(resourceAuth);
return this.resourceReader.handleSafe(resourceAuth);
}

private getRequiredAuthorization(auxiliaryAuth: AuthorizerInput): AuthorizerInput {
private getRequiredAuthorization(auxiliaryAuth: PermissionReaderInput): PermissionReaderInput {
if (!this.auxiliaryStrategy.isAuxiliaryIdentifier(auxiliaryAuth.identifier)) {
throw new NotImplementedHttpError('AuxiliaryAuthorizer only supports auxiliary resources.');
}
Expand Down
11 changes: 0 additions & 11 deletions src/authorization/DenyAllAuthorizer.ts

This file was deleted.

52 changes: 0 additions & 52 deletions src/authorization/PathBasedAuthorizer.ts

This file was deleted.

54 changes: 54 additions & 0 deletions src/authorization/PathBasedReader.ts
@@ -0,0 +1,54 @@
import type { PermissionSet } from '../ldp/permissions/Permissions';
import { NotImplementedHttpError } from '../util/errors/NotImplementedHttpError';
import { ensureTrailingSlash, trimTrailingSlashes } from '../util/PathUtil';

import type { PermissionReaderInput } from './PermissionReader';
import { PermissionReader } from './PermissionReader';

/**
* Redirects requests to specific PermissionReaders based on their identifier.
* The keys in the input map will be converted to regular expressions.
* The regular expressions should all start with a slash
* and will be evaluated relative to the base URL.
*
* Will error if no match is found.
*/
export class PathBasedReader extends PermissionReader {
private readonly baseUrl: string;
private readonly paths: Map<RegExp, PermissionReader>;

public constructor(baseUrl: string, paths: Record<string, PermissionReader>) {
super();
this.baseUrl = ensureTrailingSlash(baseUrl);
const entries = Object.entries(paths)
.map(([ key, val ]): [RegExp, PermissionReader] => [ new RegExp(key, 'u'), val ]);
this.paths = new Map(entries);
}

public async canHandle(input: PermissionReaderInput): Promise<void> {
const reader = this.findReader(input.identifier.path);
await reader.canHandle(input);
}

public async handle(input: PermissionReaderInput): Promise<PermissionSet> {
const reader = this.findReader(input.identifier.path);
return reader.handle(input);
}

/**
* Find the PermissionReader corresponding to the given path.
* Errors if there is no match.
*/
private findReader(path: string): PermissionReader {
if (path.startsWith(this.baseUrl)) {
// We want to keep the leading slash
const relative = path.slice(trimTrailingSlashes(this.baseUrl).length);
for (const [ regex, reader ] of this.paths) {
if (regex.test(relative)) {
return reader;
}
}
}
throw new NotImplementedHttpError('No regex matches the given path.');
}
}

0 comments on commit bf28c83

Please sign in to comment.