Skip to content

feat(contract): add caller-addressed RBAC grants to payment_router - #847

Merged
Abdulazeem-code merged 2 commits into
Abdulazeem-code:mainfrom
kimanicode:feature/contract-rbac
Oct 5, 2026
Merged

Abdulazeem-code merged 2 commits into
Abdulazeem-code:mainfrom
kimanicode:feature/contract-rbac

Conversation

@kimanicode

Copy link
Copy Markdown
Contributor

Refs #715

Summary

Extends the existing RBAC system in payment_router with an explicit
admin/actor argument, so role management is authorized against a
caller-supplied address instead of only the single designated signer the
contract resolves for itself.

Changes

New public API

  • grant_role(admin, grantee, role) — SuperAdmin-gated
  • revoke_role(admin, grantee, role) — SuperAdmin-gated
  • assign_role(account, role) — retained as a compatibility alias for
    existing callers; resolves the acting admin from contract state

New internals

  • require_role_of(env, caller, role) — checks a named address and
    returns Error::RoleNotFound before calling caller.require_auth()
  • has_role_internal — reports effective authority and mirrors the
    resolution order of require_role: per-address grant, then designated
    holder, then root-admin fallback. An integration that consults
    has_role before building a transaction cannot be told "yes" for an
    address the contract then refuses, or "no" for one it accepts.
  • apply_role_grant / apply_role_revoke — shared, idempotent paths
    emitting role_assigned / role_revoked

Bug fix

  • set_role_internal now clears a displaced holder's UserRole flag.
    Previously, reassigning a role left the previous holder reporting the
    role through has_role while being unable to exercise it.

Safety

  • Revoking the acting SuperAdmin's own root role returns
    Error::InvalidRole rather than bricking contract administration.

Also

  • revoke_role is an ABI break (grantee and role are now explicit);
    TypeScript bindings regenerated via npm run generate:bindings
  • scripts/generate-bindings.sh: resolve the wasm artifact from the
    workspace target dir, with a fallback for standalone checkouts
  • 32 Soroban test snapshots updated for the new role storage
  • payment_proxy/src/lib.rs: rustfmt only

Verification

Check Result
cargo build (payment_router) pass
cargo test (payment_router) 102 passed, 0 failed, 3 ignored (baseline 91/0/3)
cargo fmt --all -- --check clean
bindings generation regenerated with CI-pinned stellar 27.1.0

Please review — deviations and assumptions

These are deliberate, and worth a second opinion:

  1. The new helper is require_role_of, not require_role. The
    contract already had a private require_role(env, role) that resolves
    the single designated signer, used by ~15 call sites. The new
    caller-addressed helper was named alongside it rather than renaming the
    existing one. Happy to invert the names if you prefer the spec's naming.

  2. It returns Err, it does not panic. A caller lacking the role gets
    Error::RoleNotFound back instead of a trap.

  3. "Tests should panic on missing auth" is not testable in this SDK.
    An unmatched require_auth aborts the test host with
    thread caused non-unwinding panic. aborting. — it is not catchable via
    try_* or catch_unwind. The tests instead assert the required signer
    identity
    via env.auths(), and the role rejection via exact-args mock
    auth.

  4. One designated holder per role — a real behavioural limitation. The
    pre-existing model is DataKey::Role(Role) -> Address, and the
    pause/withdraw entrypoints take no caller argument, so enforcement has
    nothing to check but the designated address. Granting a role to a new
    address displaces the previous holder.
    The spec's "per-address role
    set" reads as additive multi-holder grants; real multi-holder
    semantics would need an actor: Address parameter on set_pause,
    set_min_limit and friends — a further ABI break. Not done here; happy
    to scope it if that is the intent.

  5. Fee governance precedence is unchanged (assumption). When a
    Governance address is configured it is the exclusive fee authority;
    otherwise FeeManager applies.

Known pre-existing failure

cargo clippy --all-targets --all-features -- -D warnings fails with 2
errors, both clippy::manual_let_else in the branch's uncommitted oracle
code that this PR does not touch:

payment_router/src/lib.rs:2596:9: error: this could be rewritten as `let...else`
payment_router/src/lib.rs:2609:9: error: this could be rewritten as `let...else`

manual_let_else postdates that code and rust-security.yml pins floating
dtolnay/rust-toolchain@stable, so CI will fail on this regardless of this
change. Left alone as out of scope — it is a two-line let...else rewrite
each if you want it fixed here.

Extend the existing RBAC system with an explicit admin/actor argument so
role management can be authorized against a caller-supplied address rather
than only the single designated signer the contract resolves for itself.

- grant_role(admin, grantee, role) and revoke_role(admin, grantee, role),
  both SuperAdmin-gated; assign_role is retained as a compatibility alias
- require_role_of(env, caller, role) checks a named address and returns
  Error::RoleNotFound before calling caller.require_auth()
- has_role reports effective authority and stays in step with require_role
  (per-address grant, then designated holder, then root admin fallback)
- set_role_internal clears a displaced holder's grant so a reassignment
  cannot leave has_role and require_role disagreeing
- grant/revoke are idempotent and emit role_assigned/role_revoked
- revoking the acting SuperAdmin's own root role is refused

Regenerate the TypeScript bindings and add 11 RBAC tests. Snapshots are
updated for the new role storage.

Verification: cargo build and cargo test (102 passed, 0 failed, 3 ignored)
in payment_router, and cargo fmt --all -- --check is clean.

Refs Abdulazeem-code#715
@drips-wave

drips-wave Bot commented Sep 29, 2026

Copy link
Copy Markdown

Hey @kimanicode! 👋 It looks like this PR isn't linked to any issue.

If this PR is for one of the issues assigned to you as part of a Wave, please link it to ensure your contribution is tracked properly. You can do this by adding a keyword to the PR description (e.g., Closes #123), or by clicking a button below:

Issue Title
#715 Smart Contracts: Implement Role-Based Access Control (RBAC) Link to this issue
#681 Backend: Implement WebSockets for Real-Time Payment Status Updates Link to this issue

ℹ️ Learn more about linking PRs to issues

@vercel

vercel Bot commented Sep 29, 2026

Copy link
Copy Markdown

@kimanicode is attempting to deploy a commit to the Abdulazeem's projects Team on Vercel.

A member of the Team first needs to authorize it.

@Abdulazeem-code

Copy link
Copy Markdown
Owner

Please resolve conflicts

1 similar comment
@Abdulazeem-code

Copy link
Copy Markdown
Owner

Please resolve conflicts

@Abdulazeem-code
Abdulazeem-code merged commit c75b6da into Abdulazeem-code:main Oct 5, 2026
13 of 19 checks passed

This branch was successfully deployed

1 active deployment
Preview — eceec635 Deployed Oct 5, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants