Sui.

Post

Share your knowledge.

SuiLover.
Jul 27, 2025
Expert Q&A

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
1
8
Share
Comments
.

Answers

8
Paul.
Paul4340
Jul 31 2025, 12:06

Implementing 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:

  1. Define Roles as Resources: Each role (admin, issuer, user) should be a resource, ensuring that you can store and manage permissions securely.
  2. Use Capabilities to Control Access: Leverage capabilities to give or revoke access to certain resources based on user roles.
  3. 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.
  4. 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.

8
Comments
.
Thorfin.
Jul 31 2025, 14:32

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:

  1. Define Role Resources:

    • Create a Role resource for each user with fields like is_admin and is_issuer.
    struct Role {
        is_admin: bool,
        is_issuer: bool,
    }
    
  2. Assign Roles:

    • Assign roles to users with assign_role functions.
    public fun assign_role(account: address, is_admin: bool, is_issuer: bool) {
        let role = Role { is_admin, is_issuer };
        move_to(account, role);
    }
    
  3. 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
    }
    
  4. 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
    }
    
  5. 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.

8
Comments
.
Alya.
Alya-14
Jul 31 2025, 14:30

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.

3
Comments
.
BigSneh.
Jul 27 2025, 08:40

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.

2
Comments
.
290697tz.
Jul 27 2025, 09:04

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.

2
Comments
.
Evgeniy CRYPTOCOIN.
Jul 29 2025, 14:06

For multi-role access control in Move (Sui), use:

  1. 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 */ }  
      
  2. Object-Based Permissions – Store roles in objects (e.g., Role<T> NFT) checked via object::id().

  3. Hybrid Approach – Combine both (e.g., AdminCap for high-power roles, dynamic objects for granular access).

Best Practices:

  • Revocable – Burn/transfer capabilities to revoke access.
  • Minimal Privilege – Restrict functions to specific Capability types.
  • Event Logging – Emit events for off-chain tracking.

Follow Sui’s sui::transfer policies for secure role management.

2
Comments
.
Bekky.
Bekky1762
Jul 30 2025, 13:14

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

  1. 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
    }
    
  2. Error Handling:

    const EACCESS_DENIED: u64 = 0;
    const EROLE_EXPIRED: u64 = 1;
    
  3. 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

MethodGas CostFlexibilityStorage Overhead
CapabilitiesLowHighMedium
Object IDsLowestLowNone
HybridMediumHighestHigh

For production use:

  • Start with capabilities for most cases
  • Switch to Object IDs for fixed admin roles
  • Use hybrid only for complex RBAC systems
1
Comments
.

Do you know the answer?

Please log in and share it.