From 819fb98847e64fae0a61244fbaeec8d66f242c2d Mon Sep 17 00:00:00 2001 From: Umar faruk Date: Mon, 27 Jul 2026 19:20:18 +0000 Subject: [PATCH] feat: add RBAC Role enum, storage key, error variant, events, and helpers --- contracts/split/src/error.rs | 2 ++ contracts/split/src/events.rs | 28 +++++++++++++++ contracts/split/src/lib.rs | 53 +++++++++++++++++++++++++++-- contracts/split/src/storage_keys.rs | 11 ++++++ contracts/split/src/types.rs | 14 ++++++++ 5 files changed, 106 insertions(+), 2 deletions(-) diff --git a/contracts/split/src/error.rs b/contracts/split/src/error.rs index b66d59e..a804740 100644 --- a/contracts/split/src/error.rs +++ b/contracts/split/src/error.rs @@ -53,4 +53,6 @@ pub enum ContractError { MemoMismatch = 31, /// Issue #439: Creator is in cooldown after cancelling an invoice. CreatorCooldownActive = 31, + /// RBAC: Caller does not hold the required role for this entry point. + RoleNotHeld = 33, } diff --git a/contracts/split/src/events.rs b/contracts/split/src/events.rs index 025cdcd..e1aed1a 100644 --- a/contracts/split/src/events.rs +++ b/contracts/split/src/events.rs @@ -1034,3 +1034,31 @@ pub fn creator_cooldown_set(env: &Env, creator: &Address, until_ledger: u64, coo (until_ledger, cooldown_ledgers), ); } + +/// RBAC: Emitted when an admin grants a role to an address. +/// Topics: (split, role_grt, grantee) +/// Data: (role_discriminant, admin) +pub fn role_granted(env: &Env, grantee: &Address, role_discriminant: u32, admin: &Address) { + env.events().publish( + ( + symbol_short!("split"), + symbol_short!("role_grt"), + grantee.clone(), + ), + (role_discriminant, admin.clone()), + ); +} + +/// RBAC: Emitted when an admin revokes a role from an address. +/// Topics: (split, role_rev, grantee) +/// Data: (role_discriminant, admin) +pub fn role_revoked(env: &Env, grantee: &Address, role_discriminant: u32, admin: &Address) { + env.events().publish( + ( + symbol_short!("split"), + symbol_short!("role_rev"), + grantee.clone(), + ), + (role_discriminant, admin.clone()), + ); +} diff --git a/contracts/split/src/lib.rs b/contracts/split/src/lib.rs index be40701..17e03ed 100644 --- a/contracts/split/src/lib.rs +++ b/contracts/split/src/lib.rs @@ -61,8 +61,8 @@ use types::{ InvoiceHot, InvoiceOptions, InvoiceOptions2, InvoicePayment, InvoiceStatus, InvoiceTemplate, LegacyInvoice, OverflowBehavior, Payment, PaymentCertificate, PaymentCommitment, PaymentProof, ProtocolFeeConfig, QueuedAction, Recipient, RebateTier, RepScore, ResolveAction, ResolveRule, - SimulateReleaseResult, SplitRule, SubscriptionParams, TimelockAction, Tranche, TreasuryRecord, - UpgradeProposal, + Role, SimulateReleaseResult, SplitRule, SubscriptionParams, TimelockAction, Tranche, + TreasuryRecord, UpgradeProposal, }; // --------------------------------------------------------------------------- @@ -199,6 +199,22 @@ fn milestone_flags_key(id: u64) -> (Symbol, u64) { (symbol_short!("ms_flgs"), id) } +/// RBAC: per-(address, role) assignment flag — persistent storage. +/// Stored as `bool`; absent key means role is not held. +fn role_key(address: &Address, role_discriminant: u32) -> (Symbol, Address, u32) { + (symbol_short!("role_asn"), address.clone(), role_discriminant) +} + +/// Convert a `Role` to its stable u32 discriminant used as the storage key component. +fn role_discriminant(role: &Role) -> u32 { + match role { + Role::Admin => 0, + Role::Creator => 1, + Role::Operator => 2, + Role::Auditor => 3, + } +} + /// Cliff + vesting schedule: bitmask (u32) of tranche indices already released /// via `release_tranche()` — bit N set means `tranches[N]` has been paid out. /// Supports up to 32 tranches per invoice (matches `paid_flags_key` convention). @@ -1826,6 +1842,39 @@ fn require_not_frozen(env: &Env) { assert!(!is_frozen, "contract is frozen for upgrade"); } +// --------------------------------------------------------------------------- +// RBAC helpers +// --------------------------------------------------------------------------- + +/// Return `true` when `address` holds `role` **or** holds `Role::Admin`. +/// Admin is a super-role that implies all other roles. +fn has_role(env: &Env, address: &Address, role: &Role) -> bool { + // Admin implies every role + let admin_disc = role_discriminant(&Role::Admin); + let role_disc = role_discriminant(role); + env.storage() + .persistent() + .get::<_, bool>(&role_key(address, admin_disc)) + .unwrap_or(false) + || env.storage() + .persistent() + .get::<_, bool>(&role_key(address, role_disc)) + .unwrap_or(false) +} + +/// Require that `caller` holds at least one of the supplied roles. +/// Also requires `caller.require_auth()` so the call is signed. +/// Panics with "RoleNotHeld" when no role matches. +fn require_role(env: &Env, caller: &Address, roles: &[Role]) { + caller.require_auth(); + for role in roles { + if has_role(env, caller, role) { + return; + } + } + panic!("RoleNotHeld"); +} + // --------------------------------------------------------------------------- // Issue #431: Duplicate payment detection // --------------------------------------------------------------------------- diff --git a/contracts/split/src/storage_keys.rs b/contracts/split/src/storage_keys.rs index 6baa1b4..75d1f43 100644 --- a/contracts/split/src/storage_keys.rs +++ b/contracts/split/src/storage_keys.rs @@ -280,3 +280,14 @@ pub fn upgrade_checkpoint_key() -> Symbol { symbol_short!("upg_ckpt") } pub fn required_memo_hash_key(invoice_id: u64) -> (Symbol, u64) { (symbol_short!("req_memo"), invoice_id) } /// Issue #452: per-invoice tags — persistent storage. pub fn invoice_tags_key(invoice_id: u64) -> (Symbol, u64) { (symbol_short!("inv_tags"), invoice_id) } + +// --------------------------------------------------------------------------- +// RBAC: Role assignment storage +// --------------------------------------------------------------------------- + +/// Per-address per-role assignment flag — persistent storage. +/// Stored as a boolean `true`; absence means the role is not held. +/// Key: ("role_asn", address, role_u32) where role_u32 is the Role discriminant. +pub fn role_key(address: &Address, role_discriminant: u32) -> (Symbol, Address, u32) { + (symbol_short!("role_asn"), address.clone(), role_discriminant) +} diff --git a/contracts/split/src/types.rs b/contracts/split/src/types.rs index 7d7e183..a2ef9ed 100644 --- a/contracts/split/src/types.rs +++ b/contracts/split/src/types.rs @@ -142,6 +142,20 @@ pub enum AdminRole { Operator, } +/// Issue RBAC: Fine-grained role assigned to an address. +/// - Admin : may perform any action (equivalent to SuperAdmin for RBAC gates). +/// - Creator : may call `create_invoice`. +/// - Operator : may call `release` / `release_invoice`. +/// - Auditor : read-only; may call `get_invoice` and other query entry points. +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub enum Role { + Admin, + Creator, + Operator, + Auditor, +} + #[contracttype] #[derive(Clone, Debug)] pub struct Payment {