Skip to content

Commit

Permalink
Make Pausable use Acl for authorization (Near-One#47)
Browse files Browse the repository at this point in the history
* Make Pausable use Acl instead of Ownable

* Update Pausable tests

* Update Readme
  • Loading branch information
mooori authored Dec 9, 2022
1 parent c4db165 commit ab68372
Show file tree
Hide file tree
Showing 6 changed files with 211 additions and 99 deletions.
74 changes: 61 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,58 +96,106 @@ Documentation of all methods provided by the derived implementation of `FullAcce
Allow contracts to implement an emergency stop mechanism that can be triggered by an authorized account. Pauses can be
used granularly to only limit certain features.

Contract example using _Pausable_ plugin. Note that it requires the contract to be Ownable.
Contract example using _Pausable_ plugin. Note that it requires the contract to be _AccessControllable_.

```rust

/// Define roles for access control of `Pausable` features. Accounts which are
/// granted a role are authorized to execute the corresponding action.
#[derive(AccessControlRole, Deserialize, Serialize, Copy, Clone)]
#[serde(crate = "near_sdk::serde")]
pub enum Role {
/// May pause and unpause features.
PauseManager,
/// May call `increase_4` even when it is paused.
Unrestricted4Increaser,
/// May call `decrease_4` even when `increase_4` is not paused.
Unrestricted4Decreaser,
/// May always call both `increase_4` and `decrease_4`.
Unrestricted4Modifier,
}

#[access_control(role_type(Role))]
#[near_bindgen]
#[derive(Ownable, Pausable)]
#[derive(Pausable)]
#[pausable(manager_roles(Role::PauseManager))]
struct Counter {
counter: u64,
}

#[near_bindgen]
impl Counter {
/// Specify the owner of the contract in the constructor
/// Initialize access control in the constructor.
#[init]
fn new() -> Self {
let mut contract = Self { counter: 0 };
contract.owner_set(Some(near_sdk::env::predecessor_account_id()));
let mut contract = Self {
counter: 0,
__acl: Default::default(),
};

// Make the contract itself access control super admin. This enables
// granting roles below.
near_sdk::require!(
contract.acl_init_super_admin(near_sdk::env::predecessor_account_id()),
"Failed to initialize super admin",
);

// Grant access control roles.
let grants: Vec<(Role, near_sdk::AccountId)> = vec![
(Role::PauseManager, "anna.test".parse().unwrap()),
(Role::Unrestricted4Increaser, "brenda.test".parse().unwrap()),
(Role::Unrestricted4Decreaser, "chris.test".parse().unwrap()),
(Role::Unrestricted4Modifier, "daniel.test".parse().unwrap()),
];
for (role, account_id) in grants {
let result = contract.acl_grant_role(role.into(), account_id);
near_sdk::require!(Some(true) == result, "Failed to grant role");
}

contract
}

/// Function can be paused using feature name "increase_1" or "ALL" like:
/// `contract.pa_pause_feature("increase_1")` or `contract.pa_pause_feature("ALL")`
///
/// If the function is paused, all calls to it will fail. Even calls started from owner or self.
/// If the function is paused, all calls to it will fail. Even calls
/// initiated by accounts which are access control super admin or role
/// grantee.
#[pause]
fn increase_1(&mut self) {
self.counter += 1;
}

/// Similar to `#[pause]` but use an explicit name for the feature. In this case the feature to be paused
/// is named "Increase by two". Note that trying to pause it using "increase_2" will not have any effect.
/// Similar to `#[pause]` but use an explicit name for the feature. In this
/// case the feature to be paused is named "Increase by two". Note that
/// trying to pause it using "increase_2" will not have any effect.
///
/// This can be used to pause a subset of the methods at once without requiring to use "ALL".
/// This can be used to pause a subset of the methods at once without
/// requiring to use "ALL".
#[pause(name = "Increase by two")]
fn increase_2(&mut self) {
self.counter += 2;
}

/// Similar to `#[pause]` but owner or self can still call this method. Any subset of {self, owner} can be specified.
#[pause(except(owner, self))]
/// Similar to `#[pause]` but roles passed as argument may still
/// successfully call this method.
#[pause(except(roles(Role::Unrestricted4Increaser, Role::Unrestricted4Modifier)))]
fn increase_4(&mut self) {
self.counter += 4;
}

/// This method can only be called when "increase_1" is paused. Use this macro to create escape hatches when some
/// features are paused. Note that if "ALL" is specified the "increase_1" is considered to be paused.
/// This method can only be called when "increase_1" is paused. Use this
/// macro to create escape hatches when some features are paused. Note that
/// if "ALL" is specified the "increase_1" is considered to be paused.
#[if_paused(name = "increase_1")]
fn decrease_1(&mut self) {
self.counter -= 1;
}

/// Custom use of pause features. Only allow increasing the counter using `careful_increase` if it is below 10.

/// Custom use of pause features. Only allow increasing the counter using
/// `careful_increase` if it is below 10.
fn careful_increase(&mut self) {
if self.counter >= 10 {
assert!(
Expand Down
2 changes: 1 addition & 1 deletion near-plugins-derive/src/access_control_role.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ pub fn derive_access_control_role(input: TokenStream) -> TokenStream {
}
}

::#cratename::bitflags::bitflags! {
#cratename::bitflags::bitflags! {
/// Encodes permissions for roles and admins.
#[derive(BorshDeserialize, BorshSerialize, Default)]
struct #bitflags_type_ident: u128 {
Expand Down
26 changes: 13 additions & 13 deletions near-plugins-derive/src/access_controllable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,11 @@ pub fn access_controllable(attrs: TokenStream, item: TokenStream) -> TokenStream
permissions.insert(flag);
self.add_bearer(flag, account_id);

let event = ::#cratename::access_controllable::events::SuperAdminAdded {
let event = #cratename::access_controllable::events::SuperAdminAdded {
account: account_id.clone(),
by: ::near_sdk::env::predecessor_account_id(),
};
::#cratename::events::AsEvent::emit(&event);
#cratename::events::AsEvent::emit(&event);
}

is_new_super_admin
Expand Down Expand Up @@ -177,11 +177,11 @@ pub fn access_controllable(attrs: TokenStream, item: TokenStream) -> TokenStream
permissions.remove(flag);
self.remove_bearer(flag, account_id);

let event = ::#cratename::access_controllable::events::SuperAdminRevoked {
let event = #cratename::access_controllable::events::SuperAdminRevoked {
account: account_id.clone(),
by: ::near_sdk::env::predecessor_account_id(),
};
::#cratename::events::AsEvent::emit(&event);
#cratename::events::AsEvent::emit(&event);
}

