junobuild_shared/
controllers.rs

1use crate::constants_internal::REVOKED_CONTROLLERS;
2use crate::env::{CONSOLE, OBSERVATORY};
3use crate::errors::{
4    JUNO_ERROR_CONTROLLERS_ANONYMOUS_NOT_ALLOWED, JUNO_ERROR_CONTROLLERS_MAX_NUMBER,
5    JUNO_ERROR_CONTROLLERS_REVOKED_NOT_ALLOWED,
6};
7use crate::types::interface::SetController;
8use crate::types::state::{Controller, ControllerId, ControllerScope, Controllers, UserId};
9use crate::utils::{principal_anonymous, principal_equal, principal_not_anonymous};
10use candid::Principal;
11use ic_cdk::api::{is_controller as is_canister_controller, time};
12use ic_cdk::id;
13use std::collections::HashMap;
14
15/// Initializes a set of controllers with default administrative scope.
16///
17/// # Arguments
18/// - `new_controllers`: Slice of `UserId` representing the new controllers to be initialized.
19///
20/// # Returns
21/// A `Controllers` collection populated with the specified new controllers.
22pub fn init_admin_controllers(new_controllers: &[UserId]) -> Controllers {
23    let mut controllers: Controllers = Controllers::new();
24
25    let controller_data: SetController = SetController {
26        metadata: HashMap::new(),
27        expires_at: None,
28        scope: ControllerScope::Admin,
29    };
30
31    set_controllers(new_controllers, &controller_data, &mut controllers);
32
33    controllers
34}
35
36/// Sets or updates controllers with specified data.
37///
38/// # Arguments
39/// - `new_controllers`: Slice of `UserId` for the controllers to be set or updated.
40/// - `controller_data`: `SetController` data to apply to the controllers.
41/// - `controllers`: Mutable reference to the current set of controllers to update.
42pub fn set_controllers(
43    new_controllers: &[UserId],
44    controller_data: &SetController,
45    controllers: &mut Controllers,
46) {
47    for controller_id in new_controllers {
48        let existing_controller = controllers.get(controller_id);
49
50        let now = time();
51
52        let created_at: u64 = match existing_controller {
53            None => now,
54            Some(existing_controller) => existing_controller.created_at,
55        };
56
57        let updated_at: u64 = now;
58
59        let controller: Controller = Controller {
60            metadata: controller_data.metadata.clone(),
61            created_at,
62            updated_at,
63            expires_at: controller_data.expires_at,
64            scope: controller_data.scope.clone(),
65        };
66
67        controllers.insert(*controller_id, controller);
68    }
69}
70
71/// Removes specified controllers from the set.
72///
73/// # Arguments
74/// - `remove_controllers`: Slice of `UserId` for the controllers to be removed.
75/// - `controllers`: Mutable reference to the current set of controllers to update.
76pub fn delete_controllers(remove_controllers: &[UserId], controllers: &mut Controllers) {
77    for c in remove_controllers {
78        controllers.remove(c);
79    }
80}
81
82/// Checks if a caller is a controller with admin or write scope (permissions).
83///
84/// # Arguments
85/// - `caller`: `UserId` of the caller.
86/// - `controllers`: Reference to the current set of controllers.
87///
88/// # Returns
89/// `true` if the caller is a controller (not anonymous, calling itself or one of the known write or admin controllers), otherwise `false`.
90pub fn controller_can_write(caller: UserId, controllers: &Controllers) -> bool {
91    principal_not_anonymous(caller)
92        && (caller_is_self(caller)
93            || controllers
94                .iter()
95                .any(|(&controller_id, controller)| match controller.scope {
96                    ControllerScope::Submit => false,
97                    _ => principal_equal(controller_id, caller),
98                }))
99}
100
101/// Checks if a caller is a controller regardless of its scope (admin, write or submit).
102///
103/// # Arguments
104/// - `caller`: `UserId` of the caller.
105/// - `controllers`: Reference to the current set of controllers.
106///
107/// # Returns
108/// `true` if the caller is a controller (not anonymous, calling itself or one of the known controllers), otherwise `false`.
109pub fn is_controller(caller: UserId, controllers: &Controllers) -> bool {
110    principal_not_anonymous(caller)
111        && (caller_is_self(caller)
112            || controllers
113                .iter()
114                .any(|(&controller_id, _)| principal_equal(controller_id, caller)))
115}
116
117/// Checks if a caller is an admin controller.
118///
119/// # Arguments
120/// - `caller`: `UserId` of the caller.
121/// - `controllers`: Reference to the current set of controllers.
122///
123/// # Returns
124/// `true` if the caller is an admin controller, otherwise `false`.
125pub fn is_admin_controller(caller: UserId, controllers: &Controllers) -> bool {
126    is_canister_controller(&caller)
127        && principal_not_anonymous(caller)
128        && controllers
129            .iter()
130            .any(|(&controller_id, controller)| match controller.scope {
131                ControllerScope::Admin => principal_equal(controller_id, caller),
132                _ => false,
133            })
134}
135
136/// Converts the controllers set into a vector of controller IDs.
137///
138/// # Arguments
139/// - `controllers`: Reference to the current set of controllers.
140///
141/// # Returns
142/// A vector of `ControllerId`.
143pub fn into_controller_ids(controllers: &Controllers) -> Vec<ControllerId> {
144    controllers
145        .clone()
146        .into_keys()
147        .collect::<Vec<ControllerId>>()
148}
149
150/// Asserts that the number of controllers does not exceed the maximum allowed.
151///
152/// # Arguments
153/// - `current_controllers`: Reference to the current set of controllers.
154/// - `controllers_ids`: Slice of `ControllerId` representing the controllers to be added.
155/// - `max_controllers`: Maximum number of allowed controllers.
156///
157/// # Returns
158/// `Ok(())` if the operation is successful, or `Err(String)` if the maximum is exceeded.
159pub fn assert_max_number_of_controllers(
160    current_controllers: &Controllers,
161    controllers_ids: &[ControllerId],
162    max_controllers: usize,
163) -> Result<(), String> {
164    let current_controller_ids = into_controller_ids(current_controllers);
165
166    let new_controller_ids = controllers_ids.iter().copied().filter(|id| {
167        !current_controller_ids
168            .iter()
169            .any(|current_id| current_id == id)
170    });
171
172    if current_controller_ids.len() + new_controller_ids.count() > max_controllers {
173        return Err(format!(
174            "{} ({})",
175            JUNO_ERROR_CONTROLLERS_MAX_NUMBER, max_controllers
176        ));
177    }
178
179    Ok(())
180}
181
182/// Asserts that the controller IDs are not anonymous and not revoked.
183///
184/// # Arguments
185/// - `controllers_ids`: Slice of `ControllerId` to validate.
186///
187/// # Returns
188/// `Ok(())` if no anonymous and no revoked IDs are present, or `Err(String)` if any are found.
189pub fn assert_controllers(controllers_ids: &[ControllerId]) -> Result<(), String> {
190    assert_no_anonymous_controller(controllers_ids)?;
191    assert_no_revoked_controller(controllers_ids)?;
192
193    Ok(())
194}
195
196/// Asserts that no controller IDs are anonymous.
197///
198/// # Arguments
199/// - `controllers_ids`: Slice of `ControllerId` to validate.
200///
201/// # Returns
202/// `Ok(())` if no anonymous IDs are present, or `Err(String)` if any are found.
203fn assert_no_anonymous_controller(controllers_ids: &[ControllerId]) -> Result<(), String> {
204    let has_anonymous = controllers_ids
205        .iter()
206        .any(|controller_id| principal_anonymous(*controller_id));
207
208    match has_anonymous {
209        true => Err(JUNO_ERROR_CONTROLLERS_ANONYMOUS_NOT_ALLOWED.to_string()),
210        false => Ok(()),
211    }
212}
213
214/// Asserts that no controller IDs are revoked for security reason.
215///
216/// # Arguments
217/// - `controllers_ids`: Slice of `ControllerId` to validate.
218///
219/// # Returns
220/// `Ok(())` if no revoked IDs are present, or `Err(String)` if any are found.
221fn assert_no_revoked_controller(controllers_ids: &[ControllerId]) -> Result<(), String> {
222    // We treat revoked controllers as anonymous controllers.
223    let has_revoked = controllers_ids.iter().any(controller_revoked);
224
225    match has_revoked {
226        true => Err(JUNO_ERROR_CONTROLLERS_REVOKED_NOT_ALLOWED.to_string()),
227        false => Ok(()),
228    }
229}
230
231/// Checks if the caller is the console.
232///
233/// # Arguments
234/// - `caller`: `UserId` of the caller.
235///
236/// # Returns
237/// `true` if the caller matches the console's principal, otherwise `false`.
238pub fn caller_is_console(caller: UserId) -> bool {
239    let console = Principal::from_text(CONSOLE).unwrap();
240
241    principal_equal(caller, console)
242}
243
244/// Checks if the caller is the observatory.
245///
246/// # Arguments
247/// - `caller`: `UserId` of the caller.
248///
249/// # Returns
250/// `true` if the caller matches the observatory's principal, otherwise `false`.
251pub fn caller_is_observatory(caller: UserId) -> bool {
252    let observatory = Principal::from_text(OBSERVATORY).unwrap();
253
254    principal_equal(caller, observatory)
255}
256
257/// Checks if the caller is the canister itself.
258///
259/// # Arguments
260/// - `caller`: `UserId` of the caller.
261///
262/// # Returns
263/// `true` if the caller is calling itself, if the canister is the caller, otherwise `false`.
264pub fn caller_is_self(caller: UserId) -> bool {
265    let itself = id();
266
267    principal_equal(caller, itself)
268}
269
270/// Filters the set of controllers, returning only those with administrative scope.
271///
272/// # Arguments
273/// - `controllers`: Reference to the current set of controllers.
274///
275/// # Returns
276/// A `Controllers` collection containing only admin controllers.
277pub fn filter_admin_controllers(controllers: &Controllers) -> Controllers {
278    #[allow(clippy::match_like_matches_macro)]
279    controllers
280        .clone()
281        .into_iter()
282        .filter(|(_, controller)| match controller.scope {
283            ControllerScope::Admin => true,
284            _ => false,
285        })
286        .collect()
287}
288
289fn controller_revoked(controller_id: &ControllerId) -> bool {
290    REVOKED_CONTROLLERS.iter().any(|revoked_controller_id| {
291        principal_equal(
292            Principal::from_text(revoked_controller_id).unwrap(),
293            *controller_id,
294        )
295    })
296}