Skip to content

Commit

Permalink
Merge d835885 into a062a71
Browse files Browse the repository at this point in the history
  • Loading branch information
joachimvh committed Aug 19, 2021
2 parents a062a71 + d835885 commit 65b0e3b
Show file tree
Hide file tree
Showing 32 changed files with 848 additions and 260 deletions.
@@ -0,0 +1,9 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
"@graph": [
{
"@id": "urn:solid-server:default:AgentAccessChecker",
"@type": "AgentAccessChecker"
}
]
}
@@ -0,0 +1,9 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
"@graph": [
{
"@id": "urn:solid-server:default:AgentClassAccessChecker",
"@type": "AgentClassAccessChecker"
}
]
}
@@ -0,0 +1,20 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
"@graph": [
{
"@id": "urn:solid-server:default:AgentGroupAccessChecker",
"@type": "AgentGroupAccessChecker",
"converter": { "@id": "urn:solid-server:default:RepresentationConverter" },
"cache": {
"@id": "urn:solid-server:default:ExpiringAclCache",
"@type": "WrappedExpiringStorage",
"source": { "@type": "MemoryMapStorage" }
}
},
{
"comment": "Makes sure the expiring storage cleanup timer is stopped when the application needs to stop.",
"@id": "urn:solid-server:default:Finalizer",
"ParallelFinalizer:_finalizers": [ { "@id": "urn:solid-server:default:ExpiringAclCache" } ]
}
]
}
13 changes: 13 additions & 0 deletions config/ldp/authorization/authorizers/acl.json
@@ -1,5 +1,10 @@
{
"@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/agentClass.json",
"files-scs:config/ldp/authorization/authorizers/access-checkers/agentGroup.json"
],
"@graph": [
{
"@id": "urn:solid-server:default:WebAclAuthorizer",
Expand All @@ -12,6 +17,14 @@
},
"identifierStrategy": {
"@id": "urn:solid-server:default:IdentifierStrategy"
},
"accessChecker": {
"@type": "BooleanHandler",
"handlers": [
{ "@id": "urn:solid-server:default:AgentAccessChecker" },
{ "@id": "urn:solid-server:default:AgentClassAccessChecker" },
{ "@id": "urn:solid-server:default:AgentGroupAccessChecker" }
]
}
}
]
Expand Down
132 changes: 44 additions & 88 deletions src/authorization/WebAclAuthorizer.ts
Expand Up @@ -15,29 +15,41 @@ import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
import { NotImplementedHttpError } from '../util/errors/NotImplementedHttpError';
import { UnauthorizedHttpError } from '../util/errors/UnauthorizedHttpError';
import type { IdentifierStrategy } from '../util/identifiers/IdentifierStrategy';
import { ACL, FOAF } from '../util/Vocabularies';
import { readableToQuads } from '../util/StreamUtil';
import { ACL, RDF } from '../util/Vocabularies';
import type { AccessChecker } from './access-checkers/AccessChecker';
import type { AuthorizerArgs } from './Authorizer';
import { Authorizer } from './Authorizer';
import { WebAclAuthorization } from './WebAclAuthorization';

const modesMap: Record<string, keyof PermissionSet> = {
[ACL.Read]: 'read',
[ACL.Write]: 'write',
[ACL.Append]: 'append',
[ACL.Control]: 'control',
} as const;

