diff --git a/CHANGELOG.md b/CHANGELOG.md index c54a65832e..9ec00e6d19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ # UNRELEASED +### feat: impersonating sender of requests to a local PocketIC instance + +`dfx canister call`, `dfx canister status`, and `dfx canister update-settings` take +an additional CLI argument `--impersonate` to specify a principal +on behalf of which requests to a local PocketIC instance are sent. + ### feat: `dfx canister [create|update-settings] --wasm-memory-threshold` This adds support for the WASM memory threshold, used in conjunction with `--wasm-memory-limit`. diff --git a/docs/cli-reference/dfx-canister.mdx b/docs/cli-reference/dfx-canister.mdx index 301d26ec93..7cefb31e7e 100644 --- a/docs/cli-reference/dfx-canister.mdx +++ b/docs/cli-reference/dfx-canister.mdx @@ -145,6 +145,7 @@ You can use the following options with the `dfx canister call` command. | `--argument-file ` | Specifies the file from which to read the argument to pass to the method. Stdin may be referred to as `-`. | | `--async` | Specifies not to wait for the result of the call to be returned by polling the replica. Instead return a response ID. | | `--candid ` | Provide the .did file with which to decode the response. Overrides value from dfx.json for project canisters. | +| `--impersonate ` | Specifies a principal on behalf of which requests to a local PocketIC instance are sent. | | `--output ` | Specifies the output format to use when displaying a method’s return result. The valid values are `idl`, 'json', `pp` and `raw`. The `pp` option is equivalent to `idl`, but is pretty-printed. | | `--query` | Sends a query request instead of an update request. For information about the difference between query and update calls, see [Canisters include both program and state](/docs/current/concepts/canisters-code#canister-state). | | `--random ` | Specifies the config for generating random arguments. | @@ -1005,10 +1006,11 @@ dfx canister status [--all | canister_name] You can use the following arguments with the `dfx canister status` command. -| Argument | Description | -|-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------| -| `--all` | Returns status information for all of the canisters configured in the `dfx.json` file. Note that you must specify `--all` or an individual canister name. | -| `canister_name` | Specifies the name of the canister you want to return information for. Note that you must specify either a canister name or the `--all` option. | +| Argument | Description | +|-----------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------| +| `--all` | Returns status information for all of the canisters configured in the `dfx.json` file. Note that you must specify `--all` or an individual canister name. | +| `--impersonate ` | Specifies a principal on behalf of which requests to a local PocketIC instance are sent. | +| `canister_name` | Specifies the name of the canister you want to return information for. Note that you must specify either a canister name or the `--all` option. | ### Examples @@ -1142,6 +1144,7 @@ You can specify the following options for the `dfx canister update-settings` com | `--add-log-viewer ` | Add a principal to the list of log viewers of the canister. Can be specified more than once to add multiple log viewers. If current log visibility is `public` or `controllers`, it will be changed to the custom allowed viewer list. | | `-c`, `--compute-allocation ` | Specifies the canister's compute allocation. This should be a percent in the range [0..100]. | | `--confirm-very-long-freezing-threshold` | Freezing thresholds above ~1.5 years require this option as confirmation. | +| `--impersonate ` | Specifies a principal on behalf of which requests to a local PocketIC instance are sent. | | `--set-controller ` | Specifies the identity name or the principal of the new controller. Can be specified more than once, indicating the canister will have multiple controllers. If any controllers are set with this parameter, any other controllers will be removed. | | `--set-log-viewer ` | Specifies the the principal of the log viewer of the canister. Can be specified more than once, indicating the canister will have multiple log viewers. If any log viewers are set with this parameter, any other log viewers will be removed. If current log visibility is `public` or `controllers`, it will be changed to the custom allowed viewer list. | | `--memory-allocation ` | Specifies how much memory the canister is allowed to use in total. This should be a value in the range [0..12 GiB]. A setting of 0 means the canister will have access to memory on a “best-effort” basis: It will only be charged for the memory it uses, but at any point in time may stop running if it tries to allocate more memory when there isn’t space available on the subnet. | diff --git a/e2e/tests-dfx/call.bash b/e2e/tests-dfx/call.bash index 8122d77868..e1c18fc82c 100644 --- a/e2e/tests-dfx/call.bash +++ b/e2e/tests-dfx/call.bash @@ -298,3 +298,88 @@ teardown() { assert_command dfx canister call inter2_mo read assert_match '(8 : nat)' } + +function impersonate_sender() { + IDENTITY_PRINCIPAL="${1}" + + dfx_start + assert_command dfx deploy hello_backend + CANISTER_ID="$(dfx canister id hello_backend)" + + # set the management canister as the only controller + assert_command dfx canister update-settings hello_backend --set-controller "${IDENTITY_PRINCIPAL}" --yes + + # updating settings now fails because the default identity does not control the canister anymore + assert_command_fail dfx canister update-settings hello_backend --freezing-threshold 0 + assert_contains "Only controllers of canister $CANISTER_ID can call ic00 method update_settings" + + # updating settings succeeds when impersonating the management canister as the sender + assert_command dfx canister update-settings hello_backend --freezing-threshold 0 --impersonate "${IDENTITY_PRINCIPAL}" + + # test management canister call failure (setting memory allocation to a low value) + assert_command_fail dfx canister update-settings hello_backend --memory-allocation 1 --impersonate "${IDENTITY_PRINCIPAL}" + assert_contains "Management canister call failed: IC0402: Canister was given 1 B memory allocation but at least" + + # canister status fails because the default identity does not control the canister anymore + assert_command_fail dfx canister status hello_backend + assert_contains "Only controllers of canister $CANISTER_ID can call ic00 method canister_status" + + # canister status succeeds when impersonating the management canister as the sender + assert_command dfx canister status hello_backend --impersonate "${IDENTITY_PRINCIPAL}" + assert_contains "Controllers: ${IDENTITY_PRINCIPAL}" + assert_contains "Freezing threshold: 0" + + # freeze the canister + assert_command dfx canister update-settings hello_backend --freezing-threshold 9223372036854775808 --confirm-very-long-freezing-threshold --impersonate "${IDENTITY_PRINCIPAL}" + + # test management canister call submission failure + assert_command_fail dfx canister status hello_backend --impersonate "${IDENTITY_PRINCIPAL}" + assert_contains "Failed to submit management canister call: IC0207: Canister $CANISTER_ID is out of cycles" + + # test update call submission failure + assert_command_fail dfx canister call aaaaa-aa canister_status "(record { canister_id=principal\"$CANISTER_ID\" })" --update --impersonate "${IDENTITY_PRINCIPAL}" + assert_contains "Failed to submit canister call: IC0207: Canister $CANISTER_ID is out of cycles" + + # test async call submission failure + assert_command_fail dfx canister call aaaaa-aa canister_status "(record { canister_id=principal\"$CANISTER_ID\" })" --async --impersonate "${IDENTITY_PRINCIPAL}" + assert_contains "Failed to submit canister call: IC0207: Canister $CANISTER_ID is out of cycles" + + # unfreeze the canister + assert_command dfx canister update-settings hello_backend --freezing-threshold 0 --impersonate "${IDENTITY_PRINCIPAL}" + + # test update call failure + assert_command_fail dfx canister call aaaaa-aa delete_canister "(record { canister_id=principal\"$CANISTER_ID\" })" --update --impersonate "${IDENTITY_PRINCIPAL}" + assert_contains "Canister call failed: IC0510: Canister $CANISTER_ID must be stopped before it is deleted." + + # test update call + assert_command dfx canister call aaaaa-aa start_canister "(record { canister_id=principal\"$CANISTER_ID\" })" --update --impersonate "${IDENTITY_PRINCIPAL}" + assert_contains "()" + + # test async call + assert_command dfx canister call aaaaa-aa canister_status "(record { canister_id=principal\"$CANISTER_ID\" })" --async --impersonate "${IDENTITY_PRINCIPAL}" + assert_contains "Request ID:" + + # test query call failure + assert_command_fail dfx canister call aaaaa-aa fetch_canister_logs "(record { canister_id=principal\"$CANISTER_ID\" })" --query --impersonate "$CANISTER_ID" + assert_contains "Failed to perform query call: IC0406: Caller $CANISTER_ID is not allowed to query ic00 method fetch_canister_logs" + + # test query call + assert_command dfx canister call aaaaa-aa fetch_canister_logs "(record { canister_id=principal\"$CANISTER_ID\" })" --query --impersonate "${IDENTITY_PRINCIPAL}" + assert_contains "(record { 1_754_302_831 = vec {} })" +} + +@test "impersonate management canister as sender" { + [[ ! "$USE_POCKETIC" ]] && skip "skipped for replica: impersonating sender is only supported for PocketIC" + + impersonate_sender "aaaaa-aa" +} + +@test "impersonate new random identity as sender" { + [[ ! "$USE_POCKETIC" ]] && skip "skipped for replica: impersonating sender is only supported for PocketIC" + + dfx identity new impersonated_identity --storage-mode plaintext + IDENTITY_PRINCIPAL="$(dfx --identity impersonated_identity identity get-principal)" + dfx identity remove impersonated_identity + + impersonate_sender "${IDENTITY_PRINCIPAL}" +} diff --git a/src/dfx-core/src/canister/mod.rs b/src/dfx-core/src/canister/mod.rs index 5e386c338c..e57d7c16a0 100644 --- a/src/dfx-core/src/canister/mod.rs +++ b/src/dfx-core/src/canister/mod.rs @@ -71,6 +71,9 @@ YOU WILL LOSE ALL DATA IN THE CANISTER. .await .map_err(CanisterInstallError::InstallWasmError) } + CallSender::Impersonate(_) => { + unreachable!("Impersonating sender when installing canisters is not supported.") + } CallSender::Wallet(wallet_id) => { let wallet = build_wallet_canister(*wallet_id, agent).await?; let install_args = CanisterInstall { diff --git a/src/dfx-core/src/config/model/local_server_descriptor.rs b/src/dfx-core/src/config/model/local_server_descriptor.rs index f9eaf562f9..e2a6f9af62 100644 --- a/src/dfx-core/src/config/model/local_server_descriptor.rs +++ b/src/dfx-core/src/config/model/local_server_descriptor.rs @@ -360,7 +360,6 @@ impl LocalServerDescriptor { logger: Option<&Logger>, ) -> Result, NetworkConfigError> { let replica_port_path = self.replica_port_path(); - let pocketic_port_path = self.pocketic_port_path(); match read_port_from(&replica_port_path)? { Some(port) => { if let Some(logger) = logger { @@ -368,15 +367,31 @@ impl LocalServerDescriptor { } Ok(Some(port)) } - None => match read_port_from(&pocketic_port_path)? { - Some(port) => { - if let Some(logger) = logger { - info!(logger, "Found local PocketIC running on port {}", port); - } - Ok(Some(port)) + None => { + let port = self + .get_running_pocketic_port(logger)? + .or(self.replica.port); + Ok(port) + } + } + } + + /// Gets the port of a local PocketIC instance. + /// + /// # Prerequisites + /// - A local PocketIC instance needs to be running, e.g. with `dfx start --pocketic`. + pub fn get_running_pocketic_port( + &self, + logger: Option<&Logger>, + ) -> Result, NetworkConfigError> { + match read_port_from(&self.pocketic_port_path())? { + Some(port) => { + if let Some(logger) = logger { + info!(logger, "Found local PocketIC running on port {}", port); } - None => Ok(self.replica.port), - }, + Ok(Some(port)) + } + None => Ok(None), } } } diff --git a/src/dfx-core/src/identity/mod.rs b/src/dfx-core/src/identity/mod.rs index 8fa5732fc4..580353e1ba 100644 --- a/src/dfx-core/src/identity/mod.rs +++ b/src/dfx-core/src/identity/mod.rs @@ -307,6 +307,7 @@ impl AsRef for Identity { #[derive(Debug, PartialEq, Eq, Copy, Clone)] pub enum CallSender { SelectedId, + Impersonate(Principal), Wallet(Principal), } diff --git a/src/dfx/Cargo.toml b/src/dfx/Cargo.toml index 1a5d37c050..16086c95be 100644 --- a/src/dfx/Cargo.toml +++ b/src/dfx/Cargo.toml @@ -86,6 +86,7 @@ os_str_bytes = { version = "6.3.0", features = ["conversions"] } patch = "0.7.0" pem.workspace = true petgraph = "0.6.0" +pocket-ic = { git = "https://github.com/dfinity/ic", rev = "3e24396441e4c7380928d4e8b4ccff7de77d0e7e" } rand = "0.8.5" regex = "1.5.5" reqwest = { workspace = true, features = ["blocking", "json"] } @@ -125,9 +126,6 @@ ci_info = "0.14" [target.'cfg(windows)'.dependencies] junction = "1.0.0" -[target.'cfg(unix)'.dependencies] -pocket-ic = { git = "https://github.com/dfinity/ic", rev = "3e24396441e4c7380928d4e8b4ccff7de77d0e7e" } - [dev-dependencies] env_logger = "0.10" proptest = "1.0" diff --git a/src/dfx/src/commands/canister/call.rs b/src/dfx/src/commands/canister/call.rs index aecf4d9a56..8a8249e19f 100644 --- a/src/dfx/src/commands/canister/call.rs +++ b/src/dfx/src/commands/canister/call.rs @@ -6,6 +6,7 @@ use crate::lib::root_key::fetch_root_key_if_needed; use crate::util::clap::argument_from_cli::ArgumentFromCliPositionalOpt; use crate::util::clap::parsers::cycle_amount_parser; use crate::util::{blob_from_arguments, fetch_remote_did_file, get_candid_type, print_idl_blob}; +use anyhow::bail; use anyhow::{anyhow, Context}; use candid::Principal as CanisterId; use candid::{CandidType, Decode, Deserialize, Principal}; @@ -14,11 +15,14 @@ use clap::Parser; use dfx_core::canister::build_wallet_canister; use dfx_core::identity::CallSender; use ic_agent::agent::CallResponse; +use ic_agent::RequestId; use ic_utils::canister::Argument; use ic_utils::interfaces::management_canister::builders::{CanisterInstall, CanisterSettings}; use ic_utils::interfaces::management_canister::MgmtMethod; use ic_utils::interfaces::wallet::{CallForwarder, CallResult}; use ic_utils::interfaces::WalletCanister; +use pocket_ic::common::rest::RawEffectivePrincipal; +use pocket_ic::WasmResult; use slog::warn; use std::option::Option; use std::path::PathBuf; @@ -83,6 +87,11 @@ pub struct CanisterCallOpts { conflicts_with("random") )] always_assist: bool, + + /// Send request on behalf of the specified principal. + /// This option only works for a local PocketIC instance. + #[arg(long)] + impersonate: Option, } #[derive(Clone, CandidType, Deserialize, Debug)] @@ -206,8 +215,13 @@ pub fn get_effective_canister_id( pub async fn exec( env: &dyn Environment, opts: CanisterCallOpts, - call_sender: &CallSender, + mut call_sender: &CallSender, ) -> DfxResult { + let call_sender_override = opts.impersonate.map(CallSender::Impersonate); + if let Some(ref call_sender_override) = call_sender_override { + call_sender = call_sender_override; + }; + let agent = env.get_agent(); fetch_root_key_if_needed(env).await?; @@ -335,6 +349,29 @@ To figure out the id of your wallet, run 'dfx identity get-wallet (--network ic) .with_arg(arg_value); query_builder.call().await.context("Failed query call.")? } + CallSender::Impersonate(sender) => { + let pocketic = env.get_pocketic(); + if let Some(pocketic) = pocketic { + let res = pocketic + .query_call_with_effective_principal( + canister_id, + RawEffectivePrincipal::CanisterId( + effective_canister_id.as_slice().to_vec(), + ), + *sender, + method_name, + arg_value, + ) + .await + .map_err(|err| anyhow!("Failed to perform query call: {}", err))?; + match res { + WasmResult::Reply(data) => data, + WasmResult::Reject(err) => bail!("Canister rejected: {}", err), + } + } else { + bail!("Impersonating sender is only supported for a local PocketIC instance.") + } + } CallSender::Wallet(wallet_id) => { let wallet = build_wallet_canister(*wallet_id, agent).await?; do_wallet_call( @@ -361,6 +398,27 @@ To figure out the id of your wallet, run 'dfx identity get-wallet (--network ic) .await .context("Failed update call.")? .map(|(res, _)| res), + CallSender::Impersonate(sender) => { + let pocketic = env.get_pocketic(); + if let Some(pocketic) = pocketic { + let msg_id = pocketic + .submit_call_with_effective_principal( + canister_id, + RawEffectivePrincipal::CanisterId( + effective_canister_id.as_slice().to_vec(), + ), + *sender, + method_name, + arg_value, + ) + .await + .map_err(|err| anyhow!("Failed to submit canister call: {}", err))? + .message_id; + CallResponse::Poll(RequestId::new(msg_id.as_slice().try_into().unwrap())) + } else { + bail!("Impersonating sender is only supported for a local PocketIC instance.") + } + } CallSender::Wallet(wallet_id) => { let wallet = build_wallet_canister(*wallet_id, agent).await?; let mut args = Argument::default(); @@ -389,6 +447,33 @@ To figure out the id of your wallet, run 'dfx identity get-wallet (--network ic) .with_arg(arg_value) .await .context("Failed update call.")?, + CallSender::Impersonate(sender) => { + let pocketic = env.get_pocketic(); + if let Some(pocketic) = pocketic { + let msg_id = pocketic + .submit_call_with_effective_principal( + canister_id, + RawEffectivePrincipal::CanisterId( + effective_canister_id.as_slice().to_vec(), + ), + *sender, + method_name, + arg_value, + ) + .await + .map_err(|err| anyhow!("Failed to submit canister call: {}", err))?; + let res = pocketic + .await_call_no_ticks(msg_id) + .await + .map_err(|err| anyhow!("Canister call failed: {}", err))?; + match res { + WasmResult::Reply(data) => data, + WasmResult::Reject(err) => bail!("Canister rejected: {}", err), + } + } else { + bail!("Impersonating sender is only supported for a local PocketIC instance.") + } + } CallSender::Wallet(wallet_id) => { let wallet = build_wallet_canister(*wallet_id, agent).await?; do_wallet_call( diff --git a/src/dfx/src/commands/canister/delete.rs b/src/dfx/src/commands/canister/delete.rs index c99c5df90d..289bf81e3f 100644 --- a/src/dfx/src/commands/canister/delete.rs +++ b/src/dfx/src/commands/canister/delete.rs @@ -127,6 +127,11 @@ async fn delete_canister( CallSender::Wallet(wallet_id) => WithdrawTarget::Canister { canister_id: *wallet_id, }, + CallSender::Impersonate(_) => { + unreachable!( + "Impersonating sender when deleting canisters is not supported." + ) + } CallSender::SelectedId => { let network = env.get_network_descriptor(); let identity_name = env diff --git a/src/dfx/src/commands/canister/deposit_cycles.rs b/src/dfx/src/commands/canister/deposit_cycles.rs index 3a2bb26e0c..5aba4a9ed5 100644 --- a/src/dfx/src/commands/canister/deposit_cycles.rs +++ b/src/dfx/src/commands/canister/deposit_cycles.rs @@ -67,6 +67,9 @@ async fn deposit_cycles( ) .await?; } + CallSender::Impersonate(_) => { + unreachable!("Impersonating sender when depositing cycles is not supported.") + } CallSender::Wallet(_) => { canister::deposit_cycles(env, canister_id, call_sender, cycles).await? } diff --git a/src/dfx/src/commands/canister/status.rs b/src/dfx/src/commands/canister/status.rs index 9218774219..e831e0de46 100644 --- a/src/dfx/src/commands/canister/status.rs +++ b/src/dfx/src/commands/canister/status.rs @@ -16,6 +16,11 @@ pub struct CanisterStatusOpts { /// You must specify either a canister name or the --all flag. canister: Option, + /// Send request on behalf of the specified principal. + /// This option only works for a local PocketIC instance. + #[arg(long)] + impersonate: Option, + /// Returns status information for all of the canisters configured in the dfx.json file. #[arg(long, required_unless_present("canister"))] all: bool, @@ -115,8 +120,13 @@ Log visibility: {log_visibility}", pub async fn exec( env: &dyn Environment, opts: CanisterStatusOpts, - call_sender: &CallSender, + mut call_sender: &CallSender, ) -> DfxResult { + let call_sender_override = opts.impersonate.map(CallSender::Impersonate); + if let Some(ref call_sender_override) = call_sender_override { + call_sender = call_sender_override; + }; + fetch_root_key_if_needed(env).await?; if let Some(canister) = opts.canister.as_deref() { diff --git a/src/dfx/src/commands/canister/update_settings.rs b/src/dfx/src/commands/canister/update_settings.rs index 1d18cb1555..d1ae33fb1a 100644 --- a/src/dfx/src/commands/canister/update_settings.rs +++ b/src/dfx/src/commands/canister/update_settings.rs @@ -17,6 +17,7 @@ use crate::util::clap::parsers::{ use anyhow::{bail, Context}; use byte_unit::Byte; use candid::Principal as CanisterId; +use candid::Principal; use clap::{ArgAction, Parser}; use dfx_core::cli::ask_for_consent; use dfx_core::error::identity::InstantiateIdentityFromNameError::GetIdentityPrincipalFailed; @@ -114,13 +115,23 @@ pub struct UpdateSettingsOpts { /// so this is not recommended outside of CI. #[arg(long, short)] yes: bool, + + /// Send request on behalf of the specified principal. + /// This option only works for a local PocketIC instance. + #[arg(long)] + impersonate: Option, } pub async fn exec( env: &dyn Environment, opts: UpdateSettingsOpts, - call_sender: &CallSender, + mut call_sender: &CallSender, ) -> DfxResult { + let call_sender_override = opts.impersonate.map(CallSender::Impersonate); + if let Some(ref call_sender_override) = call_sender_override { + call_sender = call_sender_override; + }; + // sanity checks if let Some(threshold_in_seconds) = opts.freezing_threshold { if threshold_in_seconds > 50_000_000 /* ~1.5 years */ && !opts.confirm_very_long_freezing_threshold @@ -372,6 +383,7 @@ fn user_is_removing_themselves_as_controller( .get_selected_identity_principal() .context("Selected identity is not instantiated")? .to_string(), + CallSender::Impersonate(sender) => sender.to_string(), CallSender::Wallet(principal) => principal.to_string(), }; let removes_themselves = diff --git a/src/dfx/src/lib/environment.rs b/src/dfx/src/lib/environment.rs index 8e993bbc3e..2b701f233d 100644 --- a/src/dfx/src/lib/environment.rs +++ b/src/dfx/src/lib/environment.rs @@ -12,10 +12,12 @@ use dfx_core::config::model::network_descriptor::{NetworkDescriptor, NetworkType use dfx_core::error::canister_id_store::CanisterIdStoreError; use dfx_core::error::identity::NewIdentityManagerError; use dfx_core::error::load_dfx_config::LoadDfxConfigError; +use dfx_core::error::uri::UriError; use dfx_core::extension::manager::ExtensionManager; use dfx_core::identity::identity_manager::{IdentityManager, InitializeIdentity}; use fn_error_context::context; use ic_agent::{Agent, Identity}; +use pocket_ic::nonblocking::PocketIc; use semver::Version; use slog::{Logger, Record}; use std::borrow::Cow; @@ -23,6 +25,7 @@ use std::cell::RefCell; use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; +use url::Url; pub trait Environment { fn get_cache(&self) -> Arc; @@ -44,6 +47,8 @@ pub trait Environment { #[allow(clippy::needless_lifetimes)] fn get_agent<'a>(&'a self) -> &'a Agent; + fn get_pocketic(&self) -> Option<&PocketIc>; + #[allow(clippy::needless_lifetimes)] fn get_network_descriptor<'a>(&'a self) -> &'a NetworkDescriptor; @@ -213,6 +218,10 @@ impl Environment for EnvironmentImpl { unreachable!("Agent only available from an AgentEnvironment"); } + fn get_pocketic(&self) -> Option<&PocketIc> { + unreachable!("PocketIC handle only available from an AgentEnvironment"); + } + fn get_network_descriptor(&self) -> &NetworkDescriptor { // It's not valid to call get_network_descriptor on an EnvironmentImpl. // All of the places that call this have an AgentEnvironment anyway. @@ -267,6 +276,7 @@ impl Environment for EnvironmentImpl { pub struct AgentEnvironment<'a> { backend: &'a dyn Environment, agent: Agent, + pocketic: Option, network_descriptor: NetworkDescriptor, identity_manager: IdentityManager, effective_canister_id: Option, @@ -314,9 +324,27 @@ impl<'a> AgentEnvironment<'a> { None }; + let pocketic = + if let Some(local_server_descriptor) = &network_descriptor.local_server_descriptor { + match local_server_descriptor.get_running_pocketic_port(None)? { + Some(port) => { + let mut socket_addr = local_server_descriptor.bind_address; + socket_addr.set_port(port); + let url = format!("http://{}", socket_addr); + let url = Url::parse(&url) + .map_err(|e| UriError::UrlParseError(url.to_string(), e))?; + Some(create_pocketic(&url)) + } + None => None, + } + } else { + None + }; + Ok(AgentEnvironment { backend, agent: create_agent(logger, url, identity, timeout)?, + pocketic, network_descriptor: network_descriptor.clone(), identity_manager, effective_canister_id, @@ -359,6 +387,10 @@ impl<'a> Environment for AgentEnvironment<'a> { &self.agent } + fn get_pocketic(&self) -> Option<&PocketIc> { + self.pocketic.as_ref() + } + fn get_network_descriptor(&self) -> &NetworkDescriptor { &self.network_descriptor } @@ -422,3 +454,7 @@ pub fn create_agent( .build()?; Ok(agent) } + +pub fn create_pocketic(url: &Url) -> PocketIc { + PocketIc::new_from_existing_instance(url.clone(), 0, None) +} diff --git a/src/dfx/src/lib/operations/canister/create_canister.rs b/src/dfx/src/lib/operations/canister/create_canister.rs index 03c1e3928f..11f135c98e 100644 --- a/src/dfx/src/lib/operations/canister/create_canister.rs +++ b/src/dfx/src/lib/operations/canister/create_canister.rs @@ -173,6 +173,9 @@ The command line value will be used.", .await } } + CallSender::Impersonate(_) => { + unreachable!("Impersonating sender when creating canisters is not supported.") + } CallSender::Wallet(wallet_id) => { create_with_wallet(agent, &wallet_id, with_cycles, settings, subnet_selection).await } diff --git a/src/dfx/src/lib/operations/canister/mod.rs b/src/dfx/src/lib/operations/canister/mod.rs index 9551ddc4fb..57d54490d6 100644 --- a/src/dfx/src/lib/operations/canister/mod.rs +++ b/src/dfx/src/lib/operations/canister/mod.rs @@ -13,11 +13,12 @@ use crate::lib::canister_info::CanisterInfo; use crate::lib::environment::Environment; use crate::lib::error::DfxResult; use crate::lib::ic_attributes::CanisterSettings as DfxCanisterSettings; -use anyhow::{bail, Context}; +use anyhow::{anyhow, bail, Context}; use candid::utils::ArgumentDecoder; use candid::CandidType; use candid::Principal as CanisterId; use candid::Principal; +use candid::{decode_args, encode_args}; use dfx_core::canister::build_wallet_canister; use dfx_core::config::model::dfinity::Config; use dfx_core::identity::CallSender; @@ -29,6 +30,8 @@ use ic_utils::interfaces::management_canister::{ }; use ic_utils::interfaces::ManagementCanister; use ic_utils::Argument; +use pocket_ic::common::rest::RawEffectivePrincipal; +use pocket_ic::WasmResult; use std::collections::HashSet; use std::path::PathBuf; @@ -61,6 +64,32 @@ where .await .context("Update call (without wallet) failed.")? } + CallSender::Impersonate(sender) => { + let pocketic = env.get_pocketic(); + if let Some(pocketic) = pocketic { + let msg_id = pocketic + .submit_call_with_effective_principal( + Principal::management_canister(), + RawEffectivePrincipal::CanisterId(destination_canister.as_slice().to_vec()), + *sender, + method, + encode_args((arg,)).unwrap(), + ) + .await + .map_err(|err| anyhow!("Failed to submit management canister call: {}", err))?; + let res = pocketic + .await_call_no_ticks(msg_id) + .await + .map_err(|err| anyhow!("Management canister call failed: {}", err))?; + match res { + WasmResult::Reply(data) => decode_args(&data) + .context("Could not decode management canister response.")?, + WasmResult::Reject(err) => bail!("Management canister rejected: {}", err), + } + } else { + bail!("Impersonating sender is only supported for a local PocketIC instance.") + } + } CallSender::Wallet(wallet_id) => { let wallet = build_wallet_canister(*wallet_id, agent).await?; let out: O = wallet @@ -108,6 +137,30 @@ where .await .context("Query call (without wallet) failed.")? } + CallSender::Impersonate(sender) => { + let pocketic = env.get_pocketic(); + if let Some(pocketic) = pocketic { + let res = pocketic + .query_call_with_effective_principal( + Principal::management_canister(), + RawEffectivePrincipal::CanisterId(destination_canister.as_slice().to_vec()), + *sender, + method, + encode_args((arg,)).unwrap(), + ) + .await + .map_err(|err| { + anyhow!("Failed to perform management canister query call: {}", err) + })?; + match res { + WasmResult::Reply(data) => decode_args(&data) + .context("Failed to decode management canister query call response.")?, + WasmResult::Reject(err) => bail!("Management canister rejected: {}", err), + } + } else { + bail!("Impersonating sender is only supported for a local PocketIC instance.") + } + } CallSender::Wallet(wallet_id) => { let wallet = build_wallet_canister(*wallet_id, agent).await?; let out: O = wallet