9. Role-Based Access Control

Learn how to implement a flexible permission system using roles and bit flags.

Source: instructions/rbac.rs


RBAC Overview

Role-Based Access Control (RBAC) allows you to:

  • Assign roles to users (Admin, Moderator, User)
  • Grant granular permissions (manage tokens, pause program, etc.)
  • Update permissions dynamically
  • Enforce access control in instructions

Role State Structure

Source: state/role.rs

#[account]
pub struct Role {
    pub authority: Pubkey,       // Who has this role
    pub role_type: RoleType,     // Admin, Moderator, User
    pub permissions: u8,         // Bitmask for permissions
    pub assigned_by: Pubkey,     // Who assigned this role
    pub assigned_at: i64,        // When assigned
    pub updated_at: i64,         // Last update
    pub bump: u8,                // PDA bump
}

impl Role {
    pub const LEN: usize = 8 + 32 + 1 + 1 + 32 + 8 + 8 + 1;

    // Check if role has a specific permission
    pub fn has_permission(&self, permission: u8) -> bool {
        (self.permissions & permission) != 0
    }

    // Add permission using bitwise OR
    pub fn add_permission(&mut self, permission: u8) {
        self.permissions |= permission;
    }

    // Remove permission using bitwise AND with NOT
    pub fn remove_permission(&mut self, permission: u8) {
        self.permissions &= !permission;
    }
}

Role Types and Default Permissions

#[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy, PartialEq, Eq)]
pub enum RoleType {
    Admin,
    Moderator,
    User,
}

impl RoleType {
    pub fn default_permissions(&self) -> u8 {
        match self {
            RoleType::Admin => 0xFF,     // All permissions (11111111)
            RoleType::Moderator => 0x06, // Limited permissions (00000110)
            RoleType::User => 0x00,      // No special permissions (00000000)
        }
    }
}

Permission Flags

pub mod permissions {
    pub const MANAGE_CONFIG: u8 = 1 << 0;      // 0b00000001
    pub const MANAGE_USERS: u8 = 1 << 1;       // 0b00000010
    pub const MANAGE_TOKENS: u8 = 1 << 2;      // 0b00000100
    pub const PAUSE_PROGRAM: u8 = 1 << 3;      // 0b00001000
    pub const EMERGENCY_ACTIONS: u8 = 1 << 4;  // 0b00010000
    pub const MANAGE_TREASURY: u8 = 1 << 5;    // 0b00100000
    pub const MANAGE_ROLES: u8 = 1 << 6;       // 0b01000000
    pub const BATCH_OPERATIONS: u8 = 1 << 7;   // 0b10000000
}

Why Bitmasks?

Space efficient - 8 permissions in 1 byte
Fast operations - Bitwise AND/OR
Composable - Combine permissions with |
Easy to check - Single bitwise operation


Assign Role

#[derive(Accounts)]
pub struct AssignRole<'info> {
    #[account(
        init,
        payer = admin,
        space = 8 + Role::LEN,
        seeds = [SEED_ROLE, target_authority.key().as_ref()],
        bump
    )]
    pub role: Account<'info, Role>,

    #[account(
        mut,
        seeds = [SEED_PROGRAM_CONFIG],
        bump = program_config.bump,
    )]
    pub program_config: Account<'info, ProgramConfig>,

    #[account(
        mut,
        constraint = admin.key() == program_config.admin @ ErrorCode::Unauthorized
    )]
    pub admin: Signer<'info>,

    /// CHECK: The user receiving the role
    pub target_authority: AccountInfo<'info>,

    pub system_program: Program<'info, System>,
}

pub fn assign_role_handler(ctx: Context<AssignRole>, role_type: RoleType) -> Result<()> {
    let role = &mut ctx.accounts.role;
    let clock = Clock::get()?;

    role.authority = ctx.accounts.target_authority.key();
    role.role_type = role_type;
    role.permissions = role_type.default_permissions();
    role.assigned_by = ctx.accounts.admin.key();
    role.assigned_at = clock.unix_timestamp;
    role.updated_at = clock.unix_timestamp;
    role.bump = ctx.bumps.role;

    emit!(RoleAssignedEvent {
        authority: role.authority,
        role_type,
        assigned_by: role.assigned_by,
        timestamp: clock.unix_timestamp,
    });

    Ok(())
}

