diff --git a/Cargo.toml b/Cargo.toml
index ce786ec..63a9357 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -86,10 +86,10 @@ members = [
"contracts/event_ticket",
"contracts/progressive_jackpot",
"contracts/rbac",
+ "contracts/access_control",
"contracts/proxy",
"contracts/dynamic_metadata",
"contracts/insurance_pool",
- "contracts/rbac",
"contracts/zk_proof",
"contracts/multisig_escrow",
"contracts/timelock_vault",
@@ -100,9 +100,8 @@ members = [
"contracts/royalty_distribution",
"contracts/query_engine",
+ "contracts/collateral_lending",
]
- "contracts/collateral_lending",
-]
[workspace.dependencies]
soroban-sdk = { version = "21.0.0" }
diff --git a/contracts/access_control/Cargo.toml b/contracts/access_control/Cargo.toml
new file mode 100644
index 0000000..23462db
--- /dev/null
+++ b/contracts/access_control/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "access_control"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+crate-type = ["cdylib"]
+
+[dependencies]
+soroban-sdk = { workspace = true }
+
+[dev-dependencies]
+soroban-sdk = { workspace = true, features = ["testutils"] }
+
+[features]
+testutils = ["soroban-sdk/testutils"]
diff --git a/contracts/access_control/src/lib.rs b/contracts/access_control/src/lib.rs
new file mode 100644
index 0000000..02b15d4
--- /dev/null
+++ b/contracts/access_control/src/lib.rs
@@ -0,0 +1,625 @@
+//! Advanced access control: roles with time-bound grants, permission checks,
+//! capability tokens, delegation (roles and capabilities), revocation,
+//! access logging, and a deny-overrides policy engine.
+#![no_std]
+
+mod storage;
+mod types;
+
+use storage::Storage;
+use types::{
+ AccessError, AccessLogEntry, Capability, DelegatedRole, Policy, PolicyEffect, PolicyRule,
+ RoleGrant,
+};
+use soroban_sdk::{contract, contractimpl, symbol_short, Address, Env, Symbol, Vec};
+
+#[contract]
+pub struct AccessControlContract;
+
+#[contractimpl]
+impl AccessControlContract {
+ // ---------------------------------------------------------------- admin
+
+ pub fn initialize(env: Env, admin: Address) -> Result<(), AccessError> {
+ if Storage::has_admin(&env) {
+ return Err(AccessError::AlreadyInitialized);
+ }
+ admin.require_auth();
+ Storage::set_admin(&env, &admin);
+ Storage::set_paused(&env, false);
+ env.events().publish((symbol_short!("init"),), (admin,));
+ Ok(())
+ }
+
+ pub fn pause(env: Env) -> Result<(), AccessError> {
+ let admin = require_admin(&env)?;
+ Storage::set_paused(&env, true);
+ env.events().publish((symbol_short!("pause"),), (admin,));
+ Ok(())
+ }
+
+ pub fn unpause(env: Env) -> Result<(), AccessError> {
+ let admin = require_admin(&env)?;
+ Storage::set_paused(&env, false);
+ env.events().publish((symbol_short!("unpause"),), (admin,));
+ Ok(())
+ }
+
+ pub fn transfer_admin(env: Env, new_admin: Address) -> Result<(), AccessError> {
+ let old_admin = require_admin(&env)?;
+ Storage::set_admin(&env, &new_admin);
+ env.events()
+ .publish((symbol_short!("xfer_adm"),), (old_admin, new_admin));
+ Ok(())
+ }
+
+ pub fn get_admin(env: Env) -> Result
{
+ Storage::get_admin(&env)
+ }
+
+ pub fn is_paused(env: Env) -> bool {
+ Storage::get_paused(&env)
+ }
+
+ // ---------------------------------------------------------------- roles
+
+ /// Grant `role` to `address` until `expires_at` (0 = no expiry).
+ /// Re-granting an existing role updates its expiry.
+ pub fn grant_role(
+ env: Env,
+ address: Address,
+ role: Symbol,
+ expires_at: u64,
+ ) -> Result<(), AccessError> {
+ let admin = require_admin(&env)?;
+ require_not_paused(&env)?;
+ require_future_or_zero(&env, expires_at)?;
+ let mut grants = Storage::get_role_grants(&env, &address);
+ if let Some(i) = grant_index(&grants, &role) {
+ grants.remove(i);
+ }
+ grants.push_back(RoleGrant {
+ role: role.clone(),
+ expires_at,
+ });
+ Storage::set_role_grants(&env, &address, &grants);
+ Storage::add_log(&env, &admin, symbol_short!("grant"), role.clone(), true);
+ env.events()
+ .publish((symbol_short!("grant"),), (admin, address, role, expires_at));
+ Ok(())
+ }
+
+ pub fn revoke_role(env: Env, address: Address, role: Symbol) -> Result<(), AccessError> {
+ let admin = require_admin(&env)?;
+ require_not_paused(&env)?;
+ let mut grants = Storage::get_role_grants(&env, &address);
+ let index = grant_index(&grants, &role).ok_or(AccessError::RoleNotGranted)?;
+ grants.remove(index);
+ Storage::set_role_grants(&env, &address, &grants);
+ Storage::add_log(&env, &admin, symbol_short!("revoke"), role.clone(), true);
+ env.events()
+ .publish((symbol_short!("revoke"),), (admin, address, role));
+ Ok(())
+ }
+
+ pub fn add_permission(env: Env, role: Symbol, permission: Symbol) -> Result<(), AccessError> {
+ let admin = require_admin(&env)?;
+ require_not_paused(&env)?;
+ let mut perms = Storage::get_role_perms(&env, &role);
+ if vec_contains(&perms, &permission) {
+ return Err(AccessError::PermissionAlreadyExists);
+ }
+ perms.push_back(permission.clone());
+ Storage::set_role_perms(&env, &role, &perms);
+ env.events()
+ .publish((symbol_short!("add_perm"),), (admin, role, permission));
+ Ok(())
+ }
+
+ pub fn remove_permission(
+ env: Env,
+ role: Symbol,
+ permission: Symbol,
+ ) -> Result<(), AccessError> {
+ let admin = require_admin(&env)?;
+ require_not_paused(&env)?;
+ let mut perms = Storage::get_role_perms(&env, &role);
+ let index = vec_find_index(&perms, &permission).ok_or(AccessError::PermissionNotFound)?;
+ perms.remove(index);
+ Storage::set_role_perms(&env, &role, &perms);
+ env.events()
+ .publish((symbol_short!("rm_perm"),), (admin, role, permission));
+ Ok(())
+ }
+
+ /// True if `address` holds `role` right now, directly or via a valid
+ /// delegation. Expired grants and delegations are ignored.
+ pub fn has_role(env: Env, address: Address, role: Symbol) -> bool {
+ vec_contains(&active_roles(&env, &address), &role)
+ }
+
+ /// True if any of the caller's active roles carries `permission`.
+ pub fn has_permission(env: Env, address: Address, permission: Symbol) -> bool {
+ for role in active_roles(&env, &address).iter() {
+ if vec_contains(&Storage::get_role_perms(&env, &role), &permission) {
+ return true;
+ }
+ }
+ false
+ }
+
+ pub fn get_role_grants(env: Env, address: Address) -> Vec {
+ Storage::get_role_grants(&env, &address)
+ }
+
+ pub fn get_role_permissions(env: Env, role: Symbol) -> Vec {
+ Storage::get_role_perms(&env, &role)
+ }
+
+ // ------------------------------------------------------ role delegation
+
+ /// Delegate a directly-held role to `delegate` until `expires_at`
+ /// (0 = no expiry, capped implicitly by the delegator's own grant).
+ pub fn delegate_role(
+ env: Env,
+ delegator: Address,
+ delegate: Address,
+ role: Symbol,
+ expires_at: u64,
+ ) -> Result<(), AccessError> {
+ delegator.require_auth();
+ require_not_paused(&env)?;
+ require_future_or_zero(&env, expires_at)?;
+ if delegator == delegate {
+ return Err(AccessError::SelfDelegation);
+ }
+ if !has_active_direct_grant(&env, &delegator, &role) {
+ return Err(AccessError::RoleNotGranted);
+ }
+ let mut delegations = Storage::get_delegated_roles(&env, &delegate);
+ for d in delegations.iter() {
+ if d.role == role && d.delegator == delegator {
+ return Err(AccessError::DelegationAlreadyExists);
+ }
+ }
+ delegations.push_back(DelegatedRole {
+ role: role.clone(),
+ delegator: delegator.clone(),
+ expires_at,
+ });
+ Storage::set_delegated_roles(&env, &delegate, &delegations);
+ Storage::add_log(&env, &delegator, symbol_short!("dlg_role"), role.clone(), true);
+ env.events()
+ .publish((symbol_short!("dlg_role"),), (delegator, delegate, role, expires_at));
+ Ok(())
+ }
+
+ /// Revoke a role delegation. Callable by the original delegator or admin.
+ pub fn revoke_role_delegation(
+ env: Env,
+ caller: Address,
+ delegate: Address,
+ role: Symbol,
+ delegator: Address,
+ ) -> Result<(), AccessError> {
+ caller.require_auth();
+ let admin = Storage::get_admin(&env)?;
+ if caller != delegator && caller != admin {
+ return Err(AccessError::NotAuthorized);
+ }
+ let mut delegations = Storage::get_delegated_roles(&env, &delegate);
+ let mut index: Option = None;
+ for (i, d) in delegations.iter().enumerate() {
+ if d.role == role && d.delegator == delegator {
+ index = Some(i as u32);
+ break;
+ }
+ }
+ let index = index.ok_or(AccessError::DelegationNotFound)?;
+ delegations.remove(index);
+ Storage::set_delegated_roles(&env, &delegate, &delegations);
+ Storage::add_log(&env, &caller, symbol_short!("rvk_dlg"), role.clone(), true);
+ env.events()
+ .publish((symbol_short!("rvk_dlg"),), (caller, delegate, role));
+ Ok(())
+ }
+
+ pub fn get_delegated_roles(env: Env, address: Address) -> Vec {
+ Storage::get_delegated_roles(&env, &address)
+ }
+
+ // --------------------------------------------------------- capabilities
+
+ /// Issue a capability token granting `holder` the right to perform
+ /// `action` on `resource`. `depth` bounds further delegation hops.
+ pub fn issue_capability(
+ env: Env,
+ holder: Address,
+ resource: Symbol,
+ action: Symbol,
+ expires_at: u64,
+ max_uses: u32,
+ delegatable: bool,
+ depth: u32,
+ ) -> Result {
+ let admin = require_admin(&env)?;
+ require_not_paused(&env)?;
+ require_future_or_zero(&env, expires_at)?;
+ let id = Storage::next_cap_id(&env);
+ let cap = Capability {
+ id,
+ holder: holder.clone(),
+ issuer: admin.clone(),
+ resource: resource.clone(),
+ action: action.clone(),
+ expires_at,
+ max_uses,
+ uses: 0,
+ delegatable,
+ depth,
+ parent: None,
+ revoked: false,
+ };
+ Storage::set_capability(&env, &cap);
+ let mut ids = Storage::get_holder_caps(&env, &holder);
+ ids.push_back(id);
+ Storage::set_holder_caps(&env, &holder, &ids);
+ Storage::add_log(&env, &admin, symbol_short!("cap_issue"), resource.clone(), true);
+ env.events()
+ .publish((symbol_short!("cap_issue"),), (id, holder, resource, action));
+ Ok(id)
+ }
+
+ /// Exercise a capability: validates it (including its delegation chain),
+ /// consumes one use, and records the access.
+ pub fn use_capability(env: Env, cap_id: u64) -> Result<(), AccessError> {
+ require_not_paused(&env)?;
+ let mut cap = Storage::get_capability(&env, cap_id)?;
+ cap.holder.require_auth();
+ let check = validate_capability(&env, &cap);
+ Storage::add_log(
+ &env,
+ &cap.holder,
+ cap.action.clone(),
+ cap.resource.clone(),
+ check.is_ok(),
+ );
+ check?;
+ cap.uses += 1;
+ Storage::set_capability(&env, &cap);
+ env.events()
+ .publish((symbol_short!("cap_use"),), (cap_id, cap.holder));
+ Ok(())
+ }
+
+ /// Delegate a capability to `to`, producing a child capability whose
+ /// lifetime is bounded by the parent's expiry and delegation depth.
+ pub fn delegate_capability(
+ env: Env,
+ cap_id: u64,
+ to: Address,
+ expires_at: u64,
+ ) -> Result {
+ require_not_paused(&env)?;
+ require_future_or_zero(&env, expires_at)?;
+ let parent = Storage::get_capability(&env, cap_id)?;
+ parent.holder.require_auth();
+ validate_capability(&env, &parent)?;
+ if !parent.delegatable {
+ return Err(AccessError::NotDelegatable);
+ }
+ if parent.depth == 0 {
+ return Err(AccessError::DelegationDepthExceeded);
+ }
+ if to == parent.holder {
+ return Err(AccessError::SelfDelegation);
+ }
+ // Child expiry may not outlive a bounded parent.
+ if parent.expires_at != 0 && (expires_at == 0 || expires_at > parent.expires_at) {
+ return Err(AccessError::InvalidExpiry);
+ }
+ let id = Storage::next_cap_id(&env);
+ let child = Capability {
+ id,
+ holder: to.clone(),
+ issuer: parent.holder.clone(),
+ resource: parent.resource.clone(),
+ action: parent.action.clone(),
+ expires_at,
+ max_uses: parent.max_uses,
+ uses: 0,
+ delegatable: parent.delegatable,
+ depth: parent.depth - 1,
+ parent: Some(cap_id),
+ revoked: false,
+ };
+ Storage::set_capability(&env, &child);
+ let mut ids = Storage::get_holder_caps(&env, &to);
+ ids.push_back(id);
+ Storage::set_holder_caps(&env, &to, &ids);
+ Storage::add_log(
+ &env,
+ &parent.holder,
+ symbol_short!("cap_dlg"),
+ parent.resource.clone(),
+ true,
+ );
+ env.events()
+ .publish((symbol_short!("cap_dlg"),), (cap_id, id, to));
+ Ok(id)
+ }
+
+ /// Revoke a capability. Callable by its issuer or the admin. Child
+ /// capabilities delegated from it become invalid transitively.
+ pub fn revoke_capability(env: Env, caller: Address, cap_id: u64) -> Result<(), AccessError> {
+ caller.require_auth();
+ let mut cap = Storage::get_capability(&env, cap_id)?;
+ let admin = Storage::get_admin(&env)?;
+ if caller != cap.issuer && caller != admin {
+ return Err(AccessError::NotAuthorized);
+ }
+ if cap.revoked {
+ return Err(AccessError::CapabilityRevoked);
+ }
+ cap.revoked = true;
+ Storage::set_capability(&env, &cap);
+ Storage::add_log(&env, &caller, symbol_short!("cap_rvk"), cap.resource.clone(), true);
+ env.events()
+ .publish((symbol_short!("cap_rvk"),), (caller, cap_id));
+ Ok(())
+ }
+
+ pub fn get_capability(env: Env, cap_id: u64) -> Result {
+ Storage::get_capability(&env, cap_id)
+ }
+
+ pub fn is_capability_valid(env: Env, cap_id: u64) -> bool {
+ match Storage::get_capability(&env, cap_id) {
+ Ok(cap) => validate_capability(&env, &cap).is_ok(),
+ Err(_) => false,
+ }
+ }
+
+ pub fn get_holder_capabilities(env: Env, holder: Address) -> Vec {
+ Storage::get_holder_caps(&env, &holder)
+ }
+
+ // -------------------------------------------------------- policy engine
+
+ /// Install (or replace) the policy for a (resource, action) pair.
+ pub fn set_policy(
+ env: Env,
+ resource: Symbol,
+ action: Symbol,
+ rules: Vec,
+ default_allow: bool,
+ ) -> Result<(), AccessError> {
+ let admin = require_admin(&env)?;
+ require_not_paused(&env)?;
+ let policy = Policy {
+ resource: resource.clone(),
+ action: action.clone(),
+ rules,
+ default_allow,
+ };
+ Storage::set_policy(&env, &policy);
+ env.events()
+ .publish((symbol_short!("set_pol"),), (admin, resource, action));
+ Ok(())
+ }
+
+ pub fn remove_policy(env: Env, resource: Symbol, action: Symbol) -> Result<(), AccessError> {
+ let admin = require_admin(&env)?;
+ require_not_paused(&env)?;
+ if Storage::get_policy(&env, &resource, &action).is_none() {
+ return Err(AccessError::PolicyNotFound);
+ }
+ Storage::remove_policy(&env, &resource, &action);
+ env.events()
+ .publish((symbol_short!("rm_pol"),), (admin, resource, action));
+ Ok(())
+ }
+
+ pub fn get_policy(env: Env, resource: Symbol, action: Symbol) -> Result {
+ Storage::get_policy(&env, &resource, &action).ok_or(AccessError::PolicyNotFound)
+ }
+
+ /// Full access decision for `address` on (resource, action), recorded in
+ /// the access log. Order: valid capability > policy > permission fallback.
+ pub fn check_access(env: Env, address: Address, resource: Symbol, action: Symbol) -> bool {
+ let allowed = decide_access(&env, &address, &resource, &action);
+ Storage::add_log(&env, &address, action, resource, allowed);
+ allowed
+ }
+
+ /// Same decision as `check_access` but read-only (no log entry).
+ pub fn can_access(env: Env, address: Address, resource: Symbol, action: Symbol) -> bool {
+ decide_access(&env, &address, &resource, &action)
+ }
+
+ // ----------------------------------------------------------------- logs
+
+ pub fn get_access_logs(env: Env, from: u32, max: u32) -> Vec {
+ let all = Storage::get_logs(&env);
+ let total = all.len();
+ let mut result = Vec::new(&env);
+ let start = from.min(total);
+ let end = (from + max).min(total);
+ let mut i = start;
+ while i < end {
+ if let Some(entry) = all.get(i) {
+ result.push_back(entry);
+ }
+ i += 1;
+ }
+ result
+ }
+
+ pub fn get_access_log_count(env: Env) -> u32 {
+ Storage::get_logs(&env).len()
+ }
+}
+
+// ------------------------------------------------------------------ helpers
+
+fn require_admin(env: &Env) -> Result {
+ let admin = Storage::get_admin(env)?;
+ admin.require_auth();
+ Ok(admin)
+}
+
+fn require_not_paused(env: &Env) -> Result<(), AccessError> {
+ if Storage::get_paused(env) {
+ return Err(AccessError::ContractPaused);
+ }
+ Ok(())
+}
+
+fn require_future_or_zero(env: &Env, expires_at: u64) -> Result<(), AccessError> {
+ if expires_at != 0 && expires_at <= env.ledger().timestamp() {
+ return Err(AccessError::InvalidExpiry);
+ }
+ Ok(())
+}
+
+fn is_active(expires_at: u64, now: u64) -> bool {
+ expires_at == 0 || now < expires_at
+}
+
+fn vec_contains(v: &Vec, item: &Symbol) -> bool {
+ for elem in v.iter() {
+ if elem == *item {
+ return true;
+ }
+ }
+ false
+}
+
+fn vec_find_index(v: &Vec, item: &Symbol) -> Option {
+ for (i, elem) in v.iter().enumerate() {
+ if elem == *item {
+ return Some(i as u32);
+ }
+ }
+ None
+}
+
+fn grant_index(grants: &Vec, role: &Symbol) -> Option {
+ for (i, g) in grants.iter().enumerate() {
+ if g.role == *role {
+ return Some(i as u32);
+ }
+ }
+ None
+}
+
+fn has_active_direct_grant(env: &Env, address: &Address, role: &Symbol) -> bool {
+ let now = env.ledger().timestamp();
+ for g in Storage::get_role_grants(env, address).iter() {
+ if g.role == *role && is_active(g.expires_at, now) {
+ return true;
+ }
+ }
+ false
+}
+
+/// All roles `address` holds right now: active direct grants plus delegations
+/// whose delegator still holds an active direct grant of the role.
+fn active_roles(env: &Env, address: &Address) -> Vec {
+ let now = env.ledger().timestamp();
+ let mut roles = Vec::new(env);
+ for g in Storage::get_role_grants(env, address).iter() {
+ if is_active(g.expires_at, now) && !vec_contains(&roles, &g.role) {
+ roles.push_back(g.role);
+ }
+ }
+ for d in Storage::get_delegated_roles(env, address).iter() {
+ if is_active(d.expires_at, now)
+ && has_active_direct_grant(env, &d.delegator, &d.role)
+ && !vec_contains(&roles, &d.role)
+ {
+ roles.push_back(d.role);
+ }
+ }
+ roles
+}
+
+/// Validate a capability and its whole delegation chain: not revoked, not
+/// expired, not exhausted, and every ancestor still alive.
+fn validate_capability(env: &Env, cap: &Capability) -> Result<(), AccessError> {
+ let now = env.ledger().timestamp();
+ if cap.revoked {
+ return Err(AccessError::CapabilityRevoked);
+ }
+ if !is_active(cap.expires_at, now) {
+ return Err(AccessError::CapabilityExpired);
+ }
+ if cap.max_uses != 0 && cap.uses >= cap.max_uses {
+ return Err(AccessError::CapabilityExhausted);
+ }
+ let mut parent_id = cap.parent;
+ while let Some(id) = parent_id {
+ let parent = Storage::get_capability(env, id)?;
+ if parent.revoked {
+ return Err(AccessError::CapabilityRevoked);
+ }
+ if !is_active(parent.expires_at, now) {
+ return Err(AccessError::CapabilityExpired);
+ }
+ parent_id = parent.parent;
+ }
+ Ok(())
+}
+
+fn holds_valid_capability(env: &Env, address: &Address, resource: &Symbol, action: &Symbol) -> bool {
+ for id in Storage::get_holder_caps(env, address).iter() {
+ if let Ok(cap) = Storage::get_capability(env, id) {
+ if cap.resource == *resource
+ && cap.action == *action
+ && validate_capability(env, &cap).is_ok()
+ {
+ return true;
+ }
+ }
+ }
+ false
+}
+
+fn decide_access(env: &Env, address: &Address, resource: &Symbol, action: &Symbol) -> bool {
+ if holds_valid_capability(env, address, resource, action) {
+ return true;
+ }
+ if let Some(policy) = Storage::get_policy(env, resource, action) {
+ return evaluate_policy(env, address, &policy);
+ }
+ // No policy installed: fall back to permission semantics, treating the
+ // action name as the required permission.
+ for role in active_roles(env, address).iter() {
+ if vec_contains(&Storage::get_role_perms(env, &role), action) {
+ return true;
+ }
+ }
+ false
+}
+
+/// Deny-overrides evaluation: any matching Deny rule wins, then any matching
+/// Allow rule, otherwise the policy default.
+fn evaluate_policy(env: &Env, address: &Address, policy: &Policy) -> bool {
+ let roles = active_roles(env, address);
+ let mut allowed = false;
+ for rule in policy.rules.iter() {
+ if vec_contains(&roles, &rule.role) {
+ match rule.effect {
+ PolicyEffect::Deny => return false,
+ PolicyEffect::Allow => allowed = true,
+ }
+ }
+ }
+ if allowed {
+ return true;
+ }
+ policy.default_allow
+}
+
+#[cfg(test)]
+mod test;
diff --git a/contracts/access_control/src/storage.rs b/contracts/access_control/src/storage.rs
new file mode 100644
index 0000000..ccd9b5c
--- /dev/null
+++ b/contracts/access_control/src/storage.rs
@@ -0,0 +1,168 @@
+use crate::types::{
+ AccessError, AccessLogEntry, Capability, DelegatedRole, Policy, RoleGrant,
+};
+use soroban_sdk::{contracttype, Address, Env, Symbol, Vec};
+
+/// Maximum number of retained access log entries; oldest entries are dropped.
+const MAX_LOGS: u32 = 500;
+
+#[contracttype]
+#[derive(Clone)]
+pub enum DataKey {
+ Admin,
+ Paused,
+ NextCapId,
+ AccessLogs,
+ RoleGrants(Address),
+ RolePerms(Symbol),
+ Capability(u64),
+ HolderCaps(Address),
+ DelegatedRoles(Address),
+ Policy(Symbol, Symbol),
+}
+
+pub struct Storage;
+
+impl Storage {
+ pub fn has_admin(env: &Env) -> bool {
+ env.storage().instance().has(&DataKey::Admin)
+ }
+
+ pub fn set_admin(env: &Env, admin: &Address) {
+ env.storage().instance().set(&DataKey::Admin, admin);
+ }
+
+ pub fn get_admin(env: &Env) -> Result {
+ env.storage()
+ .instance()
+ .get(&DataKey::Admin)
+ .ok_or(AccessError::NotInitialized)
+ }
+
+ pub fn set_paused(env: &Env, paused: bool) {
+ env.storage().instance().set(&DataKey::Paused, &paused);
+ }
+
+ pub fn get_paused(env: &Env) -> bool {
+ env.storage()
+ .instance()
+ .get(&DataKey::Paused)
+ .unwrap_or(false)
+ }
+
+ pub fn get_role_grants(env: &Env, address: &Address) -> Vec {
+ env.storage()
+ .persistent()
+ .get(&DataKey::RoleGrants(address.clone()))
+ .unwrap_or_else(|| Vec::new(env))
+ }
+
+ pub fn set_role_grants(env: &Env, address: &Address, grants: &Vec) {
+ env.storage()
+ .persistent()
+ .set(&DataKey::RoleGrants(address.clone()), grants);
+ }
+
+ pub fn get_role_perms(env: &Env, role: &Symbol) -> Vec {
+ env.storage()
+ .persistent()
+ .get(&DataKey::RolePerms(role.clone()))
+ .unwrap_or_else(|| Vec::new(env))
+ }
+
+ pub fn set_role_perms(env: &Env, role: &Symbol, perms: &Vec) {
+ env.storage()
+ .persistent()
+ .set(&DataKey::RolePerms(role.clone()), perms);
+ }
+
+ pub fn next_cap_id(env: &Env) -> u64 {
+ let id: u64 = env
+ .storage()
+ .instance()
+ .get(&DataKey::NextCapId)
+ .unwrap_or(1);
+ env.storage().instance().set(&DataKey::NextCapId, &(id + 1));
+ id
+ }
+
+ pub fn get_capability(env: &Env, id: u64) -> Result {
+ env.storage()
+ .persistent()
+ .get(&DataKey::Capability(id))
+ .ok_or(AccessError::CapabilityNotFound)
+ }
+
+ pub fn set_capability(env: &Env, cap: &Capability) {
+ env.storage()
+ .persistent()
+ .set(&DataKey::Capability(cap.id), cap);
+ }
+
+ pub fn get_holder_caps(env: &Env, holder: &Address) -> Vec {
+ env.storage()
+ .persistent()
+ .get(&DataKey::HolderCaps(holder.clone()))
+ .unwrap_or_else(|| Vec::new(env))
+ }
+
+ pub fn set_holder_caps(env: &Env, holder: &Address, ids: &Vec) {
+ env.storage()
+ .persistent()
+ .set(&DataKey::HolderCaps(holder.clone()), ids);
+ }
+
+ pub fn get_delegated_roles(env: &Env, address: &Address) -> Vec {
+ env.storage()
+ .persistent()
+ .get(&DataKey::DelegatedRoles(address.clone()))
+ .unwrap_or_else(|| Vec::new(env))
+ }
+
+ pub fn set_delegated_roles(env: &Env, address: &Address, delegations: &Vec) {
+ env.storage()
+ .persistent()
+ .set(&DataKey::DelegatedRoles(address.clone()), delegations);
+ }
+
+ pub fn get_policy(env: &Env, resource: &Symbol, action: &Symbol) -> Option {
+ env.storage()
+ .persistent()
+ .get(&DataKey::Policy(resource.clone(), action.clone()))
+ }
+
+ pub fn set_policy(env: &Env, policy: &Policy) {
+ env.storage().persistent().set(
+ &DataKey::Policy(policy.resource.clone(), policy.action.clone()),
+ policy,
+ );
+ }
+
+ pub fn remove_policy(env: &Env, resource: &Symbol, action: &Symbol) {
+ env.storage()
+ .persistent()
+ .remove(&DataKey::Policy(resource.clone(), action.clone()));
+ }
+
+ pub fn get_logs(env: &Env) -> Vec {
+ env.storage()
+ .persistent()
+ .get(&DataKey::AccessLogs)
+ .unwrap_or_else(|| Vec::new(env))
+ }
+
+ pub fn add_log(env: &Env, actor: &Address, action: Symbol, resource: Symbol, allowed: bool) {
+ let mut logs = Self::get_logs(env);
+ logs.push_back(AccessLogEntry {
+ timestamp: env.ledger().timestamp(),
+ actor: actor.clone(),
+ action,
+ resource,
+ allowed,
+ });
+ while logs.len() > MAX_LOGS {
+ logs.remove(0);
+ }
+ env.storage().persistent().set(&DataKey::AccessLogs, &logs);
+ }
+}
diff --git a/contracts/access_control/src/test.rs b/contracts/access_control/src/test.rs
new file mode 100644
index 0000000..e99265c
--- /dev/null
+++ b/contracts/access_control/src/test.rs
@@ -0,0 +1,617 @@
+#![cfg(test)]
+
+use super::*;
+use soroban_sdk::{
+ symbol_short,
+ testutils::{Address as _, Ledger},
+ vec, Address, Env, Symbol,
+};
+use types::{PolicyEffect, PolicyRule};
+
+fn setup() -> (Env, AccessControlContractClient<'static>, Address) {
+ let env = Env::default();
+ env.mock_all_auths();
+ env.ledger().with_mut(|li| li.timestamp = 1000);
+ let contract_id = env.register_contract(None, AccessControlContract);
+ let client = AccessControlContractClient::new(&env, &contract_id);
+ let admin = Address::generate(&env);
+ client.initialize(&admin);
+ (env, client, admin)
+}
+
+fn set_time(env: &Env, ts: u64) {
+ env.ledger().with_mut(|li| li.timestamp = ts);
+}
+
+// ------------------------------------------------------------ initialization
+
+#[test]
+fn test_initialize_and_admin() {
+ let (_env, client, admin) = setup();
+ assert_eq!(client.get_admin(), admin);
+ assert!(!client.is_paused());
+}
+
+#[test]
+fn test_double_initialize_fails() {
+ let (env, client, _admin) = setup();
+ let other = Address::generate(&env);
+ let result = client.try_initialize(&other);
+ assert_eq!(result, Err(Ok(AccessError::AlreadyInitialized)));
+}
+
+#[test]
+fn test_transfer_admin() {
+ let (env, client, _admin) = setup();
+ let new_admin = Address::generate(&env);
+ client.transfer_admin(&new_admin);
+ assert_eq!(client.get_admin(), new_admin);
+}
+
+// -------------------------------------------------------------------- roles
+
+#[test]
+fn test_grant_and_check_role() {
+ let (env, client, _admin) = setup();
+ let user = Address::generate(&env);
+ let role = symbol_short!("editor");
+ assert!(!client.has_role(&user, &role));
+ client.grant_role(&user, &role, &0);
+ assert!(client.has_role(&user, &role));
+}
+
+#[test]
+fn test_revoke_role() {
+ let (env, client, _admin) = setup();
+ let user = Address::generate(&env);
+ let role = symbol_short!("editor");
+ client.grant_role(&user, &role, &0);
+ client.revoke_role(&user, &role);
+ assert!(!client.has_role(&user, &role));
+}
+
+#[test]
+fn test_revoke_unassigned_role_fails() {
+ let (env, client, _admin) = setup();
+ let user = Address::generate(&env);
+ let result = client.try_revoke_role(&user, &symbol_short!("editor"));
+ assert_eq!(result, Err(Ok(AccessError::RoleNotGranted)));
+}
+
+#[test]
+fn test_time_bound_role_expires() {
+ let (env, client, _admin) = setup();
+ let user = Address::generate(&env);
+ let role = symbol_short!("temp");
+ client.grant_role(&user, &role, &2000);
+ assert!(client.has_role(&user, &role));
+ set_time(&env, 2000);
+ assert!(!client.has_role(&user, &role));
+}
+
+#[test]
+fn test_grant_role_with_past_expiry_fails() {
+ let (env, client, _admin) = setup();
+ let user = Address::generate(&env);
+ let result = client.try_grant_role(&user, &symbol_short!("temp"), &500);
+ assert_eq!(result, Err(Ok(AccessError::InvalidExpiry)));
+}
+
+#[test]
+fn test_regrant_updates_expiry() {
+ let (env, client, _admin) = setup();
+ let user = Address::generate(&env);
+ let role = symbol_short!("temp");
+ client.grant_role(&user, &role, &2000);
+ client.grant_role(&user, &role, &5000);
+ set_time(&env, 3000);
+ assert!(client.has_role(&user, &role));
+ assert_eq!(client.get_role_grants(&user).len(), 1);
+}
+
+// -------------------------------------------------------------- permissions
+
+#[test]
+fn test_permission_checks() {
+ let (env, client, _admin) = setup();
+ let user = Address::generate(&env);
+ let role = symbol_short!("editor");
+ let perm = symbol_short!("write");
+ client.grant_role(&user, &role, &0);
+ assert!(!client.has_permission(&user, &perm));
+ client.add_permission(&role, &perm);
+ assert!(client.has_permission(&user, &perm));
+ client.remove_permission(&role, &perm);
+ assert!(!client.has_permission(&user, &perm));
+}
+
+#[test]
+fn test_duplicate_permission_fails() {
+ let (_env, client, _admin) = setup();
+ let role = symbol_short!("editor");
+ let perm = symbol_short!("write");
+ client.add_permission(&role, &perm);
+ let result = client.try_add_permission(&role, &perm);
+ assert_eq!(result, Err(Ok(AccessError::PermissionAlreadyExists)));
+}
+
+#[test]
+fn test_permission_lost_when_role_expires() {
+ let (env, client, _admin) = setup();
+ let user = Address::generate(&env);
+ let role = symbol_short!("editor");
+ let perm = symbol_short!("write");
+ client.grant_role(&user, &role, &2000);
+ client.add_permission(&role, &perm);
+ assert!(client.has_permission(&user, &perm));
+ set_time(&env, 2500);
+ assert!(!client.has_permission(&user, &perm));
+}
+
+// ---------------------------------------------------------- role delegation
+
+#[test]
+fn test_delegate_role() {
+ let (env, client, _admin) = setup();
+ let delegator = Address::generate(&env);
+ let delegate = Address::generate(&env);
+ let role = symbol_short!("editor");
+ client.grant_role(&delegator, &role, &0);
+ client.delegate_role(&delegator, &delegate, &role, &0);
+ assert!(client.has_role(&delegate, &role));
+}
+
+#[test]
+fn test_delegate_role_without_grant_fails() {
+ let (env, client, _admin) = setup();
+ let delegator = Address::generate(&env);
+ let delegate = Address::generate(&env);
+ let result = client.try_delegate_role(&delegator, &delegate, &symbol_short!("editor"), &0);
+ assert_eq!(result, Err(Ok(AccessError::RoleNotGranted)));
+}
+
+#[test]
+fn test_self_delegation_fails() {
+ let (env, client, _admin) = setup();
+ let delegator = Address::generate(&env);
+ let role = symbol_short!("editor");
+ client.grant_role(&delegator, &role, &0);
+ let result = client.try_delegate_role(&delegator, &delegator, &role, &0);
+ assert_eq!(result, Err(Ok(AccessError::SelfDelegation)));
+}
+
+#[test]
+fn test_duplicate_delegation_fails() {
+ let (env, client, _admin) = setup();
+ let delegator = Address::generate(&env);
+ let delegate = Address::generate(&env);
+ let role = symbol_short!("editor");
+ client.grant_role(&delegator, &role, &0);
+ client.delegate_role(&delegator, &delegate, &role, &0);
+ let result = client.try_delegate_role(&delegator, &delegate, &role, &0);
+ assert_eq!(result, Err(Ok(AccessError::DelegationAlreadyExists)));
+}
+
+#[test]
+fn test_delegation_expires() {
+ let (env, client, _admin) = setup();
+ let delegator = Address::generate(&env);
+ let delegate = Address::generate(&env);
+ let role = symbol_short!("editor");
+ client.grant_role(&delegator, &role, &0);
+ client.delegate_role(&delegator, &delegate, &role, &2000);
+ assert!(client.has_role(&delegate, &role));
+ set_time(&env, 2000);
+ assert!(!client.has_role(&delegate, &role));
+ // Delegator keeps the role.
+ assert!(client.has_role(&delegator, &role));
+}
+
+#[test]
+fn test_delegation_dies_with_delegator_grant() {
+ let (env, client, _admin) = setup();
+ let delegator = Address::generate(&env);
+ let delegate = Address::generate(&env);
+ let role = symbol_short!("editor");
+ client.grant_role(&delegator, &role, &0);
+ client.delegate_role(&delegator, &delegate, &role, &0);
+ client.revoke_role(&delegator, &role);
+ assert!(!client.has_role(&delegate, &role));
+}
+
+#[test]
+fn test_revoke_role_delegation() {
+ let (env, client, _admin) = setup();
+ let delegator = Address::generate(&env);
+ let delegate = Address::generate(&env);
+ let role = symbol_short!("editor");
+ client.grant_role(&delegator, &role, &0);
+ client.delegate_role(&delegator, &delegate, &role, &0);
+ client.revoke_role_delegation(&delegator, &delegate, &role, &delegator);
+ assert!(!client.has_role(&delegate, &role));
+}
+
+#[test]
+fn test_revoke_delegation_by_stranger_fails() {
+ let (env, client, _admin) = setup();
+ let delegator = Address::generate(&env);
+ let delegate = Address::generate(&env);
+ let stranger = Address::generate(&env);
+ let role = symbol_short!("editor");
+ client.grant_role(&delegator, &role, &0);
+ client.delegate_role(&delegator, &delegate, &role, &0);
+ let result = client.try_revoke_role_delegation(&stranger, &delegate, &role, &delegator);
+ assert_eq!(result, Err(Ok(AccessError::NotAuthorized)));
+}
+
+// --------------------------------------------------------------- capability
+
+#[test]
+fn test_issue_and_use_capability() {
+ let (env, client, _admin) = setup();
+ let holder = Address::generate(&env);
+ let id = client.issue_capability(
+ &holder,
+ &symbol_short!("vault"),
+ &symbol_short!("open"),
+ &0,
+ &0,
+ &false,
+ &0,
+ );
+ assert!(client.is_capability_valid(&id));
+ client.use_capability(&id);
+ assert_eq!(client.get_capability(&id).uses, 1);
+}
+
+#[test]
+fn test_capability_max_uses_exhausted() {
+ let (env, client, _admin) = setup();
+ let holder = Address::generate(&env);
+ let id = client.issue_capability(
+ &holder,
+ &symbol_short!("vault"),
+ &symbol_short!("open"),
+ &0,
+ &2,
+ &false,
+ &0,
+ );
+ client.use_capability(&id);
+ client.use_capability(&id);
+ let result = client.try_use_capability(&id);
+ assert_eq!(result, Err(Ok(AccessError::CapabilityExhausted)));
+ assert!(!client.is_capability_valid(&id));
+}
+
+#[test]
+fn test_capability_expires() {
+ let (env, client, _admin) = setup();
+ let holder = Address::generate(&env);
+ let id = client.issue_capability(
+ &holder,
+ &symbol_short!("vault"),
+ &symbol_short!("open"),
+ &2000,
+ &0,
+ &false,
+ &0,
+ );
+ assert!(client.is_capability_valid(&id));
+ set_time(&env, 2000);
+ assert!(!client.is_capability_valid(&id));
+ let result = client.try_use_capability(&id);
+ assert_eq!(result, Err(Ok(AccessError::CapabilityExpired)));
+}
+
+#[test]
+fn test_revoke_capability() {
+ let (env, client, admin) = setup();
+ let holder = Address::generate(&env);
+ let id = client.issue_capability(
+ &holder,
+ &symbol_short!("vault"),
+ &symbol_short!("open"),
+ &0,
+ &0,
+ &false,
+ &0,
+ );
+ client.revoke_capability(&admin, &id);
+ assert!(!client.is_capability_valid(&id));
+ let result = client.try_use_capability(&id);
+ assert_eq!(result, Err(Ok(AccessError::CapabilityRevoked)));
+}
+
+#[test]
+fn test_revoke_capability_by_stranger_fails() {
+ let (env, client, _admin) = setup();
+ let holder = Address::generate(&env);
+ let stranger = Address::generate(&env);
+ let id = client.issue_capability(
+ &holder,
+ &symbol_short!("vault"),
+ &symbol_short!("open"),
+ &0,
+ &0,
+ &false,
+ &0,
+ );
+ let result = client.try_revoke_capability(&stranger, &id);
+ assert_eq!(result, Err(Ok(AccessError::NotAuthorized)));
+}
+
+// ---------------------------------------------------- capability delegation
+
+#[test]
+fn test_delegate_capability() {
+ let (env, client, _admin) = setup();
+ let holder = Address::generate(&env);
+ let delegate = Address::generate(&env);
+ let id = client.issue_capability(
+ &holder,
+ &symbol_short!("vault"),
+ &symbol_short!("open"),
+ &0,
+ &0,
+ &true,
+ &2,
+ );
+ let child_id = client.delegate_capability(&id, &delegate, &0);
+ assert!(client.is_capability_valid(&child_id));
+ let child = client.get_capability(&child_id);
+ assert_eq!(child.holder, delegate);
+ assert_eq!(child.depth, 1);
+ assert_eq!(child.parent, Some(id));
+ client.use_capability(&child_id);
+}
+
+#[test]
+fn test_non_delegatable_capability_fails() {
+ let (env, client, _admin) = setup();
+ let holder = Address::generate(&env);
+ let delegate = Address::generate(&env);
+ let id = client.issue_capability(
+ &holder,
+ &symbol_short!("vault"),
+ &symbol_short!("open"),
+ &0,
+ &0,
+ &false,
+ &2,
+ );
+ let result = client.try_delegate_capability(&id, &delegate, &0);
+ assert_eq!(result, Err(Ok(AccessError::NotDelegatable)));
+}
+
+#[test]
+fn test_delegation_depth_enforced() {
+ let (env, client, _admin) = setup();
+ let holder = Address::generate(&env);
+ let second = Address::generate(&env);
+ let third = Address::generate(&env);
+ let id = client.issue_capability(
+ &holder,
+ &symbol_short!("vault"),
+ &symbol_short!("open"),
+ &0,
+ &0,
+ &true,
+ &1,
+ );
+ let child_id = client.delegate_capability(&id, &second, &0);
+ let result = client.try_delegate_capability(&child_id, &third, &0);
+ assert_eq!(result, Err(Ok(AccessError::DelegationDepthExceeded)));
+}
+
+#[test]
+fn test_child_expiry_bounded_by_parent() {
+ let (env, client, _admin) = setup();
+ let holder = Address::generate(&env);
+ let delegate = Address::generate(&env);
+ let id = client.issue_capability(
+ &holder,
+ &symbol_short!("vault"),
+ &symbol_short!("open"),
+ &3000,
+ &0,
+ &true,
+ &1,
+ );
+ // Child may not outlive the parent (unbounded or later expiry).
+ let result = client.try_delegate_capability(&id, &delegate, &0);
+ assert_eq!(result, Err(Ok(AccessError::InvalidExpiry)));
+ let result = client.try_delegate_capability(&id, &delegate, &4000);
+ assert_eq!(result, Err(Ok(AccessError::InvalidExpiry)));
+ let child_id = client.delegate_capability(&id, &delegate, &2500);
+ assert!(client.is_capability_valid(&child_id));
+}
+
+#[test]
+fn test_parent_revocation_cascades() {
+ let (env, client, admin) = setup();
+ let holder = Address::generate(&env);
+ let second = Address::generate(&env);
+ let third = Address::generate(&env);
+ let id = client.issue_capability(
+ &holder,
+ &symbol_short!("vault"),
+ &symbol_short!("open"),
+ &0,
+ &0,
+ &true,
+ &2,
+ );
+ let child_id = client.delegate_capability(&id, &second, &0);
+ let grandchild_id = client.delegate_capability(&child_id, &third, &0);
+ client.revoke_capability(&admin, &id);
+ assert!(!client.is_capability_valid(&child_id));
+ assert!(!client.is_capability_valid(&grandchild_id));
+ let result = client.try_use_capability(&grandchild_id);
+ assert_eq!(result, Err(Ok(AccessError::CapabilityRevoked)));
+}
+
+// ------------------------------------------------------------ policy engine
+
+#[test]
+fn test_policy_allow_rule() {
+ let (env, client, _admin) = setup();
+ let user = Address::generate(&env);
+ let role = symbol_short!("editor");
+ let resource = symbol_short!("doc");
+ let action = symbol_short!("write");
+ client.grant_role(&user, &role, &0);
+ let rules = vec![
+ &env,
+ PolicyRule {
+ effect: PolicyEffect::Allow,
+ role: role.clone(),
+ },
+ ];
+ client.set_policy(&resource, &action, &rules, &false);
+ assert!(client.check_access(&user, &resource, &action));
+ let outsider = Address::generate(&env);
+ assert!(!client.check_access(&outsider, &resource, &action));
+}
+
+#[test]
+fn test_policy_deny_overrides_allow() {
+ let (env, client, _admin) = setup();
+ let user = Address::generate(&env);
+ let editor = symbol_short!("editor");
+ let banned = symbol_short!("banned");
+ let resource = symbol_short!("doc");
+ let action = symbol_short!("write");
+ client.grant_role(&user, &editor, &0);
+ client.grant_role(&user, &banned, &0);
+ let rules = vec![
+ &env,
+ PolicyRule {
+ effect: PolicyEffect::Allow,
+ role: editor.clone(),
+ },
+ PolicyRule {
+ effect: PolicyEffect::Deny,
+ role: banned.clone(),
+ },
+ ];
+ client.set_policy(&resource, &action, &rules, &false);
+ assert!(!client.check_access(&user, &resource, &action));
+}
+
+#[test]
+fn test_policy_default_allow() {
+ let (env, client, _admin) = setup();
+ let user = Address::generate(&env);
+ let resource = symbol_short!("park");
+ let action = symbol_short!("enter");
+ let rules = vec![
+ &env,
+ PolicyRule {
+ effect: PolicyEffect::Deny,
+ role: symbol_short!("banned"),
+ },
+ ];
+ client.set_policy(&resource, &action, &rules, &true);
+ assert!(client.check_access(&user, &resource, &action));
+ client.grant_role(&user, &symbol_short!("banned"), &0);
+ assert!(!client.check_access(&user, &resource, &action));
+}
+
+#[test]
+fn test_remove_policy() {
+ let (env, client, _admin) = setup();
+ let resource = symbol_short!("doc");
+ let action = symbol_short!("write");
+ client.set_policy(&resource, &action, &vec![&env], &true);
+ client.remove_policy(&resource, &action);
+ let result = client.try_get_policy(&resource, &action);
+ assert_eq!(result, Err(Ok(AccessError::PolicyNotFound)));
+}
+
+#[test]
+fn test_check_access_permission_fallback() {
+ let (env, client, _admin) = setup();
+ let user = Address::generate(&env);
+ let role = symbol_short!("editor");
+ let action = symbol_short!("write");
+ client.grant_role(&user, &role, &0);
+ client.add_permission(&role, &action);
+ // No policy for (doc, write): falls back to permission check.
+ assert!(client.check_access(&user, &symbol_short!("doc"), &action));
+ assert!(!client.check_access(&user, &symbol_short!("doc"), &symbol_short!("delete")));
+}
+
+#[test]
+fn test_check_access_via_capability() {
+ let (env, client, _admin) = setup();
+ let holder = Address::generate(&env);
+ let resource = symbol_short!("vault");
+ let action = symbol_short!("open");
+ assert!(!client.check_access(&holder, &resource, &action));
+ client.issue_capability(&holder, &resource, &action, &0, &0, &false, &0);
+ assert!(client.check_access(&holder, &resource, &action));
+}
+
+// -------------------------------------------------------------- access logs
+
+#[test]
+fn test_access_logs_recorded() {
+ let (env, client, _admin) = setup();
+ let user = Address::generate(&env);
+ let resource = symbol_short!("doc");
+ let action = symbol_short!("write");
+ let before = client.get_access_log_count();
+ client.check_access(&user, &resource, &action);
+ let logs = client.get_access_logs(&before, &10);
+ assert_eq!(logs.len(), 1);
+ let entry = logs.get(0).unwrap();
+ assert_eq!(entry.actor, user);
+ assert_eq!(entry.resource, resource);
+ assert_eq!(entry.action, action);
+ assert!(!entry.allowed);
+}
+
+#[test]
+fn test_can_access_does_not_log() {
+ let (env, client, _admin) = setup();
+ let user = Address::generate(&env);
+ let before = client.get_access_log_count();
+ client.can_access(&user, &symbol_short!("doc"), &symbol_short!("write"));
+ assert_eq!(client.get_access_log_count(), before);
+}
+
+#[test]
+fn test_capability_use_logged() {
+ let (env, client, _admin) = setup();
+ let holder = Address::generate(&env);
+ let id = client.issue_capability(
+ &holder,
+ &symbol_short!("vault"),
+ &symbol_short!("open"),
+ &0,
+ &0,
+ &false,
+ &0,
+ );
+ let before = client.get_access_log_count();
+ client.use_capability(&id);
+ let logs = client.get_access_logs(&before, &10);
+ assert_eq!(logs.len(), 1);
+ assert!(logs.get(0).unwrap().allowed);
+}
+
+// -------------------------------------------------------------------- pause
+
+#[test]
+fn test_pause_blocks_mutations() {
+ let (env, client, _admin) = setup();
+ let user = Address::generate(&env);
+ client.pause();
+ assert!(client.is_paused());
+ let result = client.try_grant_role(&user, &symbol_short!("editor"), &0);
+ assert_eq!(result, Err(Ok(AccessError::ContractPaused)));
+ client.unpause();
+ client.grant_role(&user, &symbol_short!("editor"), &0);
+ assert!(client.has_role(&user, &symbol_short!("editor")));
+}
diff --git a/contracts/access_control/src/types.rs b/contracts/access_control/src/types.rs
new file mode 100644
index 0000000..ad2131a
--- /dev/null
+++ b/contracts/access_control/src/types.rs
@@ -0,0 +1,100 @@
+use soroban_sdk::{contracterror, contracttype, Address, Symbol, Vec};
+
+#[contracterror]
+#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
+#[repr(u32)]
+pub enum AccessError {
+ AlreadyInitialized = 1,
+ NotInitialized = 2,
+ NotAuthorized = 3,
+ ContractPaused = 4,
+ RoleNotGranted = 5,
+ PermissionAlreadyExists = 6,
+ PermissionNotFound = 7,
+ CapabilityNotFound = 8,
+ CapabilityRevoked = 9,
+ CapabilityExpired = 10,
+ CapabilityExhausted = 11,
+ NotDelegatable = 12,
+ DelegationDepthExceeded = 13,
+ InvalidExpiry = 14,
+ DelegationNotFound = 15,
+ DelegationAlreadyExists = 16,
+ PolicyNotFound = 17,
+ SelfDelegation = 18,
+}
+
+/// A role granted to an address. `expires_at == 0` means the grant never expires.
+#[contracttype]
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct RoleGrant {
+ pub role: Symbol,
+ pub expires_at: u64,
+}
+
+/// A role delegated from `delegator` to another address. The delegation is only
+/// valid while the delegator still holds an active direct grant of the role.
+#[contracttype]
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct DelegatedRole {
+ pub role: Symbol,
+ pub delegator: Address,
+ pub expires_at: u64,
+}
+
+/// A capability token: unforgeable, transferable-by-delegation proof that the
+/// holder may perform `action` on `resource`.
+/// `expires_at == 0` means no expiry; `max_uses == 0` means unlimited uses.
+#[contracttype]
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct Capability {
+ pub id: u64,
+ pub holder: Address,
+ pub issuer: Address,
+ pub resource: Symbol,
+ pub action: Symbol,
+ pub expires_at: u64,
+ pub max_uses: u32,
+ pub uses: u32,
+ pub delegatable: bool,
+ pub depth: u32,
+ pub parent: Option,
+ pub revoked: bool,
+}
+
+#[contracttype]
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum PolicyEffect {
+ Allow,
+ Deny,
+}
+
+/// A single policy rule: applies to holders of `role` with the given effect.
+#[contracttype]
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct PolicyRule {
+ pub effect: PolicyEffect,
+ pub role: Symbol,
+}
+
+/// Policy for a (resource, action) pair. Deny rules override allow rules;
+/// if no rule matches the caller's roles, `default_allow` decides.
+#[contracttype]
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct Policy {
+ pub resource: Symbol,
+ pub action: Symbol,
+ pub rules: Vec,
+ pub default_allow: bool,
+}
+
+/// A recorded access decision or administrative action.
+#[contracttype]
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct AccessLogEntry {
+ pub timestamp: u64,
+ pub actor: Address,
+ pub action: Symbol,
+ pub resource: Symbol,
+ pub allowed: bool,
+}