Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions contracts/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions contracts/access_control/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ crate-type = ["cdylib"]

[dependencies]
soroban-sdk = { workspace = true }
common_types = { workspace = true }

[dev-dependencies]
soroban-sdk = { workspace = true, features = ["testutils"] }
Expand Down
96 changes: 80 additions & 16 deletions contracts/access_control/src/access_control.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
// Allow deprecated events API until migration to #[contractevent] macro
#![allow(deprecated)]

use soroban_sdk::{contracttype, symbol_short, Address, Env, IntoVal, Symbol, Vec};
use soroban_sdk::{contracttype, symbol_short, Address, Env, IntoVal, String, Symbol, Vec};

use crate::errors::{AccessControlError, AccessControlResult};
use crate::types::{
AccessControlConfig, MembershipInfo, MultiSigConfig, PendingAdminTransfer, PendingProposal,
ProposalAction, SubscriptionTierLevel, UserRole, UserSubscriptionStatus,
ProposalAction, UserSubscriptionStatus,
};
use common_types::{BatchOperationStatus, BatchSetRoleResult, SetRoleRequest, TierLevel, UserRole};

/// Storage keys for the access control module
#[contracttype]
Expand Down Expand Up @@ -140,6 +141,71 @@ impl AccessControlModule {
Ok(())
}

pub fn batch_set_roles(
env: &Env,
caller: Address,
roles: Vec<SetRoleRequest>,
) -> AccessControlResult<Vec<BatchSetRoleResult>> {
// Enforce batch size limit
if roles.len() > 25 {
return Err(AccessControlError::InvalidBatchSize);
}

Self::require_initialized(env)?;
Self::require_not_paused(env)?;
Self::require_admin(env, &caller)?;

let mut results: Vec<BatchSetRoleResult> = Vec::new(env);

for req in roles.iter() {
if Self::is_blacklisted(env, &req.user) {
results.push_back(BatchSetRoleResult {
user: req.user.clone(),
status: BatchOperationStatus::Failed,
error: String::from_str(env, "User is blacklisted"),
});
continue;
}

match Self::validate_role_assignment(env, &req.user, &req.role) {
Ok(_) => {
let old_role = Self::get_role(env, req.user.clone());
env.storage()
.persistent()
.set(&DataKey::UserRole(req.user.clone()), &req.role);

env.events().publish(
(
symbol_short!("role_set"),
req.user.clone(),
req.role.clone(),
),
(caller.clone(), old_role),
);

results.push_back(BatchSetRoleResult {
user: req.user,
status: BatchOperationStatus::Success,
error: String::from_str(env, ""),
});
}
Err(_) => {
results.push_back(BatchSetRoleResult {
user: req.user,
status: BatchOperationStatus::Failed,
error: String::from_str(env, "Validation failed"),
});
}
}
}

// Emit batch summary event
env.events()
.publish((symbol_short!("batch_rol"), roles.len()), results.len());

Ok(results)
}