Update Permissions

#[derive(Accounts)]
pub struct UpdateRole<'info> {
    #[account(
        mut,
        seeds = [SEED_ROLE, role.authority.as_ref()],
        bump = role.bump
    )]
    pub role: Account<'info, Role>,

    #[account(
        seeds = [SEED_PROGRAM_CONFIG],
        bump = program_config.bump,
    )]
    pub program_config: Account<'info, ProgramConfig>,

    #[account(
        constraint = admin.key() == program_config.admin @ ErrorCode::Unauthorized
    )]
    pub admin: Signer<'info>,
}

pub fn update_role_permissions_handler(
    ctx: Context<UpdateRole>,
    add_permissions: u8,
    remove_permissions: u8,
) -> Result<()> {
    let role = &mut ctx.accounts.role;
    let clock = Clock::get()?;

    if add_permissions > 0 {
        role.add_permission(add_permissions);
    }

    if remove_permissions > 0 {
        role.remove_permission(remove_permissions);
    }

    role.updated_at = clock.unix_timestamp;

    msg!("Updated permissions for {}", role.authority);
    Ok(())
}

Revoke Role

#[derive(Accounts)]
pub struct RevokeRole<'info> {
    #[account(
        mut,
        close = admin,  // Close and reclaim rent
        seeds = [SEED_ROLE, role.authority.as_ref()],
        bump = role.bump
    )]
    pub role: Account<'info, Role>,

    #[account(
        seeds = [SEED_PROGRAM_CONFIG],
        bump = program_config.bump,
    )]
    pub program_config: Account<'info, ProgramConfig>,

    #[account(
        mut,
        constraint = admin.key() == program_config.admin @ ErrorCode::Unauthorized
    )]
    pub admin: Signer<'info>,
}

pub fn revoke_role_handler(ctx: Context<RevokeRole>) -> Result<()> {
    msg!("Revoked role for {}", ctx.accounts.role.authority);
    Ok(())
}

Check Permission

#[derive(Accounts)]
pub struct CheckPermission<'info> {
    #[account(
        seeds = [SEED_ROLE, authority.key().as_ref()],
        bump = role.bump
    )]
    pub role: Account<'info, Role>,

    pub authority: Signer<'info>,
}

pub fn check_permission_handler(
    ctx: Context<CheckPermission>,
    required_permission: u8,
) -> Result<bool> {
    let role = &ctx.accounts.role;

    require!(
        role.authority == ctx.accounts.authority.key(),
        ErrorCode::Unauthorized
    );

    Ok(role.has_permission(required_permission))
}

Enforce Permissions in Instructions

Pattern 1: Constraint-based

#[derive(Accounts)]
pub struct ManageTokens<'info> {
    #[account(
        seeds = [SEED_ROLE, authority.key().as_ref()],
        bump = role.bump,
        constraint = role.has_permission(permissions::MANAGE_TOKENS) 
            @ ErrorCode::InsufficientPermissions
    )]
    pub role: Account<'info, Role>,

    pub authority: Signer<'info>,

    // ... other accounts
}

Pattern 2: In Handler

pub fn pause_program_handler(ctx: Context<PauseProgram>) -> Result<()> {
    // Check permission
    require!(
        ctx.accounts.role.has_permission(permissions::PAUSE_PROGRAM),
        ErrorCode::InsufficientPermissions
    );

    // Proceed with pause logic
    ctx.accounts.config.paused = true;
    
    Ok(())
}

Client-Side Usage (TypeScript)

Assign Admin Role

const [rolePda] = PublicKey.findProgramAddressSync(
  [Buffer.from("role"), user.publicKey.toBuffer()],
  program.programId
);

