diff --git a/e2e/bats/controller.bash b/e2e/bats/controller.bash new file mode 100644 index 0000000000..65f869d6f6 --- /dev/null +++ b/e2e/bats/controller.bash @@ -0,0 +1,72 @@ +#!/usr/bin/env bats + +load utils/_ + +setup() { + # We want to work from a temporary directory, different for every test. + cd $(mktemp -d -t dfx-e2e-XXXXXXXX) + + # Each test gets its own home directory in order to have its own identities. + mkdir $(pwd)/home-for-test + export HOME=$(pwd)/home-for-test + + dfx_new hello +} + +teardown() { + dfx_stop + rm -rf $(pwd)/home-for-test +} + +@test "set controller" { + # Create two identities and get their Principals + assert_command dfx identity new jose + assert_command dfx identity new juana + JOSE_PRINCIPAL=$(dfx --identity jose identity get-principal) + JUANA_PRINCIPAL=$(dfx --identity juana identity get-principal) + + assert_command dfx identity use jose + + dfx_start + dfx canister create hello + dfx build hello + dfx canister install hello + ID=$(dfx canister id hello) + + # Set controller using canister name and identity name + assert_command dfx canister set-controller hello juana + assert_match "Set \"juana\" as controller of \"hello\"." + + # Juana is controller, Jose cannot reinstall + assert_command_fail dfx canister install hello -m reinstall + if [ "$USE_IC_REF" ] + then + assert_match "${JOSE_PRINCIPAL} is not authorized to manage canister ${ID}" + else + assert_match "Only the controller of canister ${ID} can control it." + fi + + # Juana can reinstall + assert_command dfx --identity juana canister install hello -m reinstall + + assert_command dfx identity use juana + # Set controller using canister id and principal + assert_command dfx canister set-controller ${ID} ${JOSE_PRINCIPAL} + assert_match "Set \"${JOSE_PRINCIPAL}\" as controller of \"${ID}\"." + assert_command_fail dfx canister install hello -m reinstall + + # Set controller using combination of name/id and identity/principal + assert_command dfx --identity jose canister set-controller hello ${JUANA_PRINCIPAL} + assert_match "Set \"${JUANA_PRINCIPAL}\" as controller of \"hello\"." + + assert_command dfx --identity juana canister set-controller ${ID} jose + assert_match "Set \"jose\" as controller of \"${ID}\"." + + # Set controller using invalid principal/identity fails + assert_command_fail dfx --identity jose canister set-controller hello bob + assert_match "Identity bob does not exist" + + # Set controller using invalid canister name/id fails + assert_command_fail dfx --identity jose canister set-controller hello_assets juana + assert_match "Cannot find canister id. Please issue 'dfx canister create hello_assets'." +} diff --git a/src/dfx/src/commands/canister/mod.rs b/src/dfx/src/commands/canister/mod.rs index b846a35c2b..5ae18e87d0 100644 --- a/src/dfx/src/commands/canister/mod.rs +++ b/src/dfx/src/commands/canister/mod.rs @@ -11,6 +11,7 @@ mod delete; mod id; mod install; mod request_status; +mod set_controller; mod start; mod status; mod stop; @@ -23,6 +24,7 @@ fn builtins() -> Vec { CliCommand::new(id::construct(), id::exec), CliCommand::new(install::construct(), install::exec), CliCommand::new(request_status::construct(), request_status::exec), + CliCommand::new(set_controller::construct(), set_controller::exec), CliCommand::new(start::construct(), start::exec), CliCommand::new(status::construct(), status::exec), CliCommand::new(stop::construct(), stop::exec), diff --git a/src/dfx/src/commands/canister/set_controller.rs b/src/dfx/src/commands/canister/set_controller.rs new file mode 100644 index 0000000000..35b4f65edc --- /dev/null +++ b/src/dfx/src/commands/canister/set_controller.rs @@ -0,0 +1,62 @@ +use crate::lib::environment::Environment; +use crate::lib::error::{DfxError, DfxResult}; +use crate::lib::identity::identity_manager::IdentityManager; +use crate::lib::message::UserMessage; +use crate::lib::models::canister_id_store::CanisterIdStore; +use crate::lib::waiter::waiter_with_timeout; +use crate::util::expiry_duration; +use clap::{App, Arg, ArgMatches, SubCommand}; +use ic_agent::Identity; +use ic_types::principal::Principal as CanisterId; +use ic_utils::call::AsyncCall; +use ic_utils::interfaces::ManagementCanister; +use tokio::runtime::Runtime; + +pub fn construct() -> App<'static, 'static> { + SubCommand::with_name("set-controller") + .about(UserMessage::SetController.to_str()) + .arg( + Arg::with_name("canister") + .takes_value(true) + .help(UserMessage::SetControllerCanister.to_str()) + .required(true), + ) + .arg( + Arg::with_name("new-controller") + .takes_value(true) + .help(UserMessage::NewController.to_str()) + .required(true), + ) +} + +pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { + let canister = args.value_of("canister").unwrap(); + let canister_id = match CanisterId::from_text(canister) { + Ok(id) => id, + Err(_) => CanisterIdStore::for_env(env)?.get(canister)?, + }; + + let new_controller = args.value_of("new-controller").unwrap(); + let controller_principal = match CanisterId::from_text(new_controller) { + Ok(principal) => principal, + Err(_) => IdentityManager::new(env)? + .instantiate_identity_from_name(new_controller)? + .sender()?, + }; + + let timeout = expiry_duration(); + + let mgr = ManagementCanister::create( + env.get_agent() + .ok_or(DfxError::CommandMustBeRunInAProject)?, + ); + + let mut runtime = Runtime::new().expect("Unable to create a runtime"); + runtime.block_on( + mgr.set_controller(&canister_id, &controller_principal) + .call_and_wait(waiter_with_timeout(timeout)), + )?; + + println!("Set {:?} as controller of {:?}.", new_controller, canister); + Ok(()) +} diff --git a/src/dfx/src/lib/identity/identity_manager.rs b/src/dfx/src/lib/identity/identity_manager.rs index 8adcd6dc61..a467683e76 100644 --- a/src/dfx/src/lib/identity/identity_manager.rs +++ b/src/dfx/src/lib/identity/identity_manager.rs @@ -65,7 +65,16 @@ impl IdentityManager { /// Create an Identity instance for use with an Agent pub fn instantiate_selected_identity(&self) -> DfxResult> { - let pem_path = self.get_selected_identity_pem_path(); + self.instantiate_identity_from_name(self.selected_identity.as_str()) + } + + /// Provide a valid Identity name and create its Identity instance for use with an Agent + pub fn instantiate_identity_from_name( + &self, + identity_name: &str, + ) -> DfxResult> { + self.require_identity_exists(identity_name)?; + let pem_path = self.get_identity_pem_path(identity_name); Ok(Box::new(BasicIdentity::from_pem_file(&pem_path).map_err( |e| DfxError::IdentityError(IdentityErrorKind::AgentPemError(e, pem_path.clone())), )?)) @@ -206,10 +215,6 @@ impl IdentityManager { fn get_identity_pem_path(&self, identity: &str) -> PathBuf { self.get_identity_dir_path(identity).join(IDENTITY_PEM) } - - fn get_selected_identity_pem_path(&self) -> PathBuf { - self.get_identity_pem_path(&self.selected_identity) - } } fn initialize( diff --git a/src/dfx/src/lib/message.rs b/src/dfx/src/lib/message.rs index 4f02da76a2..30f976b4fe 100644 --- a/src/dfx/src/lib/message.rs +++ b/src/dfx/src/lib/message.rs @@ -65,6 +65,11 @@ user_message!( DeleteCanisterName => "Specifies the name of the canister to delete. You must specify either a canister name or the --all flag.", DeleteAll => "Deletes all of the canisters configured in the dfx.json file.", + // dfx canister set-controller + SetController => "Sets the provided identity's name or its principal as the new controller of a canister on the Internet Computer network.", + SetControllerCanister => "Specifies the canister name or the canister identifier for the canister to be controlled.", + NewController => "Specifies the identity name or the principal of the new controller.", + // dfx canister status CanisterStatus => "Returns the current status of the canister on the Internet Computer network: Running, Stopping, or Stopped.", StatusCanisterName => "Specifies the name of the canister to return information for. You must specify either a canister name or the --all flag.",