From 9d2003a2e31897dbb768083754042c52484c5f3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Libert?= Date: Tue, 26 Mar 2024 22:07:22 +0100 Subject: [PATCH 1/4] add AccessControl object --- .../__tests__/access_control.spec.ts | 127 ++++++++++++++++++ .../assembly/access-control/access_control.ts | 86 ++++++++++++ 2 files changed, 213 insertions(+) create mode 100644 smart-contracts/assembly/access-control/__tests__/access_control.spec.ts create mode 100644 smart-contracts/assembly/access-control/access_control.ts diff --git a/smart-contracts/assembly/access-control/__tests__/access_control.spec.ts b/smart-contracts/assembly/access-control/__tests__/access_control.spec.ts new file mode 100644 index 0000000..af51c2c --- /dev/null +++ b/smart-contracts/assembly/access-control/__tests__/access_control.spec.ts @@ -0,0 +1,127 @@ +import { Address, resetStorage } from "@massalabs/massa-as-sdk"; +import { AccessControl } from "../access_control"; + +describe('AccessControl', () => { + test('should create new permissions', () => { + resetStorage(); + const accessControl = new AccessControl(1); + const ADMIN = accessControl.newPermission('admin'); + expect(ADMIN).toStrictEqual(1, 'Admin permission should be 2⁰ = 1'); + const USER = accessControl.newPermission('user'); + expect(USER).toStrictEqual(2, 'User permission should be 2¹ = 2'); + const GUEST = accessControl.newPermission('guest'); + expect(GUEST).toStrictEqual(4, 'Guest permission should be 2² = 4'); + }); + + test('should panic on unknown permission', () => { + resetStorage(); + expect(() => { + const accessControl = new AccessControl(1); + const userAddress = new Address('AU12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq'); + accessControl.grantPermissionToUser(1, userAddress); + }).toThrow('permission does not exist'); + }); + + test('should panic on adding to many permissions', () => { + resetStorage(); + expect(() => { + const accessControl = new AccessControl(1); + accessControl.newPermission('p1'); + accessControl.newPermission('p2'); + accessControl.newPermission('p3'); + accessControl.newPermission('p4'); + accessControl.newPermission('p5'); + accessControl.newPermission('p6'); + accessControl.newPermission('p7'); + accessControl.newPermission('p8'); + }).toThrow('Permission index overflow'); + }); + + test('should panic on adding permission twice', () => { + resetStorage(); + expect(() => { + const accessControl = new AccessControl(1); + const ADMIN = accessControl.newPermission('admin'); + const userAddress = new Address('AU12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq'); + accessControl.grantPermissionToUser(ADMIN, userAddress); + accessControl.grantPermissionToUser(ADMIN, userAddress); + }).toThrow('User already has admin permission'); + }); + + test('should panic on missing must have permission', () => { + resetStorage(); + expect(() => { + const accessControl = new AccessControl(1); + const ADMIN = accessControl.newPermission('admin'); + const USER = accessControl.newPermission('user'); + const userAddress = new Address('AU12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq'); + accessControl.grantPermissionToUser(USER, userAddress); + accessControl.mustHavePermission(ADMIN, userAddress); + }).toThrow('User does not have admin permission'); + }); + + test('should add permissions to user', () => { + resetStorage(); + const accessControl = new AccessControl(1); + const ADMIN = accessControl.newPermission('admin'); + const USER = accessControl.newPermission('user'); + const GUEST = accessControl.newPermission('guest'); + const userAddress = new Address('AU12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq'); + accessControl.grantPermissionToUser(USER, userAddress); + accessControl.grantPermissionToUser(GUEST, userAddress); + + expect(accessControl.hasPermission(USER, userAddress)).toBeTruthy('User should have user permission'); + expect(accessControl.hasPermission(GUEST, userAddress)).toBeTruthy('User should have guest permission'); + expect(accessControl.hasPermission(ADMIN, userAddress)).toBeFalsy('User should not have admin permission'); + }); + + test('should remove permissions from user', () => { + resetStorage(); + const accessControl = new AccessControl(1); + const ADMIN = accessControl.newPermission('admin'); + const USER = accessControl.newPermission('user'); + const GUEST = accessControl.newPermission('guest'); + const userAddress = new Address('AU12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq'); + accessControl.grantPermissionToUser(USER, userAddress); + accessControl.grantPermissionToUser(GUEST, userAddress); + accessControl.grantPermissionToUser(ADMIN, userAddress); + + expect(accessControl.hasPermission(USER, userAddress)).toBeTruthy('User should have user permission'); + expect(accessControl.hasPermission(GUEST, userAddress)).toBeTruthy('User should have guest permission'); + expect(accessControl.hasPermission(ADMIN, userAddress)).toBeTruthy('User should have admin permission'); + + accessControl.removePermissionFromUser(USER, userAddress); + + expect(accessControl.hasPermission(USER, userAddress)).toBeFalsy('User should not have user permission'); + expect(accessControl.hasPermission(GUEST, userAddress)).toBeTruthy('User should have guest permission'); + expect(accessControl.hasPermission(ADMIN, userAddress)).toBeTruthy('User should have admin permission'); + }); + + test('should return proper throw message', () => { + resetStorage(); + const accessControl = new AccessControl(1); + const ADMIN = accessControl.newPermission('admin'); + const USER = accessControl.newPermission('user'); + const GUEST = accessControl.newPermission('guest'); + const userAddress = new Address('AU12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq'); + + // accessControl.mustHavePermission(ADMIN, userAddress); + // accessControl.mustHavePermission(USER, userAddress); + // accessControl.mustHavePermission(GUEST, userAddress); + }); + + test('should handle multiple access control instances', () => { + resetStorage(); + const accessControl1 = new AccessControl(1); + const ADMIN = accessControl1.newPermission('admin'); + + const accessControl2 = new AccessControl(2); + const MECANIC = accessControl2.newPermission('mecanic'); + + const userAddress = new Address('AU12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq'); + accessControl1.grantPermissionToUser(ADMIN, userAddress); + + expect(accessControl2.hasPermission(ADMIN, userAddress)).toBeFalsy('User should not have car mechanic permission'); + }); + +}); \ No newline at end of file diff --git a/smart-contracts/assembly/access-control/access_control.ts b/smart-contracts/assembly/access-control/access_control.ts new file mode 100644 index 0000000..0828f7d --- /dev/null +++ b/smart-contracts/assembly/access-control/access_control.ts @@ -0,0 +1,86 @@ +import { fromBytes, toBytes } from "@massalabs/as-types"; +import { Address, Storage } from "@massalabs/massa-as-sdk"; + +/** + * Manages roles and permissions within a blockchain context using a bitmask approach. + * Each bit in a bitmask represents a distinct permission. This class exploits + * blockchain's native, map-like storage for efficient role and permission management. + * + * Implementation details: + * - Permissions are encoded as bits in a bitmask for compact storage and easy manipulation. + * - User access rights are stored and managed in a similar bitmask format. + * - Utilizes blockchain's native storage, with ModuleId as a prefix to differentiate keys + * belonging to different modules. + */ +export class AccessControl { + // @ts-ignore non-number type + private nextPermissionIndex: T = 1; + private permissionsName: string[] = []; + private moduleId: u8; + private errPermissionDoesNotExist: string = 'Permission does not exist'; + + constructor(moduleId: u8) { + this.moduleId = moduleId; + } + + private _getStorageKey(userAddress: Address): StaticArray { + const key = new StaticArray(1); + key[0] = this.moduleId; + return key.concat(userAddress.serialize()); + } + + private _getUserAccess(userAddress: Address): T { + const key = this._getStorageKey(userAddress); + return Storage.has(key) ? fromBytes(Storage.get(key)) : 0; + } + + private _setUserAccess(userAddress: Address, access: T): void { + const key = this._getStorageKey(userAddress); + Storage.set(key, toBytes(access)); + } + + public newPermission(Permission: string): T { + this.permissionsName.push(Permission); + const r = this.nextPermissionIndex; + // @ts-ignore arithmetic operations on generic types + assert(this.nextPermissionIndex < (1 << ((sizeof()) * 8 - 1)), 'Permission index overflow'); + // @ts-ignore arithmetic operations on generic types + this.nextPermissionIndex <<= 1; + return r; + } + + public grantPermissionToUser(permission: T, userAddress: Address): void { + assert(permission < this.nextPermissionIndex, this.errPermissionDoesNotExist); + + const ua = this._getUserAccess(userAddress); + // @ts-ignore arithmetic operations on generic types + assert((ua & permission) != permission, `User already has '${this.permissionsName[permission/2]}' Permission`); + // @ts-ignore arithmetic operations on generic types + this._setUserAccess(userAddress, ua | permission); + } + + public removePermissionFromUser(permission: T, userAddress: Address): void { + assert(permission < this.nextPermissionIndex, this.errPermissionDoesNotExist); + + const ua = this._getUserAccess(userAddress); + // @ts-ignore arithmetic operations on generic types + assert((ua & permission) == permission, `User does not have '${this.permissionsName[permission/2]}' Permission`); + // @ts-ignore arithmetic operations on generic types + this._setUserAccess(userAddress, ua & ~permission); + } + + public hasPermission(permission: T, userAddress: Address): boolean { + assert(permission < this.nextPermissionIndex, this.errPermissionDoesNotExist); + + const ua = this._getUserAccess(userAddress); + // @ts-ignore arithmetic operations on generic types + return (ua & permission) == permission; + } + + public mustHavePermission(permission: T, userAddress: Address): void { + assert( + this.hasPermission(permission, userAddress), + `User does not have ${this.permissionsName[permission/2]} Permission` + ); + } +} \ No newline at end of file From 7413e85cb91a6b1788c48cc622c645ee7e247991 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Libert?= Date: Wed, 27 Mar 2024 09:24:39 +0100 Subject: [PATCH 2/4] fix permission index logic --- .../__tests__/access_control.spec.ts | 18 ++++++++++++- .../assembly/access-control/access_control.ts | 27 ++++++++++--------- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/smart-contracts/assembly/access-control/__tests__/access_control.spec.ts b/smart-contracts/assembly/access-control/__tests__/access_control.spec.ts index af51c2c..c528f78 100644 --- a/smart-contracts/assembly/access-control/__tests__/access_control.spec.ts +++ b/smart-contracts/assembly/access-control/__tests__/access_control.spec.ts @@ -22,7 +22,7 @@ describe('AccessControl', () => { }).toThrow('permission does not exist'); }); - test('should panic on adding to many permissions', () => { + test('should not panic on adding permissions', () => { resetStorage(); expect(() => { const accessControl = new AccessControl(1); @@ -34,6 +34,22 @@ describe('AccessControl', () => { accessControl.newPermission('p6'); accessControl.newPermission('p7'); accessControl.newPermission('p8'); + }).not.toThrow('Up to 8 permissions should be allowed'); + }); + + test('should panic on adding too many permissions', () => { + resetStorage(); + expect(() => { + const accessControl = new AccessControl(1); + accessControl.newPermission('p1'); + accessControl.newPermission('p2'); + accessControl.newPermission('p3'); + accessControl.newPermission('p4'); + accessControl.newPermission('p5'); + accessControl.newPermission('p6'); + accessControl.newPermission('p7'); + accessControl.newPermission('p8'); + accessControl.newPermission('p9'); }).toThrow('Permission index overflow'); }); diff --git a/smart-contracts/assembly/access-control/access_control.ts b/smart-contracts/assembly/access-control/access_control.ts index 0828f7d..50d8f25 100644 --- a/smart-contracts/assembly/access-control/access_control.ts +++ b/smart-contracts/assembly/access-control/access_control.ts @@ -14,7 +14,7 @@ import { Address, Storage } from "@massalabs/massa-as-sdk"; */ export class AccessControl { // @ts-ignore non-number type - private nextPermissionIndex: T = 1; + private permissionIndex: u8 = 0; private permissionsName: string[] = []; private moduleId: u8; private errPermissionDoesNotExist: string = 'Permission does not exist'; @@ -39,38 +39,39 @@ export class AccessControl { Storage.set(key, toBytes(access)); } + private _permissionIndexToBitmask(permissionIndex: u8): T { + return (1 << permissionIndex); + } + public newPermission(Permission: string): T { + assert(this.permissionIndex < sizeof() * 8, `Maximum number of permissions reached`); this.permissionsName.push(Permission); - const r = this.nextPermissionIndex; - // @ts-ignore arithmetic operations on generic types - assert(this.nextPermissionIndex < (1 << ((sizeof()) * 8 - 1)), 'Permission index overflow'); - // @ts-ignore arithmetic operations on generic types - this.nextPermissionIndex <<= 1; - return r; + this.permissionIndex += 1; + return this._permissionIndexToBitmask(this.permissionIndex -1); } public grantPermissionToUser(permission: T, userAddress: Address): void { - assert(permission < this.nextPermissionIndex, this.errPermissionDoesNotExist); + assert(permission < this._permissionIndexToBitmask(this.permissionIndex), this.errPermissionDoesNotExist); const ua = this._getUserAccess(userAddress); // @ts-ignore arithmetic operations on generic types - assert((ua & permission) != permission, `User already has '${this.permissionsName[permission/2]}' Permission`); + assert((ua & permission) != permission, `User already has '${this.permissionsName[permission>>1]}' Permission`); // @ts-ignore arithmetic operations on generic types this._setUserAccess(userAddress, ua | permission); } public removePermissionFromUser(permission: T, userAddress: Address): void { - assert(permission < this.nextPermissionIndex, this.errPermissionDoesNotExist); + assert(permission < this._permissionIndexToBitmask(this.permissionIndex), this.errPermissionDoesNotExist); const ua = this._getUserAccess(userAddress); // @ts-ignore arithmetic operations on generic types - assert((ua & permission) == permission, `User does not have '${this.permissionsName[permission/2]}' Permission`); + assert((ua & permission) == permission, `User does not have '${this.permissionsName[permission>>1]}' Permission`); // @ts-ignore arithmetic operations on generic types this._setUserAccess(userAddress, ua & ~permission); } public hasPermission(permission: T, userAddress: Address): boolean { - assert(permission < this.nextPermissionIndex, this.errPermissionDoesNotExist); + assert(permission < this._permissionIndexToBitmask(this.permissionIndex), this.errPermissionDoesNotExist); const ua = this._getUserAccess(userAddress); // @ts-ignore arithmetic operations on generic types @@ -80,7 +81,7 @@ export class AccessControl { public mustHavePermission(permission: T, userAddress: Address): void { assert( this.hasPermission(permission, userAddress), - `User does not have ${this.permissionsName[permission/2]} Permission` + `User does not have ${this.permissionsName[permission>>1]} Permission` ); } } \ No newline at end of file From 1c1249b66bad947d6ac2d7515c61fc1d10f26e4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Libert?= Date: Wed, 27 Mar 2024 10:59:22 +0100 Subject: [PATCH 3/4] improve readibility --- .../__tests__/access_control.spec.ts | 107 ++++++++++++------ .../assembly/access-control/access_control.ts | 75 +++++++----- 2 files changed, 119 insertions(+), 63 deletions(-) diff --git a/smart-contracts/assembly/access-control/__tests__/access_control.spec.ts b/smart-contracts/assembly/access-control/__tests__/access_control.spec.ts index c528f78..fcaf316 100644 --- a/smart-contracts/assembly/access-control/__tests__/access_control.spec.ts +++ b/smart-contracts/assembly/access-control/__tests__/access_control.spec.ts @@ -1,5 +1,5 @@ -import { Address, resetStorage } from "@massalabs/massa-as-sdk"; -import { AccessControl } from "../access_control"; +import { Address, resetStorage } from '@massalabs/massa-as-sdk'; +import { AccessControl } from '../access_control'; describe('AccessControl', () => { test('should create new permissions', () => { @@ -17,8 +17,10 @@ describe('AccessControl', () => { resetStorage(); expect(() => { const accessControl = new AccessControl(1); - const userAddress = new Address('AU12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq'); - accessControl.grantPermissionToUser(1, userAddress); + const userAddress = new Address( + 'AU12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq', + ); + accessControl.grantPermission(1, userAddress); }).toThrow('permission does not exist'); }); @@ -58,9 +60,11 @@ describe('AccessControl', () => { expect(() => { const accessControl = new AccessControl(1); const ADMIN = accessControl.newPermission('admin'); - const userAddress = new Address('AU12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq'); - accessControl.grantPermissionToUser(ADMIN, userAddress); - accessControl.grantPermissionToUser(ADMIN, userAddress); + const userAddress = new Address( + 'AU12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq', + ); + accessControl.grantPermission(ADMIN, userAddress); + accessControl.grantPermission(ADMIN, userAddress); }).toThrow('User already has admin permission'); }); @@ -70,8 +74,10 @@ describe('AccessControl', () => { const accessControl = new AccessControl(1); const ADMIN = accessControl.newPermission('admin'); const USER = accessControl.newPermission('user'); - const userAddress = new Address('AU12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq'); - accessControl.grantPermissionToUser(USER, userAddress); + const userAddress = new Address( + 'AU12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq', + ); + accessControl.grantPermission(USER, userAddress); accessControl.mustHavePermission(ADMIN, userAddress); }).toThrow('User does not have admin permission'); }); @@ -82,13 +88,21 @@ describe('AccessControl', () => { const ADMIN = accessControl.newPermission('admin'); const USER = accessControl.newPermission('user'); const GUEST = accessControl.newPermission('guest'); - const userAddress = new Address('AU12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq'); - accessControl.grantPermissionToUser(USER, userAddress); - accessControl.grantPermissionToUser(GUEST, userAddress); - - expect(accessControl.hasPermission(USER, userAddress)).toBeTruthy('User should have user permission'); - expect(accessControl.hasPermission(GUEST, userAddress)).toBeTruthy('User should have guest permission'); - expect(accessControl.hasPermission(ADMIN, userAddress)).toBeFalsy('User should not have admin permission'); + const userAddress = new Address( + 'AU12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq', + ); + accessControl.grantPermission(USER, userAddress); + accessControl.grantPermission(GUEST, userAddress); + + expect(accessControl.hasPermission(USER, userAddress)).toBeTruthy( + 'User should have user permission', + ); + expect(accessControl.hasPermission(GUEST, userAddress)).toBeTruthy( + 'User should have guest permission', + ); + expect(accessControl.hasPermission(ADMIN, userAddress)).toBeFalsy( + 'User should not have admin permission', + ); }); test('should remove permissions from user', () => { @@ -97,20 +111,34 @@ describe('AccessControl', () => { const ADMIN = accessControl.newPermission('admin'); const USER = accessControl.newPermission('user'); const GUEST = accessControl.newPermission('guest'); - const userAddress = new Address('AU12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq'); - accessControl.grantPermissionToUser(USER, userAddress); - accessControl.grantPermissionToUser(GUEST, userAddress); - accessControl.grantPermissionToUser(ADMIN, userAddress); - - expect(accessControl.hasPermission(USER, userAddress)).toBeTruthy('User should have user permission'); - expect(accessControl.hasPermission(GUEST, userAddress)).toBeTruthy('User should have guest permission'); - expect(accessControl.hasPermission(ADMIN, userAddress)).toBeTruthy('User should have admin permission'); - - accessControl.removePermissionFromUser(USER, userAddress); - - expect(accessControl.hasPermission(USER, userAddress)).toBeFalsy('User should not have user permission'); - expect(accessControl.hasPermission(GUEST, userAddress)).toBeTruthy('User should have guest permission'); - expect(accessControl.hasPermission(ADMIN, userAddress)).toBeTruthy('User should have admin permission'); + const userAddress = new Address( + 'AU12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq', + ); + accessControl.grantPermission(USER, userAddress); + accessControl.grantPermission(GUEST, userAddress); + accessControl.grantPermission(ADMIN, userAddress); + + expect(accessControl.hasPermission(USER, userAddress)).toBeTruthy( + 'User should have user permission', + ); + expect(accessControl.hasPermission(GUEST, userAddress)).toBeTruthy( + 'User should have guest permission', + ); + expect(accessControl.hasPermission(ADMIN, userAddress)).toBeTruthy( + 'User should have admin permission', + ); + + accessControl.removePermission(USER, userAddress); + + expect(accessControl.hasPermission(USER, userAddress)).toBeFalsy( + 'User should not have user permission', + ); + expect(accessControl.hasPermission(GUEST, userAddress)).toBeTruthy( + 'User should have guest permission', + ); + expect(accessControl.hasPermission(ADMIN, userAddress)).toBeTruthy( + 'User should have admin permission', + ); }); test('should return proper throw message', () => { @@ -119,7 +147,9 @@ describe('AccessControl', () => { const ADMIN = accessControl.newPermission('admin'); const USER = accessControl.newPermission('user'); const GUEST = accessControl.newPermission('guest'); - const userAddress = new Address('AU12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq'); + const userAddress = new Address( + 'AU12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq', + ); // accessControl.mustHavePermission(ADMIN, userAddress); // accessControl.mustHavePermission(USER, userAddress); @@ -132,12 +162,15 @@ describe('AccessControl', () => { const ADMIN = accessControl1.newPermission('admin'); const accessControl2 = new AccessControl(2); - const MECANIC = accessControl2.newPermission('mecanic'); + const MECHANIC = accessControl2.newPermission('mechanic'); - const userAddress = new Address('AU12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq'); - accessControl1.grantPermissionToUser(ADMIN, userAddress); + const userAddress = new Address( + 'AU12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq', + ); + accessControl1.grantPermission(ADMIN, userAddress); - expect(accessControl2.hasPermission(ADMIN, userAddress)).toBeFalsy('User should not have car mechanic permission'); + expect(accessControl2.hasPermission(ADMIN, userAddress)).toBeFalsy( + 'User should not have car mechanic permission', + ); }); - -}); \ No newline at end of file +}); diff --git a/smart-contracts/assembly/access-control/access_control.ts b/smart-contracts/assembly/access-control/access_control.ts index 50d8f25..c175523 100644 --- a/smart-contracts/assembly/access-control/access_control.ts +++ b/smart-contracts/assembly/access-control/access_control.ts @@ -1,11 +1,11 @@ -import { fromBytes, toBytes } from "@massalabs/as-types"; -import { Address, Storage } from "@massalabs/massa-as-sdk"; +import { fromBytes, toBytes } from '@massalabs/as-types'; +import { Address, Storage } from '@massalabs/massa-as-sdk'; /** * Manages roles and permissions within a blockchain context using a bitmask approach. * Each bit in a bitmask represents a distinct permission. This class exploits * blockchain's native, map-like storage for efficient role and permission management. - * + * * Implementation details: * - Permissions are encoded as bits in a bitmask for compact storage and easy manipulation. * - User access rights are stored and managed in a similar bitmask format. @@ -17,7 +17,7 @@ export class AccessControl { private permissionIndex: u8 = 0; private permissionsName: string[] = []; private moduleId: u8; - private errPermissionDoesNotExist: string = 'Permission does not exist'; + private errPermissionDoesNotExist: string = 'Permission does not exist.'; constructor(moduleId: u8) { this.moduleId = moduleId; @@ -29,49 +29,72 @@ export class AccessControl { return key.concat(userAddress.serialize()); } - private _getUserAccess(userAddress: Address): T { - const key = this._getStorageKey(userAddress); + private _getUserAccess(user: Address): T { + const key = this._getStorageKey(user); return Storage.has(key) ? fromBytes(Storage.get(key)) : 0; } - private _setUserAccess(userAddress: Address, access: T): void { - const key = this._getStorageKey(userAddress); + private _setUserAccess(user: Address, access: T): void { + const key = this._getStorageKey(user); Storage.set(key, toBytes(access)); } - private _permissionIndexToBitmask(permissionIndex: u8): T { - return (1 << permissionIndex); + private _permissionIndexToBitmask(index: u8): T { + return (1 << index); + } + + private _permissionToName(permission: T): string { + // @ts-ignore arithmetic operations on generic types + return this.permissionsName[permission >> 1]; } public newPermission(Permission: string): T { - assert(this.permissionIndex < sizeof() * 8, `Maximum number of permissions reached`); + assert( + this.permissionIndex < sizeof() * 8, + `Maximum number of permissions reached.`, + ); this.permissionsName.push(Permission); this.permissionIndex += 1; - return this._permissionIndexToBitmask(this.permissionIndex -1); + return this._permissionIndexToBitmask(this.permissionIndex - 1); } - public grantPermissionToUser(permission: T, userAddress: Address): void { - assert(permission < this._permissionIndexToBitmask(this.permissionIndex), this.errPermissionDoesNotExist); + public grantPermission(permission: T, user: Address): void { + assert( + permission < this._permissionIndexToBitmask(this.permissionIndex), + this.errPermissionDoesNotExist, + ); - const ua = this._getUserAccess(userAddress); + const ua = this._getUserAccess(user); // @ts-ignore arithmetic operations on generic types - assert((ua & permission) != permission, `User already has '${this.permissionsName[permission>>1]}' Permission`); + assert( + (ua & permission) != permission, + `User already has '${this._permissionToName(permission)}' permission.`, + ); // @ts-ignore arithmetic operations on generic types - this._setUserAccess(userAddress, ua | permission); + this._setUserAccess(user, ua | permission); } - public removePermissionFromUser(permission: T, userAddress: Address): void { - assert(permission < this._permissionIndexToBitmask(this.permissionIndex), this.errPermissionDoesNotExist); - - const ua = this._getUserAccess(userAddress); + public removePermission(permission: T, user: Address): void { + assert( + permission < this._permissionIndexToBitmask(this.permissionIndex), + this.errPermissionDoesNotExist, + ); + + const ua = this._getUserAccess(user); // @ts-ignore arithmetic operations on generic types - assert((ua & permission) == permission, `User does not have '${this.permissionsName[permission>>1]}' Permission`); + assert( + (ua & permission) == permission, + `User does not have '${this._permissionToName(permission)}' permission.`, + ); // @ts-ignore arithmetic operations on generic types - this._setUserAccess(userAddress, ua & ~permission); + this._setUserAccess(user, ua & ~permission); } public hasPermission(permission: T, userAddress: Address): boolean { - assert(permission < this._permissionIndexToBitmask(this.permissionIndex), this.errPermissionDoesNotExist); + assert( + permission < this._permissionIndexToBitmask(this.permissionIndex), + this.errPermissionDoesNotExist, + ); const ua = this._getUserAccess(userAddress); // @ts-ignore arithmetic operations on generic types @@ -81,7 +104,7 @@ export class AccessControl { public mustHavePermission(permission: T, userAddress: Address): void { assert( this.hasPermission(permission, userAddress), - `User does not have ${this.permissionsName[permission>>1]} Permission` + `User does not have '${this._permissionToName(permission)}' permission.`, ); } -} \ No newline at end of file +} From 5246a480202f146c6818627dd69c1b212b352ea7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Libert?= Date: Wed, 27 Mar 2024 11:30:53 +0100 Subject: [PATCH 4/4] add readme --- .../assembly/access-control/README.md | 39 +++++++++++++ .../__tests__/access_control.spec.ts | 56 ++++++++++++++++++- 2 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 smart-contracts/assembly/access-control/README.md diff --git a/smart-contracts/assembly/access-control/README.md b/smart-contracts/assembly/access-control/README.md new file mode 100644 index 0000000..2495975 --- /dev/null +++ b/smart-contracts/assembly/access-control/README.md @@ -0,0 +1,39 @@ +# AccessControl + +The `AccessControl` class provides a flexible and efficient system for managing roles and permissions within a blockchain context. By utilizing a bitmask approach, it encodes permissions in a compact and easily manipulable format. This approach, combined with the use of blockchain's native, map-like storage, allows for efficient and effective role and permission management. + +## Features + +- **Bitmask-based Permissions**: Each bit in a bitmask represents a unique permission, allowing for compact storage and straightforward manipulation of permissions. +- **Efficient Storage Utilization**: Leverages blockchain's native storage capabilities, using ModuleId as a prefix to differentiate keys associated with different modules. +- **Dynamic Permission Management**: Supports creating new permissions, granting/removing permissions to/from users, and checking user permissions dynamically at runtime. + +## Usage Example + +```typescript +import { Address } from '@massalabs/massa-as-sdk'; +import { AccessControl } from '@massalabs/sc-standards'; + +const controller = new AccessControl(1); + +const ADMIN = controller.newPermission('admin'); +const USER = controller.newPermission('user'); + + +function constructor(admin: Address, user: Address): void { + controller.grantPermission(ADMIN, admin); + controller.grantPermission(USER, user); +} + +function adminOnly(caller: Address): void { + controller.mustHavePermission(ADMIN, adminAddress); + // Admin-only operations go here +} + +function userOrAdmin(caller: Address): void { + controller.mustHavePermission(ADMIN||USER, adminAddress); + // Operations for users or admins go here +} +``` + +For more examples and usage scenarios, refer to the unit tests. \ No newline at end of file diff --git a/smart-contracts/assembly/access-control/__tests__/access_control.spec.ts b/smart-contracts/assembly/access-control/__tests__/access_control.spec.ts index fcaf316..01d71e5 100644 --- a/smart-contracts/assembly/access-control/__tests__/access_control.spec.ts +++ b/smart-contracts/assembly/access-control/__tests__/access_control.spec.ts @@ -1,7 +1,61 @@ import { Address, resetStorage } from '@massalabs/massa-as-sdk'; import { AccessControl } from '../access_control'; -describe('AccessControl', () => { +describe('AccessControl - use case tests', () => { + test('should control access to functions 1', () => { + resetStorage(); + + expect(() => { + const adminAddress = new Address( + 'AU12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq', + ); + const userAddress = new Address( + 'AU12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKr', + ); + + const controller = new AccessControl(1); + + const ADMIN = controller.newPermission('admin'); + controller.grantPermission(ADMIN, adminAddress); + + const USER = controller.newPermission('user'); + controller.grantPermission(USER, userAddress); + + controller.mustHavePermission(ADMIN||USER, adminAddress); + controller.mustHavePermission(ADMIN||USER, adminAddress); + }).not.toThrow('or on multiple permissions should work'); + }); + + test('should control access to functions 2', () => { + resetStorage(); + + expect(() => { + const adminAddress = new Address( + 'AU12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq', + ); + const userAddress = new Address( + 'AU12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKr', + ); + const guestAddress = new Address( + 'AU12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKs', + ); + + const controller = new AccessControl(1); + + const ADMIN = controller.newPermission('admin'); + controller.grantPermission(ADMIN, adminAddress); + + const USER = controller.newPermission('user'); + controller.grantPermission(USER, userAddress); + + controller.mustHavePermission(ADMIN||USER, guestAddress); + }).toThrow('or on multiple permissions should work'); + }); +}); + + + +describe('AccessControl - unit tests', () => { test('should create new permissions', () => { resetStorage(); const accessControl = new AccessControl(1);