/// Get role for a user
pub fn get_role(env: &Env, user: Address) -> UserRole {
env.storage()
Expand Down Expand Up @@ -761,13 +827,11 @@ impl AccessControlModule {
// Tier-Based Access Control Functions
// ============================================================================

/// Sets the subscription tier level for a user. Admin only.
/// This is used for caching tier info to avoid cross-contract calls.
pub fn set_user_tier(
env: &Env,
caller: Address,
user: Address,
tier_level: SubscriptionTierLevel,
tier_level: TierLevel,
) -> AccessControlResult<()> {
Self::require_admin(env, &caller)?;

Expand All @@ -785,19 +849,19 @@ impl AccessControlModule {
}

/// Gets the subscription tier level for a user.
pub fn get_user_tier(env: &Env, user: Address) -> SubscriptionTierLevel {
pub fn get_user_tier(env: &Env, user: Address) -> TierLevel {
env.storage()
.persistent()
.get(&DataKey::UserTierLevel(user))
.unwrap_or(SubscriptionTierLevel::Free)
.unwrap_or(TierLevel::Free)
}

/// Sets the required tier for a specific role. Admin only.
pub fn set_required_tier_for_role(
env: &Env,
caller: Address,
role: UserRole,
required_tier: SubscriptionTierLevel,
required_tier: TierLevel,
) -> AccessControlResult<()> {
Self::require_admin(env, &caller)?;

Expand All @@ -818,18 +882,18 @@ impl AccessControlModule {
}

/// Gets the required tier for a specific role.
pub fn get_required_tier_for_role(env: &Env, role: UserRole) -> SubscriptionTierLevel {
pub fn get_required_tier_for_role(env: &Env, role: UserRole) -> TierLevel {
env.storage()
.persistent()
.get(&DataKey::RequiredTierForRole(role))
.unwrap_or(SubscriptionTierLevel::Free)
.unwrap_or(TierLevel::Free)
}

/// Checks if a user has the required tier level.
pub fn check_tier_access(
env: &Env,
user: Address,
required_tier: SubscriptionTierLevel,
required_tier: TierLevel,
) -> AccessControlResult<bool> {
Self::require_initialized(env)?;
Self::require_not_paused(env)?;
Expand All @@ -839,7 +903,7 @@ impl AccessControlModule {
}

let user_tier = Self::get_user_tier(env, user.clone());
let has_access = user_tier.has_tier_access(&required_tier);
let has_access = user_tier >= required_tier;

env.events().publish(
(
Expand All @@ -857,7 +921,7 @@ impl AccessControlModule {
pub fn require_tier_access(
env: &Env,
user: Address,
required_tier: SubscriptionTierLevel,
required_tier: TierLevel,
) -> AccessControlResult<()> {
if !Self::check_tier_access(env, user, required_tier)? {
return Err(AccessControlError::InsufficientRole);
Expand All @@ -871,7 +935,7 @@ impl AccessControlModule {
env: &Env,
user: Address,
required_role: UserRole,
required_tier: SubscriptionTierLevel,
required_tier: TierLevel,
) -> AccessControlResult<bool> {
let has_role_access = Self::check_access(env, user.clone(), required_role)?;
if !has_role_access {
Expand All @@ -887,7 +951,7 @@ impl AccessControlModule {
env: &Env,
user: Address,
required_role: UserRole,
required_tier: SubscriptionTierLevel,
required_tier: TierLevel,
) -> AccessControlResult<()> {
if !Self::check_role_and_tier_access(env, user, required_role, required_tier)? {
return Err(AccessControlError::InsufficientRole);
Expand All @@ -910,7 +974,7 @@ impl AccessControlModule {
let required_tier = Self::get_required_tier_for_role(env, role);
let user_tier = Self::get_user_tier(env, user);

Ok(user_tier.has_tier_access(&required_tier))
Ok(user_tier >= required_tier)
}

/// Gets the full subscription status for a user.
Expand Down
115 changes: 114 additions & 1 deletion contracts/access_control/src/access_control_tests.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::access_control::AccessControlModule;
use crate::errors::AccessControlError;
use crate::types::{AccessControlConfig, ProposalAction, UserRole};
use crate::{AccessControlConfig, BatchOperationStatus, ProposalAction, SetRoleRequest, UserRole};
use soroban_sdk::{
testutils::{Address as _, Events},
Address, Env, Vec,
Expand Down Expand Up @@ -756,3 +756,116 @@ fn test_proposal_events_emitted() {
// Verify role was set after approval
assert_eq!(client.get_role(&user), UserRole::Member);
}

#[test]
fn test_batch_set_roles_success() {
let (env, contract_id, admin, _, _) = setup_initialized_env();

let user1 = Address::generate(&env);
let user2 = Address::generate(&env);
let user3 = Address::generate(&env);

let mut requests: Vec<SetRoleRequest> = Vec::new(&env);

requests.push_back(SetRoleRequest {
user: user1.clone(),
role: UserRole::Member,
});
requests.push_back(SetRoleRequest {
user: user2.clone(),
role: UserRole::Staff,
});
requests.push_back(SetRoleRequest {
user: user3.clone(),
role: UserRole::Visitor,
});

let results = env.as_contract(&contract_id, || {
AccessControlModule::batch_set_roles(&env, admin.clone(), requests).unwrap()
});

assert_eq!(results.len(), 3);

for result in results.iter() {
assert_eq!(result.status, BatchOperationStatus::Success);
}

// Verify roles were set
env.as_contract(&contract_id, || {
assert_eq!(AccessControlModule::get_role(&env, user1), UserRole::Member);
assert_eq!(AccessControlModule::get_role(&env, user2), UserRole::Staff);
assert_eq!(
AccessControlModule::get_role(&env, user3),
UserRole::Visitor
);
});
}

#[test]
fn test_batch_set_roles_partial_failure() {
let (env, contract_id, admin, user1, user2) = setup_initialized_env();

env.as_contract(&contract_id, || {
// Blacklist user2
AccessControlModule::blacklist_user(&env, admin.clone(), user2.clone()).unwrap();

// Ensure user1 has no role initially
assert_eq!(
AccessControlModule::get_role(&env, user1.clone()),
UserRole::Guest
);

let mut requests: Vec<SetRoleRequest> = Vec::new(&env);

// Valid request for user1
requests.push_back(SetRoleRequest {
user: user1.clone(),
role: UserRole::Member,
});

// Invalid request for blacklisted user2
requests.push_back(SetRoleRequest {
user: user2.clone(),
role: UserRole::Member,
});

let results = AccessControlModule::batch_set_roles(&env, admin.clone(), requests).unwrap();

assert_eq!(results.len(), 2);

let res1 = results.get(0).unwrap();
let res2 = results.get(1).unwrap();

assert_eq!(res1.user, user1);
assert_eq!(res1.status, BatchOperationStatus::Success);

assert_eq!(res2.user, user2);
assert_eq!(res2.status, BatchOperationStatus::Failed);
assert!(res2.error.len() > 0);

// Verify user1 role set, user2 role unchanged
assert_eq!(AccessControlModule::get_role(&env, user1), UserRole::Member);
assert_eq!(AccessControlModule::get_role(&env, user2), UserRole::Guest);
});
}

#[test]
fn test_batch_set_roles_limit_exceeded() {
let (env, contract_id, admin, _, _) = setup_initialized_env();

env.as_contract(&contract_id, || {
let mut requests: Vec<SetRoleRequest> = Vec::new(&env);
let user = Address::generate(&env);

// Create 26 requests (limit is 25)
for _ in 0..26 {
requests.push_back(SetRoleRequest {
user: user.clone(),
role: UserRole::Member,
});
}

let result = AccessControlModule::batch_set_roles(&env, admin.clone(), requests);
assert_eq!(result.unwrap_err(), AccessControlError::InvalidBatchSize);
});
}
5 changes: 5 additions & 0 deletions contracts/access_control/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ pub enum AccessControlError {
MaxRolesExceeded = 114,
/// Contract is paused
ContractPaused = 115,
/// Invalid batch size
InvalidBatchSize = 116,
}

impl AccessControlError {
Expand All @@ -62,7 +64,9 @@ impl AccessControlError {
AccessControlError::InvalidAddress => "Invalid address provided",
AccessControlError::RoleHierarchyViolation => "Role hierarchy violation",
AccessControlError::MaxRolesExceeded => "Maximum roles per user exceeded",

AccessControlError::ContractPaused => "Contract is currently paused",
AccessControlError::InvalidBatchSize => "Invalid batch size (max 25)",
}
}

Expand Down Expand Up @@ -132,5 +136,6 @@ mod tests {
assert_eq!(AccessControlError::Unauthorized as u32, 100);
assert_eq!(AccessControlError::AdminRequired as u32, 101);
assert_eq!(AccessControlError::ContractPaused as u32, 115);
assert_eq!(AccessControlError::InvalidBatchSize as u32, 116);
}
}
15 changes: 14 additions & 1 deletion contracts/access_control/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,13 @@ pub mod types;
mod access_control_tests;

pub use access_control::AccessControlModule;
pub use common_types::{
BatchOperationStatus, BatchSetRoleResult, SetRoleRequest, TierLevel, UserRole,
};
pub use errors::{AccessControlError, AccessControlResult};
pub use types::{AccessControlConfig, MembershipInfo, MultiSigConfig, ProposalAction, UserRole};
pub use types::{
AccessControlConfig, MembershipInfo, MultiSigConfig, ProposalAction, UserSubscriptionStatus,
};

#[contract]
pub struct AccessControl;
Expand All @@ -26,6 +31,14 @@ impl AccessControl {
AccessControlModule::set_role(&env, admin, user, role).unwrap()
}

pub fn batch_set_roles(
env: Env,
admin: Address,
roles: Vec<SetRoleRequest>,
) -> Vec<BatchSetRoleResult> {
AccessControlModule::batch_set_roles(&env, admin, roles).unwrap()
}

pub fn get_role(env: Env, user: Address) -> UserRole {
AccessControlModule::get_role(&env, user)
}
Expand Down
Loading
Loading