Skip to content
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
7 changes: 4 additions & 3 deletions integration/tests/multisig_permissions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use polymesh_api::{
authorization::AuthorizationData,
secondary_key::Signatory,
settlement::{VenueDetails, VenueType},
identity_id::PortfolioName,
},
TransactionResults, WrappedCall,
};
Expand Down Expand Up @@ -610,10 +611,10 @@ async fn ms_needs_to_be_linked_to_an_identity() -> Result<()> {
let mut res = ms.leave_did().await?;
res.wait_in_block().await?;

// Prepare `system.remark` call.
let remark_call = tester.api.call().system().remark(vec![])?;
// Prepare `create_portfolio` call.
let call = tester.api.call().portfolio().create_portfolio(PortfolioName(Vec::new()))?;
// Shouldn't be allowed, since the MS doesn't have a DID.
let res = ms.run_proposal(remark_call).await;
let res = ms.run_proposal(call).await;
assert!(res.is_err());

Ok(())
Expand Down
97 changes: 74 additions & 23 deletions pallets/identity/src/keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -891,23 +891,33 @@ impl<T: Config> Pallet<T> {
Ok(())
}

/// Ensures that `origin`'s key is the primary key of a DID.
/// Ensures that `origin`'s key is the primary key of a DID and has a valid CDD claim.
/// Returns the caller's account and DID.
pub fn ensure_primary_key(
origin: T::RuntimeOrigin,
) -> Result<(T::AccountId, IdentityId), DispatchError> {
let sender = ensure_signed(origin)?;
let key_rec = KeyRecords::<T>::get(&sender)
.ok_or(pallet_permissions::Error::<T>::UnauthorizedCaller)?;
let did = key_rec.is_primary_key().ok_or(Error::<T>::KeyNotAllowed)?;
ensure!(
Self::has_valid_cdd(did),
Error::<T>::UnauthorizedCallerDidMissingCdd
);
Ok((sender, did))
}

/// Ensures that `origin`'s key is linked to a DID and returns both.
/// Ensures that `origin`'s key is linked to a DID and has a valid CDD claim.
/// Returns the caller's account and DID.
pub fn ensure_did(
origin: T::RuntimeOrigin,
) -> Result<(T::AccountId, IdentityId), DispatchError> {
let sender = ensure_signed(origin)?;
let did = Self::get_identity(&sender).ok_or(Error::<T>::MissingIdentity)?;
ensure!(
Self::has_valid_cdd(did),
Error::<T>::UnauthorizedCallerDidMissingCdd
);
Ok((sender, did))
}

Expand Down Expand Up @@ -966,35 +976,76 @@ impl<T: Config> Pallet<T> {
}
Ok(())
}

/// Checks if the caller is permissioned to call the current extrinsic skipping CDD checks.
/// If `must_be_primary_key` ensures that the caller is a primary key.
pub fn ensure_valid_origin(
origin: T::RuntimeOrigin,
must_be_primary_key: bool,
) -> Result<(T::AccountId, IdentityId), DispatchError> {
let caller_acc = ensure_signed(origin)?;
let account_data = pallet_permissions::Pallet::<T>::ensure_valid_origin_permissions(
&caller_acc,
must_be_primary_key,
)?;
Ok((caller_acc, account_data.primary_did))
}
}

