Skip to content

Commit 63d04db

Browse files
committed
standards: add access control and multisig
1 parent 1c76ebc commit 63d04db

16 files changed

Lines changed: 2360 additions & 4 deletions

File tree

Makefile

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
STANDARDS_EXAMPLES :=
2-
STANDARDS_WASM_CONTRACTS :=
1+
STANDARDS_EXAMPLES := standards/examples/multisig_controller
2+
STANDARDS_WASM_CONTRACTS := $(STANDARDS_EXAMPLES)
33
LEGACY_SUBDIRS := tests/alice tests/bob tests/charlie genesis/transfer genesis/stake tests/host_fn
44
STANDARDS_PROPTEST_CASES ?= 8192
55
STANDARDS_PROPTEST_MAX_SHRINK_ITERS ?= 16384
@@ -29,10 +29,11 @@ standards-test: ## Test the Dusk standards crate without the Dusk compiler bundl
2929
$(MAKE) -C standards/dusk-contract-standards test
3030

3131
standards-wasm: ## Build standards reference contracts without the Dusk compiler bundle
32-
@true
32+
$(MAKE) $(STANDARDS_WASM_CONTRACTS) MAKECMDGOALS=wasm
3333

3434
standards-clippy: ## Run standards clippy without the Dusk compiler bundle
3535
$(MAKE) -C standards/dusk-contract-standards clippy
36+
$(MAKE) $(STANDARDS_WASM_CONTRACTS) MAKECMDGOALS=clippy
3637

3738
standards-ci: standards-fmt standards-check standards-clippy standards-test standards-wasm ## Run regular standards CI checks
3839

standards/Cargo.lock

Lines changed: 27 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

standards/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
[workspace]
22
members = [
33
"dusk-contract-standards",
4+
"examples/multisig_controller",
45
]
56

67
resolver = "2"
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
//
5+
// Copyright (c) DUSK NETWORK. All rights reserved.
6+
7+
//! Role-based access control.
8+
9+
use alloc::collections::{BTreeMap, BTreeSet};
10+
11+
use crate::auth::{
12+
ActionEnvelope, AuthorizationManager, Authorizer, SignedAuthorization,
13+
};
14+
use crate::core::{error, CallContext, Principal};
15+
16+
/// 32-byte role id.
17+
pub type Role = [u8; 32];
18+
19+
/// Default admin role.
20+
pub const DEFAULT_ADMIN_ROLE: Role = [0u8; 32];
21+
22+
#[derive(Clone, Debug)]
23+
struct RoleData {
24+
members: BTreeSet<Principal>,
25+
admin_role: Role,
26+
}
27+
28+
impl RoleData {
29+
fn new(admin_role: Role) -> Self {
30+
Self {
31+
members: BTreeSet::new(),
32+
admin_role,
33+
}
34+
}
35+
}
36+
37+
/// Role-based access-control module.
38+
#[derive(Clone, Debug, Default)]
39+
pub struct AccessControl {
40+
roles: BTreeMap<Role, RoleData>,
41+
}
42+
43+
impl AccessControl {
44+
/// Creates an empty access-control module.
45+
pub const fn new() -> Self {
46+
Self {
47+
roles: BTreeMap::new(),
48+
}
49+
}
50+
51+
/// Bootstraps the default admin role.
52+
pub fn init_admin(&mut self, admin: Principal) {
53+
if admin.is_zero() {
54+
panic!("{}", error::ZERO_PRINCIPAL);
55+
}
56+
if self.roles.contains_key(&DEFAULT_ADMIN_ROLE) {
57+
panic!("{}", error::ALREADY_INITIALIZED);
58+
}
59+
self.roles
60+
.entry(DEFAULT_ADMIN_ROLE)
61+
.or_insert_with(|| RoleData::new(DEFAULT_ADMIN_ROLE))
62+
.members
63+
.insert(admin);
64+
}
65+
66+
/// Returns true when `account` has `role`.
67+
pub fn has_role(&self, role: Role, account: Principal) -> bool {
68+
self.roles
69+
.get(&role)
70+
.map(|data| data.members.contains(&account))
71+
.unwrap_or(false)
72+
}
73+
74+
/// Panics unless `account` has `role`.
75+
pub fn assert_role(&self, role: Role, account: Principal) {
76+
if !self.has_role(role, account) {
77+
panic!("{}", error::UNAUTHORIZED);
78+
}
79+
}
80+
81+
/// Authorizes any principal with `role` through runtime context or signed
82+
/// authorization.
83+
pub fn authorize_role(
84+
&self,
85+
role: Role,
86+
authorizations: &mut AuthorizationManager,
87+
context: CallContext,
88+
authorization: Option<&SignedAuthorization>,
89+
now: u64,
90+
) -> Principal {
91+
let mut authorizer = Authorizer::new(authorizations, context, now);
92+
self.authorize_role_with(role, &mut authorizer, authorization)
93+
}
94+
95+
/// Authorizes any principal with `role` using a reusable call authorizer.
96+
pub fn authorize_role_with(
97+
&self,
98+
role: Role,
99+
authorizer: &mut Authorizer<'_>,
100+
authorization: Option<&SignedAuthorization>,
101+
) -> Principal {
102+
if let Some(principal) = authorizer.observed_principal() {
103+
if self.has_role(role, principal) {
104+
return principal;
105+
}
106+
}
107+
let Some(authorization) = authorization else {
108+
panic!("{}", error::UNAUTHORIZED);
109+
};
110+
authorizer.require_unbound_signed_if(authorization, |principal| {
111+
self.has_role(role, principal)
112+
})
113+
}
114+
115+
/// Authorizes any principal with `role` through runtime context or an
116+
/// action-bound signed authorization.
117+
pub fn authorize_role_action(
118+
&self,
119+
role: Role,
120+
authorizations: &mut AuthorizationManager,
121+
context: CallContext,
122+
authorization: Option<&SignedAuthorization>,
123+
envelope: ActionEnvelope,
124+
now: u64,
125+
) -> Principal {
126+
let mut authorizer = Authorizer::new(authorizations, context, now);
127+
self.authorize_role_action_with(
128+
role,
129+
&mut authorizer,
130+
authorization,
131+
envelope,
132+
)
133+
}
134+
135+
/// Authorizes any principal with `role` using a reusable call authorizer
136+
/// and exact call envelope for signed fallbacks.
137+
pub fn authorize_role_action_with(
138+
&self,
139+
role: Role,
140+
authorizer: &mut Authorizer<'_>,
141+
authorization: Option<&SignedAuthorization>,
142+
envelope: ActionEnvelope,
143+
) -> Principal {
144+
if let Some(principal) = authorizer.observed_principal() {
145+
if self.has_role(role, principal) {
146+
return principal;
147+
}
148+
}
149+
let Some(authorization) = authorization else {
150+
panic!("{}", error::UNAUTHORIZED);
151+
};
152+
authorizer.require_signed_action_if(
153+
authorization,
154+
envelope,
155+
|principal| self.has_role(role, principal),
156+
)
157+
}
158+
159+
/// Returns a role's admin role.
160+
pub fn get_role_admin(&self, role: Role) -> Role {
161+
self.roles
162+
.get(&role)
163+
.map(|data| data.admin_role)
164+
.unwrap_or(DEFAULT_ADMIN_ROLE)
165+
}
166+
167+
/// Sets a role's admin role. Caller must have the current admin role.
168+
pub fn set_role_admin(
169+
&mut self,
170+
caller: Principal,
171+
role: Role,
172+
admin_role: Role,
173+
) {
174+
let current_admin = self.get_role_admin(role);
175+
self.assert_role(current_admin, caller);
176+
self.roles
177+
.entry(role)
178+
.or_insert_with(|| RoleData::new(DEFAULT_ADMIN_ROLE))
179+
.admin_role = admin_role;
180+
}
181+
182+
/// Grants `role` to `account`. Caller must have the role admin.
183+
pub fn grant_role(
184+
&mut self,
185+
caller: Principal,
186+
role: Role,
187+
account: Principal,
188+
) {
189+
if account.is_zero() {
190+
panic!("{}", error::ZERO_PRINCIPAL);
191+
}
192+
let admin = self.get_role_admin(role);
193+
self.assert_role(admin, caller);
194+
self.roles
195+
.entry(role)
196+
.or_insert_with(|| RoleData::new(DEFAULT_ADMIN_ROLE))
197+
.members
198+
.insert(account);
199+
}
200+
201+
/// Revokes `role` from `account`. Caller must have the role admin.
202+
pub fn revoke_role(
203+
&mut self,
204+
caller: Principal,
205+
role: Role,
206+
account: Principal,
207+
) {
208+
let admin = self.get_role_admin(role);
209+
self.assert_role(admin, caller);
210+
if let Some(data) = self.roles.get_mut(&role) {
211+
data.members.remove(&account);
212+
}
213+
}
214+
215+
/// Renounces `role` from `caller`.
216+
pub fn renounce_role(&mut self, role: Role, caller: Principal) {
217+
if let Some(data) = self.roles.get_mut(&role) {
218+
data.members.remove(&caller);
219+
}
220+
}
221+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
//
5+
// Copyright (c) DUSK NETWORK. All rights reserved.
6+
7+
//! Access-control and emergency-stop event payloads.
8+
9+
use bytecheck::CheckBytes;
10+
use rkyv::{Archive, Deserialize, Serialize};
11+
12+
use crate::core::Principal;
13+
14+
use super::Role;
15+
16+
/// Pause event topic.
17+
pub const PAUSED_TOPIC: &str = "access/paused";
18+
/// Unpause event topic.
19+
pub const UNPAUSED_TOPIC: &str = "access/unpaused";
20+
/// Role-grant event topic.
21+
pub const ROLE_GRANTED_TOPIC: &str = "access/role_granted";
22+
/// Role-revoke event topic.
23+
pub const ROLE_REVOKED_TOPIC: &str = "access/role_revoked";
24+
25+
/// Pause event.
26+
#[derive(
27+
Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq,
28+
)]
29+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
30+
#[archive_attr(derive(CheckBytes))]
31+
pub struct Paused {
32+
/// Principal that authorized the pause.
33+
pub account: Principal,
34+
}
35+
36+
/// Unpause event.
37+
#[derive(
38+
Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq,
39+
)]
40+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
41+
#[archive_attr(derive(CheckBytes))]
42+
pub struct Unpaused {
43+
/// Principal that authorized the unpause.
44+
pub account: Principal,
45+
}
46+
47+
/// Role-grant event.
48+
#[derive(
49+
Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq,
50+
)]
51+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
52+
#[archive_attr(derive(CheckBytes))]
53+
pub struct RoleGranted {
54+
/// Role id.
55+
pub role: Role,
56+
/// Principal receiving the role.
57+
pub account: Principal,
58+
/// Principal that authorized the grant.
59+
pub sender: Principal,
60+
}
61+
62+
/// Role-revoke event.
63+
#[derive(
64+
Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq,
65+
)]
66+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
67+
#[archive_attr(derive(CheckBytes))]
68+
pub struct RoleRevoked {
69+
/// Role id.
70+
pub role: Role,
71+
/// Principal losing the role.
72+
pub account: Principal,
73+
/// Principal that authorized the revoke.
74+
pub sender: Principal,
75+
}
76+
77+
impl dusk_forge::ContractEvent for Paused {
78+
const TOPICS: &'static [&'static str] = &[PAUSED_TOPIC];
79+
}
80+
81+
impl dusk_forge::ContractEvent for Unpaused {
82+
const TOPICS: &'static [&'static str] = &[UNPAUSED_TOPIC];
83+
}
84+
85+
impl dusk_forge::ContractEvent for RoleGranted {
86+
const TOPICS: &'static [&'static str] = &[ROLE_GRANTED_TOPIC];
87+
}
88+
89+
impl dusk_forge::ContractEvent for RoleRevoked {
90+
const TOPICS: &'static [&'static str] = &[ROLE_REVOKED_TOPIC];
91+
}

0 commit comments

Comments
 (0)