Post
Share your knowledge.
Multi role access control
What’s the recommended way to implement multi-role access control (e.g., admin, issuer, user) using capabilities or object IDs
- Sui
- Move
Answers
8Implementing multi-role access control (e.g., admin, issuer, user) on the Sui network involves defining roles and enforcing their permissions on actions within your smart contracts. Capabilities and object IDs in Sui are crucial for securing access and controlling which users can perform certain operations. Here’s a recommended approach using Move’s resource and capabilities system:
1. Define Roles and Permissions
You can use resources to represent roles (admin, issuer, user) and their associated permissions, linking them to specific object IDs or capabilities.
- Admin: Full control over all actions in the contract.
- Issuer: Limited control, e.g., issuing tokens or managing specific assets.
- User: Basic control, e.g., interacting with the contract or performing actions without creating or issuing new assets.
2. Create Role Resources
Each role can be defined as a resource that holds a specific access control capability. The role could be linked to an object ID or stored as a global resource.
Example:
module AccessControl {
// Admin role resource
struct AdminRole has store {
id: address, // Address of the admin
}
// Issuer role resource
struct IssuerRole has store {
id: address, // Address of the issuer
}
// User role resource
struct UserRole has store {
id: address, // Address of the user
}
// Helper function to check admin role
public fun is_admin(sender: &signer): bool {
let admin = borrow_global<AdminRole>(sender);
return admin.id == *sender; // Check if the sender is the admin
}
// Helper function to check issuer role
public fun is_issuer(sender: &signer): bool {
let issuer = borrow_global<IssuerRole>(sender);
return issuer.id == *sender; // Check if the sender is the issuer
}
// Helper function to check user role
public fun is_user(sender: &signer): bool {
let user = borrow_global<UserRole>(sender);
return user.id == *sender; // Check if the sender is a user
}
}
3. Assign Capabilities or Object IDs to Roles
Use capabilities to control access to resources (e.g., an admin has the capability to modify specific resources, whereas a user can only read). For each role, assign capabilities tied to specific resources or object IDs.
For example, in a contract where the admin can manage the contract, the issuer can mint tokens, and the user can interact with tokens:
module TokenModule {
// Token struct with ownership tracking
struct Token has store {
id: u64,
owner: address,
}
// Admin can create tokens
public fun create_token(sender: &signer, id: u64, owner: address) {
assert!(AccessControl::is_admin(sender), 1); // Only admin can create tokens
let token = Token { id, owner };
move_to(sender, token);
}
// Issuer can transfer tokens
public fun transfer_token(sender: &signer, recipient: address, id: u64) {
assert!(AccessControl::is_issuer(sender), 2); // Only issuer can transfer tokens
let token = borrow_global_mut<Token>(sender);
token.owner = recipient;
}
// Users can view tokens
public fun view_token(sender: &signer, id: u64): Token {
assert!(AccessControl::is_user(sender), 3); // Only users can view tokens
let token = borrow_global<Token>(sender);
return token;
}
}
4. Capacities and Access Control
The capabilities system in Sui allows you to share access to objects/resources between different users based on the role, ensuring that only certain users can access sensitive functions (e.g., admin operations).
Example of how capabilities can be used to delegate access to resources:
module AccessControl {
// Function to grant capabilities to other users (e.g., for issuer or user roles)
public fun grant_role_capability(sender: &mut signer, receiver: address, role: u8) {
if (role == 1) {
// Grant admin capability
let admin_role = AdminRole { id: *sender };
move_to(receiver, admin_role);
} else if (role == 2) {
// Grant issuer capability
let issuer_role = IssuerRole { id: *sender };
move_to(receiver, issuer_role);
} else {
// Grant user capability
let user_role = UserRole { id: *sender };
move_to(receiver, user_role);
}
}
}
5. Functionality Example
Here’s a full example showing how to implement multi-role access control using capabilities for different types of users (admin, issuer, user).
module MultiRoleAccessControl {
// Admin role resource
struct AdminRole has store {
id: address,
}
// Issuer role resource
struct IssuerRole has store {
id: address,
}
// User role resource
struct UserRole has store {
id: address,
}
// Admin can add users and manage resources
public fun add_user(sender: &signer, new_user: address) {
assert!(is_admin(sender), 1); // Only admin can add a user
let user_role = UserRole { id: new_user };
move_to(sender, user_role); // Assign user role
}
// Check if sender is an admin
public fun is_admin(sender: &signer): bool {
let admin = borrow_global<AdminRole>(sender);
return admin.id == *sender;
}
// Check if sender is an issuer
public fun is_issuer(sender: &signer): bool {
let issuer = borrow_global<IssuerRole>(sender);
return issuer.id == *sender;
}
// Check if sender is a user
public fun is_user(sender: &signer): bool {
let user = borrow_global<UserRole>(sender);
return user.id == *sender;
}
}
Key Points:
- Define Roles as Resources: Each role (admin, issuer, user) should be a resource, ensuring that you can store and manage permissions securely.
- Use Capabilities to Control Access: Leverage capabilities to give or revoke access to certain resources based on user roles.
- Check Permissions Before Allowing Operations: Always check the sender’s role before allowing them to perform any sensitive operation, such as transferring tokens or adding new users.
- Granular Permissions: Ensure that your access control system is fine-grained so that roles are clearly defined and separated.
Conclusion:
To implement multi-role access control on the Sui network, use resources to represent roles, and leverage capabilities to manage access to different actions based on those roles. By structuring your smart contracts this way, you ensure that users have appropriate access to only the functionalities they are authorized to interact with.
To implement multi-role access control in Move on the Sui network, you can use capabilities and object IDs to enforce role-based permissions (e.g., admin, issuer, user).
Steps:
-
Define Role Resources:
- Create a
Roleresource for each user with fields likeis_adminandis_issuer.
struct Role { is_admin: bool, is_issuer: bool, } - Create a
-
Assign Roles:
- Assign roles to users with
assign_rolefunctions.
public fun assign_role(account: address, is_admin: bool, is_issuer: bool) { let role = Role { is_admin, is_issuer }; move_to(account, role); } - Assign roles to users with
-
Role-Based Access:
- Use role checks in functions (e.g., only admins can mint tokens).
public fun mint(account: address, amount: u64) { assert!(is_issuer(account), 1); // Only issuers can mint // Mint logic } public fun is_issuer(account: address): bool { let role = borrow_global<Role>(account); role.is_issuer } -
Using Capabilities for Fine Control:
- Use capabilities for admin-specific actions.
struct AdminCap { admin_address: address, } public fun grant_admin_cap(account: address): AdminCap { AdminCap { admin_address: account } } public fun check_admin_cap(account: address): bool { let cap = borrow_global<AdminCap>(account); cap.admin_address == account } -
Security:
- Emit events to log role changes or critical actions for transparency.
struct RoleChangedEvent { account: address, new_role: string, } public fun emit_role_changed_event(account: address, new_role: string) { let event = RoleChangedEvent { account, new_role }; emit_event(event); }
Best Practices:
- Modular role checks: Use separate functions for each role (admin, issuer).
- Event logging: Emit events to track role assignments and critical actions.
- Use capabilities: Control access at the resource level using Move’s capabilities.
This ensures secure, flexible access control with minimal code repetition.
Use capability tokens (ownable objects) to represent roles—issue AdminCap, IssuerCap, etc., to trusted addresses. Check for possession of these objects in entry functions. Store caps in a Capabilities module, and use @sui::cap pattern for revocable, composable access. Secure, flexible, and idiomatic in Sui.
The recommended way to implement multi-role access control in Sui is by using capability objects tied to specific roles. Each role (e.g., admin, issuer, user) should have a distinct capability struct, which acts as a permission token. These capability objects are typically owned or stored in a central controller object to manage delegation. Use the has key and store abilities for capability structs to allow persistent storage and controlled transfers. Admin capabilities can be allowed to mint, burn, or transfer other role capabilities, enforcing hierarchical control. Functions should require the presence of the appropriate capability as an input argument to enforce access checks. You can bind capabilities to specific ObjectIDs to make them non-transferable and role-specific. Revoking access involves deleting the capability object or reassigning it through controlled functions. Avoid using booleans or simple enums for access checks—capability objects provide stronger guarantees via ownership. Always ensure role capabilities are protected by visibility rules (public(friend) or entry) to avoid unauthorized access.
To implement multi-role access control in Move on Sui, use separate capability structs for each role, like AdminCap, IssuerCap, and UserCap. Each capability should be stored in a distinct object and granted only to authorized addresses. Use object IDs or the has keyword to check role ownership before executing privileged logic. Design your entry functions to accept the appropriate capability object as a parameter to enforce access. Make capability objects non-copyable and non-transferable unless explicitly managed through functions. Use module-only creation for these capabilities to prevent unauthorized minting. Include revocation logic to allow for role changes or access removal. Store all role-related logic in a central AccessControl module for better maintainability. Test edge cases such as capability leakage, misuse, and duplication thoroughly. Always restrict sensitive actions to accounts that hold the corresponding role capability.
For multi-role access control in Move (Sui), use:
-
Capability Pattern – Issue unique
Capability<T>objects (e.g.,AdminCap,IssuerCap) to authorized addresses.- Example:
struct AdminCap has key, store { id: UID } fun admin_only(cap: &AdminCap) { /* admin-only logic */ }
- Example:
-
Object-Based Permissions – Store roles in objects (e.g.,
Role<T>NFT) checked viaobject::id(). -
Hybrid Approach – Combine both (e.g.,
AdminCapfor high-power roles, dynamic objects for granular access).
Best Practices:
- Revocable – Burn/transfer capabilities to revoke access.
- Minimal Privilege – Restrict functions to specific
Capabilitytypes. - Event Logging – Emit events for off-chain tracking.
Follow Sui’s sui::transfer policies for secure role management.
1. Capability-Based Design (Recommended)
module my_app::auth {
// Role tokens
struct AdminCap has key, store { id: UID }
struct IssuerCap has key, store { id: UID }
struct UserCap has key, store { id: UID }
// Role assignment during init
public fun init(ctx: &mut TxContext) -> (AdminCap, IssuerCap) {
(AdminCap { id: object::new(ctx) }, IssuerCap { id: object::new(ctx) })
}
// Guarded functions
public entry fun admin_action(
_cap: &AdminCap,
ctx: &mut TxContext
) { ... }
public entry fun mint(
_cap: &IssuerCap,
amount: u64
) { ... }
}
Key Benefits:
- Decentralized control: Roles are transferable objects
- Composable: Combine multiple capabilities for granular access
- Gas-efficient: No storage overhead for role checks
2. Object ID Whitelisting (For Fixed Roles)
module my_app::auth {
const ADMIN: address = @0xADMIN;
const ISSUER: address = @0xISSUER;
public fun admin_action(
caller: address,
ctx: &mut TxContext
) {
assert!(caller == ADMIN, EACCESS_DENIED);
...
}
}
Use When:
- Roles are static (e.g., protocol-owned addresses)
- Minimal on-chain storage is critical
3. Hybrid Approach (Dynamic + Capabilities)
module my_app::auth {
struct RoleStore has key {
id: UID,
admins: Table<address, bool>,
issuers: Table<address, bool>
}
// Dynamic role assignment
public entry fun grant_issuer(
admin: &AdminCap,
store: &mut RoleStore,
user: address
) {
table::add(&mut store.issuers, user, true);
}
}
Best Practices
-
Capability Patterns:
- Revokable: Add expiry timestamps to capabilities
- Composable: Nest capabilities (e.g.,
AdminCap(IssuerCap))
struct TimedCap<phantom T> has key, store { id: UID, expiry: u64 } -
Error Handling:
const EACCESS_DENIED: u64 = 0; const EROLE_EXPIRED: u64 = 1; -
Testing:
#[test_only] module test { fun test_admin_access() { let (admin, _) = auth::init(ctx); auth::admin_action(&admin, ctx); // Should pass } }
Real-World Example (NFT Minting)
module nft::auth {
struct IssuerCap has key, store { id: UID }
public entry fun mint(
cap: &IssuerCap,
recipient: address,
ctx: &mut TxContext
) {
let nft = NFT { id: object::new(ctx) };
transfer::public_transfer(nft, recipient);
}
}
Key Tradeoffs
| Method | Gas Cost | Flexibility | Storage Overhead |
|---|---|---|---|
| Capabilities | Low | High | Medium |
| Object IDs | Lowest | Low | None |
| Hybrid | Medium | Highest | High |
For production use:
- Start with capabilities for most cases
- Switch to Object IDs for fixed admin roles
- Use hybrid only for complex RBAC systems
Do you know the answer?
Please log in and share it.
Sui is a Layer 1 protocol blockchain designed as the first internet-scale programmable blockchain platform.
- How to Maximize Profit Holding SUI: Sui Staking vs Liquid Staking616
- Why does BCS require exact field order for deserialization when Move structs have named fields?65
- Multiple Source Verification Errors" in Sui Move Module Publications - Automated Error Resolution55
- Sui Move Error - Unable to process transaction No valid gas coins found for the transaction419
- Sui Transaction Failing: Objects Reserved for Another Transaction410