Skip to content
Draft
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
7 changes: 4 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
STANDARDS_EXAMPLES :=
STANDARDS_WASM_CONTRACTS :=
STANDARDS_EXAMPLES := standards/examples/multisig_controller
STANDARDS_WASM_CONTRACTS := $(STANDARDS_EXAMPLES)
LEGACY_SUBDIRS := tests/alice tests/bob tests/charlie genesis/transfer genesis/stake tests/host_fn
STANDARDS_PROPTEST_CASES ?= 8192
STANDARDS_PROPTEST_MAX_SHRINK_ITERS ?= 16384
Expand Down Expand Up @@ -29,10 +29,11 @@ standards-test: ## Test the Dusk standards crate without the Dusk compiler bundl
$(MAKE) -C standards/dusk-contract-standards test

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

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

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

Expand Down
28 changes: 27 additions & 1 deletion standards/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 standards/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[workspace]
members = [
"dusk-contract-standards",
"examples/multisig_controller",
]

resolver = "2"
Expand Down
221 changes: 221 additions & 0 deletions standards/dusk-contract-standards/src/access/access_control.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Copyright (c) DUSK NETWORK. All rights reserved.

//! Role-based access control.

use alloc::collections::{BTreeMap, BTreeSet};

use crate::auth::{
ActionEnvelope, AuthorizationManager, Authorizer, SignedAuthorization,
};
use crate::core::{error, CallContext, Principal};

/// 32-byte role id.
pub type Role = [u8; 32];

/// Default admin role.
pub const DEFAULT_ADMIN_ROLE: Role = [0u8; 32];

#[derive(Clone, Debug)]
struct RoleData {
members: BTreeSet<Principal>,
admin_role: Role,
}

impl RoleData {
fn new(admin_role: Role) -> Self {
Self {
members: BTreeSet::new(),
admin_role,
}
}
}

/// Role-based access-control module.
#[derive(Clone, Debug, Default)]
pub struct AccessControl {
roles: BTreeMap<Role, RoleData>,
}

impl AccessControl {
/// Creates an empty access-control module.
pub const fn new() -> Self {
Self {
roles: BTreeMap::new(),
}
}

/// Bootstraps the default admin role.
pub fn init_admin(&mut self, admin: Principal) {
if admin.is_zero() {
panic!("{}", error::ZERO_PRINCIPAL);
}
if self.roles.contains_key(&DEFAULT_ADMIN_ROLE) {
panic!("{}", error::ALREADY_INITIALIZED);
}
self.roles
.entry(DEFAULT_ADMIN_ROLE)
.or_insert_with(|| RoleData::new(DEFAULT_ADMIN_ROLE))
.members
.insert(admin);
}

/// Returns true when `account` has `role`.
pub fn has_role(&self, role: Role, account: Principal) -> bool {
self.roles
.get(&role)
.map(|data| data.members.contains(&account))
.unwrap_or(false)
}

/// Panics unless `account` has `role`.
pub fn assert_role(&self, role: Role, account: Principal) {
if !self.has_role(role, account) {
panic!("{}", error::UNAUTHORIZED);
}
}

/// Authorizes any principal with `role` through runtime context or signed
/// authorization.
pub fn authorize_role(
&self,
role: Role,
authorizations: &mut AuthorizationManager,
context: CallContext,
authorization: Option<&SignedAuthorization>,
now: u64,
) -> Principal {
let mut authorizer = Authorizer::new(authorizations, context, now);
self.authorize_role_with(role, &mut authorizer, authorization)
}

/// Authorizes any principal with `role` using a reusable call authorizer.
pub fn authorize_role_with(
&self,
role: Role,
authorizer: &mut Authorizer<'_>,
authorization: Option<&SignedAuthorization>,
) -> Principal {
if let Some(principal) = authorizer.observed_principal() {
if self.has_role(role, principal) {
return principal;
}
}
let Some(authorization) = authorization else {
panic!("{}", error::UNAUTHORIZED);
};
authorizer.require_unbound_signed_if(authorization, |principal| {
self.has_role(role, principal)
})
}

/// Authorizes any principal with `role` through runtime context or an
/// action-bound signed authorization.
pub fn authorize_role_action(
&self,
role: Role,
authorizations: &mut AuthorizationManager,
context: CallContext,
authorization: Option<&SignedAuthorization>,
envelope: ActionEnvelope,
now: u64,
) -> Principal {
let mut authorizer = Authorizer::new(authorizations, context, now);
self.authorize_role_action_with(
role,
&mut authorizer,
authorization,
envelope,
)
}

/// Authorizes any principal with `role` using a reusable call authorizer
/// and exact call envelope for signed fallbacks.
pub fn authorize_role_action_with(
&self,
role: Role,
authorizer: &mut Authorizer<'_>,
authorization: Option<&SignedAuthorization>,
envelope: ActionEnvelope,
) -> Principal {
if let Some(principal) = authorizer.observed_principal() {
if self.has_role(role, principal) {
return principal;
}
}
let Some(authorization) = authorization else {
panic!("{}", error::UNAUTHORIZED);
};
authorizer.require_signed_action_if(
authorization,
envelope,
|principal| self.has_role(role, principal),
)
}

/// Returns a role's admin role.
pub fn get_role_admin(&self, role: Role) -> Role {
self.roles
.get(&role)
.map(|data| data.admin_role)
.unwrap_or(DEFAULT_ADMIN_ROLE)
}

/// Sets a role's admin role. Caller must have the current admin role.
pub fn set_role_admin(
&mut self,
caller: Principal,
role: Role,
admin_role: Role,
) {
let current_admin = self.get_role_admin(role);
self.assert_role(current_admin, caller);
self.roles
.entry(role)
.or_insert_with(|| RoleData::new(DEFAULT_ADMIN_ROLE))
.admin_role = admin_role;
}

/// Grants `role` to `account`. Caller must have the role admin.
pub fn grant_role(
&mut self,
caller: Principal,
role: Role,
account: Principal,
) {
if account.is_zero() {
panic!("{}", error::ZERO_PRINCIPAL);
}
let admin = self.get_role_admin(role);
self.assert_role(admin, caller);
self.roles
.entry(role)
.or_insert_with(|| RoleData::new(DEFAULT_ADMIN_ROLE))
.members
.insert(account);
}

/// Revokes `role` from `account`. Caller must have the role admin.
pub fn revoke_role(
&mut self,
caller: Principal,
role: Role,
account: Principal,
) {
let admin = self.get_role_admin(role);
self.assert_role(admin, caller);
if let Some(data) = self.roles.get_mut(&role) {
data.members.remove(&account);
}
}

/// Renounces `role` from `caller`.
pub fn renounce_role(&mut self, role: Role, caller: Principal) {
if let Some(data) = self.roles.get_mut(&role) {
data.members.remove(&caller);
}
}
}
91 changes: 91 additions & 0 deletions standards/dusk-contract-standards/src/access/events.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Copyright (c) DUSK NETWORK. All rights reserved.

