From e4b51c8c6fc3fd4eb93df6b5372c14a5dd02bb80 Mon Sep 17 00:00:00 2001 From: Peter Beverloo Date: Sun, 21 Jul 2024 18:58:07 +0100 Subject: [PATCH] Remove the old `AccessControl` implementation \o/ --- app/lib/auth/AccessControl.ts | 215 +--------------------------------- 1 file changed, 1 insertion(+), 214 deletions(-) diff --git a/app/lib/auth/AccessControl.ts b/app/lib/auth/AccessControl.ts index 197c06d9..962f605c 100644 --- a/app/lib/auth/AccessControl.ts +++ b/app/lib/auth/AccessControl.ts @@ -74,33 +74,6 @@ export type AccessResult = Result & { // ------------------------------------------------------------------------------------------------- -/** - * Contextualized information stored regarding a granted (or revoked) permission. - */ -interface Permission { - /** - * Events that this permission is scoped to, if any. This is in addition to global access. - */ - events?: Set; - - /** - * Teams that this permission is scoped to, if any. This is in addition to global access. - */ - teams?: Set; -} - -/** - * Map of permission names with their associated `Permission` instances. - */ -type PermissionMap = Map; - -/** - * Status that can be associated with a particular permission type. - */ -export type PermissionStatus = - 'crud-granted' | 'crud-revoked' | 'parent-granted' | 'parent-revoked' | - 'self-granted' | 'self-revoked' | 'unset'; - /** * The `AccessControl` object enables consistent access checks throughout the Volunteer Manager * system. Our permissions are hierarchical, resource-based and follow CRUD patterns. Furthermore, @@ -115,15 +88,9 @@ export type PermissionStatus = * permission that indicate the scope, e.g. "foo.bar:update". */ export class AccessControl { - #grantMap: PermissionMap = new Map; #grants: AccessList; - - #revokeMap: PermissionMap = new Map; #revokes: AccessList; - #events: Set | undefined; - #teams: Set | undefined; - constructor(grants: AccessControlParams) { this.#revokes = new AccessList({ grants: grants.revokes }) this.#grants = new AccessList({ @@ -132,24 +99,6 @@ export class AccessControl { events: grants.events, teams: grants.teams, }); - - if (!!grants.grants) { - const grantArray = Array.isArray(grants.grants) ? grants.grants : [ grants.grants ]; - for (const grant of grantArray) - this.populatePermissionMapFromInput(this.#grantMap, grant); - } - - if (!!grants.revokes) { - const revokeArray = Array.isArray(grants.revokes) ? grants.revokes : [ grants.revokes ]; - for (const revoke of revokeArray) - this.populatePermissionMapFromInput(this.#revokeMap, revoke); - } - - if (!!grants.events && !!grants.events.length) - this.#events = new Set(grants.events.split(',')); - - if (!!grants.teams && !!grants.teams.length) - this.#teams = new Set(grants.teams.split(',')); } /** @@ -251,6 +200,7 @@ export class AccessControl { * specificity, to make sure that the most specific grant (and/or revoke) will be considered. * * @todo Actually throw a HTTP 403 Forbidden error when Next.js supports it. + * @see https://github.com/vercel/next.js/pull/65993 */ require(permission: BooleanPermission, scope?: AccessScope): void; require(permission: CRUDPermission, operation: AccessOperation, scope?: AccessScope): void; @@ -258,167 +208,4 @@ export class AccessControl { if (!this.can(permission as any, second, third)) notFound(); } - - // --------------------------------------------------------------------------------------------- - - /** - * Returns the permission status for the given permission, which gives more information about - * why it would be granted or revoked. Generally this information is not important, and either - * the `can()` or `require()` methods should be used instead. - * - * Permission checks will be highly specific at first, to ensure that someone who is granted the - * "foo" permission can still have "foo.bar" explicitly revoked. CRUD permissions will be - * expanded separately at the deepest scope. - */ - getStatus(permission: BooleanPermission, scope?: AccessScope): PermissionStatus; - getStatus(permission: CRUDPermission, operation: AccessOperation, scope?: AccessScope) - : PermissionStatus; - getStatus(permission: BooleanPermission | CRUDPermission, second?: any, third?: any) - : PermissionStatus - { - if (!kPermissionPattern.test(permission)) - throw new Error(`Invalid syntax for the given permission: "${permission}"`); - - if (!Object.hasOwn(kPermissions, permission)) - throw new Error(`Unrecognised permission: "${permission}"`); - - const descriptor: AccessDescriptor = kPermissions[permission]; - const accessScope: AccessScope | undefined = descriptor.type === 'crud' ? third : second; - - if (descriptor.requireEvent && !accessScope?.event) - throw new Error(`Event is required when checking "${permission}" access`); - - if (descriptor.requireTeam && !accessScope?.team) - throw new Error(`Team is required when checking "${permission}" access`); - - if (descriptor.type === 'crud') { - if (typeof second !== 'string') - throw new Error(`Invalid operation given for a CRUD permission: "${second}"`); - - const scope = `${permission}:${second}`; - - const maybeRevoked = this.#revokeMap.get(scope); - if (maybeRevoked && this.isRevokeApplicable(maybeRevoked, third)) - return 'crud-revoked'; // permission + scope has been explicitly revoked - - const maybeGranted = this.#grantMap.get(scope); - if (maybeGranted && this.isGrantApplicable(maybeGranted, third)) - return 'crud-granted'; // permission + scope has been explicitly granted - } - - const path = permission.split('.'); - do { - const scope = path.join('.'); - const isParent = scope !== permission; - - const maybeRevoked = this.#revokeMap.get(scope); - if (maybeRevoked && this.isRevokeApplicable(maybeRevoked, accessScope)) - return isParent ? 'parent-revoked' : 'self-revoked'; // explicitly revoked - - const maybeGranted = this.#grantMap.get(scope); - if (maybeGranted && this.isGrantApplicable(maybeGranted, accessScope)) - return isParent ? 'parent-granted' : 'self-granted'; // explicitly granted - - path.pop(); - - } while (!!path.length); - - return 'unset'; // no permission has been granted - } - - // --------------------------------------------------------------------------------------------- - - /** - * Populates the given `target` map with permissions sourced from the given `input`, which - * contain a comma-separated list of permissions, and optionally scoping to a specific event - * and/or team. No information will be returned from this method. - */ - private populatePermissionMapFromInput(target: PermissionMap, input: Grant): void { - const permissions = typeof input === 'string' ? input : input.permission; - const expandedPermissions = - permissions.split(',').map(perm => kPermissionGroups[perm] ?? perm).flat(); - - for (const permission of expandedPermissions) { - if (!kPermissionPattern.test(permission)) { - console.warn(`Invalid syntax for the given grant: "${permission}" (ignoring)`); - continue; - } - - let event: string | undefined; - let team: string | undefined; - - if (typeof input !== 'string') { - if (!!input.event) - event = input.event; - - if (!!input.team) - team = input.team; - } - - const existingPermission = target.get(permission); - if (existingPermission === undefined) { - target.set(permission, { - events: event ? new Set([ event ]) : undefined, - teams: team ? new Set([ team ]) : undefined, - }); - - continue; - } - - if (!!event) { - if (!existingPermission.events) - existingPermission.events = new Set([ event ]); - else - existingPermission.events.add(event); - } - - if (!!team) { - if (!existingPermission.teams) - existingPermission.teams = new Set([ team ]); - else - existingPermission.teams.add(team); - } - } - } - - /** - * Returns whether the given `permission` is an applicable grant considering scoping information - * given in `options`. Global event and team access will be considered. - */ - private isGrantApplicable(permission: Permission, scope?: AccessScope) { - if (!!scope?.event && scope.event !== kAnyEvent) { - if (!this.#events?.has(kAnyEvent)) { - if (!permission.events?.has(scope.event) && !this.#events?.has(scope.event)) - return false; // event access has not been granted - } - } - - if (!!scope?.team && scope.team !== kAnyEvent) { - if (!this.#teams?.has(kAnyEvent)) { - if (!permission.teams?.has(scope.team) && !this.#teams?.has(scope.team)) - return false; // team access has not been granted - } - } - - return true; - } - - /** - * Returns whether the given `permission` is an applicable revocation considering scoping - * information given in `options`. Global event and team access will be not be considered, as - * revokes are exclusionary for something that could be granted. - */ - private isRevokeApplicable(permission: Permission, scope?: AccessScope) { - if (!!permission.events?.size && !!scope?.event) { - if (!permission.events.has(scope.event)) - return false; // event access has not been revoked - } - - if (!!permission.teams?.size && !!scope?.team) { - if (!permission.teams.has(scope.team)) - return false; // team access has not been revoked - } - - return true; - } }