Skip to content

RBAC allocation-free storage #21

@vobradovich

Description

@vobradovich

File Location(s)

No response

Proposal

RBAC allocation-free storage (proposal)

This proposal keeps the current behavior (named roles, role admins, membership lists, default admin as master key) but removes dynamic allocation by using fixed-size arrays.

Goals

  • Named roles with per-role admin role.
  • Role membership lists.
  • Deterministic, allocation-free storage.
  • Keep the current “super admin passes any check” behavior.

Core types

Role ID size is configurable at compile time:

pub type RoleId<const R: usize> = [u8; R];

pub const fn default_admin_role<const R: usize>() -> RoleId<R> {
    [0u8; R]
}

Storage layout (no allocation)

#[derive(Clone, Copy)]
pub struct RoleEntry<const R: usize, const M: usize> {
    pub role_id: RoleId<R>,
    pub admin_role_id: RoleId<R>,
    pub member_count: u32,
    pub members: [Option<ActorId>; M],
}

#[derive(Clone, Copy)]
pub struct AccessControlStorage<const R: usize, const N: usize, const M: usize> {
    /// Fixed capacity for roles.
    pub role_count: u32,
    pub roles: [Option<RoleEntry<R, M>>; N],
}

Invariants

  • role_count is the number of Some(RoleEntry) in roles.
  • Each RoleEntry has unique role_id.
  • member_count matches the number of Some(ActorId) in members.
  • The default admin role (default_admin_role::<R>()) exists and has at least one member (deployer) at initialization.
  • A caller with the default admin role satisfies any require_role.

Operations (behavior)

  • has_role(role_id, actor_id) checks membership or the default admin role for master-key semantics.
  • get_role_admin(role_id) returns the role’s admin_role_id if it exists, otherwise default_admin_role::<R>().
  • grant_role(role_id, actor_id) requires caller has get_role_admin(role_id). Adds role entry if missing.
  • revoke_role(role_id, actor_id) requires caller has get_role_admin(role_id).
  • renounce_role(role_id, actor_id) requires caller is actor_id.
  • set_role_admin(role_id, new_admin) requires caller has current admin of role_id.

Capacity handling

  • If roles is full when creating a new role, return Error::CapacityExceeded.
  • If members is full when granting to a new actor, return Error::CapacityExceeded.
  • Re-granting an existing membership returns false and does not change counts.

Helper functions (sketch)

fn find_role_index<const R: usize, const N: usize, const M: usize>(
    storage: &AccessControlStorage<R, N, M>,
    role_id: RoleId<R>,
) -> Option<usize>;

fn ensure_role_entry<const R: usize, const N: usize, const M: usize>(
    storage: &mut AccessControlStorage<R, N, M>,
    role_id: RoleId<R>,
) -> Result<usize, Error>;

fn insert_member<const M: usize>(
    members: &mut [Option<ActorId>; M],
    actor_id: ActorId,
) -> Result<bool, Error>;

fn remove_member<const M: usize>(
    members: &mut [Option<ActorId>; M],
    actor_id: ActorId,
) -> bool;

Notes

  • Using fixed arrays keeps deterministic iteration and removes BTreeMap/BTreeSet allocation.
  • Role and member enumeration can be built by iterating the arrays and skipping None slots.
  • Pagination can be implemented on top of enumeration by counting matches and slicing.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions