Skip to main content

canic_host/icp/
identity.rs

1//! Module: icp::identity
2//!
3//! Responsibility: bind an operation's selected ICP identity and resolve its Principal.
4//! Does not own: global identity selection, authentication policy, or controller authorization.
5//! Boundary: callers compare the returned text with their own typed authority.
6
7use super::{error::IcpCommandError, model::IcpCli, run::run_output};
8
9impl IcpCli {
10    /// Freeze the selected identity for this operation and every clone of its transport.
11    /// Callers must still compare its Principal with the reviewed operator before effects.
12    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    /// Return the Principal text for the identity that will execute ICP commands.
38    pub fn identity_principal_text(&self) -> Result<String, IcpCommandError> {
39        let mut command = self.identity_principal_command();
40        run_output(&mut command, self)
41    }
42
43    /// Return the selected identity's exact default account in the requested ledger format.
44    pub fn identity_account_id_text(
45        &self,
46        format: IcpIdentityAccountFormat,
47    ) -> Result<String, IcpCommandError> {
48        let mut command = self.identity_account_id_command(format);
49        run_output(&mut command, self)
50    }
51
52    fn identity_principal_command(&self) -> std::process::Command {
53        let mut command = self.request_command();
54        command.args(["identity", "principal"]);
55        self.add_selected_identity_arg(&mut command);
56        command
57    }
58
59    fn identity_account_id_command(
60        &self,
61        format: IcpIdentityAccountFormat,
62    ) -> std::process::Command {
63        let mut command = self.request_command();
64        command.args(["identity", "account-id", "--format", format.label()]);
65        self.add_selected_identity_arg(&mut command);
66        command
67    }
68}
69
70///
71/// IcpIdentityAccountFormat
72///
73/// Ledger account representation selected for one ICP CLI identity observation.
74///
75#[derive(Clone, Copy, Debug, Eq, PartialEq)]
76pub enum IcpIdentityAccountFormat {
77    IcpLedger,
78    Icrc1,
79}
80
81impl IcpIdentityAccountFormat {
82    const fn label(self) -> &'static str {
83        match self {
84            Self::IcpLedger => "ledger",
85            Self::Icrc1 => "icrc1",
86        }
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93    use crate::icp::command::command_display;
94
95    #[cfg(unix)]
96    #[test]
97    fn bound_identity_survives_default_change_for_clones_and_later_calls() {
98        use std::{fs, os::unix::fs::PermissionsExt};
99
100        let root = crate::test_support::temp_dir("bound-icp-identity");
101        fs::create_dir_all(&root).unwrap();
102        let executable = root.join("icp");
103        fs::write(
104            &executable,
105            r#"#!/bin/sh
106if [ "$1" = --version ]; then echo 'icp 1.5.0'; exit 0; fi
107while [ "$1" = --project-root-override ] || [ "$1" = --identity-password-file ]; do shift 2; done
108if [ "$1 $2" = 'identity default' ]; then cat selected; exit 0; fi
109selected=$(cat selected)
110for arg do
111  if [ "$previous" = --identity ]; then selected=$arg; fi
112  previous=$arg
113done
114printf '%s\n' "$selected"
115"#,
116        )
117        .unwrap();
118        fs::set_permissions(&executable, fs::Permissions::from_mode(0o755)).unwrap();
119        fs::write(root.join("selected"), "reviewed").unwrap();
120        let icp = IcpCli::new(executable.to_str().unwrap(), None).with_cwd(&root);
121        let clone = icp.clone();
122        icp.bind_selected_identity().unwrap();
123        assert_eq!(icp.identity_principal_text().unwrap(), "reviewed");
124        fs::write(root.join("selected"), "unrelated-encrypted").unwrap();
125        clone.bind_selected_identity().unwrap();
126        assert_eq!(clone.identity_principal_text().unwrap(), "reviewed");
127        assert_eq!(
128            clone
129                .identity_account_id_text(IcpIdentityAccountFormat::Icrc1)
130                .unwrap(),
131            "reviewed"
132        );
133        assert_eq!(clone.selected_identity_name().unwrap(), "reviewed");
134        assert_eq!(
135            clone
136                .canister_metadata_output("canister", "candid:service")
137                .unwrap(),
138            "reviewed"
139        );
140        let unbound = IcpCli::new(executable.to_str().unwrap(), None).with_cwd(&root);
141        assert_eq!(
142            unbound.identity_principal_text().unwrap(),
143            "unrelated-encrypted"
144        );
145        assert_ne!(clone, unbound);
146        fs::remove_dir_all(root).unwrap();
147    }
148
149    #[test]
150    fn identity_resolution_uses_the_active_identity_without_network_selection() {
151        let icp = IcpCli::new("icp", Some("ic".to_string())).with_cwd("/workspace/app");
152
153        assert_eq!(
154            command_display(&icp.identity_principal_command()),
155            "icp --project-root-override /workspace/app identity principal"
156        );
157    }
158
159    #[test]
160    fn account_resolution_selects_the_exact_ledger_representation() {
161        let icp = IcpCli::new("icp", Some("ic".to_string())).with_cwd("/workspace/app");
162
163        assert_eq!(
164            command_display(&icp.identity_account_id_command(IcpIdentityAccountFormat::Icrc1)),
165            "icp --project-root-override /workspace/app identity account-id --format icrc1"
166        );
167        assert_eq!(
168            command_display(&icp.identity_account_id_command(IcpIdentityAccountFormat::IcpLedger)),
169            "icp --project-root-override /workspace/app identity account-id --format ledger"
170        );
171    }
172}