await program.methods
  .assignRole({ admin: {} })  // RoleType::Admin
  .accounts({
    role: rolePda,
    programConfig: configPda,
    admin: admin.publicKey,
    targetAuthority: user.publicKey,
    systemProgram: SystemProgram.programId,
  })
  .signers([admin])
  .rpc();

Update Permissions

// Permission flags
const MANAGE_TOKENS = 1 << 2;   // 0b00000100
const MANAGE_USERS = 1 << 1;    // 0b00000010
const PAUSE_PROGRAM = 1 << 3;   // 0b00001000

// Add multiple permissions
const addPerms = MANAGE_TOKENS | MANAGE_USERS;  // 0b00000110

// Remove permission
const removePerms = PAUSE_PROGRAM;

await program.methods
  .updateRolePermissions(addPerms, removePerms)
  .accounts({
    role: rolePda,
    programConfig: configPda,
    admin: admin.publicKey,
  })
  .signers([admin])
  .rpc();

Check Permission

const [rolePda] = PublicKey.findProgramAddressSync(
  [Buffer.from("role"), user.publicKey.toBuffer()],
  program.programId
);

const MANAGE_TREASURY = 1 << 5;

const hasPermission = await program.methods
  .checkPermission(MANAGE_TREASURY)
  .accounts({
    role: rolePda,
    authority: user.publicKey,
  })
  .view();

console.log("Can manage treasury:", hasPermission);

Permission Combinations

// Check single permission
const canManageTokens = (role.permissions & MANAGE_TOKENS) !== 0;

// Check multiple permissions (has ANY)
const canManageUsersOrTokens = 
  (role.permissions & (MANAGE_USERS | MANAGE_TOKENS)) !== 0;

// Check multiple permissions (has ALL)
const requiredPerms = MANAGE_USERS | MANAGE_TOKENS;
const hasAllPerms = (role.permissions & requiredPerms) === requiredPerms;

// Grant multiple permissions
role.permissions |= (MANAGE_USERS | MANAGE_TOKENS);

// Revoke multiple permissions
role.permissions &= ~(MANAGE_USERS | MANAGE_TOKENS);

Advanced: Hierarchical Roles

impl RoleType {
    pub fn can_assign(&self, target: RoleType) -> bool {
        match self {
            RoleType::Admin => true,  // Admin can assign any role
            RoleType::Moderator => {
                matches!(target, RoleType::User)  // Moderator can only assign User
            }
            RoleType::User => false,  // Users can't assign roles
        }
    }
}

pub fn assign_role_with_hierarchy_handler(
    ctx: Context<AssignRole>,
    role_type: RoleType
) -> Result<()> {
    let assigner_role = &ctx.accounts.assigner_role;
    
    require!(
        assigner_role.role_type.can_assign(role_type),
        ErrorCode::InsufficientPermissions
    );

    // ... assign role
    Ok(())
}

Testing RBAC

describe("RBAC Tests", () => {
  it("Admin should assign moderator role", async () => {
    await program.methods
      .assignRole({ moderator: {} })
      .accounts({
        role: moderatorRolePda,
        programConfig: configPda,
        admin: admin.publicKey,
        targetAuthority: moderator.publicKey,
        systemProgram: SystemProgram.programId,
      })
      .signers([admin])
      .rpc();

    const role = await program.account.role.fetch(moderatorRolePda);
    expect(role.roleType).to.deep.equal({ moderator: {} });
  });

  it("Should fail without permission", async () => {
    try {
      await program.methods
        .pauseProgram()
        .accounts({
          role: userRolePda,
          authority: user.publicKey,
          config: configPda,
        })
        .signers([user])
        .rpc();

      expect.fail("Should have thrown InsufficientPermissions");
    } catch (error) {
      expect(error.error.errorCode.code).to.equal("InsufficientPermissions");
    }
  });
});

Best Practices

Use bitmasks for permissions - Efficient and composable
Emit events on role changes - Auditability
Validate role hierarchy - Prevent privilege escalation
Store assignment metadata - Track who assigned when
Use constraints - Check permissions early

Don’t skip permission checks - Always validate
Don’t use strings for roles - Use enums
Don’t hardcode admin - Use role system


Next: Treasury Management