was_super_admin
Expand All @@ -208,12 +208,12 @@ pub fn access_controllable(attrs: TokenStream, item: TokenStream) -> TokenStream
permissions.insert(flag);
self.add_bearer(flag, account_id);

let event = ::#cratename::access_controllable::events::AdminAdded {
let event = #cratename::access_controllable::events::AdminAdded {
role: role.into(),
account: account_id.clone(),
by: ::near_sdk::env::predecessor_account_id(),
};
::#cratename::events::AsEvent::emit(&event);
#cratename::events::AsEvent::emit(&event);
}

is_new_admin
Expand Down Expand Up @@ -259,12 +259,12 @@ pub fn access_controllable(attrs: TokenStream, item: TokenStream) -> TokenStream
permissions.remove(flag);
self.remove_bearer(flag, account_id);

let event = ::#cratename::access_controllable::events::AdminRevoked {
let event = #cratename::access_controllable::events::AdminRevoked {
role: role.into(),
account: account_id.clone(),
by: ::near_sdk::env::predecessor_account_id(),
};
::#cratename::events::AsEvent::emit(&event);
#cratename::events::AsEvent::emit(&event);
}

was_admin
Expand All @@ -289,12 +289,12 @@ pub fn access_controllable(attrs: TokenStream, item: TokenStream) -> TokenStream
permissions.insert(flag);
self.add_bearer(flag, account_id);

let event = ::#cratename::access_controllable::events::RoleGranted {
let event = #cratename::access_controllable::events::RoleGranted {
role: role.into(),
by: ::near_sdk::env::predecessor_account_id(),
to: account_id.clone(),
};
::#cratename::events::AsEvent::emit(&event);
#cratename::events::AsEvent::emit(&event);
}

is_new_grantee
Expand Down Expand Up @@ -324,12 +324,12 @@ pub fn access_controllable(attrs: TokenStream, item: TokenStream) -> TokenStream
permissions.remove(flag);
self.remove_bearer(flag, account_id);

let event = ::#cratename::access_controllable::events::RoleRevoked {
let event = #cratename::access_controllable::events::RoleRevoked {
role: role.into(),
from: account_id.clone(),
by: ::near_sdk::env::predecessor_account_id(),
};
::#cratename::events::AsEvent::emit(&event);
#cratename::events::AsEvent::emit(&event);
}

was_grantee
Expand Down Expand Up @@ -575,7 +575,7 @@ pub fn access_control_any(attrs: TokenStream, item: TokenStream) -> TokenStream
#function_name,
__acl_any_roles,
);
env::panic_str(&message);
near_sdk::env::panic_str(&message);
}
};