//! Access-control and emergency-stop event payloads.

use bytecheck::CheckBytes;
use rkyv::{Archive, Deserialize, Serialize};

use crate::core::Principal;

use super::Role;

/// Pause event topic.
pub const PAUSED_TOPIC: &str = "access/paused";
/// Unpause event topic.
pub const UNPAUSED_TOPIC: &str = "access/unpaused";
/// Role-grant event topic.
pub const ROLE_GRANTED_TOPIC: &str = "access/role_granted";
/// Role-revoke event topic.
pub const ROLE_REVOKED_TOPIC: &str = "access/role_revoked";

/// Pause event.
#[derive(
Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq,
)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[archive_attr(derive(CheckBytes))]
pub struct Paused {
/// Principal that authorized the pause.
pub account: Principal,
}

/// Unpause event.
#[derive(
Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq,
)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[archive_attr(derive(CheckBytes))]
pub struct Unpaused {
/// Principal that authorized the unpause.
pub account: Principal,
}

/// Role-grant event.
#[derive(
Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq,
)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[archive_attr(derive(CheckBytes))]
pub struct RoleGranted {
/// Role id.
pub role: Role,
/// Principal receiving the role.
pub account: Principal,
/// Principal that authorized the grant.
pub sender: Principal,
}

/// Role-revoke event.
#[derive(
Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq,
)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[archive_attr(derive(CheckBytes))]
pub struct RoleRevoked {
/// Role id.
pub role: Role,
/// Principal losing the role.
pub account: Principal,
/// Principal that authorized the revoke.
pub sender: Principal,
}

impl dusk_forge::ContractEvent for Paused {
const TOPICS: &'static [&'static str] = &[PAUSED_TOPIC];
}

impl dusk_forge::ContractEvent for Unpaused {
const TOPICS: &'static [&'static str] = &[UNPAUSED_TOPIC];
}

impl dusk_forge::ContractEvent for RoleGranted {
const TOPICS: &'static [&'static str] = &[ROLE_GRANTED_TOPIC];
}

impl dusk_forge::ContractEvent for RoleRevoked {
const TOPICS: &'static [&'static str] = &[ROLE_REVOKED_TOPIC];
}
Loading
Loading