/**
* 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.
* `acl:mode`, `acl:agentClass`, `acl:agent`, `acl:default`,
* `acl:accessTo` and `acl:agentGroup`.
* Does not support `acl:origin` and `acl:trustedApp` yet.
*/
export class WebAclAuthorizer extends Authorizer {
protected readonly logger = getLoggerFor(this);

private readonly aclStrategy: AuxiliaryIdentifierStrategy;
private readonly resourceStore: ResourceStore;
private readonly identifierStrategy: IdentifierStrategy;
private readonly accessChecker: AccessChecker;

public constructor(aclStrategy: AuxiliaryIdentifierStrategy, resourceStore: ResourceStore,
identifierStrategy: IdentifierStrategy) {
identifierStrategy: IdentifierStrategy, accessChecker: AccessChecker) {
super();
this.aclStrategy = aclStrategy;
this.resourceStore = resourceStore;
this.identifierStrategy = identifierStrategy;
this.accessChecker = accessChecker;
}

public async canHandle({ identifier }: AuthorizerArgs): Promise<void> {
Expand All @@ -58,7 +70,7 @@ export class WebAclAuthorizer extends Authorizer {

// Determine the full authorization for the agent granted by the applicable ACL
const acl = await this.getAclRecursive(identifier);
const authorization = this.createAuthorization(credentials, acl);
const authorization = await this.createAuthorization(credentials, acl);

// Verify that the authorization allows all required modes
for (const mode of modes) {
Expand All @@ -81,28 +93,46 @@ export class WebAclAuthorizer extends Authorizer {
* @param agent - Agent whose credentials will be used for the `user` field.
* @param acl - Store containing all relevant authorization triples.
*/
private createAuthorization(agent: Credentials, acl: Store): WebAclAuthorization {
const publicPermissions = this.determinePermissions({}, acl);
const userPermissions = this.determinePermissions(agent, acl);
private async createAuthorization(agent: Credentials, acl: Store): Promise<WebAclAuthorization> {
const publicPermissions = await this.determinePermissions({}, acl);
const agentPermissions = await this.determinePermissions(agent, acl);

return new WebAclAuthorization(userPermissions, publicPermissions);
return new WebAclAuthorization(agentPermissions, publicPermissions);
}

/**
* Determines the available permissions for the given credentials.
* @param credentials - Credentials to find the permissions for.
* @param acl - Store containing all relevant authorization triples.
*/
private determinePermissions(credentials: Credentials, acl: Store): PermissionSet {
const permissions: PermissionSet = {
private async determinePermissions(credentials: Credentials, acl: Store): Promise<PermissionSet> {
const permissions = {
read: false,
write: false,
append: false,
control: false,
};
for (const mode of (Object.keys(permissions) as (keyof PermissionSet)[])) {
permissions[mode] = this.hasPermission(credentials, acl, mode);

// Apply all ACL rules
const aclRules = acl.getSubjects(RDF.type, ACL.Authorization, null);
for (const rule of aclRules) {
const hasAccess = await this.accessChecker.handleSafe({ acl, rule, credentials });
if (hasAccess) {
// Set all allowed modes to true
const modes = acl.getObjects(rule, ACL.mode, null);
for (const { value: mode } of modes) {
if (mode in modesMap) {
permissions[modesMap[mode]] = true;
}
}
}
}

if (permissions.write) {
// Write permission implies Append permission
permissions.append = true;
}

return permissions;
}

Expand All @@ -129,75 +159,6 @@ export class WebAclAuthorizer extends Authorizer {
}
}

/**
* Checks if the given agent has permission to execute the given mode based on the triples in the ACL.
* @param agent - Agent that wants access.
* @param acl - A store containing the relevant triples for authorization.
* @param mode - Which mode is requested.
*/
private hasPermission(agent: Credentials, acl: Store, mode: keyof PermissionSet): boolean {
// Collect all authorization blocks for this specific mode
const modeString = ACL[this.capitalize(mode) as 'Write' | 'Read' | 'Append' | 'Control'];
const auths = this.getModePermissions(acl, modeString);

// Append permissions are implied by Write permissions
if (modeString === ACL.Append) {
auths.push(...this.getModePermissions(acl, ACL.Write));
}

// Check if any collected authorization block allows the specific agent
return auths.some((term): boolean => this.hasAccess(agent, term, acl));
}

/**
* 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()}`;
}

/**
* Returns the identifiers of all authorizations that grant the given mode access for a resource.
* @param acl - The store containing the quads of the ACL resource.
* @param aclMode - A valid acl mode (ACL.Write/Read/...)
*/
private getModePermissions(acl: Store, aclMode: string): Term[] {
return acl.getQuads(null, ACL.mode, aclMode, null).map((quad: Quad): Term => quad.subject);
}

/**
* 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 acl - A store containing the relevant triples of the authorization.
*
* @returns If the agent has access.
*/
private hasAccess(agent: Credentials, auth: Term, acl: Store): boolean {
// Check if public access is allowed
if (acl.countQuads(auth, ACL.agentClass, FOAF.Agent, null) !== 0) {
return true;
}

// Check if authenticated access is allowed
if (this.isAuthenticated(agent)) {
// Check if any authenticated agent is allowed
if (acl.countQuads(auth, ACL.agentClass, ACL.AuthenticatedAgent, null) !== 0) {
return true;
}
// Check if this specific agent is allowed
if (acl.countQuads(auth, ACL.agent, agent.webId, null) !== 0) {
return true;
}
}

// Neither unauthenticated nor authenticated access are allowed
return false;
}

/**
* Returns the ACL triples that are relevant for the given identifier.
* These can either be from a corresponding ACL document or an ACL document higher up with defaults.
Expand Down Expand Up @@ -254,12 +215,7 @@ export class WebAclAuthorizer extends Authorizer {
*/
private async filterData(data: Representation, predicate: string, object: string): Promise<Store> {
// Import all triples from the representation into a queryable store
const quads = new Store();
const importer = quads.import(data.data);
await new Promise((resolve, reject): void => {
importer.on('end', resolve);
importer.on('error', reject);
});
const quads = await readableToQuads(data.data);

// Find subjects that occur with a given predicate/object, and collect all their triples
const subjectData = new Store();
Expand Down
25 changes: 25 additions & 0 deletions src/authorization/access-checkers/AccessChecker.ts
@@ -0,0 +1,25 @@
import type { Store, Term } from 'n3';
import type { Credentials } from '../../authentication/Credentials';
import { AsyncHandler } from '../../util/handlers/AsyncHandler';

/**
* Performs an authorization check against the given acl resource.
*/
export abstract class AccessChecker extends AsyncHandler<AccessCheckerArgs, boolean> {}

export interface AccessCheckerArgs {
/**
* A store containing the relevant triples of the authorization.
*/
acl: Store;

/**
* Authorization rule to be processed.
*/
rule: Term;

/**
* Credentials of the entity that wants to use the resource.
*/
credentials: Credentials;
}
15 changes: 15 additions & 0 deletions src/authorization/access-checkers/AgentAccessChecker.ts
@@ -0,0 +1,15 @@
import { ACL } from '../../util/Vocabularies';
import type { AccessCheckerArgs } from './AccessChecker';
import { AccessChecker } from './AccessChecker';

/**
* Checks if the given WebID has been given access.
*/
export class AgentAccessChecker extends AccessChecker {
public async handle({ acl, rule, credentials }: AccessCheckerArgs): Promise<boolean> {
if (typeof credentials.webId === 'string') {
return acl.countQuads(rule, ACL.terms.agent, credentials.webId, null) !== 0;
}
return false;
}
}
18 changes: 18 additions & 0 deletions src/authorization/access-checkers/AgentClassAccessChecker.ts
@@ -0,0 +1,18 @@
import { ACL, FOAF } from '../../util/Vocabularies';
import type { AccessCheckerArgs } from './AccessChecker';
import { AccessChecker } from './AccessChecker';

/**
* Checks access based on the agent class.
*/
export class AgentClassAccessChecker extends AccessChecker {
public async handle({ acl, rule, credentials }: AccessCheckerArgs): Promise<boolean> {
if (acl.countQuads(rule, ACL.terms.agentClass, FOAF.terms.Agent, null) !== 0) {
return true;
}
if (typeof credentials.webId === 'string') {
return acl.countQuads(rule, ACL.terms.agentClass, ACL.terms.AuthenticatedAgent, null) !== 0;
}
return false;
}
}

0 comments on commit 65b0e3b

Please sign in to comment.