Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@
"@junobuild/cli-tools": "^0.10.0",
"@junobuild/config": "^2.10.0",
"@junobuild/config-loader": "^0.4.7",
"@junobuild/errors": "^0.2.1",
"@junobuild/errors": "^0.2.1-next-2026-02-05",
"@junobuild/functions": "^0.5.5-next-2026-02-04",
"@ltd/j-toml": "^1.38.0",
"@playwright/test": "^1.57.0",
Expand Down
6 changes: 6 additions & 0 deletions src/console/src/api/controllers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ use crate::store::heap::{
delete_controllers, get_controllers, set_controllers as set_controllers_store,
};
use ic_cdk_macros::{query, update};
use junobuild_shared::ic::UnwrapOrTrap;
use junobuild_shared::segments::controllers::{assert_controller_expiration, assert_controllers};
use junobuild_shared::types::interface::{DeleteControllersArgs, SetControllersArgs};
use junobuild_shared::types::state::Controllers;

Expand All @@ -13,6 +15,10 @@ fn set_controllers(
controller,
}: SetControllersArgs,
) {
assert_controllers(&controllers).unwrap_or_trap();

assert_controller_expiration(&controller).unwrap_or_trap();

set_controllers_store(&controllers, &controller);
}

Expand Down
4 changes: 3 additions & 1 deletion src/libs/satellite/src/api/controllers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use ic_cdk::trap;
use junobuild_shared::constants::shared::MAX_NUMBER_OF_SATELLITE_CONTROLLERS;
use junobuild_shared::ic::UnwrapOrTrap;
use junobuild_shared::segments::controllers::{
assert_controllers, assert_max_number_of_controllers,
assert_controller_expiration, assert_controllers, assert_max_number_of_controllers,
};
use junobuild_shared::types::interface::{DeleteControllersArgs, SetControllersArgs};
use junobuild_shared::types::state::{ControllerScope, Controllers};
Expand Down Expand Up @@ -33,6 +33,8 @@ pub fn set_controllers(

assert_controllers(&controllers).unwrap_or_trap();

assert_controller_expiration(&controller).unwrap_or_trap();

set_controllers_store(&controllers, &controller);

get_controllers()
Expand Down
4 changes: 4 additions & 0 deletions src/libs/shared/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ pub const JUNO_ERROR_CONTROLLERS_ANONYMOUS_NOT_ALLOWED: &str =
// Revoked controller not allowed.
pub const JUNO_ERROR_CONTROLLERS_REVOKED_NOT_ALLOWED: &str =
"juno.error.controllers.revoked_not_allowed";
// Admin controller cannot expires.
pub const JUNO_ERROR_CONTROLLERS_ADMIN_NO_EXPIRY: &str = "juno.error.controllers.admin_no_expire";
// New controller cannot expire in the past.
pub const JUNO_ERROR_CONTROLLERS_EXPIRY_IN_PAST: &str = "juno.error.controllers.expiry_in_past";
// Stable memory usage exceeded: {} bytes used, {} bytes allowed.
pub const JUNO_ERROR_MEMORY_STABLE_EXCEEDED: &str = "juno.error.memory.stable_exceeded";
// Heap memory usage exceeded: {} bytes used, {} bytes allowed.
Expand Down
114 changes: 113 additions & 1 deletion src/libs/shared/src/segments/controllers.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use crate::constants::internal::REVOKED_CONTROLLERS;
use crate::env::{CONSOLE, OBSERVATORY};
use crate::errors::{
JUNO_ERROR_CONTROLLERS_ANONYMOUS_NOT_ALLOWED, JUNO_ERROR_CONTROLLERS_MAX_NUMBER,
JUNO_ERROR_CONTROLLERS_ADMIN_NO_EXPIRY, JUNO_ERROR_CONTROLLERS_ANONYMOUS_NOT_ALLOWED,
JUNO_ERROR_CONTROLLERS_EXPIRY_IN_PAST, JUNO_ERROR_CONTROLLERS_MAX_NUMBER,
JUNO_ERROR_CONTROLLERS_REVOKED_NOT_ALLOWED,
};
use crate::ic::api::{id, is_canister_controller, time};
Expand Down Expand Up @@ -268,6 +269,31 @@ fn assert_no_revoked_controller(controllers_ids: &[ControllerId]) -> Result<(),
}
}

/// Validates controller expiration settings.
///
/// Ensures that:
/// - Admin controllers do not have an expiration date set
/// - If an expiration is set, it's not in the past
///
/// # Arguments
/// - `controller`: The controller configuration to validate
///
/// # Returns
/// `Ok(())` if validation passes, or `Err(String)` with error message if validation fails
pub fn assert_controller_expiration(controller: &SetController) -> Result<(), String> {
if matches!(controller.scope, ControllerScope::Admin) && controller.expires_at.is_some() {
return Err(JUNO_ERROR_CONTROLLERS_ADMIN_NO_EXPIRY.to_string());
}

if let Some(expires_at) = controller.expires_at {
if expires_at < time() {
return Err(JUNO_ERROR_CONTROLLERS_EXPIRY_IN_PAST.to_string());
}
}

Ok(())
}

/// Checks if the caller is the console.
///
/// # Arguments
Expand Down Expand Up @@ -665,4 +691,90 @@ mod tests {
// Will fail because test_principal(99) is not the canister controller
assert!(!is_admin_controller(not_canister, &controllers));
}

#[test]
fn test_assert_expiration_controller_admin_with_expiry_rejected() {
let controller = SetController {
metadata: HashMap::new(),
expires_at: Some(time() + 1000),
scope: ControllerScope::Admin,
kind: None,
};

let result = assert_controller_expiration(&controller);
assert!(result.is_err());
assert_eq!(
result.unwrap_err(),
JUNO_ERROR_CONTROLLERS_ADMIN_NO_EXPIRY.to_string()
);
}