impl<T: Config> CheckAccountCallPermissions<T::AccountId> for Pallet<T> {
// For weighting purposes, the function reads 4 storage values.
fn check_account_call_permissions(
who: &T::AccountId,
pallet_name: impl FnOnce() -> PalletName,
function_name: impl FnOnce() -> ExtrinsicName,
) -> Option<AccountCallPermissionsData<T::AccountId>> {
let data = |did, secondary_key| AccountCallPermissionsData {
primary_did: did,
secondary_key,
};
) -> Result<AccountCallPermissionsData<T::AccountId>, DispatchError> {
let account_call_permissions_data =
Self::ensure_valid_origin_permissions(who, false, pallet_name, function_name)?;

match KeyRecords::<T>::get(who)? {
// Primary keys do not have / require further permission checks.
KeyRecord::PrimaryKey(did) => Some(data(did, None)),
// Secondary Key. Ensure DID isn't frozen + key has sufficient permissions.
KeyRecord::SecondaryKey(did) if !IsDidFrozen::<T>::get(&did) => {
let permissions = Self::get_key_permissions(who);
let sk = SecondaryKey {
key: who.clone(),
permissions,
};
sk.has_extrinsic_permission(&pallet_name(), &function_name())
.then(|| data(did, Some(sk)))
}
// DIDs with frozen secondary keys, AKA frozen DIDs, are not permitted to call extrinsics.
_ => None,
ensure!(
Self::has_valid_cdd(account_call_permissions_data.primary_did),
Error::<T>::UnauthorizedCallerDidMissingCdd
);

Ok(account_call_permissions_data)
}

fn ensure_valid_origin_permissions(
caller_acc: &T::AccountId,
must_be_primary_key: bool,
pallet_name: impl FnOnce() -> PalletName,
function_name: impl FnOnce() -> ExtrinsicName,
) -> Result<AccountCallPermissionsData<T::AccountId>, DispatchError> {
let key_record = KeyRecords::<T>::get(&caller_acc).ok_or(Error::<T>::MissingIdentity)?;

if must_be_primary_key {
let did = key_record
.is_primary_key()
.ok_or(Error::<T>::KeyNotAllowed)?;
return Ok(AccountCallPermissionsData::new(did, None));
}

if let KeyRecord::PrimaryKey(did) = key_record {
return Ok(AccountCallPermissionsData::new(did, None));
}

let did = key_record
.is_secondary_key()
.ok_or(Error::<T>::KeyNotAllowed)?;

ensure!(
!IsDidFrozen::<T>::get(&did),
Error::<T>::UnauthorizedCallerFrozenDid
);

let permissions = Self::get_key_permissions(&caller_acc);
ensure!(
permissions
.extrinsic
.sufficient_for(&pallet_name(), &function_name()),
Error::<T>::UnauthorizedCallerMissingPermissions
);
let sk = SecondaryKey::new(caller_acc.clone(), permissions);

Ok(AccountCallPermissionsData::new(did, Some(sk)))
}
}
6 changes: 6 additions & 0 deletions pallets/identity/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1088,6 +1088,12 @@ pub mod pallet {
/// Auth identified by an `auth_id` for a given `target` does not exist.
/// The `target` might be wrong or the `auth_id` was never created at all.
InvalidAuthorization,
/// Frozen secondary key.
UnauthorizedCallerFrozenDid,
/// The DID is missing a CDD claim.
UnauthorizedCallerDidMissingCdd,
/// The key does not have permissions to execute the extrinsic.
UnauthorizedCallerMissingPermissions,
}
}

Expand Down
30 changes: 10 additions & 20 deletions pallets/multisig/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,7 @@ use sp_runtime::traits::{Dispatchable, Hash};
use sp_std::convert::TryFrom;
use sp_std::prelude::*;

use pallet_identity::{
CddAuthForPrimaryKeyRotation, Config as IdentityConfig, PermissionedCallOriginData,
};
use pallet_identity::{CddAuthForPrimaryKeyRotation, Config as IdentityConfig};
use pallet_permissions::with_call_metadata;
use polymesh_primitives::multisig::{ProposalState, ProposalVoteCount};
use polymesh_primitives::{
Expand Down Expand Up @@ -236,25 +234,17 @@ pub mod pallet {
sigs_required: u64,
permissions: Option<Permissions>,
) -> DispatchResultWithPostInfo {
let (caller, caller_did, permissions) = match permissions {
Some(permissions) => {
// Only the primary key can add a secondary key with custom permissions.
let (caller, did) = IdentityPallet::<T>::ensure_primary_key(origin)?;
(caller, did, permissions)
}
None => {
// Default to empty permissions for the new secondary key.
let PermissionedCallOriginData {
sender: caller,
primary_did,
..
} = IdentityPallet::<T>::ensure_origin_call_permissions(origin)?;
(caller, primary_did, Permissions::empty())
}
};
let (caller, caller_did) =
IdentityPallet::<T>::ensure_valid_origin(origin, permissions.is_some())?;
let signers_len: u64 = u64::try_from(signers.len()).unwrap_or_default();
Self::ensure_sigs_in_bounds(signers_len, sigs_required)?;
Self::base_create_multisig(caller, caller_did, signers, sigs_required, permissions)?;
Self::base_create_multisig(
caller,
caller_did,
signers,
sigs_required,
permissions.unwrap_or(Permissions::empty()),
)?;
Ok(().into())
}

Expand Down
40 changes: 38 additions & 2 deletions pallets/permissions/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,19 @@ pub mod pallet {
pub secondary_key: Option<SecondaryKey<AccountId>>,
}

impl<AccountId> AccountCallPermissionsData<AccountId> {
/// Constructs a new `AccountCallPermissionsData`.
pub fn new(
primary_did: IdentityId,
secondary_key: Option<SecondaryKey<AccountId>>,
) -> Self {
Self {
primary_did,
secondary_key,
}
}
}

/// A permission checker for calls from accounts to extrinsics.
pub trait CheckAccountCallPermissions<AccountId> {
/// Checks whether `who` can call the current extrinsic represented by `pallet_name` and
Expand All @@ -77,7 +90,17 @@ pub mod pallet {
who: &AccountId,
pallet_name: impl FnOnce() -> PalletName,
function_name: impl FnOnce() -> ExtrinsicName,
) -> Option<AccountCallPermissionsData<AccountId>>;
) -> Result<AccountCallPermissionsData<AccountId>, DispatchError>;

/// Checks whether `who` can call the current extrinsic represented by `pallet_name` and
/// `function_name` skipping CDD checks. If `must_be_primary_key` is true, ensures that
/// the caller is a primary key.
fn ensure_valid_origin_permissions(
who: &AccountId,
must_be_primary_key: bool,
pallet_name: impl FnOnce() -> PalletName,
function_name: impl FnOnce() -> ExtrinsicName,
) -> Result<AccountCallPermissionsData<AccountId>, DispatchError>;
}

