DEV Community

original-b
original-b

Posted on

Implementing Role-Based Access Control (RBAC) on Solana

Implementing Role-Based Access Control (RBAC) on Solana

Introduction

Role-Based Access Control (RBAC) is a security paradigm where system access is determined by a user's role. In traditional Web2 applications, RBAC is often handled via centralized databases, session tokens (JWTs), and middleware checks on a backend server.

When mapping this to Web3—specifically Solana—the architecture shifts entirely. Instead of a database row checking is_admin = true, Solana uses Program Derived Addresses (PDAs). A PDA can securely store state (like a user's assigned role) and cryptographically prove that a specific authority (the user's wallet) is allowed to perform an action. This eliminates the need for a centralized server to manage permissions.

By assigning roles to PDAs, we create an on-chain, decentralized permissions model where users must sign transactions to prove their identity, and the smart contract inherently enforces the rules.

Prerequisites

  • Rust and Anchor basics
  • Solana local environment

Architecture

In this system, we implement three primary roles: Admin, Editor, and User. The core of the architecture relies on Solana PDAs.

A PDA is derived using seeds (e.g., a hardcoded string like b"user_role" and the user's public key) and the program ID. The state stored in this PDA defines what permissions the user holds.

Example Anchor State for a Role

Here is how we might define a UserRole account in Rust using the Anchor framework:

use anchor_lang::prelude::*;

#[account]
pub struct UserRole {
    pub authority: Pubkey,
    pub role: u8, // 0 = User, 1 = Editor, 2 = Admin
    pub bump: u8,
}

impl UserRole {
    pub const SPACE: usize = 8 + 32 + 1 + 1;
}
Enter fullscreen mode Exit fullscreen mode

Deriving the PDA for Role Assignment

When a user is assigned a role, the program initializes the PDA. An Admin signs the transaction, and the program verifies that the signer has the authority to assign roles.

#[derive(Accounts)]
#[instruction(role: u8)]
pub struct AssignRole<'info> {
    #[account(
        init,
        payer = admin,
        space = UserRole::SPACE,
        seeds = [b"user_role", target_user.key().as_ref()],
        bump
    )]
    pub user_role: Account<'info, UserRole>,

    /// CHECK: The user receiving the role
    pub target_user: UncheckedAccount<'info>,

    #[account(
        mut,
        constraint = admin_role.role == 2 @ ErrorCode::UnauthorizedAdmin
    )]
    pub admin_role: Account<'info, UserRole>,

    #[account(mut)]
    pub admin: Signer<'info>,
    pub system_program: Program<'info, System>,
}
Enter fullscreen mode Exit fullscreen mode

In this snippet, we derive a unique user_role PDA for the target_user. Notice the constraint on admin_role: it strictly ensures that the account initiating this transaction has role == 2 (Admin).

Step-by-Step Implementation

  1. Defining the State
  2. Writing the Instructions
  3. Testing the System

Conclusion

  • Next Steps
  • Link to GitHub Repository

Top comments (0)