Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add AccessControl object #153

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions smart-contracts/assembly/access-control/README.md
Original file line number Diff line number Diff line change
@@ -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<u8>(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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
import { Address, resetStorage } from '@massalabs/massa-as-sdk';
import { AccessControl } from '../access_control';

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<u8>(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<u8>(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<u8>(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<u8>(1);
const userAddress = new Address(
'AU12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq',
);
accessControl.grantPermission(1, userAddress);
}).toThrow('permission does not exist');
});

test('should not panic on adding permissions', () => {
resetStorage();
expect(() => {
const accessControl = new AccessControl<u8>(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');
}).not.toThrow('Up to 8 permissions should be allowed');
});

test('should panic on adding too many permissions', () => {
resetStorage();
expect(() => {
const accessControl = new AccessControl<u8>(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');
});

test('should panic on adding permission twice', () => {
resetStorage();
expect(() => {
const accessControl = new AccessControl<u8>(1);
const ADMIN = accessControl.newPermission('admin');
const userAddress = new Address(
'AU12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq',
);
accessControl.grantPermission(ADMIN, userAddress);
accessControl.grantPermission(ADMIN, userAddress);
}).toThrow('User already has admin permission');
});

test('should panic on missing must have permission', () => {
resetStorage();
expect(() => {
const accessControl = new AccessControl<u8>(1);
const ADMIN = accessControl.newPermission('admin');
const USER = accessControl.newPermission('user');
const userAddress = new Address(
'AU12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq',
);
accessControl.grantPermission(USER, userAddress);
accessControl.mustHavePermission(ADMIN, userAddress);
}).toThrow('User does not have admin permission');
});

test('should add permissions to user', () => {
resetStorage();
const accessControl = new AccessControl<u8>(1);
const ADMIN = accessControl.newPermission('admin');
const USER = accessControl.newPermission('user');
const GUEST = accessControl.newPermission('guest');
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', () => {
resetStorage();
const accessControl = new AccessControl<u8>(1);
const ADMIN = accessControl.newPermission('admin');
const USER = accessControl.newPermission('user');
const GUEST = accessControl.newPermission('guest');
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', () => {
resetStorage();
const accessControl = new AccessControl<u8>(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<u8>(1);
const ADMIN = accessControl1.newPermission('admin');

const accessControl2 = new AccessControl<u8>(2);
const MECHANIC = accessControl2.newPermission('mechanic');

const userAddress = new Address(
'AU12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq',
);
accessControl1.grantPermission(ADMIN, userAddress);

expect(accessControl2.hasPermission(ADMIN, userAddress)).toBeFalsy(
'User should not have car mechanic permission',
);
});
});
110 changes: 110 additions & 0 deletions smart-contracts/assembly/access-control/access_control.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
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<T> {
// @ts-ignore non-number type
private permissionIndex: u8 = 0;
private permissionsName: string[] = [];
private moduleId: u8;
private errPermissionDoesNotExist: string = 'Permission does not exist.';

constructor(moduleId: u8) {
this.moduleId = moduleId;
}

private _getStorageKey(userAddress: Address): StaticArray<u8> {
const key = new StaticArray<u8>(1);
key[0] = this.moduleId;
return key.concat(userAddress.serialize());
}

private _getUserAccess(user: Address): T {
const key = this._getStorageKey(user);
return Storage.has(key) ? <T>fromBytes<T>(Storage.get(key)) : <T>0;
}

private _setUserAccess(user: Address, access: T): void {
const key = this._getStorageKey(user);
Storage.set(key, toBytes(access));
}

private _permissionIndexToBitmask(index: u8): T {
return <T>(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<T>() * 8,
`Maximum number of permissions reached.`,
);
this.permissionsName.push(Permission);
this.permissionIndex += 1;
return this._permissionIndexToBitmask(this.permissionIndex - 1);
}

public grantPermission(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 already has '${this._permissionToName(permission)}' permission.`,
);
// @ts-ignore arithmetic operations on generic types
this._setUserAccess(user, ua | permission);
}

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._permissionToName(permission)}' permission.`,
);
// @ts-ignore arithmetic operations on generic types
this._setUserAccess(user, ua & ~permission);
}

public hasPermission(permission: T, userAddress: Address): boolean {
assert(
permission < this._permissionIndexToBitmask(this.permissionIndex),
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._permissionToName(permission)}' permission.`,
);
}
}
Loading