diff --git a/contracts/split/src/error.rs b/contracts/split/src/error.rs index 591239e..3556036 100644 --- a/contracts/split/src/error.rs +++ b/contracts/split/src/error.rs @@ -53,6 +53,8 @@ pub enum ContractError { MemoMismatch = 33, /// 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, /// Issue #482: Intermediate multiplication or division overflowed i128 bounds. ArithmeticOverflow = 33, /// Issue #483: A zero-value or negative amount was passed where a positive amount is required. diff --git a/contracts/split/src/events.rs b/contracts/split/src/events.rs index d1c0d1a..64cb19c 100644 --- a/contracts/split/src/events.rs +++ b/contracts/split/src/events.rs @@ -1090,6 +1090,33 @@ pub fn creator_cooldown_set( ); } +/// 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()), + ); +} /// Issue #474: Emitted when a creator cancels an open invoice and all contributors are refunded. /// Topics: (split, inv_cncl, invoice_id) /// Data: (creator, total_refunded, ledger) diff --git a/contracts/split/src/lib.rs b/contracts/split/src/lib.rs index eba46c7..e090e3f 100644 --- a/contracts/split/src/lib.rs +++ b/contracts/split/src/lib.rs @@ -72,6 +72,9 @@ use types::{ Recipient, RebateTier, RepScore, ResolveAction, ResolveRule, SimulateReleaseResult, SplitRule, SubscriptionParams, TimelockAction, Tranche, TreasuryRecord, UpgradeProposal, LegacyInvoice, OverflowBehavior, Payment, PaymentCertificate, PaymentCommitment, PaymentProof, + ProtocolFeeConfig, QueuedAction, Recipient, RebateTier, RepScore, ResolveAction, ResolveRule, + Role, SimulateReleaseResult, SplitRule, SubscriptionParams, TimelockAction, Tranche, + TreasuryRecord, UpgradeProposal, ProtocolFeeConfig, QueuedAction, Recipient, RecipientAddress, RebateTier, RepScore, ResolveAction, ResolveRule, SimulateReleaseResult, SplitRule, SubscriptionParams, TimelockAction, Tranche, TreasuryRecord, UpgradeProposal, @@ -211,6 +214,20 @@ 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, + } /// Contract-level funding progress checkpoints in basis points. fn funding_checkpoints_key() -> Symbol { symbol_short!("fnd_chk") @@ -2043,6 +2060,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 5dd4d64..d07ec3c 100644 --- a/contracts/split/src/storage_keys.rs +++ b/contracts/split/src/storage_keys.rs @@ -282,6 +282,15 @@ pub fn required_memo_hash_key(invoice_id: u64) -> (Symbol, u64) { (symbol_short! 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) +} // Issue #474: Invoice cancellation // --------------------------------------------------------------------------- diff --git a/contracts/split/src/types.rs b/contracts/split/src/types.rs index 26a66c4..7fc5c14 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 {