Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make Pausable use Acl for authorization #47

Merged
merged 3 commits into from
Dec 9, 2022
Merged
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
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! {
mooori marked this conversation as resolved.
Show resolved Hide resolved
#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,
mooori marked this conversation as resolved.
Show resolved Hide resolved
::near_sdk::env::predecessor_account_id()
);
if may_bypass {
__check_paused = false;
}
)
}
Loading