Skip to content

Commit b1e6822

Browse files
feat: implement administrative role management functionality (#275)
1 parent cf229a5 commit b1e6822

File tree

3 files changed

+334
-0
lines changed

3 files changed

+334
-0
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[package]
2+
name = "access-control"
3+
version = "0.0.0"
4+
edition = "2021"
5+
publish = false
6+
7+
[lib]
8+
crate-type = ["lib", "cdylib"]
9+
doctest = false
10+
11+
[dependencies]
12+
soroban-sdk = { workspace = true }
13+
14+
[dev-dependencies]
15+
soroban-sdk = { workspace = true, features = ["testutils"] }
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
#![no_std]
2+
use soroban_sdk::{contract, contractimpl, contracttype, contracterror, Address, Env};
3+
4+
#[contracterror]
5+
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
6+
#[repr(u32)]
7+
pub enum Error {
8+
NotInitialized = 1,
9+
AlreadyInitialized = 2,
10+
Unauthorized = 3,
11+
RoleNotFound = 4,
12+
}
13+
14+
#[contracttype]
15+
#[derive(Clone, Debug, Eq, PartialEq)]
16+
pub enum Role {
17+
Admin = 0,
18+
Operator = 1,
19+
Moderator = 2,
20+
}
21+
22+
#[contracttype]
23+
pub enum DataKey {
24+
Admin,
25+
Role(Address, Role),
26+
}
27+
28+
#[contract]
29+
pub struct AccessControl;
30+
31+
#[contractimpl]
32+
impl AccessControl {
33+
/// Initialize the contract with an initial admin address.
34+
///
35+
/// # Arguments
36+
/// * `admin` - The address to be appointed as the initial super admin.
37+
///
38+
/// # Errors
39+
/// * `AlreadyInitialized` - If the contract has already been initialized.
40+
pub fn init(env: Env, admin: Address) -> Result<(), Error> {
41+
if env.storage().instance().has(&DataKey::Admin) {
42+
return Err(Error::AlreadyInitialized);
43+
}
44+
env.storage().instance().set(&DataKey::Admin, &admin);
45+
// Also grant the Admin role to the admin address
46+
env.storage().persistent().set(&DataKey::Role(admin, Role::Admin), &());
47+
Ok(())
48+
}
49+
50+
/// Returns the current super admin address.
51+
///
52+
/// # Returns
53+
/// The address of the current super admin.
54+
///
55+
/// # Errors
56+
/// * `NotInitialized` - If the contract hasn't been initialized yet.
57+
pub fn get_admin(env: Env) -> Result<Address, Error> {
58+
env.storage()
59+
.instance()
60+
.get(&DataKey::Admin)
61+
.ok_or(Error::NotInitialized)
62+
}
63+
64+
/// Assigns a specific role to a user.
65+
///
66+
/// Only the current super admin can call this function.
67+
///
68+
/// # Arguments
69+
/// * `admin_caller` - The address of the admin calling the function.
70+
/// * `user` - The address to receive the role.
71+
/// * `role` - The role to be assigned.
72+
///
73+
/// # Errors
74+
/// * `Unauthorized` - If the caller is not the super admin.
75+
pub fn assign_role(env: Env, admin_caller: Address, user: Address, role: Role) -> Result<(), Error> {
76+
admin_caller.require_auth();
77+
78+
let current_admin = Self::get_admin(env.clone())?;
79+
if admin_caller != current_admin {
80+
return Err(Error::Unauthorized);
81+
}
82+
83+
env.storage().persistent().set(&DataKey::Role(user, role), &());
84+
Ok(())
85+
}
86+
87+
/// Revokes a specific role from a user.
88+
///
89+
/// Only the current super admin can call this function.
90+
///
91+
/// # Arguments
92+
/// * `admin_caller` - The address of the admin calling the function.
93+
/// * `user` - The address from which the role will be revoked.
94+
/// * `role` - The role to be revoked.
95+
///
96+
/// # Errors
97+
/// * `Unauthorized` - If the caller is not the super admin.
98+
/// * `RoleNotFound` - If the user doesn't have the specified role.
99+
pub fn revoke_role(env: Env, admin_caller: Address, user: Address, role: Role) -> Result<(), Error> {
100+
admin_caller.require_auth();
101+
102+
let current_admin = Self::get_admin(env.clone())?;
103+
if admin_caller != current_admin {
104+
return Err(Error::Unauthorized);
105+
}
106+
107+
if !env.storage().persistent().has(&DataKey::Role(user.clone(), role.clone())) {
108+
return Err(Error::RoleNotFound);
109+
}
110+
111+
env.storage().persistent().remove(&DataKey::Role(user, role));
112+
Ok(())
113+
}
114+
115+
/// Checks if a user has a specific role.
116+
///
117+
/// # Arguments
118+
/// * `user` - The address to check.
119+
/// * `role` - The role to check for.
120+
///
121+
/// # Returns
122+
/// `true` if the user has the role, `false` otherwise.
123+
pub fn has_role(env: Env, user: Address, role: Role) -> bool {
124+
env.storage().persistent().has(&DataKey::Role(user, role))
125+
}
126+
127+
/// Transfers a role from one address to another.
128+
///
129+
/// Only the current super admin can call this function.
130+
///
131+
/// # Arguments
132+
/// * `admin_caller` - The address of the admin calling the function.
133+
/// * `from` - The address currently holding the role.
134+
/// * `to` - The address to receive the role.
135+
/// * `role` - The role to be transferred.
136+
///
137+
/// # Errors
138+
/// * `Unauthorized` - If the caller is not the super admin.
139+
/// * `RoleNotFound` - If the `from` address doesn't have the specified role.
140+
pub fn transfer_role(
141+
env: Env,
142+
admin_caller: Address,
143+
from: Address,
144+
to: Address,
145+
role: Role,
146+
) -> Result<(), Error> {
147+
admin_caller.require_auth();
148+
149+
let current_admin = Self::get_admin(env.clone())?;
150+
if admin_caller != current_admin {
151+
return Err(Error::Unauthorized);
152+
}
153+
154+
if !env.storage().persistent().has(&DataKey::Role(from.clone(), role.clone())) {
155+
return Err(Error::RoleNotFound);
156+
}
157+
158+
env.storage().persistent().remove(&DataKey::Role(from, role.clone()));
159+
env.storage().persistent().set(&DataKey::Role(to, role), &());
160+
Ok(())
161+
}
162+
163+
/// Transfers the super admin status to a new address.
164+
///
165+
/// Only the current super admin can call this function.
166+
///
167+
/// # Arguments
168+
/// * `admin_caller` - The address of the current admin.
169+
/// * `new_admin` - The address to become the new super admin.
170+
///
171+
/// # Errors
172+
/// * `Unauthorized` - If the caller is not the current super admin.
173+
pub fn transfer_admin(env: Env, admin_caller: Address, new_admin: Address) -> Result<(), Error> {
174+
admin_caller.require_auth();
175+
176+
let current_admin = Self::get_admin(env.clone())?;
177+
if admin_caller != current_admin {
178+
return Err(Error::Unauthorized);
179+
}
180+
181+
// Update the admin address
182+
env.storage().instance().set(&DataKey::Admin, &new_admin);
183+
184+
// Transfer the Admin role record
185+
env.storage().persistent().remove(&DataKey::Role(current_admin, Role::Admin));
186+
env.storage().persistent().set(&DataKey::Role(new_admin, Role::Admin), &());
187+
188+
Ok(())
189+
}
190+
}
191+
192+
mod test;
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
#![cfg(test)]
2+
3+
use super::*;
4+
use soroban_sdk::testutils::Address as _;
5+
use soroban_sdk::{Env, Address};
6+
7+
#[test]
8+
fn test_initialization() {
9+
let env = Env::default();
10+
let contract_id = env.register_contract(None, AccessControl);
11+
let client = AccessControlClient::new(&env, &contract_id);
12+
13+
let admin = Address::generate(&env);
14+
client.init(&admin);
15+
16+
assert_eq!(client.get_admin(), admin);
17+
}
18+
19+
#[test]
20+
#[should_panic(expected = "AlreadyInitialized")]
21+
fn test_double_initialization() {
22+
let env = Env::default();
23+
let contract_id = env.register_contract(None, AccessControl);
24+
let client = AccessControlClient::new(&env, &contract_id);
25+
26+
let admin = Address::generate(&env);
27+
client.init(&admin);
28+
client.init(&admin);
29+
}
30+
31+
#[test]
32+
fn test_role_assignment() {
33+
let env = Env::default();
34+
env.mock_all_auths();
35+
36+
let contract_id = env.register_contract(None, AccessControl);
37+
let client = AccessControlClient::new(&env, &contract_id);
38+
39+
let admin = Address::generate(&env);
40+
let user = Address::generate(&env);
41+
42+
client.init(&admin);
43+
44+
client.assign_role(&admin, &user, &Role::Operator);
45+
assert!(client.has_role(&user, &Role::Operator));
46+
}
47+
48+
#[test]
49+
fn test_role_revocation() {
50+
let env = Env::default();
51+
env.mock_all_auths();
52+
53+
let contract_id = env.register_contract(None, AccessControl);
54+
let client = AccessControlClient::new(&env, &contract_id);
55+
56+
let admin = Address::generate(&env);
57+
let user = Address::generate(&env);
58+
59+
client.init(&admin);
60+
61+
client.assign_role(&admin, &user, &Role::Operator);
62+
assert!(client.has_role(&user, &Role::Operator));
63+
64+
client.revoke_role(&admin, &user, &Role::Operator);
65+
assert!(!client.has_role(&user, &Role::Operator));
66+
}
67+
68+
#[test]
69+
fn test_role_transfer() {
70+
let env = Env::default();
71+
env.mock_all_auths();
72+
73+
let contract_id = env.register_contract(None, AccessControl);
74+
let client = AccessControlClient::new(&env, &contract_id);
75+
76+
let admin = Address::generate(&env);
77+
let user1 = Address::generate(&env);
78+
let user2 = Address::generate(&env);
79+
80+
client.init(&admin);
81+
82+
client.assign_role(&admin, &user1, &Role::Operator);
83+
assert!(client.has_role(&user1, &Role::Operator));
84+
85+
client.transfer_role(&admin, &user1, &user2, &Role::Operator);
86+
assert!(!client.has_role(&user1, &Role::Operator));
87+
assert!(client.has_role(&user2, &Role::Operator));
88+
}
89+
90+
#[test]
91+
fn test_admin_transfer() {
92+
let env = Env::default();
93+
env.mock_all_auths();
94+
95+
let contract_id = env.register_contract(None, AccessControl);
96+
let client = AccessControlClient::new(&env, &contract_id);
97+
98+
let admin1 = Address::generate(&env);
99+
let admin2 = Address::generate(&env);
100+
101+
client.init(&admin1);
102+
assert_eq!(client.get_admin(), admin1);
103+
104+
client.transfer_admin(&admin1, &admin2);
105+
assert_eq!(client.get_admin(), admin2);
106+
assert!(client.has_role(&admin2, &Role::Admin));
107+
assert!(!client.has_role(&admin1, &Role::Admin));
108+
}
109+
110+
#[test]
111+
#[should_panic(expected = "Unauthorized")]
112+
fn test_unauthorized_assignment() {
113+
let env = Env::default();
114+
env.mock_all_auths();
115+
116+
let contract_id = env.register_contract(None, AccessControl);
117+
let client = AccessControlClient::new(&env, &contract_id);
118+
119+
let admin = Address::generate(&env);
120+
let non_admin = Address::generate(&env);
121+
let user = Address::generate(&env);
122+
123+
client.init(&admin);
124+
125+
// non_admin tries to assign a role
126+
client.assign_role(&non_admin, &user, &Role::Operator);
127+
}

0 commit comments

Comments
 (0)