#[pallet::pallet]
Expand Down Expand Up @@ -110,7 +133,20 @@ pub mod pallet {
|| CurrentPalletName::<T>::get(),
|| CurrentDispatchableName::<T>::get(),
)
.ok_or_else(|| Error::<T>::UnauthorizedCaller.into())
}

/// Checks if the caller is permissioned to call the current extrinsic skipping CDD checks.
/// If `must_be_primary_key` ensures that the caller is a primary key.
pub fn ensure_valid_origin_permissions(
who: &T::AccountId,
must_be_primary_key: bool,
) -> Result<AccountCallPermissionsData<T::AccountId>, DispatchError> {
T::Checker::ensure_valid_origin_permissions(
who,
must_be_primary_key,
|| CurrentPalletName::<T>::get(),
|| CurrentDispatchableName::<T>::get(),
)
}
}
}
Expand Down
6 changes: 1 addition & 5 deletions pallets/relayer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -314,11 +314,7 @@ impl<T: Config> Pallet<T> {
user_key: T::AccountId,
polyx_limit: Balance,
) -> DispatchResult {
let PermissionedCallOriginData {
sender: paying_key,
primary_did: paying_did,
..
} = <Identity<T>>::ensure_origin_call_permissions(origin)?;
let (paying_key, paying_did) = Identity::<T>::ensure_valid_origin(origin, false)?;

// Create authorization for `paying_key` to subsidise the `user_key`, with `polyx_limit` POLYX.
Self::unverified_add_auth_for_paying_key(paying_did, user_key, paying_key, polyx_limit)?;
Expand Down
43 changes: 5 additions & 38 deletions pallets/runtime/common/src/fee_details.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,42 +34,15 @@ where
/// However, this does not set the payer context since that is meant to remain constant
/// throughout the transaction. This function can also be used to simply check CDD and update identity context.
fn get_valid_payer(call: &C, caller: &AccountId) -> ValidPayerResult {
// Check if the `did` has a valid CDD claim.
let check_did_cdd = |did: &IdentityId| {
if Identity::<A>::has_valid_cdd(*did) {
Ok(None)
} else {
CDD_REQUIRED
}
};

// Check if the `did` has a valid CDD claim
// and return the primary key as the payer.
let did_primary_pays = |did: &IdentityId| {
check_did_cdd(did)?;
Ok(Identity::<A>::get_primary_key(*did))
};

// Check if the `caller` key has a DID and a valid CDD claim.
// The caller is also the payer.
let caller_pays = |caller: &AccountId| {
match Identity::<A>::get_identity(caller) {
Some(did) => {
check_did_cdd(&did)?;
Ok(Some(caller.clone()))
}
// Return if there's no DID.
None => MISSING_ID,
}
};
// Return the primary key as the payer.
let did_primary_pays = |did: &IdentityId| Ok(Identity::<A>::get_primary_key(*did));

let handle_multisig = |multisig: &AccountId, caller: &AccountId| {
if pallet_multisig::MultiSigSigners::<A>::contains_key(multisig, caller) {
let ms_pays = caller_pays(multisig)?;
// If the `multisig` has a paying DID, then it's primary key pays.
match pallet_multisig::Pallet::<A>::get_paying_did(multisig) {
Some(did) => Ok(Identity::<A>::get_primary_key(did)),
None => Ok(ms_pays),
None => Ok(Some(multisig.clone())),
}
} else {
MISSING_ID
Expand Down Expand Up @@ -164,10 +137,8 @@ where
| pallet_multisig::Call::approve { multisig, .. }
| pallet_multisig::Call::reject { multisig, .. },
)) => handle_multisig(multisig, caller),
// All other calls.
//
// The external account must directly be linked to an identity with valid CDD.
_ => caller_pays(caller),
// All other calls
_ => Ok(Some(caller.clone())),
}
}

Expand Down Expand Up @@ -201,10 +172,6 @@ enum CallType {

type ValidPayerResult = Result<Option<AccountId>, InvalidTransaction>;

const CDD_REQUIRED: ValidPayerResult = Err(InvalidTransaction::Custom(
TransactionError::CddRequired as u8,
));

const MISSING_ID: ValidPayerResult = Err(InvalidTransaction::Custom(
TransactionError::MissingIdentity as u8,
));
Expand Down
2 changes: 1 addition & 1 deletion pallets/runtime/tests/src/contracts_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ fn chain_extension_calls() {
None,
register_ticker_input.clone()
),
pallet_permissions::Error::<TestStorage>::UnauthorizedCaller,
pallet_identity::Error::<TestStorage>::UnauthorizedCallerMissingPermissions,
));
// Successfull call
assert_ok!(Identity::set_secondary_key_permissions(
Expand Down
Loading