Expand Down
2 changes: 1 addition & 1 deletion near-plugins-derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ pub fn derive_fak_fallback(input: TokenStream) -> TokenStream {
full_access_key_fallback::derive_fak_fallback(input)
}

#[proc_macro_derive(Pausable)]
#[proc_macro_derive(Pausable, attributes(pausable))]
pub fn derive_pausable(input: TokenStream) -> TokenStream {
pausable::derive_pausable(input)
}
Expand Down
57 changes: 26 additions & 31 deletions near-plugins-derive/src/pausable.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::utils;
use crate::utils::{cratename, is_near_bindgen_wrapped_or_marshall};
use darling::util::PathList;
use darling::{FromDeriveInput, FromMeta};
use proc_macro::{self, TokenStream};
use quote::quote;
Expand All @@ -8,7 +9,11 @@ use syn::{parse, parse_macro_input, AttributeArgs, DeriveInput, ItemFn};
#[derive(FromDeriveInput, Default)]
#[darling(default, attributes(pausable), forward_attrs(allow, doc, cfg))]
struct Opts {
/// Storage key under which the set of paused features is stored. If it is
/// `None` the default value will be used.
paused_storage_key: Option<String>,
/// Access control roles whose grantees may pause and unpause features.
manager_roles: PathList,
}

pub fn derive_pausable(input: TokenStream) -> TokenStream {
Expand All @@ -21,6 +26,11 @@ pub fn derive_pausable(input: TokenStream) -> TokenStream {
let paused_storage_key = opts
.paused_storage_key
.unwrap_or_else(|| "__PAUSE__".to_string());
let manager_roles = opts.manager_roles;
assert!(
manager_roles.len() > 0,
"Specify at least one role for manager_roles"
);

let output = quote! {
#[near_bindgen]
Expand All @@ -42,7 +52,7 @@ pub fn derive_pausable(input: TokenStream) -> TokenStream {
})
}

#[#cratename::only(owner)]
#[#cratename::access_control_any(roles(#(#manager_roles),*))]
fn pa_pause_feature(&mut self, key: String) {
let mut paused_keys = self.pa_all_paused().unwrap_or_default();
paused_keys.insert(key.clone());
Expand All @@ -63,7 +73,7 @@ pub fn derive_pausable(input: TokenStream) -> TokenStream {
);
}

#[#cratename::only(owner)]
#[#cratename::access_control_any(roles(#(#manager_roles),*))]
fn pa_unpause_feature(&mut self, key: String) {
let mut paused_keys = self.pa_all_paused().unwrap_or_default();
paused_keys.remove(&key);
Expand Down Expand Up @@ -96,11 +106,8 @@ pub fn derive_pausable(input: TokenStream) -> TokenStream {
#[derive(Default, FromMeta, Debug)]
#[darling(default)]
pub struct ExceptSubArgs {
#[darling(default)]
owner: bool,
#[darling(default)]
#[darling(rename = "self")]
_self: bool,
/// Grantees of these roles are exempted and may always call the method.
roles: PathList,
}

#[derive(Debug, FromMeta)]
Expand Down Expand Up @@ -158,9 +165,9 @@ pub fn if_paused(attrs: TokenStream, item: TokenStream) -> TokenStream {
let bypass_condition = get_bypass_condition(&args.except);

let check_pause = quote!(
let mut check_paused = true;
let mut __check_paused = true;
#bypass_condition
if check_paused {
if __check_paused {
::near_sdk::require!(self.pa_is_paused(#fn_name.to_string()), "Pausable: Method must be paused");
}
);
Expand All @@ -169,28 +176,16 @@ pub fn if_paused(attrs: TokenStream, item: TokenStream) -> TokenStream {
}

fn get_bypass_condition(args: &ExceptSubArgs) -> proc_macro2::TokenStream {
let self_condition = if args._self {
quote!(
if ::near_sdk::env::predecessor_account_id() == ::near_sdk::env::current_account_id() {
__check_paused = false;
}
)
} else {
quote!()
};

let owner_condition = if args.owner {
quote!(
if Some(::near_sdk::env::predecessor_account_id()) == self.owner_get() {
__check_paused = false;
}
)
} else {
quote!()
};

let except_roles = args.roles.clone();
quote!(
#self_condition
#owner_condition
let __except_roles: Vec<&str> = vec![#(#except_roles.into()),*];
let __except_roles: Vec<String> = __except_roles.iter().map(|&x| x.into()).collect();
let may_bypass = self.acl_has_any_role(
__except_roles,
::near_sdk::env::predecessor_account_id()
);
if may_bypass {
__check_paused = false;
}
)
}
Loading

0 comments on commit ab68372

Please sign in to comment.