Skip to content

Commit 094881b

Browse files
authored
feat(Upgradable)!: use Acl for authorization (#85)
* feat(Upgradable)!: use Acl for authorization Previously authorization was based on `Ownable`. * test: update test/demo contracts * test: adapt tests to Acl based auth * test: extend coverage * test: use Acl plugin without `__acl` field * docs: update and extend * fix: make clippy happy * docs: example contract * chore: avoid unchecked method in example contract
1 parent 860ec9d commit 094881b

File tree

6 files changed

+438
-132
lines changed

6 files changed

+438
-132
lines changed

README.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,13 @@ Documentation of all methods provided by `Pausable` is available in the [definit
6161

6262
### [Upgradable](/near-plugins/src/upgradable.rs)
6363

64-
Allows a contract to be upgraded by the owner without requiring a full access key. Optionally a staging duration can be set, which defines the minimum duration that must pass before staged code can be deployed. The staging duration is a safety mechanism to protect users that interact with the contract, giving them time to opt-out before an unfavorable update is deployed.
64+
Allows a contract to be upgraded without requiring a full access key. Optionally a staging duration can be set, which defines the minimum duration that must pass before staged code can be deployed. The staging duration is a safety mechanism to protect users that interact with the contract, giving them time to opt-out before an unfavorable update is deployed.
6565

66-
Using the `Upgradable` plugin requires a contract to be `Ownable`.
66+
Using the `Upgradable` plugin requires a contract to be `AccessControllable` to handle authorization for calling `Upgradable` methods to stage or deploy updates (listed below).
6767

68-
To upgrade the contract first call `up_stage_code` passing the binary as first argument serialized as borsh. Then call `up_deploy_code`. Both functions must be called by the owner of the contract.
68+
To upgrade the contract, first call `up_stage_code` passing the binary as first argument serialized as borsh. Then call `up_deploy_code`.
6969

70-
To set a staging duration, call `up_stage_init_staging_duration`. After initialization the staging duration can be updated by calling `up_stage_update_staging_duration` followed by `up_apply_update_staging_duration`. Updating the staging duration is itself subject to a delay: at least the currently set staging duration must pass before a staged update can be applied. The functions mentioned in this paragraph must be called by the owner of the contract.
70+
To set a staging duration, call `up_init_staging_duration`. After initialization the staging duration can be updated by calling `up_stage_update_staging_duration` followed by `up_apply_update_staging_duration`. Updating the staging duration is itself subject to a delay: at least the currently set staging duration must pass before a staged update can be applied.
7171

7272
[This contract](/near-plugins-derive/tests/contracts/upgradable/src/lib.rs) provides an example of using `Upgradable`. It is compiled, deployed on chain and interacted with in [integration tests](/near-plugins-derive/tests/upgradable.rs).
7373

near-plugins-derive/src/upgradable.rs

+70-6
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,67 @@
11
use crate::utils::cratename;
2-
use darling::FromDeriveInput;
2+
use darling::util::PathList;
3+
use darling::{FromDeriveInput, FromMeta};
34
use proc_macro::{self, TokenStream};
45
use quote::quote;
56
use syn::{parse_macro_input, DeriveInput};
67

78
#[derive(FromDeriveInput, Default)]
89
#[darling(default, attributes(upgradable), forward_attrs(allow, doc, cfg))]
910
struct Opts {
11+
/// Storage prefix under which this plugin stores its state. If it is `None` the default value
12+
/// will be used.
1013
storage_prefix: Option<String>,
14+
/// Roles which are permitted to call protected methods.
15+
access_control_roles: AccessControlRoles,
16+
}
17+
18+
/// Specifies which `AccessControlRole`s may call protected methods.
19+
///
20+
/// All field names need to be passed to calls of `check_roles_specified_for!`.
21+
#[derive(Default, FromMeta, Debug)]
22+
#[darling(default)]
23+
struct AccessControlRoles {
24+
/// Grantess of these roles may successfully call `Upgradable::up_stage_code`.
25+
code_stagers: PathList,
26+
/// Grantess of these roles may successfully call `Upgradable::up_deploy_code`.
27+
code_deployers: PathList,
28+
/// Grantess of these roles may successfully call `Upgradable::up_init_staging_duration`.
29+
duration_initializers: PathList,
30+
/// Grantess of these roles may successfully call `Upgradable::up_stage_update_staging_duration`.
31+
duration_update_stagers: PathList,
32+
/// Grantess of these roles may successfully call `Upgradable::up_apply_update_staging_duration`.
33+
duration_update_appliers: PathList,
34+
}
35+
36+
impl AccessControlRoles {
37+
/// Validates the roles provided by the plugin user and panics if they are invalid.
38+
fn validate(&self) {
39+
// Ensure at least one role is provided for every field of `AccessControlRoles`.
40+
let mut missing_roles = vec![];
41+
42+
macro_rules! check_roles_specified_for {
43+
($($field_name:ident),+) => (
44+
$(
45+
if self.$field_name.len() == 0 {
46+
missing_roles.push(stringify!($field_name));
47+
}
48+
)+
49+
)
50+
}
51+
52+
check_roles_specified_for!(
53+
code_stagers,
54+
code_deployers,
55+
duration_initializers,
56+
duration_update_stagers,
57+
duration_update_appliers
58+
);
59+
assert!(
60+
missing_roles.is_empty(),
61+
"Specify access_control_roles for: {:?}",
62+
missing_roles,
63+
);
64+
}
1165
}
1266

1367
const DEFAULT_STORAGE_PREFIX: &str = "__up__";
@@ -23,6 +77,16 @@ pub fn derive_upgradable(input: TokenStream) -> TokenStream {
2377
let storage_prefix = opts
2478
.storage_prefix
2579
.unwrap_or_else(|| DEFAULT_STORAGE_PREFIX.to_string());
80+
let acl_roles = opts.access_control_roles;
81+
acl_roles.validate();
82+
83+
// To use fields of a struct inside `quote!`, they must be lifted into variables, see
84+
// https://github.com/dtolnay/quote/pull/88#pullrequestreview-180577592
85+
let acl_roles_code_stagers = acl_roles.code_stagers;
86+
let acl_roles_code_deployers = acl_roles.code_deployers;
87+
let acl_roles_duration_initializers = acl_roles.duration_initializers;
88+
let acl_roles_duration_update_stagers = acl_roles.duration_update_stagers;
89+
let acl_roles_duration_update_appliers = acl_roles.duration_update_appliers;
2690

2791
let output = quote! {
2892
/// Used to make storage prefixes unique. Not to be used directly,
@@ -93,7 +157,7 @@ pub fn derive_upgradable(input: TokenStream) -> TokenStream {
93157
}
94158
}
95159

96-
#[#cratename::only(owner)]
160+
#[#cratename::access_control_any(roles(#(#acl_roles_code_stagers),*))]
97161
fn up_stage_code(&mut self, #[serializer(borsh)] code: Vec<u8>) {
98162
if code.is_empty() {
99163
near_sdk::env::storage_remove(self.up_storage_key(__UpgradableStorageKey::Code).as_ref());
@@ -115,7 +179,7 @@ pub fn derive_upgradable(input: TokenStream) -> TokenStream {
115179
.map(|code| std::convert::TryInto::try_into(near_sdk::env::sha256(code.as_ref())).unwrap())
116180
}
117181

118-
#[#cratename::only(owner)]
182+
#[#cratename::access_control_any(roles(#(#acl_roles_code_deployers),*))]
119183
fn up_deploy_code(&mut self) -> near_sdk::Promise {
120184
let staging_timestamp = self.up_get_timestamp(__UpgradableStorageKey::StagingTimestamp)
121185
.unwrap_or_else(|| ::near_sdk::env::panic_str("Upgradable: staging timestamp isn't set"));
@@ -134,13 +198,13 @@ pub fn derive_upgradable(input: TokenStream) -> TokenStream {
134198
.deploy_contract(self.up_staged_code().unwrap_or_else(|| ::near_sdk::env::panic_str("Upgradable: No staged code")))
135199
}
136200

137-
#[#cratename::only(owner)]
201+
#[#cratename::access_control_any(roles(#(#acl_roles_duration_initializers),*))]
138202
fn up_init_staging_duration(&mut self, staging_duration: near_sdk::Duration) {
139203
near_sdk::require!(self.up_get_duration(__UpgradableStorageKey::StagingDuration).is_none(), "Upgradable: staging duration was already initialized");
140204
self.up_set_staging_duration_unchecked(staging_duration);
141205
}
142206

143-
#[#cratename::only(owner)]
207+
#[#cratename::access_control_any(roles(#(#acl_roles_duration_update_stagers),*))]
144208
fn up_stage_update_staging_duration(&mut self, staging_duration: near_sdk::Duration) {
145209
let current_staging_duration = self.up_get_duration(__UpgradableStorageKey::StagingDuration)
146210
.unwrap_or_else(|| ::near_sdk::env::panic_str("Upgradable: staging duration isn't initialized"));
@@ -150,7 +214,7 @@ pub fn derive_upgradable(input: TokenStream) -> TokenStream {
150214
self.up_set_timestamp(__UpgradableStorageKey::NewStagingDurationTimestamp, staging_duration_timestamp);
151215
}
152216

153-
#[#cratename::only(owner)]
217+
#[#cratename::access_control_any(roles(#(#acl_roles_duration_update_appliers),*))]
154218
fn up_apply_update_staging_duration(&mut self) {
155219
let staging_timestamp = self.up_get_timestamp(__UpgradableStorageKey::NewStagingDurationTimestamp)
156220
.unwrap_or_else(|| ::near_sdk::env::panic_str("Upgradable: No staged update"));
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,102 @@
1-
use near_plugins::{Ownable, Upgradable};
1+
use near_plugins::{access_control, AccessControlRole, AccessControllable, Upgradable};
22
use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
3+
use near_sdk::env;
4+
use near_sdk::serde::{Deserialize, Serialize};
35
use near_sdk::{near_bindgen, AccountId, Duration, PanicOnDefault};
46

5-
/// Deriving `Upgradable` requires the contract to be `Ownable.`
7+
/// Defines roles for access control of protected methods provided by the `Upgradable` plugin.
8+
#[derive(AccessControlRole, Deserialize, Serialize, Copy, Clone)]
9+
#[serde(crate = "near_sdk::serde")]
10+
pub enum Role {
11+
/// May successfully call any of the protected `Upgradable` methods since below it is passed to
12+
/// every attribute of `access_control_roles`.
13+
///
14+
/// Using this pattern grantees of a single role are authorized to call all `Upgradable`methods.
15+
DAO,
16+
/// May successfully call `Upgradable::up_stage_code`, but none of the other protected methods,
17+
/// since below is passed only to the `code_stagers` attribute.
18+
///
19+
/// Using this pattern grantees of a role are authorized to call only one particular protected
20+
/// `Upgradable` method.
21+
CodeStager,
22+
/// May successfully call `Upgradable::up_deploy_code`, but none of the other protected methods,
23+
/// since below is passed only to the `code_deployers` attribute.
24+
///
25+
/// Using this pattern grantees of a role are authorized to call only one particular protected
26+
/// `Upgradable` method.
27+
CodeDeployer,
28+
/// May successfully call `Upgradable` methods to initialize and update the staging duration
29+
/// since below it is passed to the attributes `duration_initializers`,
30+
/// `duration_update_stagers`, and `duration_update_appliers`.
31+
///
32+
/// Using this pattern grantees of a single role are authorized to call multiple (but not all)
33+
/// protected `Upgradable` methods.
34+
DurationManager,
35+
}
36+
37+
/// Deriving `Upgradable` requires the contract to be `AccessControllable`.
38+
///
39+
/// Variants of `Role` are passed to `upgradables`'s `access_control_roles` attribute to specify
40+
/// which roles are authorized to successfully call protected `Upgradable` methods. A protected
41+
/// method panics if it is called by an account which is not a grantee of at least one of the
42+
/// whitelisted roles.
43+
#[access_control(role_type(Role))]
644
#[near_bindgen]
7-
#[derive(Ownable, Upgradable, PanicOnDefault, BorshDeserialize, BorshSerialize)]
45+
#[derive(Upgradable, PanicOnDefault, BorshDeserialize, BorshSerialize)]
46+
#[upgradable(access_control_roles(
47+
code_stagers(Role::CodeStager, Role::DAO),
48+
code_deployers(Role::CodeDeployer, Role::DAO),
49+
duration_initializers(Role::DurationManager, Role::DAO),
50+
duration_update_stagers(Role::DurationManager, Role::DAO),
51+
duration_update_appliers(Role::DurationManager, Role::DAO),
52+
))]
853
pub struct Contract;
954

1055
#[near_bindgen]
1156
impl Contract {
12-
/// Parameter `owner` allows setting the owner in the constructor if an `AccountId` is provided.
13-
/// If `owner` is `None`, no owner will be set in the constructor. After contract initialization
14-
/// it is possible to set an owner with `Ownable::owner_set`.
57+
/// Makes the contract itself `AccessControllable` super admin to allow it granting and revoking
58+
/// permissions. If parameter `dao` is `Some(account_id)`, then `account_id` is granted
59+
/// `Role::DAO`. After initialization permissions can be managed using the methods provided by
60+
/// `AccessControllable`.
1561
///
1662
/// Parameter `staging_duration` allows initializing the time that is required to pass between
1763
/// staging and deploying code. This delay provides a safety mechanism to protect users against
1864
/// unfavorable or malicious code upgrades. If `staging_duration` is `None`, no staging duration
1965
/// will be set in the constructor. It is possible to set it later using
2066
/// `Upgradable::up_init_staging_duration`. If no staging duration is set, it defaults to zero,
2167
/// allowing immediate deployments of staged code.
22-
///
23-
/// Since this constructor uses an `*_unchecked` method, it should be combined with code
24-
/// deployment in a batch transaction.
2568
#[init]
26-
pub fn new(owner: Option<AccountId>, staging_duration: Option<Duration>) -> Self {
69+
pub fn new(dao: Option<AccountId>, staging_duration: Option<Duration>) -> Self {
2770
let mut contract = Self;
2871

29-
// Optionally set the owner.
30-
if owner.is_some() {
31-
contract.owner_set(owner);
72+
// Make the contract itself access control super admin, allowing it to grant and revoke
73+
// permissions.
74+
near_sdk::require!(
75+
contract.acl_init_super_admin(env::current_account_id()),
76+
"Failed to initialize super admin",
77+
);
78+
79+
// Optionally grant `Role::DAO`.
80+
if let Some(account_id) = dao {
81+
let res = contract.acl_grant_role(Role::DAO.into(), account_id);
82+
assert_eq!(Some(true), res, "Failed to grant role");
3283
}
3384

3485
// Optionally initialize the staging duration.
3586
if let Some(staging_duration) = staging_duration {
36-
// The owner (set above) might be an account other than the contract itself. In that
37-
// case `Upgradable::up_init_staging_duration` would fail, since only the Owner may call
38-
// it successfully. Therefore we are using an (internal) unchecked method here.
39-
//
40-
// Avoid using `*_unchecked` functions in public contract methods that are not protected
41-
// by access control. Otherwise there is a risk of unwanted state changes carried out by
42-
// malicious users. For this example, we assume the constructor is called in a batch
43-
// transaction together with code deployment.
44-
contract.up_set_staging_duration_unchecked(staging_duration);
87+
// Temporarily grant `Role::DurationManager` to the contract to authorize it for
88+
// initializing the staging duration. Granting and revoking the role is possible since
89+
// the contract was made super admin above.
90+
contract.acl_grant_role(Role::DurationManager.into(), env::current_account_id());
91+
contract.up_init_staging_duration(staging_duration);
92+
contract.acl_revoke_role(Role::DurationManager.into(), env::current_account_id());
4593
}
4694

4795
contract
4896
}
97+
98+
/// Function to verify the contract was deployed and initialized successfully.
99+
pub fn is_set_up(&self) -> bool {
100+
true
101+
}
49102
}

near-plugins-derive/tests/contracts/upgradable_2/src/lib.rs

+22-2
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,35 @@
11
//! A simple contract to be deployed via `Upgradable`.
22
3-
use near_plugins::{Ownable, Upgradable};
3+
use near_plugins::{access_control, AccessControlRole, AccessControllable, Upgradable};
44
use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
5+
use near_sdk::serde::{Deserialize, Serialize};
56
use near_sdk::{near_bindgen, PanicOnDefault};
67

8+
/// Roles correspond to those defined in the initial contract `../upgradable`, to make permissions
9+
/// granted before the upgrade remain valid.
10+
#[derive(AccessControlRole, Deserialize, Serialize, Copy, Clone)]
11+
#[serde(crate = "near_sdk::serde")]
12+
pub enum Role {
13+
DAO,
14+
CodeStager,
15+
CodeDeployer,
16+
DurationManager,
17+
}
18+
719
/// The struct is the same as in the initial contract `../upgradable`, so no [state migration] is
820
/// required.
921
///
1022
/// [state migration]: https://docs.near.org/develop/upgrade#migrating-the-state
23+
#[access_control(role_type(Role))]
1124
#[near_bindgen]
12-
#[derive(Ownable, Upgradable, PanicOnDefault, BorshDeserialize, BorshSerialize)]
25+
#[derive(Upgradable, PanicOnDefault, BorshDeserialize, BorshSerialize)]
26+
#[upgradable(access_control_roles(
27+
code_stagers(Role::CodeStager, Role::DAO),
28+
code_deployers(Role::CodeDeployer, Role::DAO),
29+
duration_initializers(Role::DurationManager, Role::DAO),
30+
duration_update_stagers(Role::DurationManager, Role::DAO),
31+
duration_update_appliers(Role::DurationManager, Role::DAO),
32+
))]
1333
pub struct Contract;
1434

1535
#[near_bindgen]

0 commit comments

Comments
 (0)