diff --git a/shared/src/apiPermissions.test.ts b/shared/src/apiPermissions.test.ts new file mode 100644 index 00000000..51c480ed --- /dev/null +++ b/shared/src/apiPermissions.test.ts @@ -0,0 +1,13 @@ +import { ApiPermissions, hasPermission } from "./apiPermissions"; +import test from "ava"; + +test("Directly granted permissions match", t => { + t.is(hasPermission([ApiPermissions.ManageAccess], ApiPermissions.ManageAccess), true); + t.is(hasPermission([ApiPermissions.ManageAccess], ApiPermissions.Owner), false); +}); + +test("Implicitly granted permissions by hierarchy match", t => { + t.is(hasPermission([ApiPermissions.ManageAccess], ApiPermissions.EditConfig), true); + t.is(hasPermission([ApiPermissions.ManageAccess], ApiPermissions.ReadConfig), true); + t.is(hasPermission([ApiPermissions.EditConfig], ApiPermissions.ManageAccess), false); +}); diff --git a/shared/src/apiPermissions.ts b/shared/src/apiPermissions.ts new file mode 100644 index 00000000..a4ac822a --- /dev/null +++ b/shared/src/apiPermissions.ts @@ -0,0 +1,79 @@ +export enum ApiPermissions { + Owner = "OWNER", + ManageAccess = "MANAGE_ACCESS", + EditConfig = "EDIT_CONFIG", + ReadConfig = "READ_CONFIG", +} + +interface IPermissionHierarchy extends Partial> {} + +export const permissionHierarchy: IPermissionHierarchy = { + [ApiPermissions.Owner]: { + [ApiPermissions.ManageAccess]: { + [ApiPermissions.EditConfig]: { + [ApiPermissions.ReadConfig]: {}, + }, + }, + }, +}; + +/** + * Checks whether granted permissions include the specified permission, taking into account permission hierarchy i.e. + * that in the case of nested permissions, having a top level permission implicitly grants you any permissions nested + * under it as well + */ +export function hasPermission(grantedPermissions: ApiPermissions[], permissionToCheck: ApiPermissions): boolean { + // Directly granted + if (grantedPermissions.includes(permissionToCheck)) { + return true; + } + + // Check by hierarchy + if (checkTreeForPermission(permissionHierarchy, grantedPermissions, permissionToCheck)) { + return true; + } + + return false; +} + +function checkTreeForPermission( + tree: IPermissionHierarchy, + grantedPermissions: ApiPermissions[], + permission: ApiPermissions, +): boolean { + for (const [perm, nested] of Object.entries(tree)) { + // Top-level permission granted, implicitly grant all nested permissions as well + if (grantedPermissions.includes(perm as ApiPermissions)) { + // Permission we were looking for was found nested under this permission -> granted + if (treeIncludesPermission(nested, permission)) { + return true; + } + + // Permission we were looking for was not found nested under this permission + // Since direct grants are not handled by this function, we can skip any further checks for this nested tree + continue; + } + + // Top-level permission not granted, check further nested permissions + if (checkTreeForPermission(nested, grantedPermissions, permission)) { + return true; + } + } + + return false; +} + +function treeIncludesPermission(tree: IPermissionHierarchy, permission: ApiPermissions): boolean { + for (const [perm, nested] of Object.entries(tree)) { + if (perm === permission) { + return true; + } + + const nestedResult = treeIncludesPermission(nested, permission); + if (nestedResult) { + return true; + } + } + + return false; +}