#[test]
fn test_assert_expiration_controller_admin_without_expiry_allowed() {
let controller = SetController {
metadata: HashMap::new(),
expires_at: None,
scope: ControllerScope::Admin,
kind: None,
};

let result = assert_controller_expiration(&controller);
assert!(result.is_ok());
}

#[test]
fn test_assert_expiration_controller_past_expiry_rejected() {
let controller = SetController {
metadata: HashMap::new(),
expires_at: Some(time() - 1000),
scope: ControllerScope::Write,
kind: None,
};

let result = assert_controller_expiration(&controller);
assert!(result.is_err());
assert_eq!(
result.unwrap_err(),
JUNO_ERROR_CONTROLLERS_EXPIRY_IN_PAST.to_string()
);
}

#[test]
fn test_assert_expiration_controller_future_expiry_allowed() {
let controller = SetController {
metadata: HashMap::new(),
expires_at: Some(time() + 1000),
scope: ControllerScope::Write,
kind: None,
};

let result = assert_controller_expiration(&controller);
assert!(result.is_ok());
}

#[test]
fn test_assert_expiration_controller_no_expiry_allowed() {
let controller = SetController {
metadata: HashMap::new(),
expires_at: None,
scope: ControllerScope::Write,
kind: None,
};

let result = assert_controller_expiration(&controller);
assert!(result.is_ok());
}

#[test]
fn test_assert_expiration_controller_submit_scope_with_future_expiry() {
let controller = SetController {
metadata: HashMap::new(),
expires_at: Some(time() + 1000),
scope: ControllerScope::Submit,
kind: None,
};

let result = assert_controller_expiration(&controller);
assert!(result.is_ok());
}
}
5 changes: 4 additions & 1 deletion src/mission_control/src/controllers/mission_control.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ use junobuild_shared::constants::shared::MAX_NUMBER_OF_MISSION_CONTROL_CONTROLLE
use junobuild_shared::ic::api::id;
use junobuild_shared::mgmt::ic::update_canister_controllers;
use junobuild_shared::segments::controllers::{
assert_controllers, assert_max_number_of_controllers, into_controller_ids,
assert_controller_expiration, assert_controllers, assert_max_number_of_controllers,
into_controller_ids,
};
use junobuild_shared::types::interface::SetController;
use junobuild_shared::types::state::{ControllerId, ControllerScope, Controllers};
Expand All @@ -27,6 +28,8 @@ pub async fn set_mission_control_controllers(

assert_controllers(controllers)?;

assert_controller_expiration(&controller)?;

set_controllers(controllers, controller);

// We update the IC controllers because it is possible that an existing controller was updated.
Expand Down
8 changes: 7 additions & 1 deletion src/mission_control/src/controllers/segment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ use candid::Principal;
use ic_cdk::call::Call;
use junobuild_shared::ic::DecodeCandid;
use junobuild_shared::mgmt::ic::update_canister_controllers;
use junobuild_shared::segments::controllers::{filter_admin_controllers, into_controller_ids};
use junobuild_shared::segments::controllers::{
assert_controller_expiration, assert_controllers, filter_admin_controllers, into_controller_ids,
};
use junobuild_shared::types::interface::{
DeleteControllersArgs, SetController, SetControllersArgs,
};
Expand All @@ -13,6 +15,10 @@ pub async fn set_segment_controllers(
controllers: &[ControllerId],
controller: &SetController,
) -> Result<(), String> {
assert_controllers(controllers)?;

assert_controller_expiration(&controller)?;

let satellite_admin_controllers = set_controllers(segment_id, controllers, controller).await?;

// We update the IC controllers because it is possible that an existing controller was updated.
Expand Down
6 changes: 6 additions & 0 deletions src/observatory/src/api/controllers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ use crate::store::heap::{
delete_controllers, get_controllers, set_controllers as set_controllers_store,
};
use ic_cdk_macros::{query, update};
use junobuild_shared::ic::UnwrapOrTrap;
use junobuild_shared::segments::controllers::{assert_controller_expiration, assert_controllers};
use junobuild_shared::types::interface::{DeleteControllersArgs, SetControllersArgs};
use junobuild_shared::types::state::Controllers;

Expand All @@ -13,6 +15,10 @@ fn set_controllers(
controller,
}: SetControllersArgs,
) {
assert_controllers(&controllers).unwrap_or_trap();

assert_controller_expiration(&controller).unwrap_or_trap();

set_controllers_store(&controllers, &controller);
}

Expand Down
4 changes: 3 additions & 1 deletion src/orbiter/src/api/controllers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use ic_cdk_macros::{query, update};
use junobuild_shared::constants::shared::MAX_NUMBER_OF_SATELLITE_CONTROLLERS;
use junobuild_shared::ic::UnwrapOrTrap;
use junobuild_shared::segments::controllers::{
assert_controllers, assert_max_number_of_controllers,
assert_controller_expiration, assert_controllers, assert_max_number_of_controllers,
};
use junobuild_shared::types::interface::{DeleteControllersArgs, SetControllersArgs};
use junobuild_shared::types::state::{ControllerScope, Controllers};
Expand Down Expand Up @@ -38,6 +38,8 @@ fn set_controllers(

assert_controllers(&controllers).unwrap_or_trap();

assert_controller_expiration(&controller).unwrap_or_trap();

set_controllers_store(&controllers, &controller);
get_controllers()
}
Expand Down
3 changes: 2 additions & 1 deletion src/tests/specs/console/console.controllers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ describe('Console > Controllers', () => {

testControllers({
actor: () => actor,
controller: () => controller
controller: () => controller,
pic: () => pic
});
});
});
Loading