-
Notifications
You must be signed in to change notification settings - Fork 176
Accounts Refactor PR 3: Addinng more call messages to accounts module #2773
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
base: nikolai/accounts-refactor-part-2
Are you sure you want to change the base?
Changes from all commits
2596cc1
28e6aa7
cb18622
6b80962
b9f15d5
bbe926a
d010e9e
a614dd9
71e96cf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| use anyhow::{bail, Context as _}; | ||
| use anyhow::{anyhow, bail, Context as _}; | ||
| use schemars::JsonSchema; | ||
| use sov_modules_api::macros::{serialize, UniversalWallet}; | ||
| use sov_modules_api::{Context, CredentialId, Spec, StateReader, TxState}; | ||
|
|
@@ -9,15 +9,55 @@ use crate::{AccountOwnerKey, Accounts}; | |
| /// Represents the available call messages for interacting with the sov-accounts module. | ||
| #[derive(Debug, PartialEq, Eq, Clone, JsonSchema, UniversalWallet)] | ||
| #[serialize(Borsh, Serde)] | ||
| #[schemars(bound = "S::Address: ::schemars::JsonSchema", rename = "CallMessage")] | ||
| #[serde(rename_all = "snake_case")] | ||
| pub enum CallMessage { | ||
| pub enum CallMessage<S: Spec> { | ||
| /// Authorizes `credential_id` as a signer for the caller's address. | ||
| /// Fails if the credential has a legacy/custom account mapping or is | ||
| /// already authorized for the caller's address. | ||
| InsertCredentialId( | ||
| /// The credential id being authorized. | ||
| CredentialId, | ||
| ), | ||
|
|
||
| /// Authorizes `credential` to sign transactions that execute as `address`. | ||
| /// The caller must currently be signing as `address`. Fails if the tuple | ||
| /// is already authorized. | ||
| AddCredentialToAddress { | ||
| /// The address whose credential set is being extended. Must equal | ||
| /// `context.sender()`. | ||
| address: S::Address, | ||
| /// The credential being authorized for `address`. | ||
| credential: CredentialId, | ||
| }, | ||
|
|
||
| /// Revokes `credential` from `address`. The caller must currently be | ||
| /// signing as `address`. No orphan guard: revoking the last credential | ||
| /// succeeds and leaves `address` unspendable via this map. | ||
| RemoveCredentialFromAddress { | ||
| /// The address whose credential set is being reduced. Must equal | ||
| /// `context.sender()`. | ||
| address: S::Address, | ||
| /// The credential being revoked from `address`. | ||
| credential: CredentialId, | ||
| }, | ||
|
|
||
| /// Atomically swaps `old_credential` for `new_credential` on `address`. | ||
| /// Functionally equivalent to a `RemoveCredentialFromAddress` followed by | ||
| /// an `AddCredentialToAddress`, collapsed into a single call so the | ||
| /// caller does not have to authorize two transactions during a rotation. | ||
| /// The caller must currently be signing as `address`. | ||
| RotateCredentialOnAddress { | ||
| /// The address whose credential set is being rotated. Must equal | ||
| /// `context.sender()`. | ||
| address: S::Address, | ||
| /// The credential being revoked from `address`. Must be currently | ||
| /// authorized. | ||
| old_credential: CredentialId, | ||
| /// The credential being authorized for `address`. Must not already | ||
| /// be authorized. | ||
| new_credential: CredentialId, | ||
| }, | ||
| } | ||
|
|
||
| impl<S: Spec> Accounts<S> { | ||
|
|
@@ -27,18 +67,124 @@ impl<S: Spec> Accounts<S> { | |
| context: &Context<S>, | ||
| state: &mut impl TxState<S>, | ||
| ) -> anyhow::Result<()> { | ||
| if !self.enable_custom_account_mappings.get(state)?.expect( | ||
| "`enable_custom_account_mappings` should not be None; it must be set at genesis.", | ||
| ) { | ||
| bail!("Custom account mappings are disabled"); | ||
| } | ||
| self.ensure_custom_account_mappings_enabled(state)?; | ||
|
|
||
| self.exit_if_credential_exists(&new_credential_id, context.sender(), state)?; | ||
|
|
||
| self.authorize_credential(context.sender(), &new_credential_id, state)?; | ||
| Ok(()) | ||
| } | ||
|
|
||
| pub(crate) fn add_credential_to_address( | ||
| &mut self, | ||
| address: S::Address, | ||
| credential: CredentialId, | ||
| context: &Context<S>, | ||
| state: &mut impl TxState<S>, | ||
| ) -> anyhow::Result<()> { | ||
| self.ensure_custom_account_mappings_enabled(state)?; | ||
| self.ensure_caller_owns(&address, context)?; | ||
| self.ensure_credential_not_authorized(&address, &credential, state)?; | ||
|
|
||
| self.authorize_credential(&address, &credential, state)?; | ||
| Ok(()) | ||
| } | ||
|
|
||
| pub(crate) fn remove_credential_from_address( | ||
| &mut self, | ||
| address: S::Address, | ||
| credential: CredentialId, | ||
| context: &Context<S>, | ||
| state: &mut impl TxState<S>, | ||
| ) -> anyhow::Result<()> { | ||
| self.ensure_custom_account_mappings_enabled(state)?; | ||
| self.ensure_caller_owns(&address, context)?; | ||
|
|
||
| anyhow::ensure!( | ||
| self.is_authorized_for(&address, &credential, state) | ||
| .map_err(|err| anyhow!("Error raised while checking authorization: {err:?}"))?, | ||
| "CredentialId is not authorized for this address" | ||
| ); | ||
|
|
||
| self.revoke_credential(&address, &credential, state)?; | ||
| Ok(()) | ||
| } | ||
|
|
||
| pub(crate) fn rotate_credential_on_address( | ||
| &mut self, | ||
| address: S::Address, | ||
| old_credential: CredentialId, | ||
| new_credential: CredentialId, | ||
| context: &Context<S>, | ||
| state: &mut impl TxState<S>, | ||
| ) -> anyhow::Result<()> { | ||
| self.ensure_custom_account_mappings_enabled(state)?; | ||
| self.ensure_caller_owns(&address, context)?; | ||
|
|
||
| anyhow::ensure!( | ||
| self.is_authorized_for(&address, &old_credential, state) | ||
| .map_err(|err| anyhow!("Error raised while checking authorization: {err:?}"))?, | ||
| "CredentialId is not authorized for this address" | ||
| ); | ||
| self.ensure_credential_not_authorized(&address, &new_credential, state)?; | ||
|
|
||
| self.revoke_credential(&address, &old_credential, state)?; | ||
| self.authorize_credential(&address, &new_credential, state)?; | ||
| Ok(()) | ||
| } | ||
|
|
||
| fn revoke_credential( | ||
| &mut self, | ||
| address: &S::Address, | ||
| credential: &CredentialId, | ||
| state: &mut impl TxState<S>, | ||
| ) -> anyhow::Result<()> { | ||
| let key = AccountOwnerKey::new(*address, *credential); | ||
| // Write `false` explicitly so a subsequent fallback (legacy | ||
| // `accounts` mapping or canonical address) cannot re-authorize the | ||
| // tuple. | ||
| self.account_owners.set(&key, &false, state)?; | ||
|
|
||
| if self | ||
| .accounts | ||
| .get(credential, state) | ||
| .map_err(|err| anyhow!("Error raised while getting account: {err:?}"))? | ||
| .is_some_and(|account| account.addr == *address) | ||
| { | ||
| self.accounts.delete(credential, state)?; | ||
| } | ||
|
|
||
| Ok(()) | ||
| } | ||
|
|
||
| fn ensure_custom_account_mappings_enabled( | ||
| &self, | ||
| state: &mut impl StateReader<User>, | ||
| ) -> anyhow::Result<()> { | ||
| if !self | ||
| .enable_custom_account_mappings | ||
| .get(state) | ||
| .map_err(|err| anyhow!("Error reading enable_custom_account_mappings: {err:?}"))? | ||
| .expect( | ||
| "`enable_custom_account_mappings` should not be None; it must be set at genesis.", | ||
| ) | ||
| { | ||
| bail!("Custom account mappings are disabled"); | ||
| } | ||
| Ok(()) | ||
| } | ||
|
|
||
| /// Enforces that the caller is signing as `address`. The upstream | ||
| /// authorization path has already verified the caller controls a | ||
| /// credential authorized for `context.sender()`. | ||
| fn ensure_caller_owns(&self, address: &S::Address, context: &Context<S>) -> anyhow::Result<()> { | ||
| anyhow::ensure!( | ||
| context.sender() == address, | ||
| "Caller is not authorized to modify credentials for this address" | ||
| ); | ||
| Ok(()) | ||
| } | ||
|
|
||
| fn exit_if_credential_exists( | ||
| &self, | ||
| new_credential_id: &CredentialId, | ||
|
|
@@ -52,11 +198,22 @@ impl<S: Spec> Accounts<S> { | |
| .is_none(), | ||
| "New CredentialId already exists" | ||
| ); | ||
| self.ensure_credential_not_authorized(address, new_credential_id, state)?; | ||
| Ok(()) | ||
| } | ||
|
|
||
| fn ensure_credential_not_authorized( | ||
| &self, | ||
| address: &S::Address, | ||
| credential: &CredentialId, | ||
| state: &mut impl StateReader<User>, | ||
| ) -> anyhow::Result<()> { | ||
| anyhow::ensure!( | ||
| self.account_owners | ||
| .get(&AccountOwnerKey::new(*address, *new_credential_id), state) | ||
| !self | ||
| .account_owners | ||
| .get(&AccountOwnerKey::new(*address, *credential), state) | ||
| .context("Failed to read account owner")? | ||
| .is_none(), | ||
| .unwrap_or(false), | ||
| "CredentialId already authorized for this address" | ||
| ); | ||
| Ok(()) | ||
|
Comment on lines
+205
to
219
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (Refers to lines 205-220) 🚩 The Was this helpful? React with 👍 or 👎 to provide feedback. |
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -49,7 +49,7 @@ impl<S: Spec> Accounts<S> { | |
| } | ||
|
|
||
| /// Returns `true` if `credential_id` has an explicit `account_owners` | ||
| /// authorization for `address`. | ||
| /// authorization for `address` via explicit `account_owners` state. | ||
| pub fn is_authorized<ST: StateReader<User>>( | ||
| &self, | ||
| address: &S::Address, | ||
|
|
@@ -59,7 +59,7 @@ impl<S: Spec> Accounts<S> { | |
| Ok(self | ||
| .account_owners | ||
| .get(&AccountOwnerKey::new(*address, *credential_id), state)? | ||
| .is_some()) | ||
| .unwrap_or(false)) | ||
|
Comment on lines
59
to
+62
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📝 Info: Semantic change: The change from Was this helpful? React with 👍 or 👎 to provide feedback. |
||
| } | ||
|
|
||
| /// Returns `true` if `credential_id` is authorized to act as `address`. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Stale doc comment on The doc comment on This violates the AGENTS.md rule: "Keep docs/comments in sync with behavior." A developer reading this doc would incorrectly believe the legacy mapping is always authoritative, which matters for reasoning about the security-sensitive revocation feature. (Refers to lines 65-71) Was this helpful? React with 👍 or 👎 to provide feedback. |
||
|
|
@@ -75,15 +75,17 @@ impl<S: Spec> Accounts<S> { | |
| credential_id: &CredentialId, | ||
| state: &mut ST, | ||
| ) -> Result<bool, ST::Error> { | ||
| if let Some(account) = self.accounts.get(credential_id, state)? { | ||
| return Ok(account.addr == *address); | ||
| if let Some(is_authorized) = self | ||
| .account_owners | ||
| .get(&AccountOwnerKey::new(*address, *credential_id), state)? | ||
| { | ||
| return Ok(is_authorized); | ||
| } | ||
|
|
||
| let canonical_address: S::Address = (*credential_id).into(); | ||
| if canonical_address == *address { | ||
| return Ok(true); | ||
| if let Some(account) = self.accounts.get(credential_id, state)? { | ||
| return Ok(account.addr == *address); | ||
| } | ||
|
|
||
| self.is_authorized(address, credential_id, state) | ||
| Ok(S::Address::from(*credential_id) == *address) | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.