Skip to content

Commit

Permalink
refactor: Refactor WebAclAuthorizer
Browse files Browse the repository at this point in the history
Co-Authored-By: Ludovico Granata <Ludogranata@gmail.com>
  • Loading branch information
2 people authored and joachimvh committed Aug 19, 2021
1 parent 01b4cd4 commit dc847d6
Show file tree
Hide file tree
Showing 12 changed files with 246 additions and 132 deletions.
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
11 changes: 11 additions & 0 deletions config/ldp/authorization/authorizers/acl.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
{
"@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"
],
"@graph": [
{
"@id": "urn:solid-server:default:WebAclAuthorizer",
Expand All @@ -12,6 +16,13 @@
},
"identifierStrategy": {
"@id": "urn:solid-server:default:IdentifierStrategy"
},
"accessChecker": {
"@type": "BooleanHandler",
"handlers": [
{ "@id": "urn:solid-server:default:AgentAccessChecker" },
{ "@id": "urn:solid-server:default:AgentClassAccessChecker" }
]
}
}
]
Expand Down
119 changes: 39 additions & 80 deletions src/authorization/WebAclAuthorizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,19 @@ import { NotImplementedHttpError } from '../util/errors/NotImplementedHttpError'
import { UnauthorizedHttpError } from '../util/errors/UnauthorizedHttpError';
import type { IdentifierStrategy } from '../util/identifiers/IdentifierStrategy';
import { readableToQuads } from '../util/StreamUtil';
import { ACL, FOAF } from '../util/Vocabularies';
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`.
Expand All @@ -32,13 +40,15 @@ export class WebAclAuthorizer extends Authorizer {
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 @@ -59,7 +69,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 @@ -82,28 +92,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 @@ -130,75 +158,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
25 changes: 25 additions & 0 deletions src/authorization/access-checkers/AccessChecker.ts
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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;
}
}
5 changes: 5 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ export * from './authorization/PathBasedAuthorizer';
export * from './authorization/WebAclAuthorization';
export * from './authorization/WebAclAuthorizer';

// Authorization/access-checkers
export * from './authorization/access-checkers/AccessChecker';
export * from './authorization/access-checkers/AgentAccessChecker';
export * from './authorization/access-checkers/AgentClassAccessChecker';

// Identity/Configuration
export * from './identity/configuration/IdentityProviderFactory';
export * from './identity/configuration/ProviderFactory';
Expand Down
1 change: 1 addition & 0 deletions src/util/Vocabularies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export const ACL = createUriAndTermNamespace('http://www.w3.org/ns/auth/acl#',
'agent',
'agentClass',
'AuthenticatedAgent',
'Authorization',
'default',
'mode',

Expand Down

0 comments on commit dc847d6

Please sign in to comment.