canic_host/icp/
identity.rs1use super::{error::IcpCommandError, model::IcpCli, run::run_output};
8
9impl IcpCli {
10 pub(crate) fn bind_selected_identity(&self) -> Result<(), IcpCommandError> {
13 if self.selected_identity.get().is_none() {
14 let identity = self.selected_identity_name()?;
15 let _ = self.selected_identity.set(identity);
16 }
17 Ok(())
18 }
19
20 pub(super) fn selected_identity_name(&self) -> Result<String, IcpCommandError> {
21 if let Some(identity) = self.selected_identity.get() {
22 return Ok(identity.clone());
23 }
24 let mut command = self.request_command();
25 command.args(["identity", "default"]);
26 let identity = run_output(&mut command, self)?;
27 if identity.is_empty() {
28 return Err(std::io::Error::new(
29 std::io::ErrorKind::InvalidData,
30 "ICP returned an empty selected identity",
31 )
32 .into());
33 }
34 Ok(identity)
35 }
36
37 pub fn identity_principal_text(&self) -> Result<String, IcpCommandError> {
39 self.identity_lookups
40 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
41 let started = std::time::Instant::now();
42 let mut command = self.identity_principal_command();
43 let result = self.measure_request(super::IcpRequestKind::Identity, None, None, || {
44 run_output(&mut command, self)
45 });
46 self.identity_lookup_millis.fetch_add(
47 u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX),
48 std::sync::atomic::Ordering::Relaxed,
49 );
50 result
51 }
52
53 pub fn identity_account_id_text(
55 &self,
56 format: IcpIdentityAccountFormat,
57 ) -> Result<String, IcpCommandError> {
58 let mut command = self.identity_account_id_command(format);
59 run_output(&mut command, self)
60 }
61
62 fn identity_principal_command(&self) -> std::process::Command {
63 let mut command = self.request_command();
64 command.args(["identity", "principal"]);
65 self.add_selected_identity_arg(&mut command);
66 command
67 }
68
69 fn identity_account_id_command(
70 &self,
71 format: IcpIdentityAccountFormat,
72 ) -> std::process::Command {
73 let mut command = self.request_command();
74 command.args(["identity", "account-id", "--format", format.label()]);
75 self.add_selected_identity_arg(&mut command);
76 command
77 }
78}
79
80#[derive(Clone, Copy, Debug, Eq, PartialEq)]
86pub enum IcpIdentityAccountFormat {
87 IcpLedger,
88 Icrc1,
89}
90
91impl IcpIdentityAccountFormat {
92 const fn label(self) -> &'static str {
93 match self {
94 Self::IcpLedger => "ledger",
95 Self::Icrc1 => "icrc1",
96 }
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103 use crate::icp::command::command_display;
104
105 #[cfg(unix)]
106 #[test]
107 fn explicit_identities_keep_concurrent_contexts_independent_without_default_access() {
108 use std::{fs, os::unix::fs::PermissionsExt};
109
110 let root = crate::test_support::temp_dir("explicit-icp-identities");
111 fs::create_dir_all(&root).unwrap();
112 let executable = root.join("icp");
113 fs::write(
114 &executable,
115 crate::test_support::tool_script(
116 r#"#!/bin/sh
117if [ "$1" = --version ]; then echo 'icp @ICP_VERSION@'; exit 0; fi
118while [ "$1" = --project-root-override ] || [ "$1" = --identity-password-file ]; do shift 2; done
119if [ "$1 $2" = 'identity default' ]; then touch default-accessed; exit 91; fi
120for arg do
121 if [ "$previous" = --identity ]; then selected=$arg; fi
122 previous=$arg
123done
124test -n "$selected" || exit 92
125printf '%s\n' "$selected"
126"#,
127 ),
128 )
129 .unwrap();
130 fs::set_permissions(&executable, fs::Permissions::from_mode(0o755)).unwrap();
131 fs::write(root.join("selected"), b"unrelated-default\n").unwrap();
132 let base = IcpCli::new(executable.to_str().unwrap(), None).with_cwd(&root);
133 std::thread::scope(|scope| {
134 for name in ["operator-a", "operator-b"] {
135 let context = base.clone().with_identity(Some(name));
136 scope.spawn(move || {
137 context.bind_selected_identity().unwrap();
138 let clone = context.clone();
139 assert_eq!(clone.selected_identity_name().unwrap(), name);
140 assert_eq!(clone.identity_principal_text().unwrap(), name);
141 assert_eq!(
142 clone
143 .identity_account_id_text(IcpIdentityAccountFormat::Icrc1)
144 .unwrap(),
145 name
146 );
147 assert_eq!(
148 clone
149 .canister_metadata_output("canister", "candid:service")
150 .unwrap(),
151 name
152 );
153 });
154 }
155 });
156 assert!(base.selected_identity.get().is_none());
157 assert!(!root.join("default-accessed").exists());
158 assert_eq!(
159 fs::read(root.join("selected")).unwrap(),
160 b"unrelated-default\n"
161 );
162 fs::remove_dir_all(root).unwrap();
163 }
164
165 #[cfg(unix)]
166 #[test]
167 fn bound_identity_survives_default_change_for_clones_and_later_calls() {
168 use std::{fs, os::unix::fs::PermissionsExt};
169
170 let root = crate::test_support::temp_dir("bound-icp-identity");
171 fs::create_dir_all(&root).unwrap();
172 let executable = root.join("icp");
173 fs::write(
174 &executable,
175 crate::test_support::tool_script(
176 r#"#!/bin/sh
177if [ "$1" = --version ]; then echo 'icp @ICP_VERSION@'; exit 0; fi
178while [ "$1" = --project-root-override ] || [ "$1" = --identity-password-file ]; do shift 2; done
179if [ "$1 $2" = 'identity default' ]; then cat selected; exit 0; fi
180selected=$(cat selected)
181for arg do
182 if [ "$previous" = --identity ]; then selected=$arg; fi
183 previous=$arg
184done
185printf '%s\n' "$selected"
186"#,
187 ),
188 )
189 .unwrap();
190 fs::set_permissions(&executable, fs::Permissions::from_mode(0o755)).unwrap();
191 fs::write(root.join("selected"), "reviewed").unwrap();
192 let icp = IcpCli::new(executable.to_str().unwrap(), None).with_cwd(&root);
193 let clone = icp.clone();
194 icp.bind_selected_identity().unwrap();
195 assert_eq!(icp.identity_principal_text().unwrap(), "reviewed");
196 fs::write(root.join("selected"), "unrelated-encrypted").unwrap();
197 clone.bind_selected_identity().unwrap();
198 assert_eq!(clone.identity_principal_text().unwrap(), "reviewed");
199 assert_eq!(
200 clone
201 .identity_account_id_text(IcpIdentityAccountFormat::Icrc1)
202 .unwrap(),
203 "reviewed"
204 );
205 assert_eq!(clone.selected_identity_name().unwrap(), "reviewed");
206 assert_eq!(
207 clone
208 .canister_metadata_output("canister", "candid:service")
209 .unwrap(),
210 "reviewed"
211 );
212 let unbound = IcpCli::new(executable.to_str().unwrap(), None).with_cwd(&root);
213 assert_eq!(
214 unbound.identity_principal_text().unwrap(),
215 "unrelated-encrypted"
216 );
217 assert_ne!(clone, unbound);
218 fs::remove_dir_all(root).unwrap();
219 }
220
221 #[test]
222 fn identity_resolution_uses_the_active_identity_without_network_selection() {
223 let icp = IcpCli::new("icp", Some("ic".to_string())).with_cwd("/workspace/app");
224
225 assert_eq!(
226 command_display(&icp.identity_principal_command()),
227 "icp --project-root-override /workspace/app identity principal"
228 );
229 }
230
231 #[test]
232 fn account_resolution_selects_the_exact_ledger_representation() {
233 let icp = IcpCli::new("icp", Some("ic".to_string())).with_cwd("/workspace/app");
234
235 assert_eq!(
236 command_display(&icp.identity_account_id_command(IcpIdentityAccountFormat::Icrc1)),
237 "icp --project-root-override /workspace/app identity account-id --format icrc1"
238 );
239 assert_eq!(
240 command_display(&icp.identity_account_id_command(IcpIdentityAccountFormat::IcpLedger)),
241 "icp --project-root-override /workspace/app identity account-id --format ledger"
242 );
243 }
244}