-
Notifications
You must be signed in to change notification settings - Fork 8
Unshielded Access Control #182
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
Merged
Merged
Changes from all commits
Commits
Show all changes
47 commits
Select commit
Hold shift + click to select a range
ffb9fe8
commit initial design
emnul 35397ac
Undo package.json changes
emnul 9010249
fmt files
emnul 3cddc66
fmt package.json
emnul 5fd5213
fix yarn.lock
emnul b6d03b5
Apply documentation suggestions from code review
emnul 770e321
Rename _checkRole -> assertOnlyRole
emnul a847331
Update module documentation
emnul d78686a
Remove Initializable module
emnul 359ece1
Remove Initializable requirements from method docs
emnul 685d062
error message consistency
emnul b2079a3
Update method docs
emnul fbd6004
Fix simulator bug
emnul dcab470
Update method docs
emnul c792cc3
Add tests
emnul 20316be
Remove unused files
emnul 82e3f12
Fmt files
emnul c48e7c0
Update method docs
emnul b1c1e58
Update asciidoc attributes
emnul 6ea39a7
Update nav and add API reference
emnul 55fa0b3
Update module docs
emnul 38abf97
Add AccessControl Overview, update AccessControl API
emnul 9feb5fb
Update imports
emnul 2a47555
Apply fmt suggestions to AccessControl.compact
emnul 5452fa4
Add @return to method docs
emnul b9dc2a5
Reword method docs
emnul 32ac7df
Update _checkRole tests
emnul 348d6f8
refactor callerTypes
emnul 9980a9c
use _lowercase instead of SCREAMING_CASE
emnul 31513c4
Refactor code code block
emnul 7ebeb83
Remove unnecessary expect
emnul ded8bdf
refactor describe each
emnul 45ba4a8
Add CUSTOM_ADMIN
emnul 4beddb0
Fmt file
emnul 247e043
Add beforeEach block
emnul ef5a62b
remove extra setter
emnul 930dccd
Check new admin and previous admin permissions
emnul b582214
fmt file
emnul c18a3a2
Add more tests, refactor code
emnul 08a2465
fmt file
emnul 232847c
Apply documentation suggestions from code review
emnul 7437b73
Update requirements and test
emnul affb1e3
Update AccessControl docs
emnul 4440a68
Merge branch 'main' into unshielded-access-control
emnul dbd9f0c
Annihilate typos
emnul 5eb3e51
Update module docs
emnul 1008842
Merge branch 'main' into unshielded-access-control
emnul File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
{ | ||
"name": "@openzeppelin-compact/access-control", | ||
"private": true, | ||
"type": "module", | ||
"main": "dist/index.js", | ||
"module": "dist/index.js", | ||
"types": "./dist/index.d.ts", | ||
"exports": { | ||
".": { | ||
"types": "./dist/index.d.ts", | ||
"require": "./dist/index.js", | ||
"import": "./dist/index.js", | ||
"default": "./dist/index.js" | ||
} | ||
}, | ||
"scripts": { | ||
"compact": "compact-compiler", | ||
"build": "compact-builder && tsc", | ||
"test": "vitest run", | ||
"types": "tsc -p tsconfig.json --noEmit", | ||
"clean": "git clean -fXd" | ||
}, | ||
"dependencies": { | ||
"@openzeppelin-compact/compact": "workspace:^" | ||
}, | ||
"devDependencies": { | ||
"@types/node": "22.14.0", | ||
"ts-node": "^10.9.2", | ||
"typescript": "^5.2.2", | ||
"vitest": "^3.1.3" | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,321 @@ | ||
// SPDX-License-Identifier: MIT | ||
|
||
pragma language_version >= 0.15.0; | ||
|
||
/** | ||
* @module AccessControl | ||
* @description An unshielded AccessControl library. | ||
* This module provides a role-based access control mechanism, where roles can be used to | ||
* represent a set of permissions. | ||
* | ||
* Roles are referred to by their `Bytes<32>` identifier. These should be exposed | ||
* in the top-level contract and be unique. One way to achieve this is by | ||
* using `export sealed ledger` hash digests that are initialized in the top-level contract: | ||
* | ||
* ```typescript | ||
* import CompactStandardLibrary; | ||
* import "./node_modules/@openzeppelin-compact/accessControl/src/AccessControl" prefix AccessControl_; | ||
* | ||
* export sealed ledger MY_ROLE: Bytes<32>; | ||
* | ||
* constructor() { | ||
* MY_ROLE = persistent_hash<Bytes<32>>(pad(32, "MY_ROLE")); | ||
* } | ||
* ``` | ||
* | ||
* To restrict access to a circuit, use {assertOnlyRole}: | ||
* | ||
* ```typescript | ||
* circuit foo(): [] { | ||
* assertOnlyRole(MY_ROLE); | ||
* ... | ||
* } | ||
* ``` | ||
* | ||
* Roles can be granted and revoked dynamically via the {grantRole} and | ||
* {revokeRole} circuits. Each role has an associated admin role, and only | ||
* accounts that have a role's admin role can call {grantRole} and {revokeRole}. | ||
* | ||
* By default, the admin role for all roles is `DEFAULT_ADMIN_ROLE`, which means | ||
* that only accounts with this role will be able to grant or revoke other | ||
* roles. More complex role relationships can be created by using | ||
* {_setRoleAdmin}. To set a custom `DEFAULT_ADMIN_ROLE`, implement the `Initializable` | ||
* module and set `DEFAULT_ADMIN_ROLE` in the `initialize()` circuit. | ||
* | ||
* WARNING: The `DEFAULT_ADMIN_ROLE` is also its own admin: it has permission to | ||
* grant and revoke this role. Extra precautions should be taken to secure | ||
* accounts that have been granted it. | ||
* | ||
* @notice Roles can only be granted to ZswapCoinPublicKeys | ||
* through the main role approval circuits (`grantRole` and `_grantRole`). | ||
* In other words, role approvals to contract addresses are disallowed through these | ||
* circuits. | ||
* This is because Compact currently does not support contract-to-contract calls which means | ||
* if a contract is granted a role, the contract cannot directly call the protected | ||
* circuit. | ||
* | ||
* @notice This module does offer an experimental circuit that allows roles to be granted | ||
* to contract addresses (`_unsafeGrantRole`). | ||
* Note that the circuit name is very explicit ("unsafe") with this experimental circuit. | ||
* Until contract-to-contract calls are supported, | ||
* there is no direct way for a contract to call protected circuits. | ||
* | ||
* @notice The unsafe circuits are planned to become deprecated once contract-to-contract calls | ||
* are supported. | ||
* | ||
* @notice Missing Features and Improvements: | ||
* | ||
* - Role events | ||
* - An ERC165-like interface | ||
*/ | ||
module AccessControl { | ||
import CompactStandardLibrary; | ||
import "../../node_modules/@openzeppelin-compact/utils/src/Utils" prefix Utils_; | ||
|
||
/** | ||
* @description Mapping from a role identifier -> account -> its permissions. | ||
* @type {Bytes<32>} roleId - A hash representing a role identifier. | ||
* @type {Map<Either<ZswapCoinPublicKey, ContractAddress>, Boolean>} hasRole - A mapping from an account to a | ||
* Boolean determining if the account is approved for a role. | ||
* @type {Map<roleId, hasRole>} | ||
* @type {Map<Bytes<32>, Map<Either<ZswapCoinPublicKey, ContractAddress>, Boolean>} _operatorRoles | ||
*/ | ||
export ledger _operatorRoles: Map<Bytes<32>, Map<Either<ZswapCoinPublicKey, ContractAddress>, Boolean>>; | ||
|
||
/** | ||
* @description Mapping from a role identifier to an admin role identifier. | ||
* @type {Bytes<32>} roleId - A hash representing a role identifier. | ||
* @type {Bytes<32>} adminId - A hash representing an admin identifier. | ||
* @type {Map<roleId, adminId>} | ||
* @type {Map<Bytes<32>, Bytes<32>>} _adminRoles | ||
*/ | ||
export ledger _adminRoles: Map<Bytes<32>, Bytes<32>>; | ||
|
||
export ledger DEFAULT_ADMIN_ROLE: Bytes<32>; | ||
|
||
/** | ||
* @description Returns `true` if `account` has been granted `roleId`. | ||
* | ||
* @circuitInfo | ||
* | ||
* @param {Bytes<32>} roleId - The role identifier. | ||
* @param {Either<ZswapCoinPublicKey, ContractAddress>} account - The account to query. | ||
* @return {Boolean} - Whether the account has the specified role. | ||
*/ | ||
export circuit hasRole(roleId: Bytes<32>, account: Either<ZswapCoinPublicKey, ContractAddress>): Boolean { | ||
if ( | ||
_operatorRoles.member(roleId) && | ||
_operatorRoles | ||
.lookup(roleId) | ||
.member(account) | ||
) { | ||
return _operatorRoles | ||
.lookup(roleId) | ||
.lookup(account); | ||
} else { | ||
return false; | ||
} | ||
} | ||
|
||
/** | ||
* @description Reverts if `own_public_key()` is missing `roleId`. | ||
* | ||
* @circuitInfo | ||
* | ||
* Requirements: | ||
* | ||
* - The caller must have `roleId`. | ||
* - The caller must not be a ContractAddress | ||
* | ||
* @param {Bytes<32>} roleId - The role identifier. | ||
* @return {[]} - Empty tuple. | ||
*/ | ||
export circuit assertOnlyRole(roleId: Bytes<32>): [] { | ||
_checkRole(roleId, left<ZswapCoinPublicKey,ContractAddress>(own_public_key())); | ||
} | ||
|
||
/** | ||
* @description Reverts if `account` is missing `roleId`. | ||
* | ||
* @circuitInfo | ||
* | ||
* Requirements: | ||
* | ||
* - `account` must have `roleId`. | ||
* | ||
* @param {Bytes<32>} roleId - The role identifier. | ||
* @param {Either<ZswapCoinPublicKey, ContractAddress>} account - The account to query. | ||
* @return {[]} - Empty tuple. | ||
*/ | ||
export circuit _checkRole(roleId: Bytes<32>, account: Either<ZswapCoinPublicKey, ContractAddress>): [] { | ||
assert hasRole(roleId, account) "AccessControl: unauthorized account"; | ||
} | ||
|
||
/** | ||
* @description Returns the admin role that controls `roleId` or | ||
* a byte array with all zero bytes if `roleId` doesn't exist. See {grantRole} and {revokeRole}. | ||
* | ||
* To change a role’s admin use {_setRoleAdmin}. | ||
* | ||
* @circuitInfo | ||
* | ||
* @param {Bytes<32>} roleId - The role identifier. | ||
* @return {Bytes<32>} roleAdmin - The admin role that controls `roleId`. | ||
*/ | ||
export circuit getRoleAdmin(roleId: Bytes<32>): Bytes<32> { | ||
if (_adminRoles.member(roleId)) { | ||
return _adminRoles.lookup(roleId); | ||
} | ||
return default<Bytes<32>>; | ||
} | ||
|
||
/** | ||
* @description Grants `roleId` to `account`. | ||
* | ||
* @circuitInfo | ||
* | ||
* Requirements: | ||
* | ||
* - `account` must not be a ContractAddress. | ||
* - The caller must have `roleId`'s admin role. | ||
* | ||
* @param {Bytes<32>} roleId - The role identifier. | ||
* @param {Either<ZswapCoinPublicKey, ContractAddress>} account - A ZswapCoinPublicKey or ContractAddress. | ||
* @return {[]} - Empty tuple. | ||
*/ | ||
export circuit grantRole(roleId: Bytes<32>, account: Either<ZswapCoinPublicKey, ContractAddress>): [] { | ||
assertOnlyRole(getRoleAdmin(roleId)); | ||
_grantRole(roleId, account); | ||
} | ||
|
||
/** | ||
* @description Revokes `roleId` from `account`. | ||
* | ||
* @circuitInfo | ||
* | ||
* Requirements: | ||
* | ||
* - The caller must have `roleId`'s admin role. | ||
* | ||
* @param {Bytes<32>} roleId - The role identifier. | ||
* @param {Either<ZswapCoinPublicKey, ContractAddress>} account - A ZswapCoinPublicKey or ContractAddress. | ||
* @return {[]} - Empty tuple. | ||
*/ | ||
export circuit revokeRole(roleId: Bytes<32>, account: Either<ZswapCoinPublicKey, ContractAddress>): [] { | ||
assertOnlyRole(getRoleAdmin(roleId)); | ||
_revokeRole(roleId, account); | ||
} | ||
|
||
/** | ||
* @description Revokes `roleId` from the calling account. | ||
* | ||
* @notice Roles are often managed via {grantRole} and {revokeRole}: this circuit's | ||
* purpose is to provide a mechanism for accounts to lose their privileges | ||
* if they are compromised (such as when a trusted device is misplaced). | ||
* | ||
* @circuitInfo | ||
* | ||
* Requirements: | ||
* | ||
* - The caller must be `callerConfirmation`. | ||
* - The caller must not be a `ContractAddress`. | ||
* | ||
* @param {Bytes<32>} roleId - The role identifier. | ||
* @param {Either<ZswapCoinPublicKey, ContractAddress>} callerConfirmation - A ZswapCoinPublicKey or ContractAddress. | ||
* @return {[]} - Empty tuple. | ||
*/ | ||
export circuit renounceRole(roleId: Bytes<32>, callerConfirmation: Either<ZswapCoinPublicKey, ContractAddress>): [] { | ||
assert callerConfirmation == left<ZswapCoinPublicKey,ContractAddress>(own_public_key()) "AccessControl: bad confirmation"; | ||
|
||
_revokeRole(roleId, callerConfirmation); | ||
} | ||
|
||
/** | ||
* @description Sets `adminRole` as `roleId`'s admin role. | ||
* | ||
* @circuitInfo | ||
* | ||
* @param {Bytes<32>} roleId - The role identifier. | ||
* @param {Bytes<32>} adminRole - The admin role identifier. | ||
* @return {[]} - Empty tuple. | ||
*/ | ||
export circuit _setRoleAdmin(roleId: Bytes<32>, adminRole: Bytes<32>): [] { | ||
_adminRoles.insert(roleId, adminRole); | ||
} | ||
|
||
/** | ||
* @description Attempts to grant `roleId` to `account` and returns a boolean indicating if `roleId` was granted. | ||
* Internal circuit without access restriction. | ||
* | ||
* @circuitInfo | ||
* | ||
* Requirements: | ||
* | ||
* - `account` must not be a ContractAddress. | ||
* | ||
* @param {Bytes<32>} roleId - The role identifier. | ||
* @param {Either<ZswapCoinPublicKey, ContractAddress>} account - A ZswapCoinPublicKey or ContractAddress. | ||
* @return {Boolean} roleGranted - A boolean indicating if `roleId` was granted. | ||
*/ | ||
export circuit _grantRole(roleId: Bytes<32>, account: Either<ZswapCoinPublicKey, ContractAddress>): Boolean { | ||
assert !Utils_isContractAddress(account) "AccessControl: unsafe role approval"; | ||
return _unsafeGrantRole(roleId, account); | ||
} | ||
|
||
/** | ||
* @description Attempts to grant `roleId` to `account` and returns a boolean indicating if `roleId` was granted. | ||
* Internal circuit without access restriction. It does NOT check if the role is granted to a ContractAddress. | ||
* | ||
* @circuitInfo | ||
* | ||
* @notice External smart contracts cannot call the token contract at this time, so granting a role to an ContractAddress may | ||
* render a circuit permanently inaccessible. | ||
* | ||
* @param {Bytes<32>} roleId - The role identifier. | ||
* @param {Either<ZswapCoinPublicKey, ContractAddress>} account - A ZswapCoinPublicKey or ContractAddress. | ||
* @return {Boolean} roleGranted - A boolean indicating if `role` was granted. | ||
*/ | ||
export circuit _unsafeGrantRole(roleId: Bytes<32>, account: Either<ZswapCoinPublicKey, ContractAddress>): Boolean { | ||
if (hasRole(roleId, account)) { | ||
return false; | ||
} | ||
|
||
if (!_operatorRoles.member(roleId)) { | ||
_operatorRoles.insert( | ||
roleId, | ||
default<Map< | ||
Either<ZswapCoinPublicKey, ContractAddress>, | ||
Boolean | ||
>> | ||
); | ||
_operatorRoles | ||
.lookup(roleId) | ||
.insert(account, true); | ||
return true; | ||
} | ||
|
||
_operatorRoles.lookup(roleId).insert(account, true); | ||
return true; | ||
} | ||
|
||
/** | ||
* @description Attempts to revoke `roleId` from `account` and returns a boolean indicating if `roleId` was revoked. | ||
* Internal circuit without access restriction. | ||
* | ||
* @circuitInfo | ||
* | ||
* @param {Bytes<32>} roleId - The role identifier. | ||
* @param {Bytes<32>} adminRole - The admin role identifier. | ||
* @return {Boolean} roleRevoked - A boolean indicating if `roleId` was revoked. | ||
*/ | ||
export circuit _revokeRole(roleId: Bytes<32>, account: Either<ZswapCoinPublicKey, ContractAddress>): Boolean { | ||
if (!hasRole(roleId, account)) { | ||
return false; | ||
} | ||
|
||
_operatorRoles | ||
.lookup(roleId) | ||
.insert(account, false); | ||
return true; | ||
} | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.