From ec9e6aa8588c56e30f3ee0e3b21ae1c9296bf98e Mon Sep 17 00:00:00 2001 From: Kepler Vital Date: Tue, 21 Jan 2025 14:14:31 +0100 Subject: [PATCH 01/33] feat(station): configurable station initialization --- core/station/api/spec.did | 79 +++++++++++++++++++++++++++++++-------- 1 file changed, 64 insertions(+), 15 deletions(-) diff --git a/core/station/api/spec.did b/core/station/api/spec.did index e050f2f55..e520d8320 100644 --- a/core/station/api/spec.did +++ b/core/station/api/spec.did @@ -2558,14 +2558,6 @@ type MeResult = variant { Err : Error; }; -// The admin that is created in the station during the init process. -type AdminInitInput = record { - // The name of the user. - name : text; - // The identity of the admin. - identity : principal; -}; - // An input type for configuring the upgrader canister. type SystemUpgraderInput = variant { // An existing upgrader canister. @@ -2606,23 +2598,80 @@ type InitAssetInput = record { metadata : vec AssetMetadata; }; +// The input type for creating a user group when initializing the canister for the first time. +type InitUserGroupInput = record { + // The id of the user group, if not provided a new UUID will be generated. + id : opt UUID; + // The name of the user group, must be unique. + name : text; +}; + +// The input type for adding identities to a user. +type UserIdentityInput = record { + // The identity of the user. + identity : principal; +}; + +// The users to create when initializing the canister for the first time. +type InitUserInput = record { + // The id of the user, if not provided a new UUID will be generated. + id : opt UUID; + // The name of the user. + name : text; + // The identities of the user. + identities : vec UserIdentityInput; + // The user groups to associate with the user (optional). + groups : opt vec UUID; + // The status of the user (e.g. `Active`). + // + // If not provided the default status is `Active` when there is at least + // one identity ot `Inactive` otherwise. + status : opt UserStatus; +}; + +// The init type for initializing the permissions when first creating the canister. +type InitPermissionInput = record { + // The resource that the permission is for. + resource : Resource; + // The allow rules for who can access the resource. + allow : Allow; +}; + +// The init type for adding a request approval policies when initializing the canister for the first time. +type InitRequestPolicyInput = record { + // The request specifier that identifies for what operation this policy is for (e.g. "transfer"). + specifier : RequestSpecifier; + // The rule to use for the request approval evaluation (e.g. "quorum"). + rule : RequestPolicyRule; +}; + // The init configuration for the canister. // // Only used when installing the canister for the first time. type SystemInit = record { // The name of the station. name : text; - // The list of admin principals to be associated with the station. - admins : vec AdminInitInput; - // Quorum of admins for initial policies. - quorum : opt nat16; // The upgrader configuration. upgrader : SystemUpgraderInput; - // An optional additional controller of the station and upgrader canisters. + // The list of users to be associated with the station. + users : vec InitUserInput; + // The list of user groups to be associated with the station (optional). + user_groups : opt vec InitUserGroupInput; + // The list of permissions to be associated with the station (optional). + // + // If not provided the default permissions will be used enabling any authenticated user to access + // and create requests for the station. + permissions : opt vec InitPermissionInput; + // The list of request approval policies to be associated with the station (optional). + // + // If not provided the default request approval policies will be used setting a quorum of 51% of all users + // to approve a request before it can be executed. + request_policies : opt vec InitRequestPolicyInput; + // An additional controller of the station and upgrader canisters (optional). fallback_controller : opt principal; - // Optional initial accounts to create. + // Initial accounts to create (optional). accounts : opt vec InitAccountInput; - // Optional initial assets to create. + // Initial assets to create (optional). assets : opt vec InitAssetInput; }; From ae442bb868e70fa82a90c537fed6078af22fa218 Mon Sep 17 00:00:00 2001 From: olaszakos Date: Fri, 28 Feb 2025 08:56:30 +0100 Subject: [PATCH 02/33] [wip] rework db entry creation at init --- .../control-panel/impl/src/services/deploy.rs | 18 +- core/station/api/spec.did | 64 ++- core/station/api/src/system.rs | 72 ++- core/station/impl/src/errors/system.rs | 10 +- .../impl/src/models/request_policy_rule.rs | 13 + core/station/impl/src/services/named_rule.rs | 37 +- .../impl/src/services/request_policy.rs | 12 +- core/station/impl/src/services/system.rs | 488 +++++++++++++----- core/station/impl/src/services/user.rs | 15 +- core/station/impl/src/services/user_group.rs | 14 +- tests/integration/src/control_panel_tests.rs | 18 +- .../src/disaster_recovery_tests.rs | 62 ++- tests/integration/src/setup.rs | 19 +- tests/integration/src/system_upgrade_tests.rs | 72 ++- 14 files changed, 675 insertions(+), 239 deletions(-) diff --git a/core/control-panel/impl/src/services/deploy.rs b/core/control-panel/impl/src/services/deploy.rs index cf02b9ef6..9de722945 100644 --- a/core/control-panel/impl/src/services/deploy.rs +++ b/core/control-panel/impl/src/services/deploy.rs @@ -118,11 +118,16 @@ impl DeployService { })?; // The initial admins added to the station. - let admins = input + let intial_users = input .admins .iter() - .map(|admin| station_api::AdminInitInput { - identity: admin.identity, + .map(|admin| station_api::UserInitInput { + id: None, + identities: vec![station_api::UserIdentityInput { + identity: admin.identity, + }], + groups: None, + status: None, name: admin.username.clone(), }) .collect::>(); @@ -131,17 +136,16 @@ impl DeployService { let station_install_arg = Encode!(&station_api::SystemInstall::Init(station_api::SystemInit { name: input.name.clone(), - admins, upgrader: station_api::SystemUpgraderInput::Deploy( station_api::DeploySystemUpgraderInput { wasm_module: upgrader_wasm_module, initial_cycles: Some(upgrader_initial_cycles), } ), - quorum: Some(1), fallback_controller: Some(NNS_ROOT_CANISTER_ID), - accounts: None, - assets: None, + entries: None, + users: intial_users, + quorum: None, })) .map_err(|err| DeployError::Failed { reason: err.to_string(), diff --git a/core/station/api/spec.did b/core/station/api/spec.did index 464a34bde..b63858249 100644 --- a/core/station/api/spec.did +++ b/core/station/api/spec.did @@ -2764,8 +2764,8 @@ type InitAssetInput = record { // The input type for creating a user group when initializing the canister for the first time. type InitUserGroupInput = record { - // The id of the user group, if not provided a new UUID will be generated. - id : opt UUID; + // The id of the user group. + id : UUID; // The name of the user group, must be unique. name : text; }; @@ -2803,42 +2803,60 @@ type InitPermissionInput = record { // The init type for adding a request approval policies when initializing the canister for the first time. type InitRequestPolicyInput = record { + // The id of the request policy, if not provided a new UUID will be generated. + id : opt UUID; // The request specifier that identifies for what operation this policy is for (e.g. "transfer"). specifier : RequestSpecifier; // The rule to use for the request approval evaluation (e.g. "quorum"). rule : RequestPolicyRule; }; -// The init configuration for the canister. -// -// Only used when installing the canister for the first time. +// The init type for adding a named rule when initializing the canister for the first time. +type InitNamedRuleInput = record { + // The id of the named rule. + id : UUID; + // The name of the named rule. + name : text; + // The description of the named rule. + description : opt text; + // The rule to use for the named rule. + rule : RequestPolicyRule; +}; + +type InitalEntries = variant { + // Initialize the station with default policies, accounts and assets. + WithDefaultPolicies : record { + accounts : vec InitAccountInput; + assets : vec InitAssetInput; + }; + // Initialize the station with all custom entries. + Complete : record { + user_groups : vec InitUserGroupInput; + permissions : vec InitPermissionInput; + request_policies : vec InitRequestPolicyInput; + named_rules : vec InitNamedRuleInput; + accounts : vec InitAccountInput; + assets : vec InitAssetInput; + }; +}; + type SystemInit = record { // The name of the station. name : text; // The upgrader configuration. upgrader : SystemUpgraderInput; - // The list of users to be associated with the station. - users : vec InitUserInput; - // The list of user groups to be associated with the station (optional). - user_groups : opt vec InitUserGroupInput; - // The list of permissions to be associated with the station (optional). - // - // If not provided the default permissions will be used enabling any authenticated user to access - // and create requests for the station. - permissions : opt vec InitPermissionInput; - // The list of request approval policies to be associated with the station (optional). - // - // If not provided the default request approval policies will be used setting a quorum of 51% of all users - // to approve a request before it can be executed. - request_policies : opt vec InitRequestPolicyInput; // An additional controller of the station and upgrader canisters (optional). fallback_controller : opt principal; - // Initial accounts to create (optional). - accounts : opt vec InitAccountInput; - // Initial assets to create (optional). - assets : opt vec InitAssetInput; + // The initial users to create. + users : vec InitUserInput; + // The quorum for the initial approval policy. + quorum : opt nat16; + // The initial entries to create. + entries: opt InitalEntries; }; + + // The upgrade configuration for the canister. type SystemUpgrade = record { // The updated name of the station. diff --git a/core/station/api/src/system.rs b/core/station/api/src/system.rs index eebdc5134..e9f69f8ba 100644 --- a/core/station/api/src/system.rs +++ b/core/station/api/src/system.rs @@ -1,5 +1,8 @@ use super::TimestampRfc3339; -use crate::{AccountSeedDTO, DisasterRecoveryCommitteeDTO, MetadataDTO, Sha256HashDTO, UuidDTO}; +use crate::{ + AccountSeedDTO, AllowDTO, DisasterRecoveryCommitteeDTO, MetadataDTO, RequestPolicyRuleDTO, + RequestSpecifierDTO, ResourceDTO, Sha256HashDTO, UserStatusDTO, UuidDTO, +}; use candid::{CandidType, Deserialize, Principal}; use orbit_essentials::types::WasmModuleExtraChunks; @@ -59,11 +62,46 @@ pub struct SystemInfoResponse { } #[derive(CandidType, serde::Serialize, Deserialize, Clone, Debug)] -pub struct AdminInitInput { +pub struct UserInitInput { + pub id: Option, pub name: String, + pub identities: Vec, + pub groups: Option>, + pub status: Option, +} + +#[derive(CandidType, serde::Serialize, Deserialize, Clone, Debug)] +pub struct UserIdentityInput { pub identity: Principal, } +#[derive(CandidType, serde::Serialize, Deserialize, Clone, Debug)] +pub struct InitUserGroupInput { + pub id: UuidDTO, + pub name: String, +} + +#[derive(CandidType, serde::Serialize, Deserialize, Clone, Debug)] +pub struct InitPermissionInput { + pub resource: ResourceDTO, + pub allow: AllowDTO, +} + +#[derive(CandidType, serde::Serialize, Deserialize, Clone, Debug)] +pub struct InitRequestPolicyInput { + pub id: Option, + pub specifier: RequestSpecifierDTO, + pub rule: RequestPolicyRuleDTO, +} + +#[derive(CandidType, serde::Serialize, Deserialize, Clone, Debug)] +pub struct InitNamedRuleInput { + pub id: UuidDTO, + pub name: String, + pub description: Option, + pub rule: RequestPolicyRuleDTO, +} + #[derive(CandidType, serde::Serialize, Deserialize, Clone, Debug)] pub struct DeploySystemUpgraderInput { #[serde(with = "serde_bytes")] @@ -97,22 +135,36 @@ pub struct InitAssetInput { pub decimals: u32, } +#[derive(CandidType, serde::Serialize, Deserialize, Clone, Debug)] +pub enum InitialEntries { + WithDefaultPolicies { + assets: Vec, + accounts: Vec, + }, + Complete { + permissions: Vec, + assets: Vec, + request_policies: Vec, + user_groups: Vec, + accounts: Vec, + named_rules: Vec, + }, +} + #[derive(CandidType, serde::Serialize, Deserialize, Clone, Debug)] pub struct SystemInit { /// The station name. pub name: String, - /// The initial admins. - pub admins: Vec, - /// The quorum of admin approvals required in initial policies. - pub quorum: Option, /// The upgrader configuration. pub upgrader: SystemUpgraderInput, /// Optional fallback controller for the station and upgrader canisters. pub fallback_controller: Option, - /// Optionally set the initial accounts. - pub accounts: Option>, - /// Optionally set the initial accounts. - pub assets: Option>, + /// The initial users. + pub users: Vec, + /// The initial quorum. + pub quorum: Option, + /// The initial database entries. + pub entries: Option, } #[derive(CandidType, serde::Serialize, Deserialize, Clone, Debug)] diff --git a/core/station/impl/src/errors/system.rs b/core/station/impl/src/errors/system.rs index b08601761..2e732909d 100644 --- a/core/station/impl/src/errors/system.rs +++ b/core/station/impl/src/errors/system.rs @@ -8,10 +8,10 @@ pub enum SystemError { /// The initialization of the canister failed. #[error(r#"The initialization of the canister failed due to {reason}"#)] InitFailed { reason: String }, - #[error(r#"The canister needs at least one admin"#)] - NoAdminsSpecified, - #[error(r#"There are too many admins defined, max allowed is {max}."#)] - TooManyAdminsSpecified { max: usize }, + #[error(r#"The canister needs at least one user"#)] + NoUsersSpecified, + #[error(r#"There are too many users defined, max allowed is {max}."#)] + TooManyUsersSpecified { max: usize }, #[error(r#"System upgrade failed."#)] UpgradeFailed { reason: String }, #[error(r#"No station upgrade request is processing."#)] @@ -27,7 +27,7 @@ impl DetailableError for SystemError { Some(details) } - SystemError::TooManyAdminsSpecified { max } => { + SystemError::TooManyUsersSpecified { max } => { details.insert("max".to_string(), max.to_string()); Some(details) diff --git a/core/station/impl/src/models/request_policy_rule.rs b/core/station/impl/src/models/request_policy_rule.rs index a655c58da..42b8635ec 100644 --- a/core/station/impl/src/models/request_policy_rule.rs +++ b/core/station/impl/src/models/request_policy_rule.rs @@ -44,6 +44,19 @@ pub enum RequestPolicyRule { NamedRule(NamedRuleId), } +impl RequestPolicyRule { + pub fn has_named_rule_id(&self, named_rule_id: &NamedRuleId) -> bool { + match self { + RequestPolicyRule::NamedRule(id) => id == named_rule_id, + RequestPolicyRule::And(rules) | RequestPolicyRule::Or(rules) => rules + .iter() + .any(|rule| rule.has_named_rule_id(named_rule_id)), + RequestPolicyRule::Not(rule) => rule.has_named_rule_id(named_rule_id), + _ => false, + } + } +} + impl ModelValidator for RequestPolicyRule { fn validate(&self) -> ModelValidatorResult { match self { diff --git a/core/station/impl/src/services/named_rule.rs b/core/station/impl/src/services/named_rule.rs index 68ffe08be..d91d3d48f 100644 --- a/core/station/impl/src/services/named_rule.rs +++ b/core/station/impl/src/services/named_rule.rs @@ -6,6 +6,7 @@ use orbit_essentials::{ model::{ModelKey, ModelValidator}, pagination::{paginated_items, PaginatedData, PaginatedItemsArgs}, repository::Repository, + types::UUID, }; use station_api::ListNamedRulesInput; use uuid::Uuid; @@ -17,7 +18,6 @@ use crate::{ resource::{Resource, ResourceAction, ResourceId}, AddNamedRuleOperationInput, EditNamedRuleOperationInput, NamedRule, NamedRuleCallerPrivileges, NamedRuleId, NamedRuleKey, RemoveNamedRuleOperationInput, - RequestPolicyRule, }, repositories::{ NamedRuleRepository, RequestPolicyRepository, NAMED_RULE_REPOSITORY, @@ -90,8 +90,12 @@ impl NamedRuleService { Ok(result) } - pub fn create(&self, input: AddNamedRuleOperationInput) -> ServiceResult { - let id = *Uuid::new_v4().as_bytes(); + pub fn create_with_id( + &self, + input: AddNamedRuleOperationInput, + with_named_rule_id: Option, + ) -> ServiceResult { + let id = with_named_rule_id.unwrap_or_else(|| *Uuid::new_v4().as_bytes()); let named_rule = NamedRule { id, @@ -108,35 +112,22 @@ impl NamedRuleService { Ok(named_rule) } - fn is_named_rule_referenced_in_rule( - rule: &RequestPolicyRule, - named_rule_id: &NamedRuleId, - ) -> bool { - match rule { - RequestPolicyRule::NamedRule(id) => id == named_rule_id, - RequestPolicyRule::And(rules) | RequestPolicyRule::Or(rules) => rules - .iter() - .any(|rule| Self::is_named_rule_referenced_in_rule(rule, named_rule_id)), - RequestPolicyRule::Not(rule) => { - Self::is_named_rule_referenced_in_rule(rule, named_rule_id) - } - _ => false, - } + pub fn create(&self, input: AddNamedRuleOperationInput) -> ServiceResult { + self.create_with_id(input, None) } fn is_named_rule_in_use_by_request_policies(&self, named_rule_id: &NamedRuleId) -> bool { self.request_policy_repository .list() .iter() - .any(|request_policy| { - Self::is_named_rule_referenced_in_rule(&request_policy.rule, named_rule_id) - }) + .any(|request_policy| request_policy.rule.has_named_rule_id(named_rule_id)) } fn is_named_rule_in_use_by_named_rules(&self, named_rule_id: &NamedRuleId) -> bool { - self.named_rule_repository.list().iter().any(|named_rule| { - Self::is_named_rule_referenced_in_rule(&named_rule.rule, named_rule_id) - }) + self.named_rule_repository + .list() + .iter() + .any(|named_rule| named_rule.rule.has_named_rule_id(named_rule_id)) } pub fn remove(&self, input: RemoveNamedRuleOperationInput) -> ServiceResult { diff --git a/core/station/impl/src/services/request_policy.rs b/core/station/impl/src/services/request_policy.rs index ed0a9e5f4..682cbe6eb 100644 --- a/core/station/impl/src/services/request_policy.rs +++ b/core/station/impl/src/services/request_policy.rs @@ -57,8 +57,18 @@ impl RequestPolicyService { &self, input: AddRequestPolicyOperationInput, ) -> ServiceResult { + self.add_request_policy_with_id(input, None) + } + + pub fn add_request_policy_with_id( + &self, + input: AddRequestPolicyOperationInput, + with_policy_id: Option, + ) -> ServiceResult { + let id = with_policy_id.unwrap_or_else(|| *Uuid::new_v4().as_bytes()); + let policy = RequestPolicy { - id: *Uuid::new_v4().as_bytes(), + id, specifier: input.specifier, rule: input.rule, }; diff --git a/core/station/impl/src/services/system.rs b/core/station/impl/src/services/system.rs index 4721100ce..ec3912149 100644 --- a/core/station/impl/src/services/system.rs +++ b/core/station/impl/src/services/system.rs @@ -29,7 +29,7 @@ use candid::Principal; use lazy_static::lazy_static; use orbit_essentials::api::ServiceResult; use orbit_essentials::repository::Repository; -use station_api::{HealthStatus, SystemInit, SystemInstall, SystemUpgrade}; +use station_api::{HealthStatus, InitialEntries, SystemInit, SystemInstall, SystemUpgrade}; use std::{ collections::{BTreeMap, BTreeSet}, sync::Arc, @@ -291,20 +291,20 @@ impl SystemService { install_canister_handlers::set_controllers(station_controllers).await?; // calculates the initial quorum based on the number of admins and the provided quorum - let admin_count = init.admins.len() as u16; - let quorum = calc_initial_quorum(admin_count, init.quorum); - - // if provided, creates the initial assets - if let Some(assets) = init.assets.clone() { - print("Adding initial assets"); - install_canister_handlers::set_initial_assets(assets).await?; - } - - // if provided, creates the initial accounts - if let Some(accounts) = init.accounts { - print("Adding initial accounts"); - install_canister_handlers::set_initial_accounts(accounts, &init.assets, quorum) - .await?; + let initial_user_count = init.users.len() as u16; + let quorum = calc_initial_quorum(initial_user_count, init.quorum); + + match init.entries { + Some(InitialEntries::WithDefaultPolicies { accounts, assets }) + | Some(InitialEntries::Complete { + accounts, assets, .. + }) => { + print("Adding initial accounts"); + // initial accounts are added in the post process work timer, since they might do inter-canister calls + install_canister_handlers::set_initial_accounts(accounts, &assets, quorum) + .await?; + } + _ => (), } if SYSTEM_SERVICE.is_healthy() { @@ -386,24 +386,52 @@ impl SystemService { pub async fn init_canister(&self, input: SystemInit) -> ServiceResult<()> { let mut system_info = SystemInfo::default(); - if input.admins.is_empty() { - return Err(SystemError::NoAdminsSpecified)?; + if input.users.is_empty() { + return Err(SystemError::NoUsersSpecified)?; } - if input.admins.len() > u16::MAX as usize { - return Err(SystemError::TooManyAdminsSpecified { + if input.users.len() > u16::MAX as usize { + return Err(SystemError::TooManyUsersSpecified { max: u16::MAX as usize, })?; } - // adds the default admin group - init_canister_sync_handlers::add_initial_groups(); - - // registers the admins of the canister - init_canister_sync_handlers::set_admins(input.admins.clone())?; + match &input.entries { + Some(InitialEntries::WithDefaultPolicies { assets, .. }) => { + // adds the default admin group + init_canister_sync_handlers::add_default_groups(); + // registers the admins of the canister + init_canister_sync_handlers::set_initial_users(input.users.clone())?; + // adds the initial assets + init_canister_sync_handlers::set_initial_assets(assets).await?; - // add initial assets - init_canister_sync_handlers::add_initial_assets(); + // initial accounts are added in the post process work timer, since they might do inter-canister calls + } + Some(InitialEntries::Complete { + user_groups, + permissions, + request_policies, + named_rules, + assets, + .. + }) => { + init_canister_sync_handlers::set_initial_user_groups(user_groups).await?; + init_canister_sync_handlers::set_initial_users(input.users.clone())?; + init_canister_sync_handlers::set_initial_named_rules(named_rules)?; + init_canister_sync_handlers::set_initial_permissions(permissions).await?; + init_canister_sync_handlers::set_initial_assets(assets).await?; + init_canister_sync_handlers::set_initial_request_policies(request_policies)?; + // accounts in post process timer + } + None => { + // // adds the default admin group + init_canister_sync_handlers::add_default_groups(); + // registers the admins of the canister + init_canister_sync_handlers::set_initial_users(input.users.clone())?; + // // add default assets + init_canister_sync_handlers::add_default_assets(); + } + } // sets the name of the canister system_info.set_name(input.name.clone()); @@ -528,10 +556,23 @@ impl SystemService { } mod init_canister_sync_handlers { + use std::cmp::Ordering; + use crate::core::ic_cdk::{api::print, next_time}; - use crate::models::{AddUserOperationInput, Asset, UserStatus, OPERATOR_GROUP_ID}; + use crate::mappers::blockchain::BlockchainMapper; + use crate::mappers::HelperMapper; + use crate::models::request_specifier::RequestSpecifier; + use crate::models::resource::ResourceIds; + use crate::models::{ + AddAssetOperationInput, AddNamedRuleOperationInput, AddRequestPolicyOperationInput, + AddUserGroupOperationInput, AddUserOperationInput, Asset, EditPermissionOperationInput, + UserStatus, OPERATOR_GROUP_ID, + }; use crate::repositories::ASSET_REPOSITORY; - use crate::services::USER_SERVICE; + use crate::services::permission::PERMISSION_SERVICE; + use crate::services::{ + ASSET_SERVICE, NAMED_RULE_SERVICE, REQUEST_POLICY_SERVICE, USER_GROUP_SERVICE, USER_SERVICE, + }; use crate::{ models::{UserGroup, ADMIN_GROUP_ID}, repositories::USER_GROUP_REPOSITORY, @@ -539,12 +580,16 @@ mod init_canister_sync_handlers { use orbit_essentials::api::ApiError; use orbit_essentials::model::ModelKey; use orbit_essentials::repository::Repository; - use station_api::AdminInitInput; + use orbit_essentials::types::UUID; + use station_api::{ + InitAssetInput, InitNamedRuleInput, InitPermissionInput, InitRequestPolicyInput, + InitUserGroupInput, UserInitInput, + }; use uuid::Uuid; use super::INITIAL_ICP_ASSET; - pub fn add_initial_groups() { + pub fn add_default_groups() { // adds the admin group which is used as the default group for admins during the canister instantiation USER_GROUP_REPOSITORY.insert( ADMIN_GROUP_ID.to_owned(), @@ -566,7 +611,7 @@ mod init_canister_sync_handlers { ); } - pub fn add_initial_assets() { + pub fn add_default_assets() { let initial_assets: Vec = vec![INITIAL_ICP_ASSET.clone()]; for asset in initial_assets { @@ -575,20 +620,246 @@ mod init_canister_sync_handlers { } } + pub async fn set_initial_user_groups( + user_groups: &[InitUserGroupInput], + ) -> Result<(), ApiError> { + let add_user_groups = user_groups + .iter() + .map(|user_group| { + let input = AddUserGroupOperationInput { + name: user_group.name.clone(), + }; + + ( + input, + *HelperMapper::to_uuid(user_group.id.clone()) + .expect("Invalid UUID") + .as_bytes(), + ) + }) + .collect::>(); + + for (new_user_group, with_user_group_id) in add_user_groups { + USER_GROUP_SERVICE + .create_with_id(new_user_group, Some(with_user_group_id)) + .await?; + } + + Ok(()) + } + + pub fn set_initial_named_rules(named_rules: &[InitNamedRuleInput]) -> Result<(), ApiError> { + let mut add_named_rules = named_rules + .iter() + .map(|named_rule| { + let input = AddNamedRuleOperationInput { + name: named_rule.name.clone(), + description: named_rule.description.clone(), + rule: named_rule.rule.clone().into(), + }; + + ( + input, + HelperMapper::to_uuid(named_rule.id.clone()).map(|uuid| *uuid.as_bytes()), + ) + }) + .map(|(input, result)| result.map(|uuid| (input, uuid))) + .collect::, _>>()?; + + // sorting criteria: + // - if a policy depends on another policy, the dependent policy should be added first + // - keep the original order of the policys otherwise + add_named_rules.sort_by(|a, b| { + if b.0.rule.has_named_rule_id(&a.1) { + Ordering::Less + } else { + Ordering::Greater + } + }); + + for (new_named_rule, with_named_rule_id) in add_named_rules { + NAMED_RULE_SERVICE.create_with_id(new_named_rule, Some(with_named_rule_id))?; + } + + Ok(()) + } + + pub async fn set_initial_permissions( + permissions: &[InitPermissionInput], + ) -> Result<(), ApiError> { + for permission in permissions { + let users = permission + .allow + .users + .iter() + .map(|id| HelperMapper::to_uuid(id.clone()).map(|uuid| *uuid.as_bytes())) + .collect::, _>>()?; + + let user_groups = permission + .allow + .user_groups + .iter() + .map(|id| HelperMapper::to_uuid(id.clone()).map(|uuid| *uuid.as_bytes())) + .collect::, _>>()?; + + let input = EditPermissionOperationInput { + resource: permission.resource.clone().into(), + auth_scope: Some(permission.allow.auth_scope.clone().into()), + users: Some(users), + user_groups: Some(user_groups), + }; + + PERMISSION_SERVICE.edit_permission(input)?; + } + + Ok(()) + } + + fn specifier_has_reference_to_policy_id( + specifier: &RequestSpecifier, + policy_id: &UUID, + ) -> bool { + match specifier { + RequestSpecifier::EditRequestPolicy(resource_ids) + | RequestSpecifier::RemoveRequestPolicy(resource_ids) => match resource_ids { + ResourceIds::Any => false, + ResourceIds::Ids(ids) => ids.contains(policy_id), + }, + _ => false, + } + } + + pub fn set_initial_request_policies( + request_policies: &[InitRequestPolicyInput], + ) -> Result<(), ApiError> { + let mut add_request_policies = request_policies + .iter() + .map(|request_policy| { + let request_policy_id = request_policy + .id + .as_ref() + .map(|id| HelperMapper::to_uuid(id.clone()).map(|uuid| *uuid.as_bytes())) + .transpose(); + + let input = AddRequestPolicyOperationInput { + specifier: request_policy.specifier.clone().into(), + rule: request_policy.rule.clone().into(), + }; + + request_policy_id.map(|request_policy_id| (input, request_policy_id)) + }) + .collect::, _>>()?; + + // sorting criteria: + // - if a policy depends on another policy, the dependent policy should be added first + // - keep the original order of the policies otherwise + add_request_policies.sort_by(|a, b| { + if let Some(a_id) = &a.1 { + if specifier_has_reference_to_policy_id(&b.0.specifier, a_id) { + return Ordering::Less; + } + } + Ordering::Greater + }); + + for (input, request_policy_id) in add_request_policies { + REQUEST_POLICY_SERVICE.add_request_policy_with_id(input, request_policy_id)?; + } + + Ok(()) + } + + // Registers the initial accounts of the canister during the canister initialization. + pub async fn set_initial_assets(assets: &[InitAssetInput]) -> Result<(), ApiError> { + let add_assets = assets + .iter() + .map(|asset| { + let input = AddAssetOperationInput { + name: asset.name.clone(), + blockchain: BlockchainMapper::to_blockchain(asset.blockchain.clone()) + .expect("Invalid blockchain"), + standards: asset + .standards + .iter() + .map(|standard| { + BlockchainMapper::to_blockchain_standard(standard.clone()) + .expect("Invalid blockchain standard") + }) + .collect(), + decimals: asset.decimals, + symbol: asset.symbol.clone(), + metadata: asset.metadata.clone().into(), + }; + + ( + input, + *HelperMapper::to_uuid(asset.id.clone()) + .expect("Invalid UUID") + .as_bytes(), + ) + }) + .collect::>(); + + for (new_asset, with_asset_id) in add_assets { + match ASSET_SERVICE.create(new_asset, Some(with_asset_id)) { + Err(ApiError { code, details, .. }) if &code == "ALREADY_EXISTS" => { + // asset already exists, can skip safely + print(format!( + "Asset already exists, skipping. Details: {:?}", + details.unwrap_or_default() + )); + } + Err(e) => Err(e)?, + Ok(_) => {} + } + } + + Ok(()) + } + /// Registers the newly added admins of the canister. - pub fn set_admins(admins: Vec) -> Result<(), ApiError> { - print(format!("Registering {} admin users", admins.len())); - for admin in admins { - let user = USER_SERVICE.add_user(AddUserOperationInput { - identities: vec![admin.identity.to_owned()], - groups: vec![ADMIN_GROUP_ID.to_owned()], - name: admin.name.to_owned(), - status: UserStatus::Active, - })?; + pub fn set_initial_users(users: Vec) -> Result<(), ApiError> { + print(format!("Registering {} admin users", users.len())); + for user in users { + let user_id = user + .id + .map(|id_str| HelperMapper::to_uuid(id_str).map(|uuid| *uuid.as_bytes())) + .transpose()?; + + let user = USER_SERVICE.add_user_with_id( + AddUserOperationInput { + identities: user + .identities + .iter() + .map(|identity| identity.identity.to_owned()) + .collect(), + groups: user + .groups + .map(|ids| { + ids.into_iter() + .map(|id| { + HelperMapper::to_uuid(id.clone()) + .map(|uuid| uuid.as_bytes().to_owned()) + }) + .collect::, _>>() + .unwrap_or_else(|_| vec![*ADMIN_GROUP_ID]) + }) + .unwrap_or_else(|| vec![*ADMIN_GROUP_ID]), + name: user.name.to_owned(), + status: user + .status + .map(UserStatus::from) + .unwrap_or(UserStatus::Active), + }, + user_id, + )?; print(&format!( - "Added admin user with principal {} and user id {}", - admin.identity.to_text(), + "Added admin user with principals {:?} and user id {}", + user.identities + .iter() + .map(|identity| identity.to_text()) + .collect::>(), Uuid::from_bytes(user.id).hyphenated() )); } @@ -636,7 +907,7 @@ mod install_canister_handlers { /// Registers the default configurations for the canister. pub async fn init_post_process(init: &SystemInit) -> Result<(), String> { - let admin_quorum = super::calc_initial_quorum(init.admins.len() as u16, init.quorum); + let admin_quorum = super::calc_initial_quorum(init.users.len() as u16, init.quorum); let (regular_named_rule_config, admin_named_rule_config) = get_default_named_rules(admin_quorum); @@ -689,7 +960,7 @@ mod install_canister_handlers { // Registers the initial accounts of the canister during the canister initialization. pub async fn set_initial_accounts( accounts: Vec, - initial_assets: &Option>, + initial_assets: &Vec, quorum: u16, ) -> Result<(), String> { let add_accounts = accounts @@ -735,49 +1006,47 @@ mod install_canister_handlers { // in the existing assets and replace the asset_id in the recreated account with the existing one. // for (mut new_account, with_account_id) in add_accounts { - if let Some(initial_assets) = initial_assets { - let mut new_account_assets = new_account.assets.clone(); - for asset_id in new_account.assets.iter() { - if ASSET_REPOSITORY.get(asset_id).is_none() { - // the asset could not be recreated, try to find the same asset in the existing assets - let asset_id_str = Uuid::from_bytes(*asset_id).hyphenated().to_string(); - let Some(original_asset_to_create) = initial_assets - .iter() - .find(|initial_asset| initial_asset.id == asset_id_str) - else { - // the asset does not exist and it could not be recreated, skip - continue; - }; - - if let Some(existing_asset_id) = ASSET_REPOSITORY.exists_unique( - &original_asset_to_create.blockchain, - &original_asset_to_create.symbol, - ) { - // replace the asset_id in the recreated account with the existing one - new_account_assets.retain(|id| asset_id != id); - new_account_assets.push(existing_asset_id); - - print(format!( - "Asset {} could not be recreated, replaced with existing asset {}", - asset_id_str, - Uuid::from_bytes(existing_asset_id).hyphenated() - )); - } else { - // the asset does not exist and it could not be recreated, skip + let mut new_account_assets = new_account.assets.clone(); + for asset_id in new_account.assets.iter() { + if ASSET_REPOSITORY.get(asset_id).is_none() { + // the asset could not be recreated, try to find the same asset in the existing assets + let asset_id_str = Uuid::from_bytes(*asset_id).hyphenated().to_string(); + let Some(original_asset_to_create) = initial_assets + .iter() + .find(|initial_asset| initial_asset.id == asset_id_str) + else { + // the asset does not exist and it could not be recreated, skip + continue; + }; - print(format!( + if let Some(existing_asset_id) = ASSET_REPOSITORY.exists_unique( + &original_asset_to_create.blockchain, + &original_asset_to_create.symbol, + ) { + // replace the asset_id in the recreated account with the existing one + new_account_assets.retain(|id| asset_id != id); + new_account_assets.push(existing_asset_id); + + print(format!( + "Asset {} could not be recreated, replaced with existing asset {}", + asset_id_str, + Uuid::from_bytes(existing_asset_id).hyphenated() + )); + } else { + // the asset does not exist and it could not be recreated, skip + + print(format!( "Asset {} could not be recreated and does not exist in the existing assets, skipping", asset_id_str )); - continue; - } + continue; } } - - new_account.assets = new_account_assets; } + new_account.assets = new_account_assets; + ACCOUNT_SERVICE .create_account(new_account, with_account_id) .await @@ -786,53 +1055,6 @@ mod install_canister_handlers { Ok(()) } - // Registers the initial accounts of the canister during the canister initialization. - pub async fn set_initial_assets(assets: Vec) -> Result<(), String> { - let add_assets = assets - .into_iter() - .map(|asset| { - let input = AddAssetOperationInput { - name: asset.name, - blockchain: BlockchainMapper::to_blockchain(asset.blockchain.clone()) - .expect("Invalid blockchain"), - standards: asset - .standards - .iter() - .map(|standard| { - BlockchainMapper::to_blockchain_standard(standard.clone()) - .expect("Invalid blockchain standard") - }) - .collect(), - decimals: asset.decimals, - symbol: asset.symbol, - metadata: asset.metadata.into(), - }; - - ( - input, - *HelperMapper::to_uuid(asset.id) - .expect("Invalid UUID") - .as_bytes(), - ) - }) - .collect::>(); - - for (new_asset, with_asset_id) in add_assets { - match ASSET_SERVICE.create(new_asset, Some(with_asset_id)) { - Err(ApiError { code, details, .. }) if &code == "ALREADY_EXISTS" => { - // asset already exists, can skip safely - print(format!( - "Asset already exists, skipping. Details: {:?}", - details.unwrap_or_default() - )); - } - Err(e) => Err(format!("Failed to add asset: {:?}", e))?, - Ok(_) => {} - } - } - - Ok(()) - } pub async fn init_upgrader( input: station_api::SystemUpgraderInput, @@ -939,18 +1161,24 @@ mod tests { use super::*; use crate::models::request_test_utils::mock_request; use candid::Principal; - use station_api::AdminInitInput; + use station_api::{UserIdentityInput, UserInitInput}; #[tokio::test] async fn canister_init() { let result = SYSTEM_SERVICE .init_canister(SystemInit { name: "Station".to_string(), - admins: vec![AdminInitInput { + users: vec![UserInitInput { name: "Admin".to_string(), - identity: Principal::from_slice(&[1; 29]), + identities: vec![UserIdentityInput { + identity: Principal::from_slice(&[1; 29]), + }], + id: None, + groups: None, + status: None, }], - quorum: Some(1), + quorum: None, + entries: None, upgrader: station_api::SystemUpgraderInput::Deploy( station_api::DeploySystemUpgraderInput { wasm_module: vec![], @@ -958,8 +1186,6 @@ mod tests { }, ), fallback_controller: None, - accounts: None, - assets: None, }) .await; diff --git a/core/station/impl/src/services/user.rs b/core/station/impl/src/services/user.rs index f03ab7488..51513eb67 100644 --- a/core/station/impl/src/services/user.rs +++ b/core/station/impl/src/services/user.rs @@ -110,13 +110,26 @@ impl UserService { /// /// This method should only be called by a system call (self canister call or controller). pub fn add_user(&self, input: AddUserOperationInput) -> ServiceResult { + self.add_user_with_id(input, None) + } + + /// Creates a new user with the given user details and returns the created user. + /// + /// This method should only be called by a system call (self canister call or controller). + pub fn add_user_with_id( + &self, + input: AddUserOperationInput, + with_id: Option, + ) -> ServiceResult { for identity in input.identities.iter() { self.assert_identity_has_no_associated_user(identity, None)?; } + let user_id = with_id.unwrap_or_else(|| *Uuid::new_v4().as_bytes()); + self.assert_name_has_no_associated_user(&input.name, None)?; - let user = UserMapper::from_create_input(*Uuid::new_v4().as_bytes(), input); + let user = UserMapper::from_create_input(user_id, input); user.validate()?; diff --git a/core/station/impl/src/services/user_group.rs b/core/station/impl/src/services/user_group.rs index c61d58e76..4e7435013 100644 --- a/core/station/impl/src/services/user_group.rs +++ b/core/station/impl/src/services/user_group.rs @@ -92,7 +92,19 @@ impl UserGroupService { } pub async fn create(&self, input: AddUserGroupOperationInput) -> ServiceResult { - let user_group_id = generate_uuid_v4().await; + self.create_with_id(input, None).await + } + + pub async fn create_with_id( + &self, + input: AddUserGroupOperationInput, + with_user_group_id: Option, + ) -> ServiceResult { + let user_group_id = match with_user_group_id { + Some(id) => Uuid::from_bytes(id), + None => generate_uuid_v4().await, + }; + let user_group = UserGroup { id: *user_group_id.as_bytes(), name: input.name.to_string(), diff --git a/tests/integration/src/control_panel_tests.rs b/tests/integration/src/control_panel_tests.rs index 9f33f5f49..ea9ac7a65 100644 --- a/tests/integration/src/control_panel_tests.rs +++ b/tests/integration/src/control_panel_tests.rs @@ -21,8 +21,8 @@ use pocket_ic::management_canister::CanisterInstallMode; use pocket_ic::{update_candid_as, CallError, PocketIc}; use sha2::{Digest, Sha256}; use station_api::{ - AdminInitInput, HealthStatus, SystemInfoResponse, SystemInit as SystemInitArg, - SystemInstall as SystemInstallArg, + HealthStatus, SystemInfoResponse, SystemInit as SystemInitArg, + SystemInstall as SystemInstallArg, UserIdentityInput, UserInitInput, }; #[test] @@ -816,12 +816,17 @@ fn deploy_station_with_insufficient_cycles() { let upgrader_wasm = get_canister_wasm("upgrader").to_vec(); let station_init_args = Encode!(&SystemInstallArg::Init(SystemInitArg { name: "Station".to_string(), - admins: vec![AdminInitInput { - identity: WALLET_ADMIN_USER, + users: vec![UserInitInput { + identities: vec![UserIdentityInput { + identity: WALLET_ADMIN_USER, + }], name: "station-admin".to_string(), + groups: None, + id: None, + status: None, }], - assets: None, - quorum: Some(1), + quorum: None, + entries: None, upgrader: station_api::SystemUpgraderInput::Deploy( station_api::DeploySystemUpgraderInput { wasm_module: upgrader_wasm, @@ -829,7 +834,6 @@ fn deploy_station_with_insufficient_cycles() { }, ), fallback_controller: None, - accounts: None, })) .unwrap(); diff --git a/tests/integration/src/disaster_recovery_tests.rs b/tests/integration/src/disaster_recovery_tests.rs index b645cd4b2..ed5b807ac 100644 --- a/tests/integration/src/disaster_recovery_tests.rs +++ b/tests/integration/src/disaster_recovery_tests.rs @@ -21,7 +21,7 @@ use serde::Deserialize; use station_api::{ AccountDTO, AddAccountOperationInput, AllowDTO, DisasterRecoveryCommitteeDTO, HealthStatus, ListAccountsResponse, RequestOperationDTO, RequestOperationInput, RequestPolicyRuleDTO, - SetDisasterRecoveryOperationInput, SystemInit, SystemInstall, SystemUpgrade, + SetDisasterRecoveryOperationInput, SystemInit, SystemInstall, SystemUpgrade, UserIdentityInput, }; use std::collections::BTreeMap; use std::str::FromStr; @@ -503,25 +503,42 @@ fn test_disaster_recovery_flow_recreates_same_accounts() { module_extra_chunks: Some(module_extra_chunks), arg: Encode!(&station_api::SystemInstall::Init(station_api::SystemInit { name: "Station".to_string(), - admins: vec![ - station_api::AdminInitInput { - identity: WALLET_ADMIN_USER, + users: vec![ + station_api::UserInitInput { + identities: vec![UserIdentityInput { + identity: WALLET_ADMIN_USER, + }], name: "updated-admin-name".to_string(), + groups: None, + id: None, + status: None, }, - station_api::AdminInitInput { - identity: Principal::from_slice(&[95; 29]), + station_api::UserInitInput { + identities: vec![UserIdentityInput { + identity: Principal::from_slice(&[95; 29]), + }], name: "another-admin".to_string(), + groups: None, + id: None, + status: None, }, - station_api::AdminInitInput { - identity: Principal::from_slice(&[97; 29]), + station_api::UserInitInput { + identities: vec![UserIdentityInput { + identity: Principal::from_slice(&[97; 29]), + }], name: "yet-another-admin".to_string(), - } + groups: None, + id: None, + status: None, + }, ], - quorum: None, fallback_controller: None, upgrader: station_api::SystemUpgraderInput::Id(upgrader_id), - accounts: Some(init_accounts_input), - assets: Some(vec![init_assets_input]), + quorum: None, + entries: Some(station_api::InitialEntries::WithDefaultPolicies { + accounts: init_accounts_input, + assets: vec![init_assets_input], + }), })) .unwrap(), install_mode: upgrader_api::InstallMode::Reinstall, @@ -686,15 +703,19 @@ fn test_disaster_recovery_flow_reuses_same_upgrader() { module_extra_chunks: Some(module_extra_chunks), arg: Encode!(&station_api::SystemInstall::Init(station_api::SystemInit { name: "Station".to_string(), - admins: vec![station_api::AdminInitInput { - identity: WALLET_ADMIN_USER, + users: vec![station_api::UserInitInput { + identities: vec![UserIdentityInput { + identity: WALLET_ADMIN_USER, + }], name: "updated-admin-name".to_string(), + groups: None, + id: None, + status: None, }], - quorum: None, fallback_controller: Some(fallback_controller), upgrader: station_api::SystemUpgraderInput::Id(upgrader_id), - accounts: None, - assets: None, + quorum: None, + entries: None, })) .unwrap(), install_mode: upgrader_api::InstallMode::Reinstall, @@ -904,7 +925,6 @@ fn test_disaster_recovery_failing() { // intentionally bad arg to fail Upgrade let arg = SystemInstall::Init(SystemInit { fallback_controller: None, - quorum: None, upgrader: station_api::SystemUpgraderInput::Deploy( station_api::DeploySystemUpgraderInput { wasm_module: vec![], @@ -912,9 +932,9 @@ fn test_disaster_recovery_failing() { }, ), name: "Station".to_string(), - admins: vec![], - accounts: None, - assets: None, + users: vec![], + quorum: None, + entries: None, }); // install with intentionally bad arg to fail diff --git a/tests/integration/src/setup.rs b/tests/integration/src/setup.rs index dce74b3aa..0e8446afd 100644 --- a/tests/integration/src/setup.rs +++ b/tests/integration/src/setup.rs @@ -10,7 +10,10 @@ use candid::{CandidType, Encode, Principal}; use ic_ledger_types::{AccountIdentifier, Tokens, DEFAULT_SUBACCOUNT}; use pocket_ic::{update_candid_as, PocketIc, PocketIcBuilder}; use serde::Serialize; -use station_api::{AdminInitInput, SystemInit as SystemInitArg, SystemInstall as SystemInstallArg}; +use station_api::{ + SystemInit as SystemInitArg, SystemInstall as SystemInstallArg, UserIdentityInput, + UserInitInput, +}; use std::collections::{HashMap, HashSet}; use std::env; use std::fs::File; @@ -303,12 +306,15 @@ fn install_canisters( let station_init_args = SystemInstallArg::Init(SystemInitArg { name: "Station".to_string(), - admins: vec![AdminInitInput { - identity: WALLET_ADMIN_USER, + users: vec![UserInitInput { + identities: vec![UserIdentityInput { + identity: WALLET_ADMIN_USER, + }], name: "station-admin".to_string(), + groups: None, + id: None, + status: None, }], - assets: None, - quorum: Some(1), upgrader: station_api::SystemUpgraderInput::Deploy( station_api::DeploySystemUpgraderInput { wasm_module: upgrader_wasm, @@ -316,7 +322,8 @@ fn install_canisters( }, ), fallback_controller: config.fallback_controller, - accounts: None, + quorum: None, + entries: None, }); env.install_canister( station, diff --git a/tests/integration/src/system_upgrade_tests.rs b/tests/integration/src/system_upgrade_tests.rs index 894435c88..69212e75b 100644 --- a/tests/integration/src/system_upgrade_tests.rs +++ b/tests/integration/src/system_upgrade_tests.rs @@ -1,7 +1,8 @@ use crate::setup::{get_canister_wasm, setup_new_env, WALLET_ADMIN_USER}; +use crate::station_test_data::asset::list_assets; use crate::utils::{ - execute_request, execute_request_with_extra_ticks, get_core_canister_health_status, - get_system_info, upload_canister_chunks_to_asset_canister, + await_station_healthy, execute_request, execute_request_with_extra_ticks, + get_core_canister_health_status, get_system_info, upload_canister_chunks_to_asset_canister, }; use crate::{CanisterIds, TestEnv}; use candid::{Encode, Principal}; @@ -9,9 +10,11 @@ use orbit_essentials::api::ApiResult; use pocket_ic::{update_candid_as, PocketIc}; use station_api::{ HealthStatus, NotifyFailedStationUpgradeInput, RequestOperationInput, RequestStatusDTO, - SystemInstall, SystemUpgrade, SystemUpgradeOperationInput, SystemUpgradeTargetDTO, + SystemInit, SystemInstall, SystemUpgrade, SystemUpgradeOperationInput, SystemUpgradeTargetDTO, + UserIdentityInput, UserInitInput, }; use upgrader_api::InitArg; +use uuid::Uuid; pub(crate) const STATION_UPGRADE_EXTRA_TICKS: u64 = 200; @@ -329,3 +332,66 @@ fn unauthorized_notify_failed_station_upgrade() { .unwrap() .contains("No station upgrade request is processing.")); } + +#[test] +fn install_with_initial_assets() { + let TestEnv { + env, controller, .. + } = setup_new_env(); + + let canister_id = env.create_canister_with_settings(Some(controller), None); + + env.set_controllers(canister_id, Some(controller), vec![canister_id, controller]) + .expect("failed to set canister controller"); + + env.add_cycles(canister_id, 5_000_000_000_000); + let station_wasm = get_canister_wasm("station").to_vec(); + let upgrader_wasm = get_canister_wasm("upgrader").to_vec(); + + let station_init_args = SystemInstall::Init(SystemInit { + name: "Station".to_string(), + users: vec![UserInitInput { + identities: vec![UserIdentityInput { + identity: WALLET_ADMIN_USER, + }], + name: "station-admin".to_string(), + groups: None, + id: None, + status: None, + }], + upgrader: station_api::SystemUpgraderInput::Deploy( + station_api::DeploySystemUpgraderInput { + wasm_module: upgrader_wasm, + initial_cycles: Some(1_000_000_000_000), + }, + ), + fallback_controller: Some(controller), + quorum: None, + entries: Some(station_api::InitialEntries::WithDefaultPolicies { + accounts: vec![], + assets: vec![station_api::InitAssetInput { + name: "test-asset".to_string(), + id: Uuid::new_v4().hyphenated().to_string(), + blockchain: "icp".to_string(), + standards: vec!["icp_native".to_owned()], + metadata: vec![], + symbol: "TEST".to_string(), + decimals: 8, + }], + }), + }); + env.install_canister( + canister_id, + station_wasm, + Encode!(&station_init_args).unwrap(), + Some(controller), + ); + + await_station_healthy(&env, canister_id, WALLET_ADMIN_USER); + + let assets = list_assets(&env, canister_id, WALLET_ADMIN_USER) + .expect("failed to call list_assets") + .0 + .expect("failed to list assets"); + println!("assets: {:?}", assets); +} From ab752e7128d776a11da994135a87d1e9ce46bb9d Mon Sep 17 00:00:00 2001 From: olaszakos Date: Thu, 6 Mar 2025 11:18:47 +0100 Subject: [PATCH 03/33] install integration tests --- apps/wallet/src/generated/station/station.did | 10 + core/station/api/spec.did | 25 +- core/station/api/src/system.rs | 17 +- .../external_canister_repository_v1.bin | Bin 0 -> 65536 bytes .../impl/src/services/disaster_recovery.rs | 4 +- core/station/impl/src/services/system.rs | 232 +++-- tests/integration/src/install_tests.rs | 842 ++++++++++++++++++ tests/integration/src/lib.rs | 1 + .../src/station_test_data/account.rs | 22 +- .../src/station_test_data/permission.rs | 20 + .../integration/src/station_test_data/user.rs | 23 +- .../src/station_test_data/user_group.rs | 21 +- tests/integration/src/system_upgrade_tests.rs | 72 +- tests/integration/src/utils.rs | 1 + 14 files changed, 1125 insertions(+), 165 deletions(-) create mode 100644 core/station/impl/src/migration_tests/snapshots/external_canister_repository_v1.bin create mode 100644 tests/integration/src/install_tests.rs diff --git a/apps/wallet/src/generated/station/station.did b/apps/wallet/src/generated/station/station.did index 3bd79d86a..68e6d2951 100644 --- a/apps/wallet/src/generated/station/station.did +++ b/apps/wallet/src/generated/station/station.did @@ -2750,6 +2750,16 @@ type InitAccountInput = record { assets : vec UUID; // Metadata associated with the account (e.g. `{"contract": "0x1234", "symbol": "ANY"}`). metadata : vec AccountMetadata; + // Who can read the account information. + read_permission : Allow; + // Who can request updates to the account. + configs_permission : Allow; + // Who can request transfers from the account. + transfer_permission : Allow; + // The approval policy for updates to the account. + configs_request_policy : opt RequestPolicyRule; + // The approval policy for transfers from the account. + transfer_request_policy : opt RequestPolicyRule; }; // The initial assets to create when initializing the canister for the first time, e.g., after disaster recovery. diff --git a/core/station/api/spec.did b/core/station/api/spec.did index b63858249..7934b44df 100644 --- a/core/station/api/spec.did +++ b/core/station/api/spec.did @@ -2744,6 +2744,29 @@ type InitAccountInput = record { metadata : vec AccountMetadata; }; +// The permissions for the account. +type InitAccountPermissionsInput = record { + // Who can read the account information. + read_permission : Allow; + // Who can request updates to the account. + configs_permission : Allow; + // Who can request transfers from the account. + transfer_permission : Allow; + // The approval policy for updates to the account. + configs_request_policy : opt RequestPolicyRule; + // The approval policy for transfers from the account. + transfer_request_policy : opt RequestPolicyRule; +}; + +// The initial account to create when initializing the canister for the first time. +type InitAccountWithPermissionsInput = record { + // The initial account to create. + account_init : InitAccountInput; + // The permissions for the account. + permissions : InitAccountPermissionsInput; +}; + + // The initial assets to create when initializing the canister for the first time, e.g., after disaster recovery. type InitAssetInput = record { // The UUID of the asset, if not provided a new UUID will be generated. @@ -2835,7 +2858,7 @@ type InitalEntries = variant { permissions : vec InitPermissionInput; request_policies : vec InitRequestPolicyInput; named_rules : vec InitNamedRuleInput; - accounts : vec InitAccountInput; + accounts : vec InitAccountWithPermissionsInput; assets : vec InitAssetInput; }; }; diff --git a/core/station/api/src/system.rs b/core/station/api/src/system.rs index e9f69f8ba..4c22970e0 100644 --- a/core/station/api/src/system.rs +++ b/core/station/api/src/system.rs @@ -124,6 +124,21 @@ pub struct InitAccountInput { pub metadata: Vec, } +#[derive(CandidType, serde::Serialize, Deserialize, Clone, Debug)] +pub struct InitAccountPermissionsInput { + pub read_permission: AllowDTO, + pub configs_permission: AllowDTO, + pub transfer_permission: AllowDTO, + pub configs_request_policy: Option, + pub transfer_request_policy: Option, +} + +#[derive(CandidType, serde::Serialize, Deserialize, Clone, Debug)] +pub struct InitAccountWithPermissionsInput { + pub account_init: InitAccountInput, + pub permissions: InitAccountPermissionsInput, +} + #[derive(CandidType, serde::Serialize, Deserialize, Clone, Debug)] pub struct InitAssetInput { pub id: UuidDTO, @@ -146,7 +161,7 @@ pub enum InitialEntries { assets: Vec, request_policies: Vec, user_groups: Vec, - accounts: Vec, + accounts: Vec, named_rules: Vec, }, } diff --git a/core/station/impl/src/migration_tests/snapshots/external_canister_repository_v1.bin b/core/station/impl/src/migration_tests/snapshots/external_canister_repository_v1.bin new file mode 100644 index 0000000000000000000000000000000000000000..907a6a3b50483132fe1fafcdaeb3deb329918589 GIT binary patch literal 65536 zcmeIyy-EW?5CGs!@D(h5n8Zd9EF^_pxZLi+nm@@c);9LOf}Ky8Rxe&+K-3~&_f2uL zGdlyvSL}FlI}B$bymwJTAA^1VEsZCmex%#m#bLhe>%M#ZUS3@f4l}#%cycrRS_tKzgOVZL0)+~m(<`~An%1PGiguzL?qUbaYp009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N M0t5&UAn>mOA7oWANdN!< literal 0 HcmV?d00001 diff --git a/core/station/impl/src/services/disaster_recovery.rs b/core/station/impl/src/services/disaster_recovery.rs index f03205d8b..86f1e763d 100644 --- a/core/station/impl/src/services/disaster_recovery.rs +++ b/core/station/impl/src/services/disaster_recovery.rs @@ -40,7 +40,7 @@ impl DisasterRecoveryService { let accounts = self.account_repository.list(); let assets = self.asset_repository.list(); - ic_cdk::call( + ic_cdk::call::<_, ()>( upgrader_canister_id, "set_disaster_recovery_accounts_and_assets", (upgrader_api::SetDisasterRecoveryAccountsAndAssetsInput { @@ -72,7 +72,7 @@ impl DisasterRecoveryService { }) .unwrap_or_default(); - ic_cdk::call( + ic_cdk::call::<_, ()>( upgrader_canister_id, "set_disaster_recovery_committee", (upgrader_api::SetDisasterRecoveryCommitteeInput { diff --git a/core/station/impl/src/services/system.rs b/core/station/impl/src/services/system.rs index ec3912149..43e11c2e1 100644 --- a/core/station/impl/src/services/system.rs +++ b/core/station/impl/src/services/system.rs @@ -12,7 +12,7 @@ use crate::{ system::{DisasterRecoveryCommittee, SystemInfo, SystemState}, Asset, Blockchain, CanisterInstallMode, CanisterUpgradeModeArgs, ManageSystemInfoOperationInput, Metadata, RequestId, RequestKey, RequestOperation, - RequestStatus, SystemUpgradeTarget, TokenStandard, WasmModuleExtraChunks, + RequestStatus, SystemUpgradeTarget, TokenStandard, WasmModuleExtraChunks, ADMIN_GROUP_ID, }, repositories::{ permission::PERMISSION_REPOSITORY, RequestRepository, ASSET_REPOSITORY, @@ -174,7 +174,7 @@ impl SystemService { ) -> ServiceResult<()> { let upgrader_canister_id = self.get_upgrader_canister_id(); - ic_cdk::call( + ic_cdk::call::<_, ()>( upgrader_canister_id, "trigger_upgrade", (UpgradeParams { @@ -267,10 +267,6 @@ impl SystemService { ) -> Result<(), String> { use crate::core::ic_cdk::api::id as self_canister_id; - // registers the default canister configurations such as policies and user groups. - print("Adding initial canister configurations"); - install_canister_handlers::init_post_process(&init).await?; - print("Init upgrader canister"); let canister_id = self_canister_id(); let mut upgrader_controllers = vec![canister_id]; @@ -295,16 +291,40 @@ impl SystemService { let quorum = calc_initial_quorum(initial_user_count, init.quorum); match init.entries { - Some(InitialEntries::WithDefaultPolicies { accounts, assets }) - | Some(InitialEntries::Complete { + Some(InitialEntries::WithDefaultPolicies { accounts, assets }) => { + print("Adding initial accounts"); + // initial accounts are added in the post process work timer, since they might do inter-canister calls + install_canister_handlers::set_initial_accounts( + accounts + .into_iter() + .map(|account| (account, None)) + .collect(), + &assets, + quorum, + ) + .await?; + } + Some(InitialEntries::Complete { accounts, assets, .. }) => { print("Adding initial accounts"); // initial accounts are added in the post process work timer, since they might do inter-canister calls - install_canister_handlers::set_initial_accounts(accounts, &assets, quorum) - .await?; + install_canister_handlers::set_initial_accounts( + accounts + .into_iter() + .map(|init_with_permissions| { + ( + init_with_permissions.account_init, + Some(init_with_permissions.permissions), + ) + }) + .collect(), + &assets, + quorum, + ) + .await?; } - _ => (), + _ => {} } if SYSTEM_SERVICE.is_healthy() { @@ -396,15 +416,23 @@ impl SystemService { })?; } + let admin_quorum = calc_initial_quorum(input.users.len() as u16, input.quorum); + match &input.entries { Some(InitialEntries::WithDefaultPolicies { assets, .. }) => { // adds the default admin group init_canister_sync_handlers::add_default_groups(); // registers the admins of the canister - init_canister_sync_handlers::set_initial_users(input.users.clone())?; + init_canister_sync_handlers::set_initial_users( + input.users.clone(), + &[*ADMIN_GROUP_ID], + )?; // adds the initial assets init_canister_sync_handlers::set_initial_assets(assets).await?; + // registers the default canister configurations such as policies and user groups. + init_canister_sync_handlers::init_default_permissions_and_policies(admin_quorum)?; + // initial accounts are added in the post process work timer, since they might do inter-canister calls } Some(InitialEntries::Complete { @@ -415,11 +443,17 @@ impl SystemService { assets, .. }) => { + print("adding initial user groups"); init_canister_sync_handlers::set_initial_user_groups(user_groups).await?; - init_canister_sync_handlers::set_initial_users(input.users.clone())?; + print("adding initial users"); + init_canister_sync_handlers::set_initial_users(input.users.clone(), &[])?; + print("adding initial named rules"); init_canister_sync_handlers::set_initial_named_rules(named_rules)?; + print("adding initial permissions"); init_canister_sync_handlers::set_initial_permissions(permissions).await?; + print("adding initial assets"); init_canister_sync_handlers::set_initial_assets(assets).await?; + print("adding initial request policies"); init_canister_sync_handlers::set_initial_request_policies(request_policies)?; // accounts in post process timer } @@ -427,7 +461,14 @@ impl SystemService { // // adds the default admin group init_canister_sync_handlers::add_default_groups(); // registers the admins of the canister - init_canister_sync_handlers::set_initial_users(input.users.clone())?; + init_canister_sync_handlers::set_initial_users( + input.users.clone(), + &[*ADMIN_GROUP_ID], + )?; + + // registers the default canister configurations such as policies and user groups. + init_canister_sync_handlers::init_default_permissions_and_policies(admin_quorum)?; + // // add default assets init_canister_sync_handlers::add_default_assets(); } @@ -559,6 +600,7 @@ mod init_canister_sync_handlers { use std::cmp::Ordering; use crate::core::ic_cdk::{api::print, next_time}; + use crate::core::init::{default_policies, get_default_named_rules, DEFAULT_PERMISSIONS}; use crate::mappers::blockchain::BlockchainMapper; use crate::mappers::HelperMapper; use crate::models::request_specifier::RequestSpecifier; @@ -566,9 +608,9 @@ mod init_canister_sync_handlers { use crate::models::{ AddAssetOperationInput, AddNamedRuleOperationInput, AddRequestPolicyOperationInput, AddUserGroupOperationInput, AddUserOperationInput, Asset, EditPermissionOperationInput, - UserStatus, OPERATOR_GROUP_ID, + NamedRule, UserStatus, OPERATOR_GROUP_ID, }; - use crate::repositories::ASSET_REPOSITORY; + use crate::repositories::{ASSET_REPOSITORY, NAMED_RULE_REPOSITORY}; use crate::services::permission::PERMISSION_SERVICE; use crate::services::{ ASSET_SERVICE, NAMED_RULE_SERVICE, REQUEST_POLICY_SERVICE, USER_GROUP_SERVICE, USER_SERVICE, @@ -818,7 +860,10 @@ mod init_canister_sync_handlers { } /// Registers the newly added admins of the canister. - pub fn set_initial_users(users: Vec) -> Result<(), ApiError> { + pub fn set_initial_users( + users: Vec, + default_groups: &[UUID], + ) -> Result<(), ApiError> { print(format!("Registering {} admin users", users.len())); for user in users { let user_id = user @@ -842,9 +887,9 @@ mod init_canister_sync_handlers { .map(|uuid| uuid.as_bytes().to_owned()) }) .collect::, _>>() - .unwrap_or_else(|_| vec![*ADMIN_GROUP_ID]) + .unwrap_or_else(|_| default_groups.to_vec()) }) - .unwrap_or_else(|| vec![*ADMIN_GROUP_ID]), + .unwrap_or_else(|| default_groups.to_vec()), name: user.name.to_owned(), status: user .status @@ -865,50 +910,9 @@ mod init_canister_sync_handlers { } Ok(()) } -} - -// Calculates the initial quorum based on the number of admins and the provided quorum, if not provided -// the quorum is set to the majority of the admins. -#[cfg(any(target_arch = "wasm32", test))] -pub fn calc_initial_quorum(admin_count: u16, quorum: Option) -> u16 { - quorum.unwrap_or(admin_count / 2 + 1).clamp(1, admin_count) -} - -#[cfg(target_arch = "wasm32")] -mod install_canister_handlers { - use crate::core::ic_cdk::api::id as self_canister_id; - use crate::core::init::{default_policies, get_default_named_rules, DEFAULT_PERMISSIONS}; - use crate::core::DEFAULT_INITIAL_UPGRADER_CYCLES; - use crate::mappers::blockchain::BlockchainMapper; - use crate::mappers::HelperMapper; - use crate::models::permission::Allow; - use crate::models::request_specifier::UserSpecifier; - use crate::models::{ - AddAccountOperationInput, AddAssetOperationInput, AddRequestPolicyOperationInput, - CycleObtainStrategy, EditPermissionOperationInput, MonitorExternalCanisterStrategy, - MonitoringExternalCanisterEstimatedRuntimeInput, NamedRule, RequestPolicyRule, - ADMIN_GROUP_ID, - }; - use crate::repositories::{ASSET_REPOSITORY, NAMED_RULE_REPOSITORY}; - use crate::services::permission::PERMISSION_SERVICE; - use crate::services::{ACCOUNT_SERVICE, ASSET_SERVICE}; - use crate::services::{EXTERNAL_CANISTER_SERVICE, REQUEST_POLICY_SERVICE}; - use candid::{Encode, Principal}; - use ic_cdk::api::management_canister::main::{self as mgmt}; - use ic_cdk::{id, print}; - use orbit_essentials::model::ModelKey; - - use crate::services::cycle_manager::CYCLE_MANAGER; - use orbit_essentials::api::ApiError; - use orbit_essentials::repository::Repository; - use orbit_essentials::types::UUID; - use station_api::{InitAccountInput, InitAssetInput, SystemInit}; - use uuid::Uuid; /// Registers the default configurations for the canister. - pub async fn init_post_process(init: &SystemInit) -> Result<(), String> { - let admin_quorum = super::calc_initial_quorum(init.users.len() as u16, init.quorum); - + pub fn init_default_permissions_and_policies(admin_quorum: u16) -> Result<(), ApiError> { let (regular_named_rule_config, admin_named_rule_config) = get_default_named_rules(admin_quorum); @@ -933,39 +937,97 @@ mod install_canister_handlers { // adds the default request policies which sets safe defaults for the canister for policy in policies_to_create.iter() { - REQUEST_POLICY_SERVICE - .add_request_policy(AddRequestPolicyOperationInput { - specifier: policy.0.to_owned(), - rule: policy.1.to_owned(), - }) - .map_err(|e| format!("Failed to add default request policy: {:?}", e))?; + REQUEST_POLICY_SERVICE.add_request_policy(AddRequestPolicyOperationInput { + specifier: policy.0.to_owned(), + rule: policy.1.to_owned(), + })?; } // adds the default permissions which sets safe defaults for the canister for policy in DEFAULT_PERMISSIONS.iter() { let allow = policy.0.to_owned(); - PERMISSION_SERVICE - .edit_permission(EditPermissionOperationInput { - auth_scope: Some(allow.auth_scope), - user_groups: Some(allow.user_groups), - users: Some(allow.users), - resource: policy.1.to_owned(), - }) - .map_err(|e| format!("Failed to add default permission: {:?}", e))?; + PERMISSION_SERVICE.edit_permission(EditPermissionOperationInput { + auth_scope: Some(allow.auth_scope), + user_groups: Some(allow.user_groups), + users: Some(allow.users), + resource: policy.1.to_owned(), + })?; } Ok(()) } +} + +// Calculates the initial quorum based on the number of admins and the provided quorum, if not provided +// the quorum is set to the majority of the admins. +pub fn calc_initial_quorum(admin_count: u16, quorum: Option) -> u16 { + quorum.unwrap_or(admin_count / 2 + 1).clamp(1, admin_count) +} + +#[cfg(target_arch = "wasm32")] +mod install_canister_handlers { + use crate::core::ic_cdk::api::id as self_canister_id; + use crate::core::DEFAULT_INITIAL_UPGRADER_CYCLES; + use crate::mappers::HelperMapper; + use crate::models::permission::Allow; + use crate::models::request_specifier::UserSpecifier; + use crate::models::{ + AddAccountOperationInput, CycleObtainStrategy, MonitorExternalCanisterStrategy, + MonitoringExternalCanisterEstimatedRuntimeInput, RequestPolicyRule, ADMIN_GROUP_ID, + }; + use crate::repositories::ASSET_REPOSITORY; + use crate::services::cycle_manager::CYCLE_MANAGER; + use crate::services::ACCOUNT_SERVICE; + use crate::services::EXTERNAL_CANISTER_SERVICE; + use candid::{Encode, Principal}; + use ic_cdk::api::management_canister::main::{self as mgmt}; + use ic_cdk::{id, print}; + use orbit_essentials::repository::Repository; + use orbit_essentials::types::UUID; + use station_api::{InitAccountInput, InitAccountPermissionsInput, InitAssetInput}; + use uuid::Uuid; // Registers the initial accounts of the canister during the canister initialization. pub async fn set_initial_accounts( - accounts: Vec, + accounts: Vec<(InitAccountInput, Option)>, initial_assets: &Vec, quorum: u16, ) -> Result<(), String> { let add_accounts = accounts .into_iter() - .map(|account| { + .map(|(account, permissions)| { + let ( + transfer_request_policy, + configs_request_policy, + read_permission, + configs_permission, + transfer_permission, + ) = permissions + .map(|permissions| { + ( + permissions.transfer_request_policy.map(|rule| rule.into()), + permissions.configs_request_policy.map(|rule| rule.into()), + permissions.read_permission.into(), + permissions.configs_permission.into(), + permissions.transfer_permission.into(), + ) + }) + .unwrap_or_else(|| { + ( + Some(RequestPolicyRule::Quorum( + UserSpecifier::Group(vec![*ADMIN_GROUP_ID]), + quorum, + )), + Some(RequestPolicyRule::Quorum( + UserSpecifier::Group(vec![*ADMIN_GROUP_ID]), + quorum, + )), + Allow::user_groups(vec![*ADMIN_GROUP_ID]), + Allow::user_groups(vec![*ADMIN_GROUP_ID]), + Allow::user_groups(vec![*ADMIN_GROUP_ID]), + ) + }); + let input = AddAccountOperationInput { name: account.name, assets: account @@ -978,17 +1040,11 @@ mod install_canister_handlers { }) .collect(), metadata: account.metadata.into(), - transfer_request_policy: Some(RequestPolicyRule::Quorum( - UserSpecifier::Group(vec![*ADMIN_GROUP_ID]), - quorum, - )), - configs_request_policy: Some(RequestPolicyRule::Quorum( - UserSpecifier::Group(vec![*ADMIN_GROUP_ID]), - quorum, - )), - read_permission: Allow::user_groups(vec![*ADMIN_GROUP_ID]), - configs_permission: Allow::user_groups(vec![*ADMIN_GROUP_ID]), - transfer_permission: Allow::user_groups(vec![*ADMIN_GROUP_ID]), + transfer_request_policy, + configs_request_policy, + read_permission, + configs_permission, + transfer_permission, }; ( diff --git a/tests/integration/src/install_tests.rs b/tests/integration/src/install_tests.rs new file mode 100644 index 000000000..e10e68d13 --- /dev/null +++ b/tests/integration/src/install_tests.rs @@ -0,0 +1,842 @@ +use std::mem; + +use candid::{Encode, Principal}; +use station_api::{ + AccountDTO, AccountResourceActionDTO, AllowDTO, AssetDTO, AuthScopeDTO, InitAccountInput, + InitAssetInput, InitNamedRuleInput, InitPermissionInput, InitRequestPolicyInput, + InitUserGroupInput, MetadataDTO, NamedRuleDTO, PermissionDTO, PermissionResourceActionDTO, + RequestPolicyDTO, RequestPolicyRuleDTO, RequestSpecifierDTO, ResourceActionDTO, ResourceDTO, + ResourceIdDTO, SystemInit, SystemInstall, UserDTO, UserGroupDTO, UserIdentityInput, + UserInitInput, UserResourceActionDTO, UserStatusDTO, UuidDTO, +}; +use uuid::Uuid; + +use crate::{ + setup::{get_canister_wasm, setup_new_env, WALLET_ADMIN_USER}, + station_test_data::{ + account::list_accounts, asset::list_assets, named_rule::list_named_rules, + permission::list_permissions, request_policy::list_request_policies, user::list_users, + user_group::list_user_groups, + }, + utils::{await_station_healthy, ADMIN_GROUP_ID, OPERATOR_GROUP_ID}, + TestEnv, +}; + +fn assert_initial_users( + listed_users: &Vec, + expected_users: &Vec, + default_groups: &Vec, +) -> Result<(), String> { + if expected_users.len() != listed_users.len() { + return Err(format!( + "expected {} users, got {}", + expected_users.len(), + listed_users.len() + )); + } + + for expected_user in expected_users { + let user = listed_users + .iter() + .find(|user| user.name == expected_user.name) + .ok_or(format!("user {} not found", expected_user.name))?; + + expected_user.identities.iter().all(|identity| { + user.identities + .iter() + .any(|user_identity| user_identity == &identity.identity) + }); + + expected_user + .groups + .as_ref() + .unwrap_or(default_groups) + .iter() + .all(|group| user.groups.iter().any(|user_group| &user_group.id == group)); + + let expected_status = expected_user + .status + .as_ref() + .unwrap_or(&UserStatusDTO::Active); + + if mem::discriminant(&user.status) != mem::discriminant(expected_status) { + return Err(format!( + "user {} has status {:?}, expected {:?}", + expected_user.name, user.status, expected_status + )); + } + } + + Ok(()) +} + +fn assert_initial_user_groups( + listed_user_groups: &Vec, + expected_user_groups: &Vec, +) -> Result<(), String> { + if expected_user_groups.len() != listed_user_groups.len() { + return Err(format!( + "expected {} user groups, got {}", + expected_user_groups.len(), + listed_user_groups.len() + )); + } + + for expected_user_group in expected_user_groups { + let _user_group = listed_user_groups + .iter() + .find(|user_group| user_group.name == expected_user_group.name) + .ok_or(format!("user group {} not found", expected_user_group.name))?; + } + + Ok(()) +} + +fn assert_initial_permissions( + listed_permissions: &Vec, + expected_permissions: &Vec, + expected_extra_permissions: usize, +) -> Result<(), String> { + if listed_permissions.len() != expected_permissions.len() + expected_extra_permissions { + return Err(format!( + "expected {} permissions, got {}", + expected_permissions.len() + expected_extra_permissions, + listed_permissions.len() + )); + } + + Ok(()) +} + +fn assert_initial_request_policies( + listed_request_policies: &Vec, + expected_request_policies: &Vec, + expected_extra_request_policies: usize, +) -> Result<(), String> { + if listed_request_policies.len() + != expected_request_policies.len() + expected_extra_request_policies + { + return Err(format!( + "expected {} request policies, got {}", + expected_request_policies.len() + expected_extra_request_policies, + listed_request_policies.len() + )); + } + + Ok(()) +} + +fn assert_initial_named_rules( + listed_named_rules: &Vec, + expected_named_rules: &Vec, +) -> Result<(), String> { + if expected_named_rules.len() != listed_named_rules.len() { + return Err(format!( + "expected {} named rules, got {}", + expected_named_rules.len(), + listed_named_rules.len() + )); + } + + for expected_named_rule in expected_named_rules { + let _named_rule = listed_named_rules + .iter() + .find(|named_rule| named_rule.name == expected_named_rule.name) + .ok_or(format!("named rule {} not found", expected_named_rule.name))?; + } + + Ok(()) +} + +fn assert_initial_assets( + listed_assets: &Vec, + expected_assets: &Vec, +) -> Result<(), String> { + if expected_assets.len() != listed_assets.len() { + return Err(format!( + "expected {} assets, got {}", + expected_assets.len(), + listed_assets.len() + )); + } + + for expected_asset in expected_assets { + let asset = listed_assets + .iter() + .find(|asset| asset.id == expected_asset.id) + .ok_or(format!("asset {} not found", expected_asset.id))?; + + if asset.id != expected_asset.id + || asset.name != expected_asset.name + || asset.blockchain != expected_asset.blockchain + || asset.standards != expected_asset.standards + || asset.metadata != expected_asset.metadata + || asset.symbol != expected_asset.symbol + || asset.decimals != expected_asset.decimals + { + return Err(format!("asset {} does not match expected asset", asset.id)); + } + } + + Ok(()) +} + +fn compare_arrays(a: &Vec, b: &Vec) -> bool { + a.len() == b.len() && a.iter().all(|item| b.contains(item)) +} + +fn assert_initial_accounts( + listed_accounts: &Vec, + expected_accounts: &Vec, +) -> Result<(), String> { + if expected_accounts.len() != listed_accounts.len() { + return Err(format!( + "expected {} accounts, got {}", + expected_accounts.len(), + listed_accounts.len() + )); + } + + for expected_account in expected_accounts { + let account = listed_accounts + .iter() + .find(|account| account.name == expected_account.name) + .ok_or(format!("account {} not found", expected_account.name))?; + + if expected_account.assets.len() > 0 && account.addresses.len() == 0 { + return Err(format!( + "account {} has no addresses, expected some", + expected_account.name + )); + } + + if account.name != expected_account.name + || account.metadata != expected_account.metadata + || !compare_arrays( + &account + .assets + .iter() + .map(|asset| asset.asset_id.clone()) + .collect(), + &expected_account.assets, + ) + { + return Err(format!( + "account {} does not match expected account", + account.id + )); + } + } + + Ok(()) +} + +#[test] +fn install_with_default_policies() { + let TestEnv { + env, controller, .. + } = setup_new_env(); + + let canister_id = env.create_canister_with_settings(Some(controller), None); + + env.set_controllers(canister_id, Some(controller), vec![canister_id, controller]) + .expect("failed to set canister controller"); + + env.add_cycles(canister_id, 5_000_000_000_000); + let station_wasm = get_canister_wasm("station").to_vec(); + let upgrader_wasm = get_canister_wasm("upgrader").to_vec(); + + let asset_1_id = Uuid::new_v4().hyphenated().to_string(); + let asset_2_id = Uuid::new_v4().hyphenated().to_string(); + + let account_1_id = Uuid::new_v4().hyphenated().to_string(); + + let users = vec![ + UserInitInput { + identities: vec![UserIdentityInput { + identity: WALLET_ADMIN_USER, + }], + name: "station-admin".to_string(), + groups: None, + id: None, + status: None, + }, + UserInitInput { + identities: vec![UserIdentityInput { + identity: Principal::from_slice(&[2; 29]), + }], + name: "inactive-station-admin".to_string(), + groups: Some(vec![ADMIN_GROUP_ID.hyphenated().to_string()]), + id: None, + status: Some(station_api::UserStatusDTO::Inactive), + }, + UserInitInput { + identities: vec![UserIdentityInput { + identity: Principal::from_slice(&[3; 29]), + }], + name: "other-user".to_string(), + groups: Some(vec![]), + id: None, + status: Some(station_api::UserStatusDTO::Active), + }, + ]; + + let accounts = vec![station_api::InitAccountInput { + name: "test-account".to_string(), + id: Some(account_1_id.clone()), + metadata: vec![MetadataDTO { + key: "test-key".to_string(), + value: "test-value".to_string(), + }], + assets: vec![asset_1_id.clone(), asset_2_id.clone()], + seed: Uuid::new_v4().to_bytes_le(), + }]; + + let assets = vec![ + station_api::InitAssetInput { + name: "test-asset-1".to_string(), + id: asset_1_id.clone(), + blockchain: "icp".to_string(), + standards: vec!["icp_native".to_owned()], + metadata: vec![], + symbol: "TEST1".to_string(), + decimals: 8, + }, + station_api::InitAssetInput { + name: "test-asset-2".to_string(), + id: asset_2_id.clone(), + blockchain: "icp".to_string(), + standards: vec!["icp_native".to_owned()], + metadata: vec![], + symbol: "TEST2".to_string(), + decimals: 2, + }, + ]; + + let station_init_args = SystemInstall::Init(SystemInit { + name: "Station".to_string(), + users: users.clone(), + upgrader: station_api::SystemUpgraderInput::Deploy( + station_api::DeploySystemUpgraderInput { + wasm_module: upgrader_wasm, + initial_cycles: Some(1_000_000_000_000), + }, + ), + fallback_controller: Some(controller), + quorum: None, + entries: Some(station_api::InitialEntries::WithDefaultPolicies { + accounts: accounts.clone(), + assets: assets.clone(), + }), + }); + env.install_canister( + canister_id, + station_wasm, + Encode!(&station_init_args).unwrap(), + Some(controller), + ); + + await_station_healthy(&env, canister_id, WALLET_ADMIN_USER); + + let lised_assets = list_assets(&env, canister_id, WALLET_ADMIN_USER) + .expect("failed to call list_assets") + .0 + .expect("failed to list assets"); + + let listed_accounts = list_accounts(&env, canister_id, WALLET_ADMIN_USER) + .expect("failed to get account") + .0 + .expect("failed to get account"); + + let listed_users = list_users(&env, canister_id, WALLET_ADMIN_USER) + .expect("failed to get users") + .0 + .expect("failed to get users"); + + assert_eq!(listed_users.users.len(), 3); + + assert_initial_users( + &listed_users.users, + &users, + &vec![ADMIN_GROUP_ID.hyphenated().to_string()], + ) + .expect("failed to assert initial users"); + + assert_initial_assets(&lised_assets.assets, &assets).expect("failed to assert initial assets"); + + assert_initial_accounts(&listed_accounts.accounts, &accounts) + .expect("failed to assert initial accounts"); + + let listed_policies = list_request_policies(&env, canister_id, WALLET_ADMIN_USER) + .expect("failed to get request policies") + .0 + .expect("failed to get request policies"); + + assert!(listed_policies.policies.len() > 0); + + let permissions = list_permissions(&env, canister_id, WALLET_ADMIN_USER) + .expect("failed to get permissions") + .0 + .expect("failed to get permissions"); + + assert!(permissions.permissions.len() > 0); + + let named_rules = list_named_rules(&env, canister_id, WALLET_ADMIN_USER) + .expect("failed to get named rules") + .0 + .expect("failed to get named rules"); + + assert!(named_rules.named_rules.len() > 0); + + let user_groups = list_user_groups(&env, canister_id, WALLET_ADMIN_USER) + .expect("failed to get user groups") + .0 + .expect("failed to get user groups"); + + assert_eq!(user_groups.user_groups.len(), 2); + assert_eq!( + user_groups.user_groups[0].id, + ADMIN_GROUP_ID.hyphenated().to_string() + ); + assert_eq!( + user_groups.user_groups[1].id, + OPERATOR_GROUP_ID.hyphenated().to_string() + ); +} + +#[test] +fn install_with_all_entries() { + let TestEnv { + env, controller, .. + } = setup_new_env(); + + let canister_id = env.create_canister_with_settings(Some(controller), None); + + env.set_controllers(canister_id, Some(controller), vec![canister_id, controller]) + .expect("failed to set canister controller"); + + env.add_cycles(canister_id, 5_000_000_000_000); + let station_wasm = get_canister_wasm("station").to_vec(); + let upgrader_wasm = get_canister_wasm("upgrader").to_vec(); + + let custom_user_group_id = Uuid::new_v4().hyphenated().to_string(); + + let asset_1_id = Uuid::new_v4().hyphenated().to_string(); + let asset_2_id = Uuid::new_v4().hyphenated().to_string(); + + let account_1_id = Uuid::new_v4().hyphenated().to_string(); + + let allow_custom_user_group_users = AllowDTO { + auth_scope: AuthScopeDTO::Restricted, + users: vec![], + user_groups: vec![custom_user_group_id.clone()], + }; + + let one_from_custom_user_group = RequestPolicyRuleDTO::Quorum(station_api::QuorumDTO { + approvers: station_api::UserSpecifierDTO::Group(vec![custom_user_group_id.clone()]), + min_approved: 1, + }); + + let request_policy_add_user_id = Uuid::new_v4().hyphenated().to_string(); + let named_rule_dependent_id = Uuid::new_v4().hyphenated().to_string(); + + let permissions = vec![ + InitPermissionInput { + resource: ResourceDTO::User(UserResourceActionDTO::List), + allow: allow_custom_user_group_users.clone(), + }, + InitPermissionInput { + resource: ResourceDTO::User(UserResourceActionDTO::Read(ResourceIdDTO::Any)), + allow: allow_custom_user_group_users.clone(), + }, + InitPermissionInput { + resource: ResourceDTO::UserGroup(ResourceActionDTO::List), + allow: allow_custom_user_group_users.clone(), + }, + InitPermissionInput { + resource: ResourceDTO::UserGroup(ResourceActionDTO::Read(ResourceIdDTO::Any)), + allow: allow_custom_user_group_users.clone(), + }, + InitPermissionInput { + resource: ResourceDTO::Account(AccountResourceActionDTO::List), + allow: allow_custom_user_group_users.clone(), + }, + InitPermissionInput { + resource: ResourceDTO::Account(AccountResourceActionDTO::Read(ResourceIdDTO::Any)), + allow: allow_custom_user_group_users.clone(), + }, + InitPermissionInput { + resource: ResourceDTO::Asset(ResourceActionDTO::List), + allow: allow_custom_user_group_users.clone(), + }, + InitPermissionInput { + resource: ResourceDTO::Asset(ResourceActionDTO::Read(ResourceIdDTO::Any)), + allow: allow_custom_user_group_users.clone(), + }, + InitPermissionInput { + resource: ResourceDTO::NamedRule(ResourceActionDTO::List), + allow: allow_custom_user_group_users.clone(), + }, + InitPermissionInput { + resource: ResourceDTO::NamedRule(ResourceActionDTO::Read(ResourceIdDTO::Any)), + allow: allow_custom_user_group_users.clone(), + }, + InitPermissionInput { + resource: ResourceDTO::RequestPolicy(ResourceActionDTO::List), + allow: allow_custom_user_group_users.clone(), + }, + InitPermissionInput { + resource: ResourceDTO::RequestPolicy(ResourceActionDTO::Read(ResourceIdDTO::Any)), + allow: allow_custom_user_group_users.clone(), + }, + InitPermissionInput { + resource: ResourceDTO::Permission(PermissionResourceActionDTO::Read), + allow: allow_custom_user_group_users.clone(), + }, + ]; + + let request_policies = vec![ + // edit specific request policy, in the wrong order on purpose + InitRequestPolicyInput { + id: None, + specifier: RequestSpecifierDTO::EditRequestPolicy(station_api::ResourceIdsDTO::Ids( + vec![request_policy_add_user_id.clone()], + )), + rule: RequestPolicyRuleDTO::AutoApproved, + }, + // create user + InitRequestPolicyInput { + id: Some(request_policy_add_user_id), + specifier: RequestSpecifierDTO::AddUser, + rule: RequestPolicyRuleDTO::AutoApproved, + }, + ]; + + let accounts = vec![station_api::InitAccountWithPermissionsInput { + account_init: station_api::InitAccountInput { + name: "test-account".to_string(), + id: Some(account_1_id.clone()), + metadata: vec![MetadataDTO { + key: "test-key".to_string(), + value: "test-value".to_string(), + }], + assets: vec![asset_1_id.clone(), asset_2_id.clone()], + seed: Uuid::new_v4().to_bytes_le(), + }, + permissions: station_api::InitAccountPermissionsInput { + read_permission: allow_custom_user_group_users.clone(), + configs_permission: allow_custom_user_group_users.clone(), + transfer_permission: allow_custom_user_group_users.clone(), + configs_request_policy: Some(one_from_custom_user_group.clone()), + transfer_request_policy: Some(one_from_custom_user_group.clone()), + }, + }]; + + let users = vec![ + UserInitInput { + identities: vec![UserIdentityInput { + identity: WALLET_ADMIN_USER, + }], + name: "station-admin".to_string(), + groups: Some(vec![custom_user_group_id.clone()]), + id: None, + status: None, + }, + UserInitInput { + identities: vec![UserIdentityInput { + identity: Principal::from_slice(&[2; 29]), + }], + name: "inactive-station-admin".to_string(), + groups: Some(vec![custom_user_group_id.clone()]), + id: None, + status: Some(station_api::UserStatusDTO::Inactive), + }, + UserInitInput { + identities: vec![UserIdentityInput { + identity: Principal::from_slice(&[3; 29]), + }], + name: "other-user".to_string(), + groups: Some(vec![]), + id: None, + status: Some(station_api::UserStatusDTO::Active), + }, + ]; + + let assets = vec![ + station_api::InitAssetInput { + name: "test-asset-1".to_string(), + id: asset_1_id.clone(), + blockchain: "icp".to_string(), + standards: vec!["icp_native".to_owned()], + metadata: vec![], + symbol: "TEST1".to_string(), + decimals: 8, + }, + station_api::InitAssetInput { + name: "test-asset-2".to_string(), + id: asset_2_id.clone(), + blockchain: "icp".to_string(), + standards: vec!["icp_native".to_owned()], + metadata: vec![], + symbol: "TEST2".to_string(), + decimals: 2, + }, + ]; + + let named_rules = vec![ + InitNamedRuleInput { + id: Uuid::new_v4().hyphenated().to_string(), + name: "custom-named-rule-with-dependency".to_string(), + description: None, + rule: RequestPolicyRuleDTO::NamedRule(named_rule_dependent_id.clone()), + }, + InitNamedRuleInput { + id: named_rule_dependent_id.clone(), + name: "custom-named-rule".to_string(), + description: None, + rule: RequestPolicyRuleDTO::AutoApproved, + }, + ]; + + let user_groups = vec![InitUserGroupInput { + id: custom_user_group_id.clone(), + name: "custom-user-group".to_string(), + }]; + + let station_init_args = SystemInstall::Init(SystemInit { + name: "Station".to_string(), + users: users.clone(), + upgrader: station_api::SystemUpgraderInput::Deploy( + station_api::DeploySystemUpgraderInput { + wasm_module: upgrader_wasm, + initial_cycles: Some(1_000_000_000_000), + }, + ), + fallback_controller: Some(controller), + quorum: None, + entries: Some(station_api::InitialEntries::Complete { + accounts: accounts.clone(), + assets: assets.clone(), + permissions: permissions.clone(), + request_policies: request_policies.clone(), + user_groups: user_groups.clone(), + named_rules: named_rules.clone(), + }), + }); + env.install_canister( + canister_id, + station_wasm, + Encode!(&station_init_args).unwrap(), + Some(controller), + ); + + await_station_healthy(&env, canister_id, WALLET_ADMIN_USER); + + // assert that the users are in the right groups + let list_users_response = list_users(&env, canister_id, WALLET_ADMIN_USER) + .expect("failed to get users") + .0 + .expect("failed to get users"); + + assert_initial_users(&list_users_response.users, &users, &vec![]) + .expect("failed to assert initial users"); + + // assert the number of request policies + let request_policies_response = list_request_policies(&env, canister_id, WALLET_ADMIN_USER) + .expect("failed to get request policies") + .0 + .expect("failed to get request policies"); + + assert_initial_request_policies( + &request_policies_response.policies, + &request_policies, + accounts.len() * 2, + ) + .expect("failed to assert initial request policies"); + + // assert the number of permissions + let permissions_response = list_permissions(&env, canister_id, WALLET_ADMIN_USER) + .expect("failed to get permissions") + .0 + .expect("failed to get permissions"); + + assert_initial_permissions( + &permissions_response.permissions, + &permissions, + accounts.len() * 3, + ) + .expect("failed to assert initial permissions"); + + // assert the named rules + let list_named_rules_response = list_named_rules(&env, canister_id, WALLET_ADMIN_USER) + .expect("failed to get named rules") + .0 + .expect("failed to get named rules"); + + assert_initial_named_rules(&list_named_rules_response.named_rules, &named_rules) + .expect("failed to assert initial named rules"); + + // assert the names of the user groups + let list_user_groups_response = list_user_groups(&env, canister_id, WALLET_ADMIN_USER) + .expect("failed to get user groups") + .0 + .expect("failed to get user groups"); + + assert_initial_user_groups(&list_user_groups_response.user_groups, &user_groups) + .expect("failed to assert initial user groups"); + + // assert the number of accounts and that they have addresses + let list_accounts_response = list_accounts(&env, canister_id, WALLET_ADMIN_USER) + .expect("failed to get accounts") + .0 + .expect("failed to get accounts"); + + assert_initial_accounts( + &list_accounts_response.accounts, + &accounts + .iter() + .map(|init| init.account_init.clone()) + .collect(), + ) + .expect("failed to assert initial accounts"); + + // assert the number of assets and their names + let list_assets_response = list_assets(&env, canister_id, WALLET_ADMIN_USER) + .expect("failed to get assets") + .0 + .expect("failed to get assets"); + + assert_initial_assets(&list_assets_response.assets, &assets) + .expect("failed to assert initial assets"); +} + +#[test] +fn install_with_all_defaults() { + let TestEnv { + env, controller, .. + } = setup_new_env(); + + let canister_id = env.create_canister_with_settings(Some(controller), None); + + env.set_controllers(canister_id, Some(controller), vec![canister_id, controller]) + .expect("failed to set canister controller"); + + env.add_cycles(canister_id, 5_000_000_000_000); + let station_wasm = get_canister_wasm("station").to_vec(); + let upgrader_wasm = get_canister_wasm("upgrader").to_vec(); + + let users = vec![ + UserInitInput { + identities: vec![UserIdentityInput { + identity: WALLET_ADMIN_USER, + }], + name: "station-admin".to_string(), + groups: None, + id: None, + status: None, + }, + UserInitInput { + identities: vec![UserIdentityInput { + identity: Principal::from_slice(&[2; 29]), + }], + name: "inactive-operator".to_string(), + groups: Some(vec![OPERATOR_GROUP_ID.hyphenated().to_string()]), + id: None, + status: Some(station_api::UserStatusDTO::Inactive), + }, + UserInitInput { + identities: vec![UserIdentityInput { + identity: Principal::from_slice(&[3; 29]), + }], + name: "other-user".to_string(), + groups: Some(vec![]), + id: None, + status: Some(station_api::UserStatusDTO::Active), + }, + ]; + + let station_init_args = SystemInstall::Init(SystemInit { + name: "Station".to_string(), + users: users.clone(), + upgrader: station_api::SystemUpgraderInput::Deploy( + station_api::DeploySystemUpgraderInput { + wasm_module: upgrader_wasm, + initial_cycles: Some(1_000_000_000_000), + }, + ), + fallback_controller: Some(controller), + quorum: None, + entries: None, + }); + env.install_canister( + canister_id, + station_wasm, + Encode!(&station_init_args).unwrap(), + Some(controller), + ); + + await_station_healthy(&env, canister_id, WALLET_ADMIN_USER); + + let listed_assets = list_assets(&env, canister_id, WALLET_ADMIN_USER) + .expect("failed to call list_assets") + .0 + .expect("failed to list assets"); + + let listed_accounts = list_accounts(&env, canister_id, WALLET_ADMIN_USER) + .expect("failed to get account") + .0 + .expect("failed to get account"); + + let listed_users = list_users(&env, canister_id, WALLET_ADMIN_USER) + .expect("failed to get users") + .0 + .expect("failed to get users"); + + assert_initial_users( + &listed_users.users, + &users, + &vec![ADMIN_GROUP_ID.hyphenated().to_string()], + ) + .expect("failed to assert initial users"); + + assert!(listed_assets.assets.len() > 0); + + assert_initial_accounts(&listed_accounts.accounts, &vec![]) + .expect("failed to assert initial accounts"); + + let policies = list_request_policies(&env, canister_id, WALLET_ADMIN_USER) + .expect("failed to get request policies") + .0 + .expect("failed to get request policies"); + + assert!(policies.policies.len() > 0); + + let permissions = list_permissions(&env, canister_id, WALLET_ADMIN_USER) + .expect("failed to get permissions") + .0 + .expect("failed to get permissions"); + + assert!(permissions.permissions.len() > 0); + + let named_rules = list_named_rules(&env, canister_id, WALLET_ADMIN_USER) + .expect("failed to get named rules") + .0 + .expect("failed to get named rules"); + + assert!(named_rules.named_rules.len() > 0); + + let user_groups = list_user_groups(&env, canister_id, WALLET_ADMIN_USER) + .expect("failed to get user groups") + .0 + .expect("failed to get user groups"); + + assert_eq!(user_groups.user_groups.len(), 2); + assert_eq!( + user_groups.user_groups[0].id, + ADMIN_GROUP_ID.hyphenated().to_string() + ); + assert_eq!( + user_groups.user_groups[1].id, + OPERATOR_GROUP_ID.hyphenated().to_string() + ); +} diff --git a/tests/integration/src/lib.rs b/tests/integration/src/lib.rs index 11c0995d5..73147f52a 100644 --- a/tests/integration/src/lib.rs +++ b/tests/integration/src/lib.rs @@ -12,6 +12,7 @@ mod dfx_orbit; mod disaster_recovery_tests; mod external_canister_tests; mod http; +mod install_tests; mod interfaces; mod named_rule_tests; mod notification; diff --git a/tests/integration/src/station_test_data/account.rs b/tests/integration/src/station_test_data/account.rs index 02c01c17d..ad06b8aa5 100644 --- a/tests/integration/src/station_test_data/account.rs +++ b/tests/integration/src/station_test_data/account.rs @@ -1,8 +1,9 @@ use super::next_unique_id; use crate::utils::{get_icp_asset, submit_request, wait_for_request}; use candid::Principal; -use pocket_ic::PocketIc; -use station_api::ChangeAssets; +use orbit_essentials::api::ApiResult; +use pocket_ic::{query_candid_as, PocketIc, RejectResponse}; +use station_api::{ChangeAssets, ListAccountsInput, ListAccountsResponse}; pub fn add_account( env: &PocketIc, @@ -107,3 +108,20 @@ pub fn edit_account_assets( wait_for_request(env, requester, station_canister_id, edit_account_request) .expect("Failed to edit account"); } + +pub fn list_accounts( + env: &PocketIc, + station_canister_id: Principal, + requester: Principal, +) -> Result<(ApiResult,), RejectResponse> { + query_candid_as::<(ListAccountsInput,), (ApiResult,)>( + env, + station_canister_id, + requester, + "list_accounts", + (ListAccountsInput { + search_term: None, + paginate: None, + },), + ) +} diff --git a/tests/integration/src/station_test_data/permission.rs b/tests/integration/src/station_test_data/permission.rs index 367841f03..fcdabbda6 100644 --- a/tests/integration/src/station_test_data/permission.rs +++ b/tests/integration/src/station_test_data/permission.rs @@ -1,6 +1,9 @@ use crate::utils::{submit_request, wait_for_request}; use candid::Principal; +use orbit_essentials::api::ApiResult; use pocket_ic::PocketIc; +use pocket_ic::{query_candid_as, RejectResponse}; +use station_api::{ListPermissionsInput, ListPermissionsResponse}; pub fn edit_permission( env: &PocketIc, @@ -25,3 +28,20 @@ pub fn edit_permission( wait_for_request(env, requester, station_canister_id, edit_account_request) .expect("Failed to edit permission"); } + +pub fn list_permissions( + env: &PocketIc, + station_canister_id: Principal, + requester: Principal, +) -> Result<(ApiResult,), RejectResponse> { + query_candid_as::<(ListPermissionsInput,), (ApiResult,)>( + env, + station_canister_id, + requester, + "list_permissions", + (ListPermissionsInput { + resources: None, + paginate: None, + },), + ) +} diff --git a/tests/integration/src/station_test_data/user.rs b/tests/integration/src/station_test_data/user.rs index 7b0639bc5..fb5a9052a 100644 --- a/tests/integration/src/station_test_data/user.rs +++ b/tests/integration/src/station_test_data/user.rs @@ -1,7 +1,9 @@ use super::next_unique_id; use crate::utils::{submit_request, wait_for_request}; use candid::Principal; -use pocket_ic::PocketIc; +use orbit_essentials::api::ApiResult; +use pocket_ic::{query_candid_as, PocketIc, RejectResponse}; +use station_api::{ListUsersInput, ListUsersResponse}; pub fn add_user( env: &PocketIc, @@ -51,3 +53,22 @@ pub fn edit_user_name( wait_for_request(env, requester, station_canister_id, edit_user_request) .expect("Failed to edit user"); } + +pub fn list_users( + env: &PocketIc, + station_canister_id: Principal, + requester: Principal, +) -> Result<(ApiResult,), RejectResponse> { + query_candid_as::<(ListUsersInput,), (ApiResult,)>( + env, + station_canister_id, + requester, + "list_users", + (ListUsersInput { + search_term: None, + statuses: None, + groups: None, + paginate: None, + },), + ) +} diff --git a/tests/integration/src/station_test_data/user_group.rs b/tests/integration/src/station_test_data/user_group.rs index a8852af1e..0fdd8241d 100644 --- a/tests/integration/src/station_test_data/user_group.rs +++ b/tests/integration/src/station_test_data/user_group.rs @@ -1,7 +1,9 @@ use super::next_unique_id; use crate::utils::{submit_request, wait_for_request}; use candid::Principal; -use pocket_ic::PocketIc; +use orbit_essentials::api::ApiResult; +use pocket_ic::{query_candid_as, PocketIc, RejectResponse}; +use station_api::{ListUserGroupsInput, ListUserGroupsResponse}; pub fn add_user_group( env: &PocketIc, @@ -50,3 +52,20 @@ pub fn edit_user_group( wait_for_request(env, requester, station_canister_id, edit_user_group_request) .expect("Failed to edit user group"); } + +pub fn list_user_groups( + env: &PocketIc, + station_canister_id: Principal, + requester: Principal, +) -> Result<(ApiResult,), RejectResponse> { + query_candid_as::<(ListUserGroupsInput,), (ApiResult,)>( + env, + station_canister_id, + requester, + "list_user_groups", + (ListUserGroupsInput { + search_term: None, + paginate: None, + },), + ) +} diff --git a/tests/integration/src/system_upgrade_tests.rs b/tests/integration/src/system_upgrade_tests.rs index 69212e75b..894435c88 100644 --- a/tests/integration/src/system_upgrade_tests.rs +++ b/tests/integration/src/system_upgrade_tests.rs @@ -1,8 +1,7 @@ use crate::setup::{get_canister_wasm, setup_new_env, WALLET_ADMIN_USER}; -use crate::station_test_data::asset::list_assets; use crate::utils::{ - await_station_healthy, execute_request, execute_request_with_extra_ticks, - get_core_canister_health_status, get_system_info, upload_canister_chunks_to_asset_canister, + execute_request, execute_request_with_extra_ticks, get_core_canister_health_status, + get_system_info, upload_canister_chunks_to_asset_canister, }; use crate::{CanisterIds, TestEnv}; use candid::{Encode, Principal}; @@ -10,11 +9,9 @@ use orbit_essentials::api::ApiResult; use pocket_ic::{update_candid_as, PocketIc}; use station_api::{ HealthStatus, NotifyFailedStationUpgradeInput, RequestOperationInput, RequestStatusDTO, - SystemInit, SystemInstall, SystemUpgrade, SystemUpgradeOperationInput, SystemUpgradeTargetDTO, - UserIdentityInput, UserInitInput, + SystemInstall, SystemUpgrade, SystemUpgradeOperationInput, SystemUpgradeTargetDTO, }; use upgrader_api::InitArg; -use uuid::Uuid; pub(crate) const STATION_UPGRADE_EXTRA_TICKS: u64 = 200; @@ -332,66 +329,3 @@ fn unauthorized_notify_failed_station_upgrade() { .unwrap() .contains("No station upgrade request is processing.")); } - -#[test] -fn install_with_initial_assets() { - let TestEnv { - env, controller, .. - } = setup_new_env(); - - let canister_id = env.create_canister_with_settings(Some(controller), None); - - env.set_controllers(canister_id, Some(controller), vec![canister_id, controller]) - .expect("failed to set canister controller"); - - env.add_cycles(canister_id, 5_000_000_000_000); - let station_wasm = get_canister_wasm("station").to_vec(); - let upgrader_wasm = get_canister_wasm("upgrader").to_vec(); - - let station_init_args = SystemInstall::Init(SystemInit { - name: "Station".to_string(), - users: vec![UserInitInput { - identities: vec![UserIdentityInput { - identity: WALLET_ADMIN_USER, - }], - name: "station-admin".to_string(), - groups: None, - id: None, - status: None, - }], - upgrader: station_api::SystemUpgraderInput::Deploy( - station_api::DeploySystemUpgraderInput { - wasm_module: upgrader_wasm, - initial_cycles: Some(1_000_000_000_000), - }, - ), - fallback_controller: Some(controller), - quorum: None, - entries: Some(station_api::InitialEntries::WithDefaultPolicies { - accounts: vec![], - assets: vec![station_api::InitAssetInput { - name: "test-asset".to_string(), - id: Uuid::new_v4().hyphenated().to_string(), - blockchain: "icp".to_string(), - standards: vec!["icp_native".to_owned()], - metadata: vec![], - symbol: "TEST".to_string(), - decimals: 8, - }], - }), - }); - env.install_canister( - canister_id, - station_wasm, - Encode!(&station_init_args).unwrap(), - Some(controller), - ); - - await_station_healthy(&env, canister_id, WALLET_ADMIN_USER); - - let assets = list_assets(&env, canister_id, WALLET_ADMIN_USER) - .expect("failed to call list_assets") - .0 - .expect("failed to list assets"); - println!("assets: {:?}", assets); -} diff --git a/tests/integration/src/utils.rs b/tests/integration/src/utils.rs index 6a7ae682a..016d0150d 100644 --- a/tests/integration/src/utils.rs +++ b/tests/integration/src/utils.rs @@ -38,6 +38,7 @@ use upgrader_api::{ use uuid::Uuid; pub const ADMIN_GROUP_ID: Uuid = Uuid::from_u128(302240678275694148452352); // very first uuidv4 generated +pub const OPERATOR_GROUP_ID: Uuid = Uuid::from_u128(302240678275694148452353); // very first uuidv4 generated pub const NNS_ROOT_CANISTER_ID: Principal = Principal::from_slice(&[0, 0, 0, 0, 0, 0, 0, 3, 1, 1]); pub const COUNTER_WAT: &str = r#" From 1ae4d88a9d114bd020f4870f5bb964ed4ebd660c Mon Sep 17 00:00:00 2001 From: olaszakos Date: Thu, 6 Mar 2025 12:25:40 +0100 Subject: [PATCH 04/33] cleanup --- apps/wallet/src/generated/station/station.did | 121 +++- .../src/generated/station/station.did.d.ts | 54 +- .../src/generated/station/station.did.js | 521 ++++++++++---- core/station/api/spec.did | 1 + tests/integration/src/install_tests.rs | 669 +++++++++--------- 5 files changed, 866 insertions(+), 500 deletions(-) diff --git a/apps/wallet/src/generated/station/station.did b/apps/wallet/src/generated/station/station.did index 68e6d2951..3d9a3871c 100644 --- a/apps/wallet/src/generated/station/station.did +++ b/apps/wallet/src/generated/station/station.did @@ -2715,14 +2715,6 @@ type MeResult = variant { Err : Error; }; -// The admin that is created in the station during the init process. -type AdminInitInput = record { - // The name of the user. - name : text; - // The identity of the admin. - identity : principal; -}; - // An input type for configuring the upgrader canister. type SystemUpgraderInput = variant { // An existing upgrader canister. @@ -2750,6 +2742,10 @@ type InitAccountInput = record { assets : vec UUID; // Metadata associated with the account (e.g. `{"contract": "0x1234", "symbol": "ANY"}`). metadata : vec AccountMetadata; +}; + +// The permissions for the account. +type InitAccountPermissionsInput = record { // Who can read the account information. read_permission : Allow; // Who can request updates to the account. @@ -2762,6 +2758,15 @@ type InitAccountInput = record { transfer_request_policy : opt RequestPolicyRule; }; +// The initial account to create when initializing the canister for the first time. +type InitAccountWithPermissionsInput = record { + // The initial account to create. + account_init : InitAccountInput; + // The permissions for the account. + permissions : InitAccountPermissionsInput; +}; + + // The initial assets to create when initializing the canister for the first time, e.g., after disaster recovery. type InitAssetInput = record { // The UUID of the asset, if not provided a new UUID will be generated. @@ -2780,26 +2785,102 @@ type InitAssetInput = record { metadata : vec AssetMetadata; }; -// The init configuration for the canister. -// -// Only used when installing the canister for the first time. +// The input type for creating a user group when initializing the canister for the first time. +type InitUserGroupInput = record { + // The id of the user group. + id : UUID; + // The name of the user group, must be unique. + name : text; +}; + +// The input type for adding identities to a user. +type UserIdentityInput = record { + // The identity of the user. + identity : principal; +}; + +// The users to create when initializing the canister for the first time. +type InitUserInput = record { + // The id of the user, if not provided a new UUID will be generated. + id : opt UUID; + // The name of the user. + name : text; + // The identities of the user. + identities : vec UserIdentityInput; + // The user groups to associate with the user (optional). + // If not provided it defaults to the `admin` group if default user groups are created. + groups : opt vec UUID; + // The status of the user (e.g. `Active`). + // + // If not provided the default status is `Active` when there is at least + // one identity ot `Inactive` otherwise. + status : opt UserStatus; +}; + +// The init type for initializing the permissions when first creating the canister. +type InitPermissionInput = record { + // The resource that the permission is for. + resource : Resource; + // The allow rules for who can access the resource. + allow : Allow; +}; + +// The init type for adding a request approval policies when initializing the canister for the first time. +type InitRequestPolicyInput = record { + // The id of the request policy, if not provided a new UUID will be generated. + id : opt UUID; + // The request specifier that identifies for what operation this policy is for (e.g. "transfer"). + specifier : RequestSpecifier; + // The rule to use for the request approval evaluation (e.g. "quorum"). + rule : RequestPolicyRule; +}; + +// The init type for adding a named rule when initializing the canister for the first time. +type InitNamedRuleInput = record { + // The id of the named rule. + id : UUID; + // The name of the named rule. + name : text; + // The description of the named rule. + description : opt text; + // The rule to use for the named rule. + rule : RequestPolicyRule; +}; + +type InitalEntries = variant { + // Initialize the station with default policies, accounts and assets. + WithDefaultPolicies : record { + accounts : vec InitAccountInput; + assets : vec InitAssetInput; + }; + // Initialize the station with all custom entries. + Complete : record { + user_groups : vec InitUserGroupInput; + permissions : vec InitPermissionInput; + request_policies : vec InitRequestPolicyInput; + named_rules : vec InitNamedRuleInput; + accounts : vec InitAccountWithPermissionsInput; + assets : vec InitAssetInput; + }; +}; + type SystemInit = record { // The name of the station. name : text; - // The list of admin principals to be associated with the station. - admins : vec AdminInitInput; - // Quorum of admins for initial policies. - quorum : opt nat16; // The upgrader configuration. upgrader : SystemUpgraderInput; - // An optional additional controller of the station and upgrader canisters. + // An additional controller of the station and upgrader canisters (optional). fallback_controller : opt principal; - // Optional initial accounts to create. - accounts : opt vec InitAccountInput; - // Optional initial assets to create. - assets : opt vec InitAssetInput; + // The initial users to create. + users : vec InitUserInput; + // The quorum for the initial approval policy. + quorum : opt nat16; + // The initial entries to create. + entries: opt InitalEntries; }; + + // The upgrade configuration for the canister. type SystemUpgrade = record { // The updated name of the station. diff --git a/apps/wallet/src/generated/station/station.did.d.ts b/apps/wallet/src/generated/station/station.did.d.ts index db4296c25..1f19218e9 100644 --- a/apps/wallet/src/generated/station/station.did.d.ts +++ b/apps/wallet/src/generated/station/station.did.d.ts @@ -128,7 +128,6 @@ export interface AddressBookEntryCallerPrivileges { 'can_edit' : boolean, } export interface AddressBookMetadata { 'key' : string, 'value' : string } -export interface AdminInitInput { 'name' : string, 'identity' : Principal } export interface Allow { 'user_groups' : Array, 'auth_scope' : AuthScope, @@ -731,6 +730,17 @@ export interface InitAccountInput { 'assets' : Array, 'seed' : AccountSeed, } +export interface InitAccountPermissionsInput { + 'configs_request_policy' : [] | [RequestPolicyRule], + 'read_permission' : Allow, + 'configs_permission' : Allow, + 'transfer_request_policy' : [] | [RequestPolicyRule], + 'transfer_permission' : Allow, +} +export interface InitAccountWithPermissionsInput { + 'permissions' : InitAccountPermissionsInput, + 'account_init' : InitAccountInput, +} export interface InitAssetInput { 'id' : UUID, 'decimals' : number, @@ -740,6 +750,42 @@ export interface InitAssetInput { 'blockchain' : string, 'symbol' : string, } +export interface InitNamedRuleInput { + 'id' : UUID, + 'name' : string, + 'rule' : RequestPolicyRule, + 'description' : [] | [string], +} +export interface InitPermissionInput { 'resource' : Resource, 'allow' : Allow } +export interface InitRequestPolicyInput { + 'id' : [] | [UUID], + 'rule' : RequestPolicyRule, + 'specifier' : RequestSpecifier, +} +export interface InitUserGroupInput { 'id' : UUID, 'name' : string } +export interface InitUserInput { + 'id' : [] | [UUID], + 'status' : [] | [UserStatus], + 'groups' : [] | [Array], + 'name' : string, + 'identities' : Array, +} +export type InitalEntries = { + 'WithDefaultPolicies' : { + 'assets' : Array, + 'accounts' : Array, + } + } | + { + 'Complete' : { + 'permissions' : Array, + 'assets' : Array, + 'request_policies' : Array, + 'user_groups' : Array, + 'accounts' : Array, + 'named_rules' : Array, + } + }; export interface ListAccountTransfersInput { 'account_id' : UUID, 'status' : [] | [TransferStatusType], @@ -1385,11 +1431,10 @@ export type SystemInfoResult = { 'Ok' : { 'system' : SystemInfo } } | { 'Err' : Error }; export interface SystemInit { 'name' : string, - 'assets' : [] | [Array], 'fallback_controller' : [] | [Principal], 'upgrader' : SystemUpgraderInput, - 'accounts' : [] | [Array], - 'admins' : Array, + 'entries' : [] | [InitalEntries], + 'users' : Array, 'quorum' : [] | [number], } export type SystemInstall = { 'Upgrade' : SystemUpgrade } | @@ -1488,6 +1533,7 @@ export interface UserGroupCallerPrivileges { 'can_delete' : boolean, 'can_edit' : boolean, } +export interface UserIdentityInput { 'identity' : Principal } export type UserPrivilege = { 'AddUserGroup' : null } | { 'ListRequestPolicies' : null } | { 'ListNamedRules' : null } | diff --git a/apps/wallet/src/generated/station/station.did.js b/apps/wallet/src/generated/station/station.did.js index 92e74c097..e5836e32f 100644 --- a/apps/wallet/src/generated/station/station.did.js +++ b/apps/wallet/src/generated/station/station.did.js @@ -2,6 +2,13 @@ export const idlFactory = ({ IDL }) => { const RequestPolicyRule = IDL.Rec(); const RequestPolicyRuleResult = IDL.Rec(); const SystemUpgrade = IDL.Record({ 'name' : IDL.Opt(IDL.Text) }); + const SystemUpgraderInput = IDL.Variant({ + 'Id' : IDL.Principal, + 'Deploy' : IDL.Record({ + 'initial_cycles' : IDL.Opt(IDL.Nat), + 'wasm_module' : IDL.Vec(IDL.Nat8), + }), + }); const UUID = IDL.Text; const AssetMetadata = IDL.Record({ 'key' : IDL.Text, 'value' : IDL.Text }); const InitAssetInput = IDL.Record({ @@ -13,13 +20,6 @@ export const idlFactory = ({ IDL }) => { 'blockchain' : IDL.Text, 'symbol' : IDL.Text, }); - const SystemUpgraderInput = IDL.Variant({ - 'Id' : IDL.Principal, - 'Deploy' : IDL.Record({ - 'initial_cycles' : IDL.Opt(IDL.Nat), - 'wasm_module' : IDL.Vec(IDL.Nat8), - }), - }); const AccountMetadata = IDL.Record({ 'key' : IDL.Text, 'value' : IDL.Text }); const AccountSeed = IDL.Vec(IDL.Nat8); const InitAccountInput = IDL.Record({ @@ -29,52 +29,6 @@ export const idlFactory = ({ IDL }) => { 'assets' : IDL.Vec(UUID), 'seed' : AccountSeed, }); - const AdminInitInput = IDL.Record({ - 'name' : IDL.Text, - 'identity' : IDL.Principal, - }); - const SystemInit = IDL.Record({ - 'name' : IDL.Text, - 'assets' : IDL.Opt(IDL.Vec(InitAssetInput)), - 'fallback_controller' : IDL.Opt(IDL.Principal), - 'upgrader' : SystemUpgraderInput, - 'accounts' : IDL.Opt(IDL.Vec(InitAccountInput)), - 'admins' : IDL.Vec(AdminInitInput), - 'quorum' : IDL.Opt(IDL.Nat16), - }); - const SystemInstall = IDL.Variant({ - 'Upgrade' : SystemUpgrade, - 'Init' : SystemInit, - }); - const CancelRequestInput = IDL.Record({ - 'request_id' : UUID, - 'reason' : IDL.Opt(IDL.Text), - }); - const TimestampRFC3339 = IDL.Text; - const RequestStatus = IDL.Variant({ - 'Failed' : IDL.Record({ 'reason' : IDL.Opt(IDL.Text) }), - 'Approved' : IDL.Null, - 'Rejected' : IDL.Null, - 'Scheduled' : IDL.Record({ 'scheduled_at' : TimestampRFC3339 }), - 'Cancelled' : IDL.Record({ 'reason' : IDL.Opt(IDL.Text) }), - 'Processing' : IDL.Record({ 'started_at' : TimestampRFC3339 }), - 'Created' : IDL.Null, - 'Completed' : IDL.Record({ 'completed_at' : TimestampRFC3339 }), - }); - const RequestExecutionSchedule = IDL.Variant({ - 'Immediate' : IDL.Null, - 'Scheduled' : IDL.Record({ 'execution_time' : TimestampRFC3339 }), - }); - const RemoveAssetOperationInput = IDL.Record({ 'asset_id' : UUID }); - const RemoveAssetOperation = IDL.Record({ - 'input' : RemoveAssetOperationInput, - }); - const UserGroup = IDL.Record({ 'id' : UUID, 'name' : IDL.Text }); - const AddUserGroupOperationInput = IDL.Record({ 'name' : IDL.Text }); - const AddUserGroupOperation = IDL.Record({ - 'user_group' : IDL.Opt(UserGroup), - 'input' : AddUserGroupOperationInput, - }); const ResourceId = IDL.Variant({ 'Id' : UUID, 'Any' : IDL.Null }); const RequestResourceAction = IDL.Variant({ 'List' : IDL.Null, @@ -161,34 +115,14 @@ export const idlFactory = ({ IDL }) => { 'Public' : IDL.Null, 'Restricted' : IDL.Null, }); - const EditPermissionOperationInput = IDL.Record({ - 'resource' : Resource, - 'user_groups' : IDL.Opt(IDL.Vec(UUID)), - 'auth_scope' : IDL.Opt(AuthScope), - 'users' : IDL.Opt(IDL.Vec(UUID)), - }); - const EditPermissionOperation = IDL.Record({ - 'input' : EditPermissionOperationInput, - }); - const SnapshotExternalCanisterOperationInput = IDL.Record({ - 'force' : IDL.Bool, - 'replace_snapshot' : IDL.Opt(IDL.Text), - 'canister_id' : IDL.Principal, - }); - const SnapshotExternalCanisterOperation = IDL.Record({ - 'input' : SnapshotExternalCanisterOperationInput, - 'snapshot_id' : IDL.Opt(IDL.Text), - }); - const PruneExternalCanisterOperationInput = IDL.Record({ - 'canister_id' : IDL.Principal, - 'prune' : IDL.Variant({ - 'snapshot' : IDL.Text, - 'state' : IDL.Null, - 'chunk_store' : IDL.Null, - }), + const Allow = IDL.Record({ + 'user_groups' : IDL.Vec(UUID), + 'auth_scope' : AuthScope, + 'users' : IDL.Vec(UUID), }); - const PruneExternalCanisterOperation = IDL.Record({ - 'input' : PruneExternalCanisterOperationInput, + const InitPermissionInput = IDL.Record({ + 'resource' : Resource, + 'allow' : Allow, }); const UserSpecifier = IDL.Variant({ 'Id' : IDL.Vec(UUID), @@ -220,6 +154,160 @@ export const idlFactory = ({ IDL }) => { 'NamedRule' : UUID, }) ); + const ResourceIds = IDL.Variant({ 'Any' : IDL.Null, 'Ids' : IDL.Vec(UUID) }); + const ResourceSpecifier = IDL.Variant({ + 'Any' : IDL.Null, + 'Resource' : Resource, + }); + const RequestSpecifier = IDL.Variant({ + 'RemoveAsset' : ResourceIds, + 'AddUserGroup' : IDL.Null, + 'EditPermission' : ResourceSpecifier, + 'EditNamedRule' : ResourceIds, + 'ChangeExternalCanister' : ExternalCanisterId, + 'AddUser' : IDL.Null, + 'EditAsset' : ResourceIds, + 'EditUserGroup' : ResourceIds, + 'SetDisasterRecovery' : IDL.Null, + 'EditRequestPolicy' : ResourceIds, + 'RemoveRequestPolicy' : ResourceIds, + 'AddAsset' : IDL.Null, + 'SystemUpgrade' : IDL.Null, + 'RemoveAddressBookEntry' : ResourceIds, + 'CreateExternalCanister' : IDL.Null, + 'EditAddressBookEntry' : ResourceIds, + 'FundExternalCanister' : ExternalCanisterId, + 'EditUser' : ResourceIds, + 'ManageSystemInfo' : IDL.Null, + 'Transfer' : ResourceIds, + 'EditAccount' : ResourceIds, + 'AddAddressBookEntry' : IDL.Null, + 'AddRequestPolicy' : IDL.Null, + 'RemoveNamedRule' : ResourceIds, + 'RemoveUserGroup' : ResourceIds, + 'CallExternalCanister' : CallExternalCanisterResourceTarget, + 'AddNamedRule' : IDL.Null, + 'AddAccount' : IDL.Null, + }); + const InitRequestPolicyInput = IDL.Record({ + 'id' : IDL.Opt(UUID), + 'rule' : RequestPolicyRule, + 'specifier' : RequestSpecifier, + }); + const InitUserGroupInput = IDL.Record({ 'id' : UUID, 'name' : IDL.Text }); + const InitAccountPermissionsInput = IDL.Record({ + 'configs_request_policy' : IDL.Opt(RequestPolicyRule), + 'read_permission' : Allow, + 'configs_permission' : Allow, + 'transfer_request_policy' : IDL.Opt(RequestPolicyRule), + 'transfer_permission' : Allow, + }); + const InitAccountWithPermissionsInput = IDL.Record({ + 'permissions' : InitAccountPermissionsInput, + 'account_init' : InitAccountInput, + }); + const InitNamedRuleInput = IDL.Record({ + 'id' : UUID, + 'name' : IDL.Text, + 'rule' : RequestPolicyRule, + 'description' : IDL.Opt(IDL.Text), + }); + const InitalEntries = IDL.Variant({ + 'WithDefaultPolicies' : IDL.Record({ + 'assets' : IDL.Vec(InitAssetInput), + 'accounts' : IDL.Vec(InitAccountInput), + }), + 'Complete' : IDL.Record({ + 'permissions' : IDL.Vec(InitPermissionInput), + 'assets' : IDL.Vec(InitAssetInput), + 'request_policies' : IDL.Vec(InitRequestPolicyInput), + 'user_groups' : IDL.Vec(InitUserGroupInput), + 'accounts' : IDL.Vec(InitAccountWithPermissionsInput), + 'named_rules' : IDL.Vec(InitNamedRuleInput), + }), + }); + const UserStatus = IDL.Variant({ + 'Inactive' : IDL.Null, + 'Active' : IDL.Null, + }); + const UserIdentityInput = IDL.Record({ 'identity' : IDL.Principal }); + const InitUserInput = IDL.Record({ + 'id' : IDL.Opt(UUID), + 'status' : IDL.Opt(UserStatus), + 'groups' : IDL.Opt(IDL.Vec(UUID)), + 'name' : IDL.Text, + 'identities' : IDL.Vec(UserIdentityInput), + }); + const SystemInit = IDL.Record({ + 'name' : IDL.Text, + 'fallback_controller' : IDL.Opt(IDL.Principal), + 'upgrader' : SystemUpgraderInput, + 'entries' : IDL.Opt(InitalEntries), + 'users' : IDL.Vec(InitUserInput), + 'quorum' : IDL.Opt(IDL.Nat16), + }); + const SystemInstall = IDL.Variant({ + 'Upgrade' : SystemUpgrade, + 'Init' : SystemInit, + }); + const CancelRequestInput = IDL.Record({ + 'request_id' : UUID, + 'reason' : IDL.Opt(IDL.Text), + }); + const TimestampRFC3339 = IDL.Text; + const RequestStatus = IDL.Variant({ + 'Failed' : IDL.Record({ 'reason' : IDL.Opt(IDL.Text) }), + 'Approved' : IDL.Null, + 'Rejected' : IDL.Null, + 'Scheduled' : IDL.Record({ 'scheduled_at' : TimestampRFC3339 }), + 'Cancelled' : IDL.Record({ 'reason' : IDL.Opt(IDL.Text) }), + 'Processing' : IDL.Record({ 'started_at' : TimestampRFC3339 }), + 'Created' : IDL.Null, + 'Completed' : IDL.Record({ 'completed_at' : TimestampRFC3339 }), + }); + const RequestExecutionSchedule = IDL.Variant({ + 'Immediate' : IDL.Null, + 'Scheduled' : IDL.Record({ 'execution_time' : TimestampRFC3339 }), + }); + const RemoveAssetOperationInput = IDL.Record({ 'asset_id' : UUID }); + const RemoveAssetOperation = IDL.Record({ + 'input' : RemoveAssetOperationInput, + }); + const UserGroup = IDL.Record({ 'id' : UUID, 'name' : IDL.Text }); + const AddUserGroupOperationInput = IDL.Record({ 'name' : IDL.Text }); + const AddUserGroupOperation = IDL.Record({ + 'user_group' : IDL.Opt(UserGroup), + 'input' : AddUserGroupOperationInput, + }); + const EditPermissionOperationInput = IDL.Record({ + 'resource' : Resource, + 'user_groups' : IDL.Opt(IDL.Vec(UUID)), + 'auth_scope' : IDL.Opt(AuthScope), + 'users' : IDL.Opt(IDL.Vec(UUID)), + }); + const EditPermissionOperation = IDL.Record({ + 'input' : EditPermissionOperationInput, + }); + const SnapshotExternalCanisterOperationInput = IDL.Record({ + 'force' : IDL.Bool, + 'replace_snapshot' : IDL.Opt(IDL.Text), + 'canister_id' : IDL.Principal, + }); + const SnapshotExternalCanisterOperation = IDL.Record({ + 'input' : SnapshotExternalCanisterOperationInput, + 'snapshot_id' : IDL.Opt(IDL.Text), + }); + const PruneExternalCanisterOperationInput = IDL.Record({ + 'canister_id' : IDL.Principal, + 'prune' : IDL.Variant({ + 'snapshot' : IDL.Text, + 'state' : IDL.Null, + 'chunk_store' : IDL.Null, + }), + }); + const PruneExternalCanisterOperation = IDL.Record({ + 'input' : PruneExternalCanisterOperationInput, + }); const EditNamedRuleOperationInput = IDL.Record({ 'name' : IDL.Opt(IDL.Text), 'rule' : IDL.Opt(RequestPolicyRule), @@ -229,11 +317,6 @@ export const idlFactory = ({ IDL }) => { const EditNamedRuleOperation = IDL.Record({ 'input' : EditNamedRuleOperationInput, }); - const Allow = IDL.Record({ - 'user_groups' : IDL.Vec(UUID), - 'auth_scope' : AuthScope, - 'users' : IDL.Vec(UUID), - }); const CanisterExecutionAndValidationMethodPair = IDL.Record({ 'execution_method' : IDL.Text, 'validation_method' : ValidationMethodResourceTarget, @@ -396,10 +479,6 @@ export const idlFactory = ({ IDL }) => { 'canister_id' : IDL.Principal, }); const MonitorExternalCanisterOperation = MonitorExternalCanisterOperationInput; - const UserStatus = IDL.Variant({ - 'Inactive' : IDL.Null, - 'Active' : IDL.Null, - }); const User = IDL.Record({ 'id' : UUID, 'status' : UserStatus, @@ -447,41 +526,6 @@ export const idlFactory = ({ IDL }) => { const SetDisasterRecoveryOperation = IDL.Record({ 'committee' : IDL.Opt(DisasterRecoveryCommittee), }); - const ResourceIds = IDL.Variant({ 'Any' : IDL.Null, 'Ids' : IDL.Vec(UUID) }); - const ResourceSpecifier = IDL.Variant({ - 'Any' : IDL.Null, - 'Resource' : Resource, - }); - const RequestSpecifier = IDL.Variant({ - 'RemoveAsset' : ResourceIds, - 'AddUserGroup' : IDL.Null, - 'EditPermission' : ResourceSpecifier, - 'EditNamedRule' : ResourceIds, - 'ChangeExternalCanister' : ExternalCanisterId, - 'AddUser' : IDL.Null, - 'EditAsset' : ResourceIds, - 'EditUserGroup' : ResourceIds, - 'SetDisasterRecovery' : IDL.Null, - 'EditRequestPolicy' : ResourceIds, - 'RemoveRequestPolicy' : ResourceIds, - 'AddAsset' : IDL.Null, - 'SystemUpgrade' : IDL.Null, - 'RemoveAddressBookEntry' : ResourceIds, - 'CreateExternalCanister' : IDL.Null, - 'EditAddressBookEntry' : ResourceIds, - 'FundExternalCanister' : ExternalCanisterId, - 'EditUser' : ResourceIds, - 'ManageSystemInfo' : IDL.Null, - 'Transfer' : ResourceIds, - 'EditAccount' : ResourceIds, - 'AddAddressBookEntry' : IDL.Null, - 'AddRequestPolicy' : IDL.Null, - 'RemoveNamedRule' : ResourceIds, - 'RemoveUserGroup' : ResourceIds, - 'CallExternalCanister' : CallExternalCanisterResourceTarget, - 'AddNamedRule' : IDL.Null, - 'AddAccount' : IDL.Null, - }); const EditRequestPolicyOperationInput = IDL.Record({ 'rule' : IDL.Opt(RequestPolicyRule), 'specifier' : IDL.Opt(RequestSpecifier), @@ -1815,7 +1859,15 @@ export const idlFactory = ({ IDL }) => { }); }; export const init = ({ IDL }) => { + const RequestPolicyRule = IDL.Rec(); const SystemUpgrade = IDL.Record({ 'name' : IDL.Opt(IDL.Text) }); + const SystemUpgraderInput = IDL.Variant({ + 'Id' : IDL.Principal, + 'Deploy' : IDL.Record({ + 'initial_cycles' : IDL.Opt(IDL.Nat), + 'wasm_module' : IDL.Vec(IDL.Nat8), + }), + }); const UUID = IDL.Text; const AssetMetadata = IDL.Record({ 'key' : IDL.Text, 'value' : IDL.Text }); const InitAssetInput = IDL.Record({ @@ -1827,13 +1879,6 @@ export const init = ({ IDL }) => { 'blockchain' : IDL.Text, 'symbol' : IDL.Text, }); - const SystemUpgraderInput = IDL.Variant({ - 'Id' : IDL.Principal, - 'Deploy' : IDL.Record({ - 'initial_cycles' : IDL.Opt(IDL.Nat), - 'wasm_module' : IDL.Vec(IDL.Nat8), - }), - }); const AccountMetadata = IDL.Record({ 'key' : IDL.Text, 'value' : IDL.Text }); const AccountSeed = IDL.Vec(IDL.Nat8); const InitAccountInput = IDL.Record({ @@ -1843,17 +1888,221 @@ export const init = ({ IDL }) => { 'assets' : IDL.Vec(UUID), 'seed' : AccountSeed, }); - const AdminInitInput = IDL.Record({ + const ResourceId = IDL.Variant({ 'Id' : UUID, 'Any' : IDL.Null }); + const RequestResourceAction = IDL.Variant({ + 'List' : IDL.Null, + 'Read' : ResourceId, + }); + const NotificationResourceAction = IDL.Variant({ + 'List' : IDL.Null, + 'Update' : ResourceId, + }); + const SystemResourceAction = IDL.Variant({ + 'Upgrade' : IDL.Null, + 'ManageSystemInfo' : IDL.Null, + 'SystemInfo' : IDL.Null, + 'Capabilities' : IDL.Null, + }); + const UserResourceAction = IDL.Variant({ + 'List' : IDL.Null, + 'Read' : ResourceId, + 'Create' : IDL.Null, + 'Update' : ResourceId, + }); + const CanisterMethod = IDL.Record({ + 'canister_id' : IDL.Principal, + 'method_name' : IDL.Text, + }); + const ExecutionMethodResourceTarget = IDL.Variant({ + 'Any' : IDL.Null, + 'ExecutionMethod' : CanisterMethod, + }); + const ValidationMethodResourceTarget = IDL.Variant({ + 'No' : IDL.Null, + 'ValidationMethod' : CanisterMethod, + }); + const CallExternalCanisterResourceTarget = IDL.Record({ + 'execution_method' : ExecutionMethodResourceTarget, + 'validation_method' : ValidationMethodResourceTarget, + }); + const ExternalCanisterId = IDL.Variant({ + 'Any' : IDL.Null, + 'Canister' : IDL.Principal, + }); + const ExternalCanisterResourceAction = IDL.Variant({ + 'Call' : CallExternalCanisterResourceTarget, + 'Fund' : ExternalCanisterId, + 'List' : IDL.Null, + 'Read' : ExternalCanisterId, + 'Create' : IDL.Null, + 'Change' : ExternalCanisterId, + }); + const AccountResourceAction = IDL.Variant({ + 'List' : IDL.Null, + 'Read' : ResourceId, + 'Create' : IDL.Null, + 'Transfer' : ResourceId, + 'Update' : ResourceId, + }); + const ResourceAction = IDL.Variant({ + 'List' : IDL.Null, + 'Read' : ResourceId, + 'Delete' : ResourceId, + 'Create' : IDL.Null, + 'Update' : ResourceId, + }); + const PermissionResourceAction = IDL.Variant({ + 'Read' : IDL.Null, + 'Update' : IDL.Null, + }); + const Resource = IDL.Variant({ + 'Request' : RequestResourceAction, + 'Notification' : NotificationResourceAction, + 'System' : SystemResourceAction, + 'User' : UserResourceAction, + 'ExternalCanister' : ExternalCanisterResourceAction, + 'Account' : AccountResourceAction, + 'AddressBook' : ResourceAction, + 'Asset' : ResourceAction, + 'NamedRule' : ResourceAction, + 'UserGroup' : ResourceAction, + 'Permission' : PermissionResourceAction, + 'RequestPolicy' : ResourceAction, + }); + const AuthScope = IDL.Variant({ + 'Authenticated' : IDL.Null, + 'Public' : IDL.Null, + 'Restricted' : IDL.Null, + }); + const Allow = IDL.Record({ + 'user_groups' : IDL.Vec(UUID), + 'auth_scope' : AuthScope, + 'users' : IDL.Vec(UUID), + }); + const InitPermissionInput = IDL.Record({ + 'resource' : Resource, + 'allow' : Allow, + }); + const UserSpecifier = IDL.Variant({ + 'Id' : IDL.Vec(UUID), + 'Any' : IDL.Null, + 'Group' : IDL.Vec(UUID), + }); + const Quorum = IDL.Record({ + 'min_approved' : IDL.Nat16, + 'approvers' : UserSpecifier, + }); + const QuorumPercentage = IDL.Record({ + 'min_approved' : IDL.Nat16, + 'approvers' : UserSpecifier, + }); + const AddressBookMetadata = IDL.Record({ + 'key' : IDL.Text, + 'value' : IDL.Text, + }); + RequestPolicyRule.fill( + IDL.Variant({ + 'Not' : RequestPolicyRule, + 'Quorum' : Quorum, + 'AllowListed' : IDL.Null, + 'QuorumPercentage' : QuorumPercentage, + 'AutoApproved' : IDL.Null, + 'AllOf' : IDL.Vec(RequestPolicyRule), + 'AnyOf' : IDL.Vec(RequestPolicyRule), + 'AllowListedByMetadata' : AddressBookMetadata, + 'NamedRule' : UUID, + }) + ); + const ResourceIds = IDL.Variant({ 'Any' : IDL.Null, 'Ids' : IDL.Vec(UUID) }); + const ResourceSpecifier = IDL.Variant({ + 'Any' : IDL.Null, + 'Resource' : Resource, + }); + const RequestSpecifier = IDL.Variant({ + 'RemoveAsset' : ResourceIds, + 'AddUserGroup' : IDL.Null, + 'EditPermission' : ResourceSpecifier, + 'EditNamedRule' : ResourceIds, + 'ChangeExternalCanister' : ExternalCanisterId, + 'AddUser' : IDL.Null, + 'EditAsset' : ResourceIds, + 'EditUserGroup' : ResourceIds, + 'SetDisasterRecovery' : IDL.Null, + 'EditRequestPolicy' : ResourceIds, + 'RemoveRequestPolicy' : ResourceIds, + 'AddAsset' : IDL.Null, + 'SystemUpgrade' : IDL.Null, + 'RemoveAddressBookEntry' : ResourceIds, + 'CreateExternalCanister' : IDL.Null, + 'EditAddressBookEntry' : ResourceIds, + 'FundExternalCanister' : ExternalCanisterId, + 'EditUser' : ResourceIds, + 'ManageSystemInfo' : IDL.Null, + 'Transfer' : ResourceIds, + 'EditAccount' : ResourceIds, + 'AddAddressBookEntry' : IDL.Null, + 'AddRequestPolicy' : IDL.Null, + 'RemoveNamedRule' : ResourceIds, + 'RemoveUserGroup' : ResourceIds, + 'CallExternalCanister' : CallExternalCanisterResourceTarget, + 'AddNamedRule' : IDL.Null, + 'AddAccount' : IDL.Null, + }); + const InitRequestPolicyInput = IDL.Record({ + 'id' : IDL.Opt(UUID), + 'rule' : RequestPolicyRule, + 'specifier' : RequestSpecifier, + }); + const InitUserGroupInput = IDL.Record({ 'id' : UUID, 'name' : IDL.Text }); + const InitAccountPermissionsInput = IDL.Record({ + 'configs_request_policy' : IDL.Opt(RequestPolicyRule), + 'read_permission' : Allow, + 'configs_permission' : Allow, + 'transfer_request_policy' : IDL.Opt(RequestPolicyRule), + 'transfer_permission' : Allow, + }); + const InitAccountWithPermissionsInput = IDL.Record({ + 'permissions' : InitAccountPermissionsInput, + 'account_init' : InitAccountInput, + }); + const InitNamedRuleInput = IDL.Record({ + 'id' : UUID, + 'name' : IDL.Text, + 'rule' : RequestPolicyRule, + 'description' : IDL.Opt(IDL.Text), + }); + const InitalEntries = IDL.Variant({ + 'WithDefaultPolicies' : IDL.Record({ + 'assets' : IDL.Vec(InitAssetInput), + 'accounts' : IDL.Vec(InitAccountInput), + }), + 'Complete' : IDL.Record({ + 'permissions' : IDL.Vec(InitPermissionInput), + 'assets' : IDL.Vec(InitAssetInput), + 'request_policies' : IDL.Vec(InitRequestPolicyInput), + 'user_groups' : IDL.Vec(InitUserGroupInput), + 'accounts' : IDL.Vec(InitAccountWithPermissionsInput), + 'named_rules' : IDL.Vec(InitNamedRuleInput), + }), + }); + const UserStatus = IDL.Variant({ + 'Inactive' : IDL.Null, + 'Active' : IDL.Null, + }); + const UserIdentityInput = IDL.Record({ 'identity' : IDL.Principal }); + const InitUserInput = IDL.Record({ + 'id' : IDL.Opt(UUID), + 'status' : IDL.Opt(UserStatus), + 'groups' : IDL.Opt(IDL.Vec(UUID)), 'name' : IDL.Text, - 'identity' : IDL.Principal, + 'identities' : IDL.Vec(UserIdentityInput), }); const SystemInit = IDL.Record({ 'name' : IDL.Text, - 'assets' : IDL.Opt(IDL.Vec(InitAssetInput)), 'fallback_controller' : IDL.Opt(IDL.Principal), 'upgrader' : SystemUpgraderInput, - 'accounts' : IDL.Opt(IDL.Vec(InitAccountInput)), - 'admins' : IDL.Vec(AdminInitInput), + 'entries' : IDL.Opt(InitalEntries), + 'users' : IDL.Vec(InitUserInput), 'quorum' : IDL.Opt(IDL.Nat16), }); const SystemInstall = IDL.Variant({ diff --git a/core/station/api/spec.did b/core/station/api/spec.did index 7934b44df..3d9a3871c 100644 --- a/core/station/api/spec.did +++ b/core/station/api/spec.did @@ -2808,6 +2808,7 @@ type InitUserInput = record { // The identities of the user. identities : vec UserIdentityInput; // The user groups to associate with the user (optional). + // If not provided it defaults to the `admin` group if default user groups are created. groups : opt vec UUID; // The status of the user (e.g. `Active`). // diff --git a/tests/integration/src/install_tests.rs b/tests/integration/src/install_tests.rs index e10e68d13..dabcca957 100644 --- a/tests/integration/src/install_tests.rs +++ b/tests/integration/src/install_tests.rs @@ -1,12 +1,12 @@ use std::mem; use candid::{Encode, Principal}; +use pocket_ic::PocketIc; use station_api::{ - AccountDTO, AccountResourceActionDTO, AllowDTO, AssetDTO, AuthScopeDTO, InitAccountInput, - InitAssetInput, InitNamedRuleInput, InitPermissionInput, InitRequestPolicyInput, - InitUserGroupInput, MetadataDTO, NamedRuleDTO, PermissionDTO, PermissionResourceActionDTO, - RequestPolicyDTO, RequestPolicyRuleDTO, RequestSpecifierDTO, ResourceActionDTO, ResourceDTO, - ResourceIdDTO, SystemInit, SystemInstall, UserDTO, UserGroupDTO, UserIdentityInput, + AccountResourceActionDTO, AllowDTO, AuthScopeDTO, InitAccountInput, InitAssetInput, + InitNamedRuleInput, InitPermissionInput, InitRequestPolicyInput, InitUserGroupInput, + MetadataDTO, PermissionResourceActionDTO, RequestPolicyRuleDTO, RequestSpecifierDTO, + ResourceActionDTO, ResourceDTO, ResourceIdDTO, SystemInit, SystemInstall, UserIdentityInput, UserInitInput, UserResourceActionDTO, UserStatusDTO, UuidDTO, }; use uuid::Uuid; @@ -22,215 +22,6 @@ use crate::{ TestEnv, }; -fn assert_initial_users( - listed_users: &Vec, - expected_users: &Vec, - default_groups: &Vec, -) -> Result<(), String> { - if expected_users.len() != listed_users.len() { - return Err(format!( - "expected {} users, got {}", - expected_users.len(), - listed_users.len() - )); - } - - for expected_user in expected_users { - let user = listed_users - .iter() - .find(|user| user.name == expected_user.name) - .ok_or(format!("user {} not found", expected_user.name))?; - - expected_user.identities.iter().all(|identity| { - user.identities - .iter() - .any(|user_identity| user_identity == &identity.identity) - }); - - expected_user - .groups - .as_ref() - .unwrap_or(default_groups) - .iter() - .all(|group| user.groups.iter().any(|user_group| &user_group.id == group)); - - let expected_status = expected_user - .status - .as_ref() - .unwrap_or(&UserStatusDTO::Active); - - if mem::discriminant(&user.status) != mem::discriminant(expected_status) { - return Err(format!( - "user {} has status {:?}, expected {:?}", - expected_user.name, user.status, expected_status - )); - } - } - - Ok(()) -} - -fn assert_initial_user_groups( - listed_user_groups: &Vec, - expected_user_groups: &Vec, -) -> Result<(), String> { - if expected_user_groups.len() != listed_user_groups.len() { - return Err(format!( - "expected {} user groups, got {}", - expected_user_groups.len(), - listed_user_groups.len() - )); - } - - for expected_user_group in expected_user_groups { - let _user_group = listed_user_groups - .iter() - .find(|user_group| user_group.name == expected_user_group.name) - .ok_or(format!("user group {} not found", expected_user_group.name))?; - } - - Ok(()) -} - -fn assert_initial_permissions( - listed_permissions: &Vec, - expected_permissions: &Vec, - expected_extra_permissions: usize, -) -> Result<(), String> { - if listed_permissions.len() != expected_permissions.len() + expected_extra_permissions { - return Err(format!( - "expected {} permissions, got {}", - expected_permissions.len() + expected_extra_permissions, - listed_permissions.len() - )); - } - - Ok(()) -} - -fn assert_initial_request_policies( - listed_request_policies: &Vec, - expected_request_policies: &Vec, - expected_extra_request_policies: usize, -) -> Result<(), String> { - if listed_request_policies.len() - != expected_request_policies.len() + expected_extra_request_policies - { - return Err(format!( - "expected {} request policies, got {}", - expected_request_policies.len() + expected_extra_request_policies, - listed_request_policies.len() - )); - } - - Ok(()) -} - -fn assert_initial_named_rules( - listed_named_rules: &Vec, - expected_named_rules: &Vec, -) -> Result<(), String> { - if expected_named_rules.len() != listed_named_rules.len() { - return Err(format!( - "expected {} named rules, got {}", - expected_named_rules.len(), - listed_named_rules.len() - )); - } - - for expected_named_rule in expected_named_rules { - let _named_rule = listed_named_rules - .iter() - .find(|named_rule| named_rule.name == expected_named_rule.name) - .ok_or(format!("named rule {} not found", expected_named_rule.name))?; - } - - Ok(()) -} - -fn assert_initial_assets( - listed_assets: &Vec, - expected_assets: &Vec, -) -> Result<(), String> { - if expected_assets.len() != listed_assets.len() { - return Err(format!( - "expected {} assets, got {}", - expected_assets.len(), - listed_assets.len() - )); - } - - for expected_asset in expected_assets { - let asset = listed_assets - .iter() - .find(|asset| asset.id == expected_asset.id) - .ok_or(format!("asset {} not found", expected_asset.id))?; - - if asset.id != expected_asset.id - || asset.name != expected_asset.name - || asset.blockchain != expected_asset.blockchain - || asset.standards != expected_asset.standards - || asset.metadata != expected_asset.metadata - || asset.symbol != expected_asset.symbol - || asset.decimals != expected_asset.decimals - { - return Err(format!("asset {} does not match expected asset", asset.id)); - } - } - - Ok(()) -} - -fn compare_arrays(a: &Vec, b: &Vec) -> bool { - a.len() == b.len() && a.iter().all(|item| b.contains(item)) -} - -fn assert_initial_accounts( - listed_accounts: &Vec, - expected_accounts: &Vec, -) -> Result<(), String> { - if expected_accounts.len() != listed_accounts.len() { - return Err(format!( - "expected {} accounts, got {}", - expected_accounts.len(), - listed_accounts.len() - )); - } - - for expected_account in expected_accounts { - let account = listed_accounts - .iter() - .find(|account| account.name == expected_account.name) - .ok_or(format!("account {} not found", expected_account.name))?; - - if expected_account.assets.len() > 0 && account.addresses.len() == 0 { - return Err(format!( - "account {} has no addresses, expected some", - expected_account.name - )); - } - - if account.name != expected_account.name - || account.metadata != expected_account.metadata - || !compare_arrays( - &account - .assets - .iter() - .map(|asset| asset.asset_id.clone()) - .collect(), - &expected_account.assets, - ) - { - return Err(format!( - "account {} does not match expected account", - account.id - )); - } - } - - Ok(()) -} - #[test] fn install_with_default_policies() { let TestEnv { @@ -338,70 +129,24 @@ fn install_with_default_policies() { await_station_healthy(&env, canister_id, WALLET_ADMIN_USER); - let lised_assets = list_assets(&env, canister_id, WALLET_ADMIN_USER) - .expect("failed to call list_assets") - .0 - .expect("failed to list assets"); - - let listed_accounts = list_accounts(&env, canister_id, WALLET_ADMIN_USER) - .expect("failed to get account") - .0 - .expect("failed to get account"); - - let listed_users = list_users(&env, canister_id, WALLET_ADMIN_USER) - .expect("failed to get users") - .0 - .expect("failed to get users"); - - assert_eq!(listed_users.users.len(), 3); - assert_initial_users( - &listed_users.users, + &env, + canister_id, + WALLET_ADMIN_USER, &users, &vec![ADMIN_GROUP_ID.hyphenated().to_string()], ) .expect("failed to assert initial users"); - assert_initial_assets(&lised_assets.assets, &assets).expect("failed to assert initial assets"); + assert_initial_assets(&env, canister_id, WALLET_ADMIN_USER, &assets) + .expect("failed to assert initial assets"); - assert_initial_accounts(&listed_accounts.accounts, &accounts) + assert_initial_accounts(&env, canister_id, WALLET_ADMIN_USER, &accounts) .expect("failed to assert initial accounts"); - let listed_policies = list_request_policies(&env, canister_id, WALLET_ADMIN_USER) - .expect("failed to get request policies") - .0 - .expect("failed to get request policies"); - - assert!(listed_policies.policies.len() > 0); - - let permissions = list_permissions(&env, canister_id, WALLET_ADMIN_USER) - .expect("failed to get permissions") - .0 - .expect("failed to get permissions"); - - assert!(permissions.permissions.len() > 0); + assert_default_policies_and_permissions_exist(&env, canister_id, WALLET_ADMIN_USER); - let named_rules = list_named_rules(&env, canister_id, WALLET_ADMIN_USER) - .expect("failed to get named rules") - .0 - .expect("failed to get named rules"); - - assert!(named_rules.named_rules.len() > 0); - - let user_groups = list_user_groups(&env, canister_id, WALLET_ADMIN_USER) - .expect("failed to get user groups") - .0 - .expect("failed to get user groups"); - - assert_eq!(user_groups.user_groups.len(), 2); - assert_eq!( - user_groups.user_groups[0].id, - ADMIN_GROUP_ID.hyphenated().to_string() - ); - assert_eq!( - user_groups.user_groups[1].id, - OPERATOR_GROUP_ID.hyphenated().to_string() - ); + assert_default_groups_exist(&env, canister_id, WALLET_ADMIN_USER); } #[test] @@ -633,66 +378,42 @@ fn install_with_all_entries() { await_station_healthy(&env, canister_id, WALLET_ADMIN_USER); // assert that the users are in the right groups - let list_users_response = list_users(&env, canister_id, WALLET_ADMIN_USER) - .expect("failed to get users") - .0 - .expect("failed to get users"); - - assert_initial_users(&list_users_response.users, &users, &vec![]) + assert_initial_users(&env, canister_id, WALLET_ADMIN_USER, &users, &vec![]) .expect("failed to assert initial users"); // assert the number of request policies - let request_policies_response = list_request_policies(&env, canister_id, WALLET_ADMIN_USER) - .expect("failed to get request policies") - .0 - .expect("failed to get request policies"); - assert_initial_request_policies( - &request_policies_response.policies, + &env, + canister_id, + WALLET_ADMIN_USER, &request_policies, accounts.len() * 2, ) .expect("failed to assert initial request policies"); // assert the number of permissions - let permissions_response = list_permissions(&env, canister_id, WALLET_ADMIN_USER) - .expect("failed to get permissions") - .0 - .expect("failed to get permissions"); - assert_initial_permissions( - &permissions_response.permissions, + &env, + canister_id, + WALLET_ADMIN_USER, &permissions, accounts.len() * 3, ) .expect("failed to assert initial permissions"); // assert the named rules - let list_named_rules_response = list_named_rules(&env, canister_id, WALLET_ADMIN_USER) - .expect("failed to get named rules") - .0 - .expect("failed to get named rules"); - - assert_initial_named_rules(&list_named_rules_response.named_rules, &named_rules) + assert_initial_named_rules(&env, canister_id, WALLET_ADMIN_USER, &named_rules) .expect("failed to assert initial named rules"); // assert the names of the user groups - let list_user_groups_response = list_user_groups(&env, canister_id, WALLET_ADMIN_USER) - .expect("failed to get user groups") - .0 - .expect("failed to get user groups"); - - assert_initial_user_groups(&list_user_groups_response.user_groups, &user_groups) + assert_initial_user_groups(&env, canister_id, WALLET_ADMIN_USER, &user_groups) .expect("failed to assert initial user groups"); // assert the number of accounts and that they have addresses - let list_accounts_response = list_accounts(&env, canister_id, WALLET_ADMIN_USER) - .expect("failed to get accounts") - .0 - .expect("failed to get accounts"); - assert_initial_accounts( - &list_accounts_response.accounts, + &env, + canister_id, + WALLET_ADMIN_USER, &accounts .iter() .map(|init| init.account_init.clone()) @@ -701,12 +422,7 @@ fn install_with_all_entries() { .expect("failed to assert initial accounts"); // assert the number of assets and their names - let list_assets_response = list_assets(&env, canister_id, WALLET_ADMIN_USER) - .expect("failed to get assets") - .0 - .expect("failed to get assets"); - - assert_initial_assets(&list_assets_response.assets, &assets) + assert_initial_assets(&env, canister_id, WALLET_ADMIN_USER, &assets) .expect("failed to assert initial assets"); } @@ -777,66 +493,339 @@ fn install_with_all_defaults() { await_station_healthy(&env, canister_id, WALLET_ADMIN_USER); - let listed_assets = list_assets(&env, canister_id, WALLET_ADMIN_USER) - .expect("failed to call list_assets") - .0 - .expect("failed to list assets"); - - let listed_accounts = list_accounts(&env, canister_id, WALLET_ADMIN_USER) - .expect("failed to get account") - .0 - .expect("failed to get account"); - - let listed_users = list_users(&env, canister_id, WALLET_ADMIN_USER) - .expect("failed to get users") - .0 - .expect("failed to get users"); - assert_initial_users( - &listed_users.users, + &env, + canister_id, + WALLET_ADMIN_USER, &users, &vec![ADMIN_GROUP_ID.hyphenated().to_string()], ) .expect("failed to assert initial users"); - assert!(listed_assets.assets.len() > 0); + assert_default_assets_exist(&env, canister_id, WALLET_ADMIN_USER); + + assert_default_policies_and_permissions_exist(&env, canister_id, WALLET_ADMIN_USER); - assert_initial_accounts(&listed_accounts.accounts, &vec![]) + assert_default_groups_exist(&env, canister_id, WALLET_ADMIN_USER); + + assert_initial_accounts(&env, canister_id, WALLET_ADMIN_USER, &vec![]) .expect("failed to assert initial accounts"); +} - let policies = list_request_policies(&env, canister_id, WALLET_ADMIN_USER) - .expect("failed to get request policies") +fn assert_initial_users( + env: &PocketIc, + canister_id: Principal, + requester: Principal, + expected_users: &Vec, + default_groups: &Vec, +) -> Result<(), String> { + let listed_users = list_users(env, canister_id, requester) + .expect("failed to get users") .0 - .expect("failed to get request policies"); + .expect("failed to get users"); + + if expected_users.len() != listed_users.users.len() { + return Err(format!( + "expected {} users, got {}", + expected_users.len(), + listed_users.users.len() + )); + } + + for expected_user in expected_users { + let user = listed_users + .users + .iter() + .find(|user| user.name == expected_user.name) + .ok_or(format!("user {} not found", expected_user.name))?; + + expected_user.identities.iter().all(|identity| { + user.identities + .iter() + .any(|user_identity| user_identity == &identity.identity) + }); + + expected_user + .groups + .as_ref() + .unwrap_or(default_groups) + .iter() + .all(|group| user.groups.iter().any(|user_group| &user_group.id == group)); + + let expected_status = expected_user + .status + .as_ref() + .unwrap_or(&UserStatusDTO::Active); + + if mem::discriminant(&user.status) != mem::discriminant(expected_status) { + return Err(format!( + "user {} has status {:?}, expected {:?}", + expected_user.name, user.status, expected_status + )); + } + } + + Ok(()) +} + +fn assert_initial_user_groups( + env: &PocketIc, + canister_id: Principal, + requester: Principal, + expected_user_groups: &Vec, +) -> Result<(), String> { + let listed_user_groups = list_user_groups(env, canister_id, requester) + .expect("failed to get user groups") + .0 + .expect("failed to get user groups"); + + if expected_user_groups.len() != listed_user_groups.user_groups.len() { + return Err(format!( + "expected {} user groups, got {}", + expected_user_groups.len(), + listed_user_groups.user_groups.len() + )); + } + + for expected_user_group in expected_user_groups { + let _user_group = listed_user_groups + .user_groups + .iter() + .find(|user_group| user_group.name == expected_user_group.name) + .ok_or(format!("user group {} not found", expected_user_group.name))?; + } - assert!(policies.policies.len() > 0); + Ok(()) +} - let permissions = list_permissions(&env, canister_id, WALLET_ADMIN_USER) +fn assert_initial_permissions( + env: &PocketIc, + canister_id: Principal, + requester: Principal, + expected_permissions: &Vec, + expected_extra_permissions: usize, +) -> Result<(), String> { + let listed_permissions = list_permissions(env, canister_id, requester) .expect("failed to get permissions") .0 .expect("failed to get permissions"); - assert!(permissions.permissions.len() > 0); + if listed_permissions.permissions.len() + != expected_permissions.len() + expected_extra_permissions + { + return Err(format!( + "expected {} permissions, got {}", + expected_permissions.len() + expected_extra_permissions, + listed_permissions.permissions.len() + )); + } + + Ok(()) +} - let named_rules = list_named_rules(&env, canister_id, WALLET_ADMIN_USER) +fn assert_initial_request_policies( + env: &PocketIc, + canister_id: Principal, + requester: Principal, + expected_request_policies: &Vec, + expected_extra_request_policies: usize, +) -> Result<(), String> { + let listed_request_policies = list_request_policies(env, canister_id, requester) + .expect("failed to get request policies") + .0 + .expect("failed to get request policies"); + + if listed_request_policies.policies.len() + != expected_request_policies.len() + expected_extra_request_policies + { + return Err(format!( + "expected {} request policies, got {}", + expected_request_policies.len() + expected_extra_request_policies, + listed_request_policies.policies.len() + )); + } + + Ok(()) +} + +fn assert_initial_named_rules( + env: &PocketIc, + canister_id: Principal, + requester: Principal, + expected_named_rules: &Vec, +) -> Result<(), String> { + let listed_named_rules = list_named_rules(env, canister_id, requester) .expect("failed to get named rules") .0 .expect("failed to get named rules"); - assert!(named_rules.named_rules.len() > 0); + if expected_named_rules.len() != listed_named_rules.named_rules.len() { + return Err(format!( + "expected {} named rules, got {}", + expected_named_rules.len(), + listed_named_rules.named_rules.len() + )); + } + + for expected_named_rule in expected_named_rules { + let _named_rule = listed_named_rules + .named_rules + .iter() + .find(|named_rule| named_rule.name == expected_named_rule.name) + .ok_or(format!("named rule {} not found", expected_named_rule.name))?; + } + + Ok(()) +} + +fn assert_initial_assets( + env: &PocketIc, + canister_id: Principal, + requester: Principal, + expected_assets: &Vec, +) -> Result<(), String> { + let listed_assets = list_assets(env, canister_id, requester) + .expect("failed to call list_assets") + .0 + .expect("failed to list assets"); + + if expected_assets.len() != listed_assets.assets.len() { + return Err(format!( + "expected {} assets, got {}", + expected_assets.len(), + listed_assets.assets.len() + )); + } + + for expected_asset in expected_assets { + let asset = listed_assets + .assets + .iter() + .find(|asset| asset.id == expected_asset.id) + .ok_or(format!("asset {} not found", expected_asset.id))?; + + if asset.id != expected_asset.id + || asset.name != expected_asset.name + || asset.blockchain != expected_asset.blockchain + || asset.standards != expected_asset.standards + || asset.metadata != expected_asset.metadata + || asset.symbol != expected_asset.symbol + || asset.decimals != expected_asset.decimals + { + return Err(format!("asset {} does not match expected asset", asset.id)); + } + } + + Ok(()) +} + +fn compare_arrays(a: &Vec, b: &Vec) -> bool { + a.len() == b.len() && a.iter().all(|item| b.contains(item)) +} + +fn assert_initial_accounts( + env: &PocketIc, + canister_id: Principal, + requester: Principal, + expected_accounts: &Vec, +) -> Result<(), String> { + let listed_accounts = list_accounts(env, canister_id, requester) + .expect("failed to get account") + .0 + .expect("failed to get account"); + + if expected_accounts.len() != listed_accounts.accounts.len() { + return Err(format!( + "expected {} accounts, got {}", + expected_accounts.len(), + listed_accounts.accounts.len() + )); + } + + for expected_account in expected_accounts { + let account = listed_accounts + .accounts + .iter() + .find(|account| account.name == expected_account.name) + .ok_or(format!("account {} not found", expected_account.name))?; + + if expected_account.assets.len() > 0 && account.addresses.len() == 0 { + return Err(format!( + "account {} has no addresses, expected some", + expected_account.name + )); + } - let user_groups = list_user_groups(&env, canister_id, WALLET_ADMIN_USER) + if account.name != expected_account.name + || account.metadata != expected_account.metadata + || !compare_arrays( + &account + .assets + .iter() + .map(|asset| asset.asset_id.clone()) + .collect(), + &expected_account.assets, + ) + { + return Err(format!( + "account {} does not match expected account", + account.id + )); + } + } + + Ok(()) +} + +fn assert_default_groups_exist(env: &PocketIc, canister_id: Principal, requester: Principal) { + let listed_user_groups = list_user_groups(env, canister_id, requester) .expect("failed to get user groups") .0 .expect("failed to get user groups"); - assert_eq!(user_groups.user_groups.len(), 2); + assert_eq!(listed_user_groups.user_groups.len(), 2); assert_eq!( - user_groups.user_groups[0].id, + listed_user_groups.user_groups[0].id, ADMIN_GROUP_ID.hyphenated().to_string() ); assert_eq!( - user_groups.user_groups[1].id, + listed_user_groups.user_groups[1].id, OPERATOR_GROUP_ID.hyphenated().to_string() ); } + +fn assert_default_policies_and_permissions_exist( + env: &PocketIc, + canister_id: Principal, + requester: Principal, +) { + let listed_policies = list_request_policies(env, canister_id, requester) + .expect("failed to get request policies") + .0 + .expect("failed to get request policies"); + + assert!(listed_policies.policies.len() > 0); + + let listed_permissions = list_permissions(env, canister_id, requester) + .expect("failed to get permissions") + .0 + .expect("failed to get permissions"); + + assert!(listed_permissions.permissions.len() > 0); + + let listed_named_rules = list_named_rules(env, canister_id, requester) + .expect("failed to get named rules") + .0 + .expect("failed to get named rules"); + + assert!(listed_named_rules.named_rules.len() > 0); +} + +fn assert_default_assets_exist(env: &PocketIc, canister_id: Principal, requester: Principal) { + let listed_assets = list_assets(env, canister_id, requester) + .expect("failed to get assets") + .0 + .expect("failed to get assets"); + + assert!(listed_assets.assets.len() > 0); +} From 59352738a35d4eeefca52623dd53fd906b73ee0c Mon Sep 17 00:00:00 2001 From: olaszakos Date: Thu, 6 Mar 2025 14:50:40 +0100 Subject: [PATCH 05/33] add more tests --- core/station/impl/src/services/system.rs | 88 ++++++++++++++- tests/integration/Cargo.toml | 1 + tests/integration/src/install_tests.rs | 130 +++++++++++++++++++++++ 3 files changed, 217 insertions(+), 2 deletions(-) diff --git a/core/station/impl/src/services/system.rs b/core/station/impl/src/services/system.rs index 9670f1d85..f4c7289ff 100644 --- a/core/station/impl/src/services/system.rs +++ b/core/station/impl/src/services/system.rs @@ -1215,9 +1215,13 @@ mod install_canister_handlers { #[cfg(test)] mod tests { use super::*; - use crate::models::request_test_utils::mock_request; + use crate::{ + core::validation::disable_mock_resource_validation, + models::request_test_utils::mock_request, + services::system::init_canister_sync_handlers::set_initial_named_rules, + }; use candid::Principal; - use station_api::{UserIdentityInput, UserInitInput}; + use station_api::{InitNamedRuleInput, UserIdentityInput, UserInitInput}; #[tokio::test] async fn canister_init() { @@ -1305,4 +1309,84 @@ mod tests { // larger than the number of admins assert_eq!(calc_initial_quorum(4, Some(5)), 4); } + + #[tokio::test] + async fn test_initial_named_rules_with_correct_dependencies() { + let id_1 = Uuid::new_v4().hyphenated().to_string(); + let id_2 = Uuid::new_v4().hyphenated().to_string(); + let id_3 = Uuid::new_v4().hyphenated().to_string(); + + // incorrect named rule order still succeeds because of sorting + let initial_named_rules = vec![ + InitNamedRuleInput { + name: "NamedRule3".to_string(), + id: id_1.clone(), + description: None, + rule: station_api::RequestPolicyRuleDTO::NamedRule(id_2.clone()), + }, + InitNamedRuleInput { + name: "NamedRule2".to_string(), + id: id_2.clone(), + description: None, + rule: station_api::RequestPolicyRuleDTO::NamedRule(id_3.clone()), + }, + InitNamedRuleInput { + name: "NamedRule1".to_string(), + id: id_3.clone(), + description: None, + rule: station_api::RequestPolicyRuleDTO::AutoApproved, + }, + ]; + set_initial_named_rules(&initial_named_rules).expect("Failed to set initial named rules"); + } + + #[tokio::test] + async fn test_initial_named_rules_with_circular_dependencies() { + let id_1 = Uuid::new_v4().hyphenated().to_string(); + let id_2 = Uuid::new_v4().hyphenated().to_string(); + let id_3 = Uuid::new_v4().hyphenated().to_string(); + + // circular reference throws an error + let initial_named_rules = vec![ + InitNamedRuleInput { + name: "NamedRule3".to_string(), + id: id_1.clone(), + description: None, + rule: station_api::RequestPolicyRuleDTO::NamedRule(id_2.clone()), + }, + InitNamedRuleInput { + name: "NamedRule2".to_string(), + id: id_2.clone(), + description: None, + rule: station_api::RequestPolicyRuleDTO::NamedRule(id_3.clone()), + }, + InitNamedRuleInput { + name: "NamedRule1".to_string(), + id: id_3.clone(), + description: None, + rule: station_api::RequestPolicyRuleDTO::NamedRule(id_1.clone()), + }, + ]; + + set_initial_named_rules(&initial_named_rules) + .expect_err("Should have failed due to circular reference"); + } + + #[tokio::test] + async fn test_initial_named_rules_with_unknown_key() { + disable_mock_resource_validation(); + + let id_1 = Uuid::new_v4().hyphenated().to_string(); + let id_2 = Uuid::new_v4().hyphenated().to_string(); + // unknown key throws an error + let initial_named_rules = vec![InitNamedRuleInput { + name: "NamedRule3".to_string(), + id: id_1.clone(), + description: None, + rule: station_api::RequestPolicyRuleDTO::NamedRule(id_2.clone()), + }]; + + set_initial_named_rules(&initial_named_rules) + .expect_err("Should have failed due to unknown key"); + } } diff --git a/tests/integration/Cargo.toml b/tests/integration/Cargo.toml index a212b2ff0..e674cca02 100644 --- a/tests/integration/Cargo.toml +++ b/tests/integration/Cargo.toml @@ -34,3 +34,4 @@ control-panel-api = { path = '../../core/control-panel/api', version = '0.2.0' } upgrader-api = { path = '../../core/upgrader/api', version = '0.1.0' } station-api = { path = '../../core/station/api', version = '0.4.0' } wat = { workspace = true } +rstest = { workspace = true } diff --git a/tests/integration/src/install_tests.rs b/tests/integration/src/install_tests.rs index dabcca957..99a16b04d 100644 --- a/tests/integration/src/install_tests.rs +++ b/tests/integration/src/install_tests.rs @@ -2,6 +2,7 @@ use std::mem; use candid::{Encode, Principal}; use pocket_ic::PocketIc; +use rstest::rstest; use station_api::{ AccountResourceActionDTO, AllowDTO, AuthScopeDTO, InitAccountInput, InitAssetInput, InitNamedRuleInput, InitPermissionInput, InitRequestPolicyInput, InitUserGroupInput, @@ -512,6 +513,135 @@ fn install_with_all_defaults() { .expect("failed to assert initial accounts"); } +#[rstest] +#[should_panic] +#[case::empty_entries(station_api::InitialEntries::Complete { + accounts: vec![], + assets: vec![], + permissions: vec![], + request_policies: vec![], + user_groups: vec![], // no user groups yet user is referencing the ADMIN group + named_rules: vec![], +})] +#[should_panic] +#[case::circular_named_rules({ + let id_1 = Uuid::new_v4().hyphenated().to_string(); + let id_2 = Uuid::new_v4().hyphenated().to_string(); + + station_api::InitialEntries::Complete { + accounts: vec![], + assets: vec![], + permissions: vec![], + request_policies: vec![], + user_groups: vec![ + InitUserGroupInput { + id: ADMIN_GROUP_ID.hyphenated().to_string(), + name: "admin".to_string(), + }, + ], + named_rules: vec![ + // circular reference + InitNamedRuleInput { + id: id_1.clone(), + name: "named_rule".to_string(), + description: None, + rule: RequestPolicyRuleDTO::NamedRule(id_2.clone()), + }, + InitNamedRuleInput { + id: id_2.clone(), + name: "named_rule_2".to_string(), + description: None, + rule: RequestPolicyRuleDTO::NamedRule(id_1.clone()), + }, + ], + } +})] +#[should_panic] +#[case::non_existent_asset_id({ + let id_1 = Uuid::new_v4().hyphenated().to_string(); + station_api::InitialEntries::WithDefaultPolicies { + accounts: vec![InitAccountInput { + name: "account".to_string(), + metadata: vec![], + assets: vec![ + id_1.clone(), // non-existent asset id + ], + id: Some(id_1.clone()), + seed: Uuid::new_v4().to_bytes_le(), + }], + assets: vec![], + } +})] +#[should_panic] +#[case::non_existent_policy_id({ + let id_1 = Uuid::new_v4().hyphenated().to_string(); + station_api::InitialEntries::Complete { + accounts: vec![], + assets: vec![], + permissions: vec![], + request_policies: vec![InitRequestPolicyInput { + id: None, + rule: RequestPolicyRuleDTO::AutoApproved, + specifier: RequestSpecifierDTO::EditRequestPolicy( + station_api::ResourceIdsDTO::Ids(vec![id_1.clone()]), // non-existent policy id + ), + }], + user_groups: vec![ + InitUserGroupInput { + id: ADMIN_GROUP_ID.hyphenated().to_string(), + name: "admin".to_string(), + }, + ], + named_rules: vec![], + } +})] +fn install_with_bad_input(#[case] bad_input: station_api::InitialEntries) { + let TestEnv { + env, controller, .. + } = setup_new_env(); + + let canister_id = env.create_canister_with_settings(Some(controller), None); + + env.set_controllers(canister_id, Some(controller), vec![canister_id, controller]) + .expect("failed to set canister controller"); + + env.add_cycles(canister_id, 5_000_000_000_000); + let station_wasm = get_canister_wasm("station").to_vec(); + let upgrader_wasm = get_canister_wasm("upgrader").to_vec(); + + let users = vec![UserInitInput { + identities: vec![UserIdentityInput { + identity: WALLET_ADMIN_USER, + }], + name: "station-admin".to_string(), + groups: Some(vec![ADMIN_GROUP_ID.hyphenated().to_string()]), + id: None, + status: None, + }]; + + let station_init_args = SystemInstall::Init(SystemInit { + name: "Station".to_string(), + users: users.clone(), + upgrader: station_api::SystemUpgraderInput::Deploy( + station_api::DeploySystemUpgraderInput { + wasm_module: upgrader_wasm.clone(), + initial_cycles: Some(1_000_000_000_000), + }, + ), + fallback_controller: Some(controller), + quorum: None, + entries: Some(bad_input), + }); + env.install_canister( + canister_id, + station_wasm.clone(), + Encode!(&station_init_args).unwrap(), + Some(controller), + ); + + await_station_healthy(&env, canister_id, WALLET_ADMIN_USER); +} + fn assert_initial_users( env: &PocketIc, canister_id: Principal, From 7b2c9ce8ee3705fb0cbd0556a7f106b068a9fe55 Mon Sep 17 00:00:00 2001 From: olaszakos Date: Thu, 6 Mar 2025 14:53:47 +0100 Subject: [PATCH 06/33] update lock file --- Cargo.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.lock b/Cargo.lock index 55c3186f0..23f0b332e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2786,6 +2786,7 @@ dependencies = [ "rand", "rand_chacha", "reqwest", + "rstest", "serde", "sha2 0.10.8", "slog", From 46a32193ee0a4626df8556ed4c7a5cf97b1b136e Mon Sep 17 00:00:00 2001 From: olaszakos Date: Thu, 6 Mar 2025 15:09:06 +0100 Subject: [PATCH 07/33] fix lint --- core/station/impl/src/services/system.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/station/impl/src/services/system.rs b/core/station/impl/src/services/system.rs index f4c7289ff..afa93d71c 100644 --- a/core/station/impl/src/services/system.rs +++ b/core/station/impl/src/services/system.rs @@ -990,7 +990,7 @@ mod install_canister_handlers { // Registers the initial accounts of the canister during the canister initialization. pub async fn set_initial_accounts( accounts: Vec<(InitAccountInput, Option)>, - initial_assets: &Vec, + initial_assets: &[InitAssetInput], quorum: u16, ) -> Result<(), String> { let add_accounts = accounts From 2d12c17ba2437c160fd9a4c77a295b4b5b279ece Mon Sep 17 00:00:00 2001 From: olaszakos Date: Thu, 6 Mar 2025 15:24:24 +0100 Subject: [PATCH 08/33] fix more lints --- tests/integration/src/install_tests.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/integration/src/install_tests.rs b/tests/integration/src/install_tests.rs index 99a16b04d..c55f185e2 100644 --- a/tests/integration/src/install_tests.rs +++ b/tests/integration/src/install_tests.rs @@ -732,7 +732,7 @@ fn assert_initial_permissions( env: &PocketIc, canister_id: Principal, requester: Principal, - expected_permissions: &Vec, + expected_permissions: &[InitPermissionInput], expected_extra_permissions: usize, ) -> Result<(), String> { let listed_permissions = list_permissions(env, canister_id, requester) @@ -757,7 +757,7 @@ fn assert_initial_request_policies( env: &PocketIc, canister_id: Principal, requester: Principal, - expected_request_policies: &Vec, + expected_request_policies: &[InitRequestPolicyInput], expected_extra_request_policies: usize, ) -> Result<(), String> { let listed_request_policies = list_request_policies(env, canister_id, requester) @@ -849,7 +849,7 @@ fn assert_initial_assets( Ok(()) } -fn compare_arrays(a: &Vec, b: &Vec) -> bool { +fn compare_arrays(a: &[T], b: &[T]) -> bool { a.len() == b.len() && a.iter().all(|item| b.contains(item)) } @@ -879,7 +879,7 @@ fn assert_initial_accounts( .find(|account| account.name == expected_account.name) .ok_or(format!("account {} not found", expected_account.name))?; - if expected_account.assets.len() > 0 && account.addresses.len() == 0 { + if !expected_account.assets.is_empty() && account.addresses.is_empty() { return Err(format!( "account {} has no addresses, expected some", expected_account.name @@ -893,7 +893,7 @@ fn assert_initial_accounts( .assets .iter() .map(|asset| asset.asset_id.clone()) - .collect(), + .collect::>(), &expected_account.assets, ) { @@ -934,21 +934,21 @@ fn assert_default_policies_and_permissions_exist( .0 .expect("failed to get request policies"); - assert!(listed_policies.policies.len() > 0); + assert!(!listed_policies.policies.is_empty()); let listed_permissions = list_permissions(env, canister_id, requester) .expect("failed to get permissions") .0 .expect("failed to get permissions"); - assert!(listed_permissions.permissions.len() > 0); + assert!(!listed_permissions.permissions.is_empty()); let listed_named_rules = list_named_rules(env, canister_id, requester) .expect("failed to get named rules") .0 .expect("failed to get named rules"); - assert!(listed_named_rules.named_rules.len() > 0); + assert!(!listed_named_rules.named_rules.is_empty()); } fn assert_default_assets_exist(env: &PocketIc, canister_id: Principal, requester: Principal) { @@ -957,5 +957,5 @@ fn assert_default_assets_exist(env: &PocketIc, canister_id: Principal, requester .0 .expect("failed to get assets"); - assert!(listed_assets.assets.len() > 0); + assert!(!listed_assets.assets.is_empty()); } From e29e695cba81a30e04ec345b191cb7b3b2a1cab5 Mon Sep 17 00:00:00 2001 From: olaszakos Date: Thu, 6 Mar 2025 16:33:38 +0100 Subject: [PATCH 09/33] use quorum=1 for deployment from control panel --- core/control-panel/impl/src/services/deploy.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/control-panel/impl/src/services/deploy.rs b/core/control-panel/impl/src/services/deploy.rs index 9de722945..9c0f8bd4a 100644 --- a/core/control-panel/impl/src/services/deploy.rs +++ b/core/control-panel/impl/src/services/deploy.rs @@ -145,7 +145,7 @@ impl DeployService { fallback_controller: Some(NNS_ROOT_CANISTER_ID), entries: None, users: intial_users, - quorum: None, + quorum: Some(1), })) .map_err(|err| DeployError::Failed { reason: err.to_string(), From d34ff3403a19923931e25eab8c95d0fb2b3ddbc2 Mon Sep 17 00:00:00 2001 From: olaszakos Date: Fri, 7 Mar 2025 09:31:23 +0100 Subject: [PATCH 10/33] fix quorum in control panel test --- tests/integration/src/control_panel_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/src/control_panel_tests.rs b/tests/integration/src/control_panel_tests.rs index af9a38c3e..d705cbe89 100644 --- a/tests/integration/src/control_panel_tests.rs +++ b/tests/integration/src/control_panel_tests.rs @@ -652,7 +652,7 @@ fn deploy_station_with_insufficient_cycles() { id: None, status: None, }], - quorum: None, + quorum: Some(1), entries: None, upgrader: station_api::SystemUpgraderInput::Deploy( station_api::DeploySystemUpgraderInput { From b906eee8b4cda7482d6bd8f18ee4b2929601bfaa Mon Sep 17 00:00:00 2001 From: olaszakos Date: Mon, 10 Mar 2025 12:17:38 +0100 Subject: [PATCH 11/33] Update core/station/impl/src/services/system.rs Co-authored-by: mraszyk <31483726+mraszyk@users.noreply.github.com> --- core/station/impl/src/services/system.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/station/impl/src/services/system.rs b/core/station/impl/src/services/system.rs index afa93d71c..fed158af8 100644 --- a/core/station/impl/src/services/system.rs +++ b/core/station/impl/src/services/system.rs @@ -811,7 +811,7 @@ mod init_canister_sync_handlers { Ok(()) } - // Registers the initial accounts of the canister during the canister initialization. + // Registers the initial assets of the canister during the canister initialization. pub async fn set_initial_assets(assets: &[InitAssetInput]) -> Result<(), ApiError> { let add_assets = assets .iter() From 608efc3568d4e4ae022c81bc470dad7e2c569380 Mon Sep 17 00:00:00 2001 From: olaszakos Date: Mon, 10 Mar 2025 12:17:46 +0100 Subject: [PATCH 12/33] Update core/station/impl/src/services/system.rs Co-authored-by: mraszyk <31483726+mraszyk@users.noreply.github.com> --- core/station/impl/src/services/system.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/station/impl/src/services/system.rs b/core/station/impl/src/services/system.rs index fed158af8..bfefc06df 100644 --- a/core/station/impl/src/services/system.rs +++ b/core/station/impl/src/services/system.rs @@ -900,7 +900,7 @@ mod init_canister_sync_handlers { )?; print(&format!( - "Added admin user with principals {:?} and user id {}", + "Added user with principals {:?} and user id {}", user.identities .iter() .map(|identity| identity.to_text()) From 58db75c454bc3a65e5b7844a31b86a5ebb958936 Mon Sep 17 00:00:00 2001 From: olaszakos Date: Mon, 10 Mar 2025 12:17:55 +0100 Subject: [PATCH 13/33] Update core/station/impl/src/services/system.rs Co-authored-by: mraszyk <31483726+mraszyk@users.noreply.github.com> --- core/station/impl/src/services/system.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/station/impl/src/services/system.rs b/core/station/impl/src/services/system.rs index bfefc06df..a2cc4073e 100644 --- a/core/station/impl/src/services/system.rs +++ b/core/station/impl/src/services/system.rs @@ -864,7 +864,7 @@ mod init_canister_sync_handlers { users: Vec, default_groups: &[UUID], ) -> Result<(), ApiError> { - print(format!("Registering {} admin users", users.len())); + print(format!("Registering {} users", users.len())); for user in users { let user_id = user .id From dc10d8d235b51306cf80312f01c923b29c699d0d Mon Sep 17 00:00:00 2001 From: olaszakos Date: Mon, 10 Mar 2025 12:18:02 +0100 Subject: [PATCH 14/33] Update core/station/impl/src/services/system.rs Co-authored-by: mraszyk <31483726+mraszyk@users.noreply.github.com> --- core/station/impl/src/services/system.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/station/impl/src/services/system.rs b/core/station/impl/src/services/system.rs index a2cc4073e..7103c1cc2 100644 --- a/core/station/impl/src/services/system.rs +++ b/core/station/impl/src/services/system.rs @@ -859,7 +859,7 @@ mod init_canister_sync_handlers { Ok(()) } - /// Registers the newly added admins of the canister. + /// Registers the newly added users of the canister. pub fn set_initial_users( users: Vec, default_groups: &[UUID], From 5c061ecce4458c9795fd3b254bc4e9cb06bb36e9 Mon Sep 17 00:00:00 2001 From: olaszakos Date: Mon, 10 Mar 2025 12:18:21 +0100 Subject: [PATCH 15/33] Update tests/integration/src/install_tests.rs Co-authored-by: mraszyk <31483726+mraszyk@users.noreply.github.com> --- tests/integration/src/install_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/src/install_tests.rs b/tests/integration/src/install_tests.rs index c55f185e2..032b37d8a 100644 --- a/tests/integration/src/install_tests.rs +++ b/tests/integration/src/install_tests.rs @@ -850,7 +850,7 @@ fn assert_initial_assets( } fn compare_arrays(a: &[T], b: &[T]) -> bool { - a.len() == b.len() && a.iter().all(|item| b.contains(item)) + a.len() == b.len() && a.iter().all(|item| b.contains(item)) && b.iter().all(|item| a.contains(item)) } fn assert_initial_accounts( From 11fcd07bc99bc12cbec7eee5ec4a79bc6781dc76 Mon Sep 17 00:00:00 2001 From: olaszakos Date: Mon, 10 Mar 2025 12:42:18 +0100 Subject: [PATCH 16/33] Update core/station/api/spec.did Co-authored-by: mraszyk <31483726+mraszyk@users.noreply.github.com> --- core/station/api/spec.did | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/station/api/spec.did b/core/station/api/spec.did index 3d9a3871c..14df80679 100644 --- a/core/station/api/spec.did +++ b/core/station/api/spec.did @@ -2825,7 +2825,7 @@ type InitPermissionInput = record { allow : Allow; }; -// The init type for adding a request approval policies when initializing the canister for the first time. +// The init type for adding a request approval policy when initializing the canister for the first time. type InitRequestPolicyInput = record { // The id of the request policy, if not provided a new UUID will be generated. id : opt UUID; From a6cbc7adb7715f7064343d819e9c8bfc879c8a6d Mon Sep 17 00:00:00 2001 From: olaszakos Date: Mon, 10 Mar 2025 14:24:06 +0100 Subject: [PATCH 17/33] address review comments --- core/control-panel/impl/src/services/deploy.rs | 6 +++--- core/station/api/spec.did | 10 ++++------ tests/integration/src/utils.rs | 4 ++-- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/core/control-panel/impl/src/services/deploy.rs b/core/control-panel/impl/src/services/deploy.rs index 9c0f8bd4a..44bae0cf9 100644 --- a/core/control-panel/impl/src/services/deploy.rs +++ b/core/control-panel/impl/src/services/deploy.rs @@ -121,14 +121,14 @@ impl DeployService { let intial_users = input .admins .iter() - .map(|admin| station_api::UserInitInput { + .map(|user| station_api::UserInitInput { id: None, identities: vec![station_api::UserIdentityInput { - identity: admin.identity, + identity: user.identity, }], groups: None, status: None, - name: admin.username.clone(), + name: user.username.clone(), }) .collect::>(); diff --git a/core/station/api/spec.did b/core/station/api/spec.did index 14df80679..037d1a826 100644 --- a/core/station/api/spec.did +++ b/core/station/api/spec.did @@ -2766,7 +2766,6 @@ type InitAccountWithPermissionsInput = record { permissions : InitAccountPermissionsInput; }; - // The initial assets to create when initializing the canister for the first time, e.g., after disaster recovery. type InitAssetInput = record { // The UUID of the asset, if not provided a new UUID will be generated. @@ -2808,7 +2807,8 @@ type InitUserInput = record { // The identities of the user. identities : vec UserIdentityInput; // The user groups to associate with the user (optional). - // If not provided it defaults to the `admin` group if default user groups are created. + // If not provided it defaults to the `admin` group if default user groups are created, + // i.e., when the field `entries` in `SystemInit` is `null` or has the form of `WithDefaultPolicies`. groups : opt vec UUID; // The status of the user (e.g. `Active`). // @@ -2847,7 +2847,7 @@ type InitNamedRuleInput = record { rule : RequestPolicyRule; }; -type InitalEntries = variant { +type InitialEntries = variant { // Initialize the station with default policies, accounts and assets. WithDefaultPolicies : record { accounts : vec InitAccountInput; @@ -2876,11 +2876,9 @@ type SystemInit = record { // The quorum for the initial approval policy. quorum : opt nat16; // The initial entries to create. - entries: opt InitalEntries; + entries: opt InitialEntries; }; - - // The upgrade configuration for the canister. type SystemUpgrade = record { // The updated name of the station. diff --git a/tests/integration/src/utils.rs b/tests/integration/src/utils.rs index 016d0150d..a6565a4d1 100644 --- a/tests/integration/src/utils.rs +++ b/tests/integration/src/utils.rs @@ -37,8 +37,8 @@ use upgrader_api::{ }; use uuid::Uuid; -pub const ADMIN_GROUP_ID: Uuid = Uuid::from_u128(302240678275694148452352); // very first uuidv4 generated -pub const OPERATOR_GROUP_ID: Uuid = Uuid::from_u128(302240678275694148452353); // very first uuidv4 generated +pub const ADMIN_GROUP_ID: Uuid = Uuid::from_u128(302240678275694148452352); // 00000000-0000-4000-8000-000000000000 +pub const OPERATOR_GROUP_ID: Uuid = Uuid::from_u128(302240678275694148452353); // 00000000-0000-4000-8000-000000000001 pub const NNS_ROOT_CANISTER_ID: Principal = Principal::from_slice(&[0, 0, 0, 0, 0, 0, 0, 3, 1, 1]); pub const COUNTER_WAT: &str = r#" From 9cc63ee14e30848df9a6170c6ec7647eaf420560 Mon Sep 17 00:00:00 2001 From: olaszakos Date: Mon, 10 Mar 2025 16:56:21 +0100 Subject: [PATCH 18/33] check for existing ids at initial entry creation --- core/station/impl/src/errors/asset.rs | 7 + core/station/impl/src/errors/named_rule.rs | 9 + .../station/impl/src/errors/request_policy.rs | 7 + core/station/impl/src/errors/user_group.rs | 7 + core/station/impl/src/services/asset.rs | 6 + core/station/impl/src/services/named_rule.rs | 10 + .../impl/src/services/request_policy.rs | 8 +- core/station/impl/src/services/system.rs | 196 ++++++++++++++---- core/station/impl/src/services/user_group.rs | 12 +- 9 files changed, 218 insertions(+), 44 deletions(-) diff --git a/core/station/impl/src/errors/asset.rs b/core/station/impl/src/errors/asset.rs index cbf91cace..0df425044 100644 --- a/core/station/impl/src/errors/asset.rs +++ b/core/station/impl/src/errors/asset.rs @@ -43,6 +43,9 @@ pub enum AssetError { /// The asset blockchain. blockchain: String, }, + /// The asset with id `{id}` already exists. + #[error(r#"The asset with id `{id}` already exists."#)] + IdAlreadyExists { id: String }, } impl DetailableError for AssetError { @@ -97,6 +100,10 @@ impl DetailableError for AssetError { details.insert("resource".to_string(), resource.to_string()); Some(details) } + AssetError::IdAlreadyExists { id } => { + details.insert("id".to_string(), id.to_string()); + Some(details) + } } } } diff --git a/core/station/impl/src/errors/named_rule.rs b/core/station/impl/src/errors/named_rule.rs index 562fe287a..155458852 100644 --- a/core/station/impl/src/errors/named_rule.rs +++ b/core/station/impl/src/errors/named_rule.rs @@ -42,6 +42,10 @@ pub enum NamedRuleError { // The named rule has a circular reference. #[error("The named rule has a circular reference.")] CircularReference, + + // The named rule with id `{id}` already exists. + #[error(r#"The named rule with id `{id}` already exists."#)] + IdAlreadyExists { id: String }, } impl DetailableError for NamedRuleError { @@ -84,6 +88,11 @@ impl DetailableError for NamedRuleError { NamedRuleError::InUse => None, NamedRuleError::CircularReference => None, + + NamedRuleError::IdAlreadyExists { id } => { + details.insert("id".to_string(), id.to_string()); + Some(details) + } } } } diff --git a/core/station/impl/src/errors/request_policy.rs b/core/station/impl/src/errors/request_policy.rs index c533bed69..0f8ebd7b8 100644 --- a/core/station/impl/src/errors/request_policy.rs +++ b/core/station/impl/src/errors/request_policy.rs @@ -9,6 +9,9 @@ pub enum RequestPolicyError { /// The request policy has failed validation. #[error(r#"The request policy has failed validation."#)] ValidationError { info: String }, + /// Request policy with id `{id}` already exists. + #[error(r#"Request policy with id `{id}` already exists."#)] + IdAlreadyExists { id: String }, } impl DetailableError for RequestPolicyError { @@ -19,6 +22,10 @@ impl DetailableError for RequestPolicyError { details.insert("info".to_string(), info.to_string()); Some(details) } + RequestPolicyError::IdAlreadyExists { id } => { + details.insert("id".to_string(), id.to_string()); + Some(details) + } } } } diff --git a/core/station/impl/src/errors/user_group.rs b/core/station/impl/src/errors/user_group.rs index dea07ac38..013c89fe0 100644 --- a/core/station/impl/src/errors/user_group.rs +++ b/core/station/impl/src/errors/user_group.rs @@ -33,6 +33,9 @@ pub enum UserGroupError { /// The user group id. id: String, }, + /// The user group with id `{id}` already exists. + #[error(r#"The user group with id `{id}` already exists."#)] + IdAlreadyExists { id: String }, } impl DetailableError for UserGroupError { @@ -59,6 +62,10 @@ impl DetailableError for UserGroupError { details.insert("id".to_string(), id.to_string()); Some(details) } + UserGroupError::IdAlreadyExists { id } => { + details.insert("id".to_string(), id.to_string()); + Some(details) + } } } } diff --git a/core/station/impl/src/services/asset.rs b/core/station/impl/src/services/asset.rs index be07c252f..15407192a 100644 --- a/core/station/impl/src/services/asset.rs +++ b/core/station/impl/src/services/asset.rs @@ -56,6 +56,12 @@ impl AssetService { ) -> ServiceResult { let id = with_asset_id.unwrap_or(*Uuid::new_v4().as_bytes()); + if self.asset_repository.get(&id).is_some() { + Err(AssetError::IdAlreadyExists { + id: Uuid::from_bytes(id).hyphenated().to_string(), + })?; + } + let asset = Asset { id, blockchain: input.blockchain, diff --git a/core/station/impl/src/services/named_rule.rs b/core/station/impl/src/services/named_rule.rs index d91d3d48f..e15d81277 100644 --- a/core/station/impl/src/services/named_rule.rs +++ b/core/station/impl/src/services/named_rule.rs @@ -97,6 +97,16 @@ impl NamedRuleService { ) -> ServiceResult { let id = with_named_rule_id.unwrap_or_else(|| *Uuid::new_v4().as_bytes()); + if self + .named_rule_repository + .get(&NamedRuleKey { id }) + .is_some() + { + Err(NamedRuleError::IdAlreadyExists { + id: Uuid::from_bytes(id).hyphenated().to_string(), + })?; + } + let named_rule = NamedRule { id, name: input.name, diff --git a/core/station/impl/src/services/request_policy.rs b/core/station/impl/src/services/request_policy.rs index 682cbe6eb..7e26733dc 100644 --- a/core/station/impl/src/services/request_policy.rs +++ b/core/station/impl/src/services/request_policy.rs @@ -4,7 +4,7 @@ use crate::{ utils::{paginated_items, retain_accessible_resources, PaginatedData, PaginatedItemsArgs}, CallContext, }, - errors::RequestError, + errors::{RequestError, RequestPolicyError}, models::{ request_policy_rule::RequestPolicyRuleInput, request_specifier::RequestSpecifier, @@ -67,6 +67,12 @@ impl RequestPolicyService { ) -> ServiceResult { let id = with_policy_id.unwrap_or_else(|| *Uuid::new_v4().as_bytes()); + if self.request_policy_repository.get(&id).is_some() { + Err(RequestPolicyError::IdAlreadyExists { + id: Uuid::from_bytes(id).hyphenated().to_string(), + })?; + } + let policy = RequestPolicy { id, specifier: input.specifier, diff --git a/core/station/impl/src/services/system.rs b/core/station/impl/src/services/system.rs index 7103c1cc2..50c463fe3 100644 --- a/core/station/impl/src/services/system.rs +++ b/core/station/impl/src/services/system.rs @@ -294,7 +294,7 @@ impl SystemService { Some(InitialEntries::WithDefaultPolicies { accounts, assets }) => { print("Adding initial accounts"); // initial accounts are added in the post process work timer, since they might do inter-canister calls - install_canister_handlers::set_initial_accounts( + init_canister_sync_handlers::set_initial_accounts( accounts .into_iter() .map(|account| (account, None)) @@ -309,7 +309,7 @@ impl SystemService { }) => { print("Adding initial accounts"); // initial accounts are added in the post process work timer, since they might do inter-canister calls - install_canister_handlers::set_initial_accounts( + init_canister_sync_handlers::set_initial_accounts( accounts .into_iter() .map(|init_with_permissions| { @@ -491,6 +491,9 @@ impl SystemService { /// Updates the canister with the given settings. /// /// Must only be called within a canister post_upgrade call. + /// + /// This function is not implemented in the original file or the provided code block. + /// It's assumed to exist as it's called in the test case. pub async fn upgrade_canister(&self, input: Option) -> ServiceResult<()> { // initializes the cache of the canister data, must happen during the same call as the upgrade self.init_cache(); @@ -603,17 +606,19 @@ mod init_canister_sync_handlers { use crate::core::init::{default_policies, get_default_named_rules, DEFAULT_PERMISSIONS}; use crate::mappers::blockchain::BlockchainMapper; use crate::mappers::HelperMapper; - use crate::models::request_specifier::RequestSpecifier; + use crate::models::permission::Allow; + use crate::models::request_specifier::{RequestSpecifier, UserSpecifier}; use crate::models::resource::ResourceIds; use crate::models::{ - AddAssetOperationInput, AddNamedRuleOperationInput, AddRequestPolicyOperationInput, - AddUserGroupOperationInput, AddUserOperationInput, Asset, EditPermissionOperationInput, - NamedRule, UserStatus, OPERATOR_GROUP_ID, + AddAccountOperationInput, AddAssetOperationInput, AddNamedRuleOperationInput, + AddRequestPolicyOperationInput, AddUserGroupOperationInput, AddUserOperationInput, Asset, + EditPermissionOperationInput, NamedRule, RequestPolicyRule, UserStatus, OPERATOR_GROUP_ID, }; use crate::repositories::{ASSET_REPOSITORY, NAMED_RULE_REPOSITORY}; use crate::services::permission::PERMISSION_SERVICE; use crate::services::{ - ASSET_SERVICE, NAMED_RULE_SERVICE, REQUEST_POLICY_SERVICE, USER_GROUP_SERVICE, USER_SERVICE, + ACCOUNT_SERVICE, ASSET_SERVICE, NAMED_RULE_SERVICE, REQUEST_POLICY_SERVICE, + USER_GROUP_SERVICE, USER_SERVICE, }; use crate::{ models::{UserGroup, ADMIN_GROUP_ID}, @@ -624,8 +629,8 @@ mod init_canister_sync_handlers { use orbit_essentials::repository::Repository; use orbit_essentials::types::UUID; use station_api::{ - InitAssetInput, InitNamedRuleInput, InitPermissionInput, InitRequestPolicyInput, - InitUserGroupInput, UserInitInput, + InitAccountInput, InitAccountPermissionsInput, InitAssetInput, InitNamedRuleInput, + InitPermissionInput, InitRequestPolicyInput, InitUserGroupInput, UserInitInput, }; use uuid::Uuid; @@ -956,38 +961,10 @@ mod init_canister_sync_handlers { Ok(()) } -} - -// Calculates the initial quorum based on the number of admins and the provided quorum, if not provided -// the quorum is set to the majority of the admins. -pub fn calc_initial_quorum(admin_count: u16, quorum: Option) -> u16 { - quorum.unwrap_or(admin_count / 2 + 1).clamp(1, admin_count) -} - -#[cfg(target_arch = "wasm32")] -mod install_canister_handlers { - use crate::core::ic_cdk::api::id as self_canister_id; - use crate::core::DEFAULT_INITIAL_UPGRADER_CYCLES; - use crate::mappers::HelperMapper; - use crate::models::permission::Allow; - use crate::models::request_specifier::UserSpecifier; - use crate::models::{ - AddAccountOperationInput, CycleObtainStrategy, MonitorExternalCanisterStrategy, - MonitoringExternalCanisterEstimatedRuntimeInput, RequestPolicyRule, ADMIN_GROUP_ID, - }; - use crate::repositories::ASSET_REPOSITORY; - use crate::services::cycle_manager::CYCLE_MANAGER; - use crate::services::ACCOUNT_SERVICE; - use crate::services::EXTERNAL_CANISTER_SERVICE; - use candid::{Encode, Principal}; - use ic_cdk::api::management_canister::main::{self as mgmt}; - use ic_cdk::{id, print}; - use orbit_essentials::repository::Repository; - use orbit_essentials::types::UUID; - use station_api::{InitAccountInput, InitAccountPermissionsInput, InitAssetInput}; - use uuid::Uuid; + #[allow(unused)] // Registers the initial accounts of the canister during the canister initialization. + // Used pub async fn set_initial_accounts( accounts: Vec<(InitAccountInput, Option)>, initial_assets: &[InitAssetInput], @@ -1111,6 +1088,27 @@ mod install_canister_handlers { Ok(()) } +} + +// Calculates the initial quorum based on the number of admins and the provided quorum, if not provided +// the quorum is set to the majority of the admins. +pub fn calc_initial_quorum(admin_count: u16, quorum: Option) -> u16 { + quorum.unwrap_or(admin_count / 2 + 1).clamp(1, admin_count) +} + +#[cfg(target_arch = "wasm32")] +mod install_canister_handlers { + use crate::core::ic_cdk::api::id as self_canister_id; + use crate::core::DEFAULT_INITIAL_UPGRADER_CYCLES; + use crate::models::{ + CycleObtainStrategy, MonitorExternalCanisterStrategy, + MonitoringExternalCanisterEstimatedRuntimeInput, + }; + use crate::services::cycle_manager::CYCLE_MANAGER; + use crate::services::EXTERNAL_CANISTER_SERVICE; + use candid::{Encode, Principal}; + use ic_cdk::api::management_canister::main::{self as mgmt}; + use ic_cdk::id; pub async fn init_upgrader( input: station_api::SystemUpgraderInput, @@ -1218,10 +1216,17 @@ mod tests { use crate::{ core::validation::disable_mock_resource_validation, models::request_test_utils::mock_request, - services::system::init_canister_sync_handlers::set_initial_named_rules, + services::system::init_canister_sync_handlers::{ + set_initial_accounts, set_initial_assets, set_initial_named_rules, + set_initial_request_policies, set_initial_user_groups, + }, }; use candid::Principal; - use station_api::{InitNamedRuleInput, UserIdentityInput, UserInitInput}; + use station_api::{ + AccountSeedDTO, InitAccountInput, InitAssetInput, InitNamedRuleInput, + InitRequestPolicyInput, InitUserGroupInput, UserIdentityInput, UserInitInput, + }; + use uuid::Uuid; #[tokio::test] async fn canister_init() { @@ -1389,4 +1394,113 @@ mod tests { set_initial_named_rules(&initial_named_rules) .expect_err("Should have failed due to unknown key"); } + + #[tokio::test] + async fn test_duplicate_uuids() { + disable_mock_resource_validation(); + + // Test duplicate UUIDs in named rules + let named_rule_id = Uuid::new_v4().hyphenated().to_string(); + set_initial_named_rules(&[ + InitNamedRuleInput { + name: "NamedRule1".to_string(), + id: named_rule_id.clone(), + description: None, + rule: station_api::RequestPolicyRuleDTO::AutoApproved, + }, + InitNamedRuleInput { + name: "NamedRule2".to_string(), + id: named_rule_id.clone(), + description: None, + rule: station_api::RequestPolicyRuleDTO::AutoApproved, + }, + ]) + .expect_err("Should have failed due to duplicate UUID in named rules"); + + // Test duplicate UUIDs in request policies + let request_policy_id = Uuid::new_v4().hyphenated().to_string(); + set_initial_request_policies(&[ + InitRequestPolicyInput { + id: Some(request_policy_id.clone()), + specifier: station_api::RequestSpecifierDTO::AddAccount, + rule: station_api::RequestPolicyRuleDTO::AutoApproved, + }, + InitRequestPolicyInput { + id: Some(request_policy_id.clone()), + specifier: station_api::RequestSpecifierDTO::AddUser, + rule: station_api::RequestPolicyRuleDTO::AutoApproved, + }, + ]) + .expect_err("Should have failed due to duplicate UUID in request policies"); + + // Test duplicate UUIDs in user groups + let user_group_id = Uuid::new_v4().hyphenated().to_string(); + set_initial_user_groups(&[ + InitUserGroupInput { + name: "UserGroup1".to_string(), + id: user_group_id.clone(), + }, + InitUserGroupInput { + name: "UserGroup2".to_string(), + id: user_group_id.clone(), + }, + ]) + .await + .expect_err("Should have failed due to duplicate UUID in user groups"); + + // Test duplicate UUIDs in assets + let asset_id = Uuid::new_v4().hyphenated().to_string(); + set_initial_assets(&[ + InitAssetInput { + id: asset_id.clone(), + name: "Asset1".to_string(), + blockchain: "icp".to_string(), + standards: vec!["icrc1".to_string()], + metadata: vec![], + symbol: "AST1".to_string(), + decimals: 8, + }, + InitAssetInput { + id: asset_id.clone(), + name: "Asset2".to_string(), + blockchain: "icp".to_string(), + standards: vec!["icrc1".to_string()], + metadata: vec![], + symbol: "AST2".to_string(), + decimals: 8, + }, + ]) + .await + .expect_err("Should have failed due to duplicate UUID in assets"); + + // Test duplicate UUIDs in accounts + let account_id = Uuid::new_v4().hyphenated().to_string(); + let empty_seed: AccountSeedDTO = [0; 16]; // Create a zero-filled array for the seed + let account_inputs = vec![ + ( + InitAccountInput { + id: Some(account_id.clone()), + name: "Account1".to_string(), + seed: empty_seed, + assets: vec![], + metadata: vec![], + }, + None, + ), + ( + InitAccountInput { + id: Some(account_id.clone()), + name: "Account2".to_string(), + seed: empty_seed, + assets: vec![], + metadata: vec![], + }, + None, + ), + ]; + + set_initial_accounts(account_inputs, &[], 1) + .await + .expect_err("Should have failed due to duplicate UUID in accounts"); + } } diff --git a/core/station/impl/src/services/user_group.rs b/core/station/impl/src/services/user_group.rs index 4e7435013..5cf3d5de0 100644 --- a/core/station/impl/src/services/user_group.rs +++ b/core/station/impl/src/services/user_group.rs @@ -100,13 +100,21 @@ impl UserGroupService { input: AddUserGroupOperationInput, with_user_group_id: Option, ) -> ServiceResult { - let user_group_id = match with_user_group_id { + let user_group_uuid = match with_user_group_id { Some(id) => Uuid::from_bytes(id), None => generate_uuid_v4().await, }; + let user_group_id = *user_group_uuid.as_bytes(); + + if self.user_group_repository.get(&user_group_id).is_some() { + Err(UserGroupError::IdAlreadyExists { + id: Uuid::from_bytes(user_group_id).hyphenated().to_string(), + })?; + } + let user_group = UserGroup { - id: *user_group_id.as_bytes(), + id: user_group_id, name: input.name.to_string(), last_modification_timestamp: next_time(), }; From b7354c6ea0f6d07272f0e13339922fc37b9a23ad Mon Sep 17 00:00:00 2001 From: olaszakos Date: Mon, 10 Mar 2025 16:57:28 +0100 Subject: [PATCH 19/33] Update core/station/api/spec.did Co-authored-by: mraszyk <31483726+mraszyk@users.noreply.github.com> --- core/station/api/spec.did | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/station/api/spec.did b/core/station/api/spec.did index 037d1a826..370dede47 100644 --- a/core/station/api/spec.did +++ b/core/station/api/spec.did @@ -2813,7 +2813,7 @@ type InitUserInput = record { // The status of the user (e.g. `Active`). // // If not provided the default status is `Active` when there is at least - // one identity ot `Inactive` otherwise. + // one identity or `Inactive` otherwise. status : opt UserStatus; }; From dd1d3dd61845eb1854593163525f22ede5026e0b Mon Sep 17 00:00:00 2001 From: olaszakos Date: Mon, 10 Mar 2025 17:11:23 +0100 Subject: [PATCH 20/33] unfold wildcard cases --- .../impl/src/models/request_policy_rule.rs | 6 +++- core/station/impl/src/services/system.rs | 29 +++++++++++++++++-- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/core/station/impl/src/models/request_policy_rule.rs b/core/station/impl/src/models/request_policy_rule.rs index 42b8635ec..0afc6293c 100644 --- a/core/station/impl/src/models/request_policy_rule.rs +++ b/core/station/impl/src/models/request_policy_rule.rs @@ -52,7 +52,11 @@ impl RequestPolicyRule { .iter() .any(|rule| rule.has_named_rule_id(named_rule_id)), RequestPolicyRule::Not(rule) => rule.has_named_rule_id(named_rule_id), - _ => false, + RequestPolicyRule::AutoApproved + | RequestPolicyRule::QuorumPercentage(..) + | RequestPolicyRule::Quorum(.., _) + | RequestPolicyRule::AllowListedByMetadata(..) + | RequestPolicyRule::AllowListed => false, } } } diff --git a/core/station/impl/src/services/system.rs b/core/station/impl/src/services/system.rs index 50c463fe3..a900afafc 100644 --- a/core/station/impl/src/services/system.rs +++ b/core/station/impl/src/services/system.rs @@ -324,7 +324,7 @@ impl SystemService { ) .await?; } - _ => {} + None => {} } if SYSTEM_SERVICE.is_healthy() { @@ -772,7 +772,32 @@ mod init_canister_sync_handlers { ResourceIds::Any => false, ResourceIds::Ids(ids) => ids.contains(policy_id), }, - _ => false, + RequestSpecifier::AddAccount + | RequestSpecifier::AddUser + | RequestSpecifier::EditAccount(..) + | RequestSpecifier::EditUser(..) + | RequestSpecifier::AddAddressBookEntry + | RequestSpecifier::EditAddressBookEntry(..) + | RequestSpecifier::RemoveAddressBookEntry(..) + | RequestSpecifier::Transfer(..) + | RequestSpecifier::SetDisasterRecovery + | RequestSpecifier::CreateExternalCanister + | RequestSpecifier::ChangeExternalCanister(..) + | RequestSpecifier::CallExternalCanister(..) + | RequestSpecifier::FundExternalCanister(..) + | RequestSpecifier::EditPermission(..) + | RequestSpecifier::AddRequestPolicy + | RequestSpecifier::AddUserGroup + | RequestSpecifier::EditUserGroup(..) + | RequestSpecifier::RemoveUserGroup(..) + | RequestSpecifier::ManageSystemInfo + | RequestSpecifier::SystemUpgrade + | RequestSpecifier::AddAsset + | RequestSpecifier::EditAsset(..) + | RequestSpecifier::RemoveAsset(..) + | RequestSpecifier::AddNamedRule + | RequestSpecifier::EditNamedRule(..) + | RequestSpecifier::RemoveNamedRule(..) => false, } } From f4cf46136c885a21bac8dd3b6b09f6cee81a9420 Mon Sep 17 00:00:00 2001 From: olaszakos Date: Mon, 10 Mar 2025 17:41:57 +0100 Subject: [PATCH 21/33] remove allowing initial asset to exist --- core/station/impl/src/services/system.rs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/core/station/impl/src/services/system.rs b/core/station/impl/src/services/system.rs index a900afafc..831f4178e 100644 --- a/core/station/impl/src/services/system.rs +++ b/core/station/impl/src/services/system.rs @@ -873,17 +873,7 @@ mod init_canister_sync_handlers { .collect::>(); for (new_asset, with_asset_id) in add_assets { - match ASSET_SERVICE.create(new_asset, Some(with_asset_id)) { - Err(ApiError { code, details, .. }) if &code == "ALREADY_EXISTS" => { - // asset already exists, can skip safely - print(format!( - "Asset already exists, skipping. Details: {:?}", - details.unwrap_or_default() - )); - } - Err(e) => Err(e)?, - Ok(_) => {} - } + ASSET_SERVICE.create(new_asset, Some(with_asset_id))?; } Ok(()) From 0f806f7721ffc9ff95dcf371b47a7bba68d13b1f Mon Sep 17 00:00:00 2001 From: olaszakos Date: Mon, 10 Mar 2025 17:42:04 +0100 Subject: [PATCH 22/33] run format --- tests/integration/src/install_tests.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/integration/src/install_tests.rs b/tests/integration/src/install_tests.rs index 032b37d8a..a4ce9dc40 100644 --- a/tests/integration/src/install_tests.rs +++ b/tests/integration/src/install_tests.rs @@ -850,7 +850,9 @@ fn assert_initial_assets( } fn compare_arrays(a: &[T], b: &[T]) -> bool { - a.len() == b.len() && a.iter().all(|item| b.contains(item)) && b.iter().all(|item| a.contains(item)) + a.len() == b.len() + && a.iter().all(|item| b.contains(item)) + && b.iter().all(|item| a.contains(item)) } fn assert_initial_accounts( From 99880b18a756d4afe551bd14c5cde5d43c07c470 Mon Sep 17 00:00:00 2001 From: olaszakos Date: Tue, 11 Mar 2025 14:32:24 +0100 Subject: [PATCH 23/33] change all input id's to optional for consistency --- core/station/api/spec.did | 6 +- core/station/api/src/system.rs | 6 +- core/station/impl/src/services/system.rs | 131 ++++++------------ .../src/disaster_recovery_tests.rs | 2 +- tests/integration/src/install_tests.rs | 32 +++-- 5 files changed, 70 insertions(+), 107 deletions(-) diff --git a/core/station/api/spec.did b/core/station/api/spec.did index 370dede47..ea8f1a0af 100644 --- a/core/station/api/spec.did +++ b/core/station/api/spec.did @@ -2769,7 +2769,7 @@ type InitAccountWithPermissionsInput = record { // The initial assets to create when initializing the canister for the first time, e.g., after disaster recovery. type InitAssetInput = record { // The UUID of the asset, if not provided a new UUID will be generated. - id : UUID; + id : opt UUID; // The name of the asset. name : text; // The blockchain identifier (e.g., `ethereum`, `bitcoin`, `icp`, etc.) @@ -2787,7 +2787,7 @@ type InitAssetInput = record { // The input type for creating a user group when initializing the canister for the first time. type InitUserGroupInput = record { // The id of the user group. - id : UUID; + id : opt UUID; // The name of the user group, must be unique. name : text; }; @@ -2838,7 +2838,7 @@ type InitRequestPolicyInput = record { // The init type for adding a named rule when initializing the canister for the first time. type InitNamedRuleInput = record { // The id of the named rule. - id : UUID; + id : opt UUID; // The name of the named rule. name : text; // The description of the named rule. diff --git a/core/station/api/src/system.rs b/core/station/api/src/system.rs index 4c22970e0..f2f6a4cb8 100644 --- a/core/station/api/src/system.rs +++ b/core/station/api/src/system.rs @@ -77,7 +77,7 @@ pub struct UserIdentityInput { #[derive(CandidType, serde::Serialize, Deserialize, Clone, Debug)] pub struct InitUserGroupInput { - pub id: UuidDTO, + pub id: Option, pub name: String, } @@ -96,7 +96,7 @@ pub struct InitRequestPolicyInput { #[derive(CandidType, serde::Serialize, Deserialize, Clone, Debug)] pub struct InitNamedRuleInput { - pub id: UuidDTO, + pub id: Option, pub name: String, pub description: Option, pub rule: RequestPolicyRuleDTO, @@ -141,7 +141,7 @@ pub struct InitAccountWithPermissionsInput { #[derive(CandidType, serde::Serialize, Deserialize, Clone, Debug)] pub struct InitAssetInput { - pub id: UuidDTO, + pub id: Option, pub name: String, pub blockchain: String, pub standards: Vec, diff --git a/core/station/impl/src/services/system.rs b/core/station/impl/src/services/system.rs index 831f4178e..235b2b352 100644 --- a/core/station/impl/src/services/system.rs +++ b/core/station/impl/src/services/system.rs @@ -677,18 +677,19 @@ mod init_canister_sync_handlers { name: user_group.name.clone(), }; - ( - input, - *HelperMapper::to_uuid(user_group.id.clone()) - .expect("Invalid UUID") - .as_bytes(), - ) + let user_group_id = user_group + .id + .as_ref() + .map(|id| HelperMapper::to_uuid(id.clone()).map(|uuid| *uuid.as_bytes())) + .transpose(); + + user_group_id.map(|user_group_id| (input, user_group_id)) }) - .collect::>(); + .collect::, _>>()?; for (new_user_group, with_user_group_id) in add_user_groups { USER_GROUP_SERVICE - .create_with_id(new_user_group, Some(with_user_group_id)) + .create_with_id(new_user_group, with_user_group_id) .await?; } @@ -705,27 +706,30 @@ mod init_canister_sync_handlers { rule: named_rule.rule.clone().into(), }; - ( - input, - HelperMapper::to_uuid(named_rule.id.clone()).map(|uuid| *uuid.as_bytes()), - ) + let named_rule_id = named_rule + .id + .as_ref() + .map(|id| HelperMapper::to_uuid(id.clone()).map(|uuid| *uuid.as_bytes())) + .transpose(); + + named_rule_id.map(|named_rule_id| (input, named_rule_id)) }) - .map(|(input, result)| result.map(|uuid| (input, uuid))) .collect::, _>>()?; // sorting criteria: // - if a policy depends on another policy, the dependent policy should be added first // - keep the original order of the policys otherwise add_named_rules.sort_by(|a, b| { - if b.0.rule.has_named_rule_id(&a.1) { - Ordering::Less - } else { - Ordering::Greater + if let Some(a_id) = &a.1 { + if b.0.rule.has_named_rule_id(a_id) { + return Ordering::Less; + } } + Ordering::Greater }); for (new_named_rule, with_named_rule_id) in add_named_rules { - NAMED_RULE_SERVICE.create_with_id(new_named_rule, Some(with_named_rule_id))?; + NAMED_RULE_SERVICE.create_with_id(new_named_rule, with_named_rule_id)?; } Ok(()) @@ -863,17 +867,18 @@ mod init_canister_sync_handlers { metadata: asset.metadata.clone().into(), }; - ( - input, - *HelperMapper::to_uuid(asset.id.clone()) - .expect("Invalid UUID") - .as_bytes(), - ) + let asset_id = asset + .id + .as_ref() + .map(|id| HelperMapper::to_uuid(id.clone()).map(|uuid| *uuid.as_bytes())) + .transpose(); + + asset_id.map(|asset_id| (input, asset_id)) }) - .collect::>(); + .collect::, _>>()?; for (new_asset, with_asset_id) in add_assets { - ASSET_SERVICE.create(new_asset, Some(with_asset_id))?; + ASSET_SERVICE.create(new_asset, with_asset_id)?; } Ok(()) @@ -1048,53 +1053,7 @@ mod init_canister_sync_handlers { }) .collect::)>>(); - // - // In case there are assets existing in the Asset repository at the time of recovering the assets - // some of the assets might not be able to be recreated, in this case we try to find the same asset - // in the existing assets and replace the asset_id in the recreated account with the existing one. - // - for (mut new_account, with_account_id) in add_accounts { - let mut new_account_assets = new_account.assets.clone(); - for asset_id in new_account.assets.iter() { - if ASSET_REPOSITORY.get(asset_id).is_none() { - // the asset could not be recreated, try to find the same asset in the existing assets - let asset_id_str = Uuid::from_bytes(*asset_id).hyphenated().to_string(); - let Some(original_asset_to_create) = initial_assets - .iter() - .find(|initial_asset| initial_asset.id == asset_id_str) - else { - // the asset does not exist and it could not be recreated, skip - continue; - }; - - if let Some(existing_asset_id) = ASSET_REPOSITORY.exists_unique( - &original_asset_to_create.blockchain, - &original_asset_to_create.symbol, - ) { - // replace the asset_id in the recreated account with the existing one - new_account_assets.retain(|id| asset_id != id); - new_account_assets.push(existing_asset_id); - - print(format!( - "Asset {} could not be recreated, replaced with existing asset {}", - asset_id_str, - Uuid::from_bytes(existing_asset_id).hyphenated() - )); - } else { - // the asset does not exist and it could not be recreated, skip - - print(format!( - "Asset {} could not be recreated and does not exist in the existing assets, skipping", - asset_id_str - )); - - continue; - } - } - } - - new_account.assets = new_account_assets; - + for (new_account, with_account_id) in add_accounts { ACCOUNT_SERVICE .create_account(new_account, with_account_id) .await @@ -1340,19 +1299,19 @@ mod tests { let initial_named_rules = vec![ InitNamedRuleInput { name: "NamedRule3".to_string(), - id: id_1.clone(), + id: Some(id_1.clone()), description: None, rule: station_api::RequestPolicyRuleDTO::NamedRule(id_2.clone()), }, InitNamedRuleInput { name: "NamedRule2".to_string(), - id: id_2.clone(), + id: Some(id_2.clone()), description: None, rule: station_api::RequestPolicyRuleDTO::NamedRule(id_3.clone()), }, InitNamedRuleInput { name: "NamedRule1".to_string(), - id: id_3.clone(), + id: Some(id_3.clone()), description: None, rule: station_api::RequestPolicyRuleDTO::AutoApproved, }, @@ -1370,19 +1329,19 @@ mod tests { let initial_named_rules = vec![ InitNamedRuleInput { name: "NamedRule3".to_string(), - id: id_1.clone(), + id: Some(id_1.clone()), description: None, rule: station_api::RequestPolicyRuleDTO::NamedRule(id_2.clone()), }, InitNamedRuleInput { name: "NamedRule2".to_string(), - id: id_2.clone(), + id: Some(id_2.clone()), description: None, rule: station_api::RequestPolicyRuleDTO::NamedRule(id_3.clone()), }, InitNamedRuleInput { name: "NamedRule1".to_string(), - id: id_3.clone(), + id: Some(id_3.clone()), description: None, rule: station_api::RequestPolicyRuleDTO::NamedRule(id_1.clone()), }, @@ -1401,7 +1360,7 @@ mod tests { // unknown key throws an error let initial_named_rules = vec![InitNamedRuleInput { name: "NamedRule3".to_string(), - id: id_1.clone(), + id: Some(id_1.clone()), description: None, rule: station_api::RequestPolicyRuleDTO::NamedRule(id_2.clone()), }]; @@ -1419,13 +1378,13 @@ mod tests { set_initial_named_rules(&[ InitNamedRuleInput { name: "NamedRule1".to_string(), - id: named_rule_id.clone(), + id: Some(named_rule_id.clone()), description: None, rule: station_api::RequestPolicyRuleDTO::AutoApproved, }, InitNamedRuleInput { name: "NamedRule2".to_string(), - id: named_rule_id.clone(), + id: Some(named_rule_id.clone()), description: None, rule: station_api::RequestPolicyRuleDTO::AutoApproved, }, @@ -1453,11 +1412,11 @@ mod tests { set_initial_user_groups(&[ InitUserGroupInput { name: "UserGroup1".to_string(), - id: user_group_id.clone(), + id: Some(user_group_id.clone()), }, InitUserGroupInput { name: "UserGroup2".to_string(), - id: user_group_id.clone(), + id: Some(user_group_id.clone()), }, ]) .await @@ -1467,7 +1426,7 @@ mod tests { let asset_id = Uuid::new_v4().hyphenated().to_string(); set_initial_assets(&[ InitAssetInput { - id: asset_id.clone(), + id: Some(asset_id.clone()), name: "Asset1".to_string(), blockchain: "icp".to_string(), standards: vec!["icrc1".to_string()], @@ -1476,7 +1435,7 @@ mod tests { decimals: 8, }, InitAssetInput { - id: asset_id.clone(), + id: Some(asset_id.clone()), name: "Asset2".to_string(), blockchain: "icp".to_string(), standards: vec!["icrc1".to_string()], diff --git a/tests/integration/src/disaster_recovery_tests.rs b/tests/integration/src/disaster_recovery_tests.rs index ed5b807ac..364224e25 100644 --- a/tests/integration/src/disaster_recovery_tests.rs +++ b/tests/integration/src/disaster_recovery_tests.rs @@ -465,7 +465,7 @@ fn test_disaster_recovery_flow_recreates_same_accounts() { } let init_assets_input = station_api::InitAssetInput { - id: icp_asset.id.clone(), + id: Some(icp_asset.id.clone()), name: icp_asset.name.clone(), symbol: icp_asset.symbol.clone(), decimals: icp_asset.decimals, diff --git a/tests/integration/src/install_tests.rs b/tests/integration/src/install_tests.rs index a4ce9dc40..80b61ba96 100644 --- a/tests/integration/src/install_tests.rs +++ b/tests/integration/src/install_tests.rs @@ -87,7 +87,7 @@ fn install_with_default_policies() { let assets = vec![ station_api::InitAssetInput { name: "test-asset-1".to_string(), - id: asset_1_id.clone(), + id: Some(asset_1_id.clone()), blockchain: "icp".to_string(), standards: vec!["icp_native".to_owned()], metadata: vec![], @@ -96,7 +96,7 @@ fn install_with_default_policies() { }, station_api::InitAssetInput { name: "test-asset-2".to_string(), - id: asset_2_id.clone(), + id: Some(asset_2_id.clone()), blockchain: "icp".to_string(), standards: vec!["icp_native".to_owned()], metadata: vec![], @@ -311,7 +311,7 @@ fn install_with_all_entries() { let assets = vec![ station_api::InitAssetInput { name: "test-asset-1".to_string(), - id: asset_1_id.clone(), + id: Some(asset_1_id.clone()), blockchain: "icp".to_string(), standards: vec!["icp_native".to_owned()], metadata: vec![], @@ -320,7 +320,7 @@ fn install_with_all_entries() { }, station_api::InitAssetInput { name: "test-asset-2".to_string(), - id: asset_2_id.clone(), + id: Some(asset_2_id.clone()), blockchain: "icp".to_string(), standards: vec!["icp_native".to_owned()], metadata: vec![], @@ -331,13 +331,13 @@ fn install_with_all_entries() { let named_rules = vec![ InitNamedRuleInput { - id: Uuid::new_v4().hyphenated().to_string(), + id: None, name: "custom-named-rule-with-dependency".to_string(), description: None, rule: RequestPolicyRuleDTO::NamedRule(named_rule_dependent_id.clone()), }, InitNamedRuleInput { - id: named_rule_dependent_id.clone(), + id: Some(named_rule_dependent_id.clone()), name: "custom-named-rule".to_string(), description: None, rule: RequestPolicyRuleDTO::AutoApproved, @@ -345,7 +345,7 @@ fn install_with_all_entries() { ]; let user_groups = vec![InitUserGroupInput { - id: custom_user_group_id.clone(), + id: Some(custom_user_group_id.clone()), name: "custom-user-group".to_string(), }]; @@ -535,20 +535,20 @@ fn install_with_all_defaults() { request_policies: vec![], user_groups: vec![ InitUserGroupInput { - id: ADMIN_GROUP_ID.hyphenated().to_string(), + id: Some(ADMIN_GROUP_ID.hyphenated().to_string()), name: "admin".to_string(), }, ], named_rules: vec![ // circular reference InitNamedRuleInput { - id: id_1.clone(), + id: Some(id_1.clone()), name: "named_rule".to_string(), description: None, rule: RequestPolicyRuleDTO::NamedRule(id_2.clone()), }, InitNamedRuleInput { - id: id_2.clone(), + id: Some(id_2.clone()), name: "named_rule_2".to_string(), description: None, rule: RequestPolicyRuleDTO::NamedRule(id_1.clone()), @@ -588,7 +588,7 @@ fn install_with_all_defaults() { }], user_groups: vec![ InitUserGroupInput { - id: ADMIN_GROUP_ID.hyphenated().to_string(), + id: Some(ADMIN_GROUP_ID.hyphenated().to_string()), name: "admin".to_string(), }, ], @@ -831,10 +831,14 @@ fn assert_initial_assets( let asset = listed_assets .assets .iter() - .find(|asset| asset.id == expected_asset.id) - .ok_or(format!("asset {} not found", expected_asset.id))?; + .find(|asset| asset.name == expected_asset.name) + .ok_or(format!("asset {} not found", expected_asset.name))?; - if asset.id != expected_asset.id + if expected_asset + .id + .as_ref() + .map(|id| id != &asset.id) + .unwrap_or(false) || asset.name != expected_asset.name || asset.blockchain != expected_asset.blockchain || asset.standards != expected_asset.standards From 5a8f697e24bb48d903cc51bc7c9ed43bf18f28a5 Mon Sep 17 00:00:00 2001 From: olaszakos Date: Tue, 11 Mar 2025 15:04:46 +0100 Subject: [PATCH 24/33] fail on bad group uuid; improve uuid map errors --- core/station/impl/src/services/system.rs | 78 +++++++++++++++--------- 1 file changed, 49 insertions(+), 29 deletions(-) diff --git a/core/station/impl/src/services/system.rs b/core/station/impl/src/services/system.rs index 235b2b352..50396401c 100644 --- a/core/station/impl/src/services/system.rs +++ b/core/station/impl/src/services/system.rs @@ -896,6 +896,18 @@ mod init_canister_sync_handlers { .map(|id_str| HelperMapper::to_uuid(id_str).map(|uuid| *uuid.as_bytes())) .transpose()?; + let groups = user + .groups + .map(|ids| { + ids.into_iter() + .map(|id| { + HelperMapper::to_uuid(id.clone()).map(|uuid| uuid.as_bytes().to_owned()) + }) + .collect::, _>>() + }) + .transpose()? + .unwrap_or_else(|| default_groups.to_vec()); + let user = USER_SERVICE.add_user_with_id( AddUserOperationInput { identities: user @@ -903,18 +915,7 @@ mod init_canister_sync_handlers { .iter() .map(|identity| identity.identity.to_owned()) .collect(), - groups: user - .groups - .map(|ids| { - ids.into_iter() - .map(|id| { - HelperMapper::to_uuid(id.clone()) - .map(|uuid| uuid.as_bytes().to_owned()) - }) - .collect::, _>>() - .unwrap_or_else(|_| default_groups.to_vec()) - }) - .unwrap_or_else(|| default_groups.to_vec()), + groups, name: user.name.to_owned(), status: user .status @@ -1025,17 +1026,17 @@ mod init_canister_sync_handlers { ) }); + let assets = account + .assets + .into_iter() + .map(|id| { + HelperMapper::to_uuid(id.clone()).map(|uuid| uuid.as_bytes().to_owned()) + }) + .collect::, _>>()?; + let input = AddAccountOperationInput { name: account.name, - assets: account - .assets - .into_iter() - .map(|asset| { - *HelperMapper::to_uuid(asset) - .expect("Invalid UUID") - .as_bytes() - }) - .collect(), + assets: vec![], metadata: account.metadata.into(), transfer_request_policy, configs_request_policy, @@ -1044,14 +1045,15 @@ mod init_canister_sync_handlers { transfer_permission, }; - ( - input, - account - .id - .map(|id| *HelperMapper::to_uuid(id).expect("Invalid UUID").as_bytes()), - ) + let account_id = account + .id + .map(|id| HelperMapper::to_uuid(id).map(|uuid| uuid.as_bytes().to_owned())) + .transpose()?; + + Ok((input, account_id)) }) - .collect::)>>(); + .collect::)>, ApiError>>() + .map_err(|e| format!("Invalid input: {:?}", e))?; for (new_account, with_account_id) in add_accounts { ACCOUNT_SERVICE @@ -1192,7 +1194,7 @@ mod tests { models::request_test_utils::mock_request, services::system::init_canister_sync_handlers::{ set_initial_accounts, set_initial_assets, set_initial_named_rules, - set_initial_request_policies, set_initial_user_groups, + set_initial_request_policies, set_initial_user_groups, set_initial_users, }, }; use candid::Principal; @@ -1477,4 +1479,22 @@ mod tests { .await .expect_err("Should have failed due to duplicate UUID in accounts"); } + + #[tokio::test] + async fn test_initial_users_with_bad_groups() { + let user_id = Uuid::new_v4().hyphenated().to_string(); + + let user = UserInitInput { + name: "User".to_string(), + identities: vec![UserIdentityInput { + identity: Principal::from_slice(&[1; 29]), + }], + id: Some(user_id.clone()), + groups: Some(vec!["abc".to_string()]), + status: None, + }; + + set_initial_users(vec![user], &[]) + .expect_err("Should have failed due to malformed group uuid"); + } } From 3eed09a21398e337d77f24a090356f0b86c9c125 Mon Sep 17 00:00:00 2001 From: olaszakos Date: Tue, 11 Mar 2025 15:05:06 +0100 Subject: [PATCH 25/33] run dfx generate --- apps/wallet/src/generated/station/station.did | 20 +++++++------- .../src/generated/station/station.did.d.ts | 10 +++---- .../src/generated/station/station.did.js | 26 ++++++++++++------- 3 files changed, 30 insertions(+), 26 deletions(-) diff --git a/apps/wallet/src/generated/station/station.did b/apps/wallet/src/generated/station/station.did index 3d9a3871c..ea8f1a0af 100644 --- a/apps/wallet/src/generated/station/station.did +++ b/apps/wallet/src/generated/station/station.did @@ -2766,11 +2766,10 @@ type InitAccountWithPermissionsInput = record { permissions : InitAccountPermissionsInput; }; - // The initial assets to create when initializing the canister for the first time, e.g., after disaster recovery. type InitAssetInput = record { // The UUID of the asset, if not provided a new UUID will be generated. - id : UUID; + id : opt UUID; // The name of the asset. name : text; // The blockchain identifier (e.g., `ethereum`, `bitcoin`, `icp`, etc.) @@ -2788,7 +2787,7 @@ type InitAssetInput = record { // The input type for creating a user group when initializing the canister for the first time. type InitUserGroupInput = record { // The id of the user group. - id : UUID; + id : opt UUID; // The name of the user group, must be unique. name : text; }; @@ -2808,12 +2807,13 @@ type InitUserInput = record { // The identities of the user. identities : vec UserIdentityInput; // The user groups to associate with the user (optional). - // If not provided it defaults to the `admin` group if default user groups are created. + // If not provided it defaults to the `admin` group if default user groups are created, + // i.e., when the field `entries` in `SystemInit` is `null` or has the form of `WithDefaultPolicies`. groups : opt vec UUID; // The status of the user (e.g. `Active`). // // If not provided the default status is `Active` when there is at least - // one identity ot `Inactive` otherwise. + // one identity or `Inactive` otherwise. status : opt UserStatus; }; @@ -2825,7 +2825,7 @@ type InitPermissionInput = record { allow : Allow; }; -// The init type for adding a request approval policies when initializing the canister for the first time. +// The init type for adding a request approval policy when initializing the canister for the first time. type InitRequestPolicyInput = record { // The id of the request policy, if not provided a new UUID will be generated. id : opt UUID; @@ -2838,7 +2838,7 @@ type InitRequestPolicyInput = record { // The init type for adding a named rule when initializing the canister for the first time. type InitNamedRuleInput = record { // The id of the named rule. - id : UUID; + id : opt UUID; // The name of the named rule. name : text; // The description of the named rule. @@ -2847,7 +2847,7 @@ type InitNamedRuleInput = record { rule : RequestPolicyRule; }; -type InitalEntries = variant { +type InitialEntries = variant { // Initialize the station with default policies, accounts and assets. WithDefaultPolicies : record { accounts : vec InitAccountInput; @@ -2876,11 +2876,9 @@ type SystemInit = record { // The quorum for the initial approval policy. quorum : opt nat16; // The initial entries to create. - entries: opt InitalEntries; + entries: opt InitialEntries; }; - - // The upgrade configuration for the canister. type SystemUpgrade = record { // The updated name of the station. diff --git a/apps/wallet/src/generated/station/station.did.d.ts b/apps/wallet/src/generated/station/station.did.d.ts index 1f19218e9..e88f56ba4 100644 --- a/apps/wallet/src/generated/station/station.did.d.ts +++ b/apps/wallet/src/generated/station/station.did.d.ts @@ -742,7 +742,7 @@ export interface InitAccountWithPermissionsInput { 'account_init' : InitAccountInput, } export interface InitAssetInput { - 'id' : UUID, + 'id' : [] | [UUID], 'decimals' : number, 'standards' : Array, 'metadata' : Array, @@ -751,7 +751,7 @@ export interface InitAssetInput { 'symbol' : string, } export interface InitNamedRuleInput { - 'id' : UUID, + 'id' : [] | [UUID], 'name' : string, 'rule' : RequestPolicyRule, 'description' : [] | [string], @@ -762,7 +762,7 @@ export interface InitRequestPolicyInput { 'rule' : RequestPolicyRule, 'specifier' : RequestSpecifier, } -export interface InitUserGroupInput { 'id' : UUID, 'name' : string } +export interface InitUserGroupInput { 'id' : [] | [UUID], 'name' : string } export interface InitUserInput { 'id' : [] | [UUID], 'status' : [] | [UserStatus], @@ -770,7 +770,7 @@ export interface InitUserInput { 'name' : string, 'identities' : Array, } -export type InitalEntries = { +export type InitialEntries = { 'WithDefaultPolicies' : { 'assets' : Array, 'accounts' : Array, @@ -1433,7 +1433,7 @@ export interface SystemInit { 'name' : string, 'fallback_controller' : [] | [Principal], 'upgrader' : SystemUpgraderInput, - 'entries' : [] | [InitalEntries], + 'entries' : [] | [InitialEntries], 'users' : Array, 'quorum' : [] | [number], } diff --git a/apps/wallet/src/generated/station/station.did.js b/apps/wallet/src/generated/station/station.did.js index e5836e32f..f932889f7 100644 --- a/apps/wallet/src/generated/station/station.did.js +++ b/apps/wallet/src/generated/station/station.did.js @@ -12,7 +12,7 @@ export const idlFactory = ({ IDL }) => { const UUID = IDL.Text; const AssetMetadata = IDL.Record({ 'key' : IDL.Text, 'value' : IDL.Text }); const InitAssetInput = IDL.Record({ - 'id' : UUID, + 'id' : IDL.Opt(UUID), 'decimals' : IDL.Nat32, 'standards' : IDL.Vec(IDL.Text), 'metadata' : IDL.Vec(AssetMetadata), @@ -194,7 +194,10 @@ export const idlFactory = ({ IDL }) => { 'rule' : RequestPolicyRule, 'specifier' : RequestSpecifier, }); - const InitUserGroupInput = IDL.Record({ 'id' : UUID, 'name' : IDL.Text }); + const InitUserGroupInput = IDL.Record({ + 'id' : IDL.Opt(UUID), + 'name' : IDL.Text, + }); const InitAccountPermissionsInput = IDL.Record({ 'configs_request_policy' : IDL.Opt(RequestPolicyRule), 'read_permission' : Allow, @@ -207,12 +210,12 @@ export const idlFactory = ({ IDL }) => { 'account_init' : InitAccountInput, }); const InitNamedRuleInput = IDL.Record({ - 'id' : UUID, + 'id' : IDL.Opt(UUID), 'name' : IDL.Text, 'rule' : RequestPolicyRule, 'description' : IDL.Opt(IDL.Text), }); - const InitalEntries = IDL.Variant({ + const InitialEntries = IDL.Variant({ 'WithDefaultPolicies' : IDL.Record({ 'assets' : IDL.Vec(InitAssetInput), 'accounts' : IDL.Vec(InitAccountInput), @@ -242,7 +245,7 @@ export const idlFactory = ({ IDL }) => { 'name' : IDL.Text, 'fallback_controller' : IDL.Opt(IDL.Principal), 'upgrader' : SystemUpgraderInput, - 'entries' : IDL.Opt(InitalEntries), + 'entries' : IDL.Opt(InitialEntries), 'users' : IDL.Vec(InitUserInput), 'quorum' : IDL.Opt(IDL.Nat16), }); @@ -1871,7 +1874,7 @@ export const init = ({ IDL }) => { const UUID = IDL.Text; const AssetMetadata = IDL.Record({ 'key' : IDL.Text, 'value' : IDL.Text }); const InitAssetInput = IDL.Record({ - 'id' : UUID, + 'id' : IDL.Opt(UUID), 'decimals' : IDL.Nat32, 'standards' : IDL.Vec(IDL.Text), 'metadata' : IDL.Vec(AssetMetadata), @@ -2053,7 +2056,10 @@ export const init = ({ IDL }) => { 'rule' : RequestPolicyRule, 'specifier' : RequestSpecifier, }); - const InitUserGroupInput = IDL.Record({ 'id' : UUID, 'name' : IDL.Text }); + const InitUserGroupInput = IDL.Record({ + 'id' : IDL.Opt(UUID), + 'name' : IDL.Text, + }); const InitAccountPermissionsInput = IDL.Record({ 'configs_request_policy' : IDL.Opt(RequestPolicyRule), 'read_permission' : Allow, @@ -2066,12 +2072,12 @@ export const init = ({ IDL }) => { 'account_init' : InitAccountInput, }); const InitNamedRuleInput = IDL.Record({ - 'id' : UUID, + 'id' : IDL.Opt(UUID), 'name' : IDL.Text, 'rule' : RequestPolicyRule, 'description' : IDL.Opt(IDL.Text), }); - const InitalEntries = IDL.Variant({ + const InitialEntries = IDL.Variant({ 'WithDefaultPolicies' : IDL.Record({ 'assets' : IDL.Vec(InitAssetInput), 'accounts' : IDL.Vec(InitAccountInput), @@ -2101,7 +2107,7 @@ export const init = ({ IDL }) => { 'name' : IDL.Text, 'fallback_controller' : IDL.Opt(IDL.Principal), 'upgrader' : SystemUpgraderInput, - 'entries' : IDL.Opt(InitalEntries), + 'entries' : IDL.Opt(InitialEntries), 'users' : IDL.Vec(InitUserInput), 'quorum' : IDL.Opt(IDL.Nat16), }); From 51fb2e347588bcd968e0a221618bb9e53edbfa7a Mon Sep 17 00:00:00 2001 From: olaszakos Date: Tue, 11 Mar 2025 15:18:51 +0100 Subject: [PATCH 26/33] fix lint --- core/station/impl/src/models/request_policy_rule.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/station/impl/src/models/request_policy_rule.rs b/core/station/impl/src/models/request_policy_rule.rs index 0afc6293c..95fbed8be 100644 --- a/core/station/impl/src/models/request_policy_rule.rs +++ b/core/station/impl/src/models/request_policy_rule.rs @@ -54,7 +54,7 @@ impl RequestPolicyRule { RequestPolicyRule::Not(rule) => rule.has_named_rule_id(named_rule_id), RequestPolicyRule::AutoApproved | RequestPolicyRule::QuorumPercentage(..) - | RequestPolicyRule::Quorum(.., _) + | RequestPolicyRule::Quorum(..) | RequestPolicyRule::AllowListedByMetadata(..) | RequestPolicyRule::AllowListed => false, } From a87be32653db8e0c1586591a2b182dc801c25663 Mon Sep 17 00:00:00 2001 From: olaszakos Date: Tue, 11 Mar 2025 16:02:29 +0100 Subject: [PATCH 27/33] keep named rule and policy creation order in sorting, if possible --- core/station/impl/src/services/system.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/station/impl/src/services/system.rs b/core/station/impl/src/services/system.rs index 50396401c..9a53c76fa 100644 --- a/core/station/impl/src/services/system.rs +++ b/core/station/impl/src/services/system.rs @@ -725,7 +725,7 @@ mod init_canister_sync_handlers { return Ordering::Less; } } - Ordering::Greater + Ordering::Equal }); for (new_named_rule, with_named_rule_id) in add_named_rules { @@ -835,7 +835,7 @@ mod init_canister_sync_handlers { return Ordering::Less; } } - Ordering::Greater + Ordering::Equal }); for (input, request_policy_id) in add_request_policies { From 4087b3095581ea2bc37be61b1aab5315c13762ca Mon Sep 17 00:00:00 2001 From: olaszakos Date: Wed, 12 Mar 2025 14:24:08 +0100 Subject: [PATCH 28/33] refactor InitialEntries --- apps/wallet/src/generated/station/station.did | 39 ++- .../src/generated/station/station.did.d.ts | 18 +- .../src/generated/station/station.did.js | 92 ++++-- .../control-panel/impl/src/services/deploy.rs | 8 +- core/station/api/spec.did | 39 ++- core/station/api/src/system.rs | 44 ++- core/station/impl/src/core/init.rs | 13 +- core/station/impl/src/services/system.rs | 308 +++++++++--------- tests/integration/src/control_panel_tests.rs | 22 +- .../src/disaster_recovery_tests.rs | 98 +++--- tests/integration/src/install_tests.rs | 110 +++++-- tests/integration/src/setup.rs | 25 +- 12 files changed, 490 insertions(+), 326 deletions(-) diff --git a/apps/wallet/src/generated/station/station.did b/apps/wallet/src/generated/station/station.did index ea8f1a0af..37aeabe94 100644 --- a/apps/wallet/src/generated/station/station.did +++ b/apps/wallet/src/generated/station/station.did @@ -2847,20 +2847,49 @@ type InitNamedRuleInput = record { rule : RequestPolicyRule; }; -type InitialEntries = variant { +// The initial configuration for the station. +type InitialConfig = variant { + // Initialize the station with default policies, permissions, and assets. + // This does not create an initial account. + WithAllDefaults : record { + // The initial users to create. + users : vec InitUserInput; + // The initial admin quorum in the admin level approval rule. + admin_quorum : nat16; + // The initial operator quorum in the operator level approval rule. + operator_quorum : nat16; + }; // Initialize the station with default policies, accounts and assets. WithDefaultPolicies : record { + // The initial users to create. + users : vec InitUserInput; + // The initial accounts to create. accounts : vec InitAccountInput; + // The initial assets to create. assets : vec InitAssetInput; + // The initial admin quorum in the admin level approval rule. + admin_quorum : nat16; + // The initial operator quorum in the operator level approval rule. + operator_quorum : nat16; }; // Initialize the station with all custom entries. Complete : record { + // The initial users to create. + users : vec InitUserInput; + // The initial user groups to create. user_groups : vec InitUserGroupInput; + // The initial permissions to create. permissions : vec InitPermissionInput; + // The initial request policies to create. request_policies : vec InitRequestPolicyInput; + // The initial named rules to create. named_rules : vec InitNamedRuleInput; + // The initial accounts to create. accounts : vec InitAccountWithPermissionsInput; + // The initial assets to create. assets : vec InitAssetInput; + // The initial disaster recovery committee to create. + disaster_recovery_committee : opt DisasterRecoveryCommittee; }; }; @@ -2871,12 +2900,8 @@ type SystemInit = record { upgrader : SystemUpgraderInput; // An additional controller of the station and upgrader canisters (optional). fallback_controller : opt principal; - // The initial users to create. - users : vec InitUserInput; - // The quorum for the initial approval policy. - quorum : opt nat16; - // The initial entries to create. - entries: opt InitialEntries; + // The initial configuration to apply. + entries: InitialConfig; }; // The upgrade configuration for the canister. diff --git a/apps/wallet/src/generated/station/station.did.d.ts b/apps/wallet/src/generated/station/station.did.d.ts index e88f56ba4..d59934cb4 100644 --- a/apps/wallet/src/generated/station/station.did.d.ts +++ b/apps/wallet/src/generated/station/station.did.d.ts @@ -770,10 +770,20 @@ export interface InitUserInput { 'name' : string, 'identities' : Array, } -export type InitialEntries = { +export type InitialConfig = { 'WithDefaultPolicies' : { 'assets' : Array, + 'admin_quorum' : number, 'accounts' : Array, + 'users' : Array, + 'operator_quorum' : number, + } + } | + { + 'WithAllDefaults' : { + 'admin_quorum' : number, + 'users' : Array, + 'operator_quorum' : number, } } | { @@ -783,6 +793,8 @@ export type InitialEntries = { 'request_policies' : Array, 'user_groups' : Array, 'accounts' : Array, + 'disaster_recovery_committee' : [] | [DisasterRecoveryCommittee], + 'users' : Array, 'named_rules' : Array, } }; @@ -1433,9 +1445,7 @@ export interface SystemInit { 'name' : string, 'fallback_controller' : [] | [Principal], 'upgrader' : SystemUpgraderInput, - 'entries' : [] | [InitialEntries], - 'users' : Array, - 'quorum' : [] | [number], + 'entries' : InitialConfig, } export type SystemInstall = { 'Upgrade' : SystemUpgrade } | { 'Init' : SystemInit }; diff --git a/apps/wallet/src/generated/station/station.did.js b/apps/wallet/src/generated/station/station.did.js index f932889f7..0b045df1f 100644 --- a/apps/wallet/src/generated/station/station.did.js +++ b/apps/wallet/src/generated/station/station.did.js @@ -29,6 +29,18 @@ export const idlFactory = ({ IDL }) => { 'assets' : IDL.Vec(UUID), 'seed' : AccountSeed, }); + const UserStatus = IDL.Variant({ + 'Inactive' : IDL.Null, + 'Active' : IDL.Null, + }); + const UserIdentityInput = IDL.Record({ 'identity' : IDL.Principal }); + const InitUserInput = IDL.Record({ + 'id' : IDL.Opt(UUID), + 'status' : IDL.Opt(UserStatus), + 'groups' : IDL.Opt(IDL.Vec(UUID)), + 'name' : IDL.Text, + 'identities' : IDL.Vec(UserIdentityInput), + }); const ResourceId = IDL.Variant({ 'Id' : UUID, 'Any' : IDL.Null }); const RequestResourceAction = IDL.Variant({ 'List' : IDL.Null, @@ -209,16 +221,28 @@ export const idlFactory = ({ IDL }) => { 'permissions' : InitAccountPermissionsInput, 'account_init' : InitAccountInput, }); + const DisasterRecoveryCommittee = IDL.Record({ + 'user_group_id' : UUID, + 'quorum' : IDL.Nat16, + }); const InitNamedRuleInput = IDL.Record({ 'id' : IDL.Opt(UUID), 'name' : IDL.Text, 'rule' : RequestPolicyRule, 'description' : IDL.Opt(IDL.Text), }); - const InitialEntries = IDL.Variant({ + const InitialConfig = IDL.Variant({ 'WithDefaultPolicies' : IDL.Record({ 'assets' : IDL.Vec(InitAssetInput), + 'admin_quorum' : IDL.Nat16, 'accounts' : IDL.Vec(InitAccountInput), + 'users' : IDL.Vec(InitUserInput), + 'operator_quorum' : IDL.Nat16, + }), + 'WithAllDefaults' : IDL.Record({ + 'admin_quorum' : IDL.Nat16, + 'users' : IDL.Vec(InitUserInput), + 'operator_quorum' : IDL.Nat16, }), 'Complete' : IDL.Record({ 'permissions' : IDL.Vec(InitPermissionInput), @@ -226,28 +250,16 @@ export const idlFactory = ({ IDL }) => { 'request_policies' : IDL.Vec(InitRequestPolicyInput), 'user_groups' : IDL.Vec(InitUserGroupInput), 'accounts' : IDL.Vec(InitAccountWithPermissionsInput), + 'disaster_recovery_committee' : IDL.Opt(DisasterRecoveryCommittee), + 'users' : IDL.Vec(InitUserInput), 'named_rules' : IDL.Vec(InitNamedRuleInput), }), }); - const UserStatus = IDL.Variant({ - 'Inactive' : IDL.Null, - 'Active' : IDL.Null, - }); - const UserIdentityInput = IDL.Record({ 'identity' : IDL.Principal }); - const InitUserInput = IDL.Record({ - 'id' : IDL.Opt(UUID), - 'status' : IDL.Opt(UserStatus), - 'groups' : IDL.Opt(IDL.Vec(UUID)), - 'name' : IDL.Text, - 'identities' : IDL.Vec(UserIdentityInput), - }); const SystemInit = IDL.Record({ 'name' : IDL.Text, 'fallback_controller' : IDL.Opt(IDL.Principal), 'upgrader' : SystemUpgraderInput, - 'entries' : IDL.Opt(InitialEntries), - 'users' : IDL.Vec(InitUserInput), - 'quorum' : IDL.Opt(IDL.Nat16), + 'entries' : InitialConfig, }); const SystemInstall = IDL.Variant({ 'Upgrade' : SystemUpgrade, @@ -522,10 +534,6 @@ export const idlFactory = ({ IDL }) => { const EditUserGroupOperation = IDL.Record({ 'input' : EditUserGroupOperationInput, }); - const DisasterRecoveryCommittee = IDL.Record({ - 'user_group_id' : UUID, - 'quorum' : IDL.Nat16, - }); const SetDisasterRecoveryOperation = IDL.Record({ 'committee' : IDL.Opt(DisasterRecoveryCommittee), }); @@ -1891,6 +1899,18 @@ export const init = ({ IDL }) => { 'assets' : IDL.Vec(UUID), 'seed' : AccountSeed, }); + const UserStatus = IDL.Variant({ + 'Inactive' : IDL.Null, + 'Active' : IDL.Null, + }); + const UserIdentityInput = IDL.Record({ 'identity' : IDL.Principal }); + const InitUserInput = IDL.Record({ + 'id' : IDL.Opt(UUID), + 'status' : IDL.Opt(UserStatus), + 'groups' : IDL.Opt(IDL.Vec(UUID)), + 'name' : IDL.Text, + 'identities' : IDL.Vec(UserIdentityInput), + }); const ResourceId = IDL.Variant({ 'Id' : UUID, 'Any' : IDL.Null }); const RequestResourceAction = IDL.Variant({ 'List' : IDL.Null, @@ -2071,16 +2091,28 @@ export const init = ({ IDL }) => { 'permissions' : InitAccountPermissionsInput, 'account_init' : InitAccountInput, }); + const DisasterRecoveryCommittee = IDL.Record({ + 'user_group_id' : UUID, + 'quorum' : IDL.Nat16, + }); const InitNamedRuleInput = IDL.Record({ 'id' : IDL.Opt(UUID), 'name' : IDL.Text, 'rule' : RequestPolicyRule, 'description' : IDL.Opt(IDL.Text), }); - const InitialEntries = IDL.Variant({ + const InitialConfig = IDL.Variant({ 'WithDefaultPolicies' : IDL.Record({ 'assets' : IDL.Vec(InitAssetInput), + 'admin_quorum' : IDL.Nat16, 'accounts' : IDL.Vec(InitAccountInput), + 'users' : IDL.Vec(InitUserInput), + 'operator_quorum' : IDL.Nat16, + }), + 'WithAllDefaults' : IDL.Record({ + 'admin_quorum' : IDL.Nat16, + 'users' : IDL.Vec(InitUserInput), + 'operator_quorum' : IDL.Nat16, }), 'Complete' : IDL.Record({ 'permissions' : IDL.Vec(InitPermissionInput), @@ -2088,28 +2120,16 @@ export const init = ({ IDL }) => { 'request_policies' : IDL.Vec(InitRequestPolicyInput), 'user_groups' : IDL.Vec(InitUserGroupInput), 'accounts' : IDL.Vec(InitAccountWithPermissionsInput), + 'disaster_recovery_committee' : IDL.Opt(DisasterRecoveryCommittee), + 'users' : IDL.Vec(InitUserInput), 'named_rules' : IDL.Vec(InitNamedRuleInput), }), }); - const UserStatus = IDL.Variant({ - 'Inactive' : IDL.Null, - 'Active' : IDL.Null, - }); - const UserIdentityInput = IDL.Record({ 'identity' : IDL.Principal }); - const InitUserInput = IDL.Record({ - 'id' : IDL.Opt(UUID), - 'status' : IDL.Opt(UserStatus), - 'groups' : IDL.Opt(IDL.Vec(UUID)), - 'name' : IDL.Text, - 'identities' : IDL.Vec(UserIdentityInput), - }); const SystemInit = IDL.Record({ 'name' : IDL.Text, 'fallback_controller' : IDL.Opt(IDL.Principal), 'upgrader' : SystemUpgraderInput, - 'entries' : IDL.Opt(InitialEntries), - 'users' : IDL.Vec(InitUserInput), - 'quorum' : IDL.Opt(IDL.Nat16), + 'entries' : InitialConfig, }); const SystemInstall = IDL.Variant({ 'Upgrade' : SystemUpgrade, diff --git a/core/control-panel/impl/src/services/deploy.rs b/core/control-panel/impl/src/services/deploy.rs index 44bae0cf9..34fdc0a0f 100644 --- a/core/control-panel/impl/src/services/deploy.rs +++ b/core/control-panel/impl/src/services/deploy.rs @@ -143,9 +143,11 @@ impl DeployService { } ), fallback_controller: Some(NNS_ROOT_CANISTER_ID), - entries: None, - users: intial_users, - quorum: Some(1), + initial_config: station_api::InitialConfig::WithAllDefaults { + users: intial_users, + admin_quorum: 1, + operator_quorum: 1, + }, })) .map_err(|err| DeployError::Failed { reason: err.to_string(), diff --git a/core/station/api/spec.did b/core/station/api/spec.did index ea8f1a0af..37aeabe94 100644 --- a/core/station/api/spec.did +++ b/core/station/api/spec.did @@ -2847,20 +2847,49 @@ type InitNamedRuleInput = record { rule : RequestPolicyRule; }; -type InitialEntries = variant { +// The initial configuration for the station. +type InitialConfig = variant { + // Initialize the station with default policies, permissions, and assets. + // This does not create an initial account. + WithAllDefaults : record { + // The initial users to create. + users : vec InitUserInput; + // The initial admin quorum in the admin level approval rule. + admin_quorum : nat16; + // The initial operator quorum in the operator level approval rule. + operator_quorum : nat16; + }; // Initialize the station with default policies, accounts and assets. WithDefaultPolicies : record { + // The initial users to create. + users : vec InitUserInput; + // The initial accounts to create. accounts : vec InitAccountInput; + // The initial assets to create. assets : vec InitAssetInput; + // The initial admin quorum in the admin level approval rule. + admin_quorum : nat16; + // The initial operator quorum in the operator level approval rule. + operator_quorum : nat16; }; // Initialize the station with all custom entries. Complete : record { + // The initial users to create. + users : vec InitUserInput; + // The initial user groups to create. user_groups : vec InitUserGroupInput; + // The initial permissions to create. permissions : vec InitPermissionInput; + // The initial request policies to create. request_policies : vec InitRequestPolicyInput; + // The initial named rules to create. named_rules : vec InitNamedRuleInput; + // The initial accounts to create. accounts : vec InitAccountWithPermissionsInput; + // The initial assets to create. assets : vec InitAssetInput; + // The initial disaster recovery committee to create. + disaster_recovery_committee : opt DisasterRecoveryCommittee; }; }; @@ -2871,12 +2900,8 @@ type SystemInit = record { upgrader : SystemUpgraderInput; // An additional controller of the station and upgrader canisters (optional). fallback_controller : opt principal; - // The initial users to create. - users : vec InitUserInput; - // The quorum for the initial approval policy. - quorum : opt nat16; - // The initial entries to create. - entries: opt InitialEntries; + // The initial configuration to apply. + entries: InitialConfig; }; // The upgrade configuration for the canister. diff --git a/core/station/api/src/system.rs b/core/station/api/src/system.rs index f2f6a4cb8..c15c22168 100644 --- a/core/station/api/src/system.rs +++ b/core/station/api/src/system.rs @@ -151,18 +151,46 @@ pub struct InitAssetInput { } #[derive(CandidType, serde::Serialize, Deserialize, Clone, Debug)] -pub enum InitialEntries { +pub enum InitialConfig { + WithAllDefaults { + /// The initial users to create. + users: Vec, + /// The initial admin quorum in the admin level approval rule. + admin_quorum: u16, + /// The initial operator quorum in the operator level approval rule. + operator_quorum: u16, + }, + /// Initialize the station with default policies, accounts and assets. WithDefaultPolicies { - assets: Vec, + /// The initial users to create. + users: Vec, + /// The initial accounts to create. accounts: Vec, + /// The initial assets to create. + assets: Vec, + /// The initial admin quorum in the admin level approval rule. + admin_quorum: u16, + /// The initial operator quorum in the operator level approval rule. + operator_quorum: u16, }, + /// Initialize the station with all custom entries. Complete { + /// The initial users to create. + users: Vec, + /// The initial user groups to create. + user_groups: Vec, + /// The initial permissions to create. permissions: Vec, - assets: Vec, + /// The initial request policies to create. request_policies: Vec, - user_groups: Vec, - accounts: Vec, + /// The initial named rules to create. named_rules: Vec, + /// The initial accounts to create. + accounts: Vec, + /// The initial assets to create. + assets: Vec, + /// The initial disaster recovery committee to create. + disaster_recovery_committee: Option, }, } @@ -174,12 +202,8 @@ pub struct SystemInit { pub upgrader: SystemUpgraderInput, /// Optional fallback controller for the station and upgrader canisters. pub fallback_controller: Option, - /// The initial users. - pub users: Vec, - /// The initial quorum. - pub quorum: Option, /// The initial database entries. - pub entries: Option, + pub initial_config: InitialConfig, } #[derive(CandidType, serde::Serialize, Deserialize, Clone, Debug)] diff --git a/core/station/impl/src/core/init.rs b/core/station/impl/src/core/init.rs index 8f489e00d..677cb3923 100644 --- a/core/station/impl/src/core/init.rs +++ b/core/station/impl/src/core/init.rs @@ -212,20 +212,21 @@ lazy_static! { } pub fn get_default_named_rules( - quorum: u16, + admin_quorum: u16, + operator_quorum: u16, ) -> ((String, RequestPolicyRule), (String, RequestPolicyRule)) { ( + ( + "Admin approval".to_string(), + RequestPolicyRule::Quorum(UserSpecifier::Group(vec![*ADMIN_GROUP_ID]), admin_quorum), + ), ( "Operator approval".to_string(), RequestPolicyRule::Quorum( UserSpecifier::Group(vec![*OPERATOR_GROUP_ID, *ADMIN_GROUP_ID]), - quorum, + operator_quorum, ), ), - ( - "Admin approval".to_string(), - RequestPolicyRule::Quorum(UserSpecifier::Group(vec![*ADMIN_GROUP_ID]), quorum), - ), ) } diff --git a/core/station/impl/src/services/system.rs b/core/station/impl/src/services/system.rs index 9a53c76fa..2ea936c4d 100644 --- a/core/station/impl/src/services/system.rs +++ b/core/station/impl/src/services/system.rs @@ -29,7 +29,7 @@ use candid::Principal; use lazy_static::lazy_static; use orbit_essentials::api::ServiceResult; use orbit_essentials::repository::Repository; -use station_api::{HealthStatus, InitialEntries, SystemInit, SystemInstall, SystemUpgrade}; +use station_api::{HealthStatus, InitialConfig, SystemInit, SystemInstall, SystemUpgrade}; use std::{ collections::{BTreeMap, BTreeSet}, sync::Arc, @@ -287,44 +287,69 @@ impl SystemService { install_canister_handlers::set_controllers(station_controllers).await?; // calculates the initial quorum based on the number of admins and the provided quorum - let initial_user_count = init.users.len() as u16; - let quorum = calc_initial_quorum(initial_user_count, init.quorum); - match init.entries { - Some(InitialEntries::WithDefaultPolicies { accounts, assets }) => { + match &init.entries { + InitialConfig::WithAllDefaults { .. } => {} + InitialConfig::WithDefaultPolicies { + accounts, + assets, + admin_quorum, + .. + } => { + let admin_group_id = Uuid::from_bytes(*ADMIN_GROUP_ID).hyphenated().to_string(); + + let policy = + station_api::RequestPolicyRuleDTO::Quorum(station_api::QuorumDTO { + approvers: station_api::UserSpecifierDTO::Group(vec![ + admin_group_id.clone() + ]), + min_approved: *admin_quorum, + }); + + let permission = station_api::AllowDTO { + user_groups: vec![admin_group_id.clone()], + auth_scope: station_api::AuthScopeDTO::Restricted, + users: vec![], + }; + + let default_permissions_policies = station_api::InitAccountPermissionsInput { + configs_request_policy: Some(policy.clone()), + transfer_request_policy: Some(policy.clone()), + configs_permission: permission.clone(), + transfer_permission: permission.clone(), + read_permission: permission.clone(), + }; + print("Adding initial accounts"); // initial accounts are added in the post process work timer, since they might do inter-canister calls init_canister_sync_handlers::set_initial_accounts( accounts - .into_iter() - .map(|account| (account, None)) + .iter() + .map(|account| (account.clone(), default_permissions_policies.clone())) .collect(), &assets, - quorum, ) .await?; } - Some(InitialEntries::Complete { + InitialConfig::Complete { accounts, assets, .. - }) => { + } => { print("Adding initial accounts"); // initial accounts are added in the post process work timer, since they might do inter-canister calls init_canister_sync_handlers::set_initial_accounts( accounts - .into_iter() + .iter() .map(|init_with_permissions| { ( - init_with_permissions.account_init, - Some(init_with_permissions.permissions), + init_with_permissions.account_init.clone(), + init_with_permissions.permissions.clone(), ) }) .collect(), &assets, - quorum, ) .await?; } - None => {} } if SYSTEM_SERVICE.is_healthy() { @@ -333,10 +358,25 @@ impl SystemService { install_canister_post_process_finish(system_info); - SystemService::set_disaster_recovery_committee(Some(DisasterRecoveryCommittee { - quorum, - user_group_id: *crate::models::ADMIN_GROUP_ID, - })); + match init.entries { + InitialConfig::WithAllDefaults { admin_quorum, .. } + | InitialConfig::WithDefaultPolicies { admin_quorum, .. } => { + SystemService::set_disaster_recovery_committee(Some( + DisasterRecoveryCommittee { + quorum: admin_quorum, + user_group_id: *crate::models::ADMIN_GROUP_ID, + }, + )); + } + InitialConfig::Complete { + disaster_recovery_committee, + .. + } => { + SystemService::set_disaster_recovery_committee( + disaster_recovery_committee.map(|committee| committee.into()), + ); + } + } crate::core::ic_cdk::spawn(async { DISASTER_RECOVERY_SERVICE.sync_all().await; @@ -406,47 +446,59 @@ impl SystemService { pub async fn init_canister(&self, input: SystemInit) -> ServiceResult<()> { let mut system_info = SystemInfo::default(); - if input.users.is_empty() { - return Err(SystemError::NoUsersSpecified)?; - } - - if input.users.len() > u16::MAX as usize { - return Err(SystemError::TooManyUsersSpecified { - max: u16::MAX as usize, - })?; - } - - let admin_quorum = calc_initial_quorum(input.users.len() as u16, input.quorum); - - match &input.entries { - Some(InitialEntries::WithDefaultPolicies { assets, .. }) => { + match &input.initial_config { + InitialConfig::WithAllDefaults { + admin_quorum, + operator_quorum, + users, + } => { // adds the default admin group init_canister_sync_handlers::add_default_groups(); // registers the admins of the canister - init_canister_sync_handlers::set_initial_users( - input.users.clone(), - &[*ADMIN_GROUP_ID], + init_canister_sync_handlers::set_initial_users(users.clone(), &[*ADMIN_GROUP_ID])?; + // registers the default canister configurations such as policies and user groups. + init_canister_sync_handlers::init_default_permissions_and_policies( + *admin_quorum, + *operator_quorum, )?; + // add default assets + init_canister_sync_handlers::add_default_assets(); + } + InitialConfig::WithDefaultPolicies { + assets, + users, + admin_quorum, + operator_quorum, + .. + } => { + // adds the default admin group + init_canister_sync_handlers::add_default_groups(); + // registers the admins of the canister + init_canister_sync_handlers::set_initial_users(users.clone(), &[*ADMIN_GROUP_ID])?; // adds the initial assets init_canister_sync_handlers::set_initial_assets(assets).await?; // registers the default canister configurations such as policies and user groups. - init_canister_sync_handlers::init_default_permissions_and_policies(admin_quorum)?; + init_canister_sync_handlers::init_default_permissions_and_policies( + *admin_quorum, + *operator_quorum, + )?; // initial accounts are added in the post process work timer, since they might do inter-canister calls } - Some(InitialEntries::Complete { + InitialConfig::Complete { + users, user_groups, permissions, request_policies, named_rules, assets, .. - }) => { + } => { print("adding initial user groups"); init_canister_sync_handlers::set_initial_user_groups(user_groups).await?; print("adding initial users"); - init_canister_sync_handlers::set_initial_users(input.users.clone(), &[])?; + init_canister_sync_handlers::set_initial_users(users.clone(), &[])?; print("adding initial named rules"); init_canister_sync_handlers::set_initial_named_rules(named_rules)?; print("adding initial permissions"); @@ -457,21 +509,6 @@ impl SystemService { init_canister_sync_handlers::set_initial_request_policies(request_policies)?; // accounts in post process timer } - None => { - // // adds the default admin group - init_canister_sync_handlers::add_default_groups(); - // registers the admins of the canister - init_canister_sync_handlers::set_initial_users( - input.users.clone(), - &[*ADMIN_GROUP_ID], - )?; - - // registers the default canister configurations such as policies and user groups. - init_canister_sync_handlers::init_default_permissions_and_policies(admin_quorum)?; - - // // add default assets - init_canister_sync_handlers::add_default_assets(); - } } // sets the name of the canister @@ -604,15 +641,15 @@ mod init_canister_sync_handlers { use crate::core::ic_cdk::{api::print, next_time}; use crate::core::init::{default_policies, get_default_named_rules, DEFAULT_PERMISSIONS}; + use crate::errors::SystemError; use crate::mappers::blockchain::BlockchainMapper; use crate::mappers::HelperMapper; - use crate::models::permission::Allow; - use crate::models::request_specifier::{RequestSpecifier, UserSpecifier}; + use crate::models::request_specifier::RequestSpecifier; use crate::models::resource::ResourceIds; use crate::models::{ AddAccountOperationInput, AddAssetOperationInput, AddNamedRuleOperationInput, AddRequestPolicyOperationInput, AddUserGroupOperationInput, AddUserOperationInput, Asset, - EditPermissionOperationInput, NamedRule, RequestPolicyRule, UserStatus, OPERATOR_GROUP_ID, + EditPermissionOperationInput, NamedRule, UserStatus, OPERATOR_GROUP_ID, }; use crate::repositories::{ASSET_REPOSITORY, NAMED_RULE_REPOSITORY}; use crate::services::permission::PERMISSION_SERVICE; @@ -889,6 +926,16 @@ mod init_canister_sync_handlers { users: Vec, default_groups: &[UUID], ) -> Result<(), ApiError> { + if users.is_empty() { + return Err(SystemError::NoUsersSpecified)?; + } + + if users.len() > u16::MAX as usize { + return Err(SystemError::TooManyUsersSpecified { + max: u16::MAX as usize, + })?; + } + print(format!("Registering {} users", users.len())); for user in users { let user_id = user @@ -938,9 +985,12 @@ mod init_canister_sync_handlers { } /// Registers the default configurations for the canister. - pub fn init_default_permissions_and_policies(admin_quorum: u16) -> Result<(), ApiError> { + pub fn init_default_permissions_and_policies( + admin_quorum: u16, + operator_quorum: u16, + ) -> Result<(), ApiError> { let (regular_named_rule_config, admin_named_rule_config) = - get_default_named_rules(admin_quorum); + get_default_named_rules(admin_quorum, operator_quorum); let regular_named_rule = NamedRule { id: *Uuid::new_v4().as_bytes(), @@ -987,45 +1037,12 @@ mod init_canister_sync_handlers { // Registers the initial accounts of the canister during the canister initialization. // Used pub async fn set_initial_accounts( - accounts: Vec<(InitAccountInput, Option)>, + accounts: Vec<(InitAccountInput, InitAccountPermissionsInput)>, initial_assets: &[InitAssetInput], - quorum: u16, ) -> Result<(), String> { let add_accounts = accounts .into_iter() .map(|(account, permissions)| { - let ( - transfer_request_policy, - configs_request_policy, - read_permission, - configs_permission, - transfer_permission, - ) = permissions - .map(|permissions| { - ( - permissions.transfer_request_policy.map(|rule| rule.into()), - permissions.configs_request_policy.map(|rule| rule.into()), - permissions.read_permission.into(), - permissions.configs_permission.into(), - permissions.transfer_permission.into(), - ) - }) - .unwrap_or_else(|| { - ( - Some(RequestPolicyRule::Quorum( - UserSpecifier::Group(vec![*ADMIN_GROUP_ID]), - quorum, - )), - Some(RequestPolicyRule::Quorum( - UserSpecifier::Group(vec![*ADMIN_GROUP_ID]), - quorum, - )), - Allow::user_groups(vec![*ADMIN_GROUP_ID]), - Allow::user_groups(vec![*ADMIN_GROUP_ID]), - Allow::user_groups(vec![*ADMIN_GROUP_ID]), - ) - }); - let assets = account .assets .into_iter() @@ -1038,11 +1055,15 @@ mod init_canister_sync_handlers { name: account.name, assets: vec![], metadata: account.metadata.into(), - transfer_request_policy, - configs_request_policy, - read_permission, - configs_permission, - transfer_permission, + transfer_request_policy: permissions + .transfer_request_policy + .map(|rule| rule.into()), + configs_request_policy: permissions + .configs_request_policy + .map(|rule| rule.into()), + read_permission: permissions.read_permission.into(), + configs_permission: permissions.configs_permission.into(), + transfer_permission: permissions.transfer_permission.into(), }; let account_id = account @@ -1066,12 +1087,6 @@ mod init_canister_sync_handlers { } } -// Calculates the initial quorum based on the number of admins and the provided quorum, if not provided -// the quorum is set to the majority of the admins. -pub fn calc_initial_quorum(admin_count: u16, quorum: Option) -> u16 { - quorum.unwrap_or(admin_count / 2 + 1).clamp(1, admin_count) -} - #[cfg(target_arch = "wasm32")] mod install_canister_handlers { use crate::core::ic_cdk::api::id as self_canister_id; @@ -1199,8 +1214,9 @@ mod tests { }; use candid::Principal; use station_api::{ - AccountSeedDTO, InitAccountInput, InitAssetInput, InitNamedRuleInput, - InitRequestPolicyInput, InitUserGroupInput, UserIdentityInput, UserInitInput, + AccountSeedDTO, AllowDTO, InitAccountInput, InitAccountPermissionsInput, InitAssetInput, + InitNamedRuleInput, InitRequestPolicyInput, InitUserGroupInput, RequestPolicyRuleDTO, + UserIdentityInput, UserInitInput, }; use uuid::Uuid; @@ -1209,17 +1225,20 @@ mod tests { let result = SYSTEM_SERVICE .init_canister(SystemInit { name: "Station".to_string(), - users: vec![UserInitInput { - name: "Admin".to_string(), - identities: vec![UserIdentityInput { - identity: Principal::from_slice(&[1; 29]), + + initial_config: InitialConfig::WithAllDefaults { + users: vec![UserInitInput { + name: "Admin".to_string(), + identities: vec![UserIdentityInput { + identity: Principal::from_slice(&[1; 29]), + }], + id: None, + groups: None, + status: None, }], - id: None, - groups: None, - status: None, - }], - quorum: None, - entries: None, + admin_quorum: 1, + operator_quorum: 1, + }, upgrader: station_api::SystemUpgraderInput::Deploy( station_api::DeploySystemUpgraderInput { wasm_module: vec![], @@ -1259,38 +1278,6 @@ mod tests { assert!(system_info.get_change_canister_request().is_none()); } - #[test] - fn test_initial_quorum_is_majority() { - assert_eq!(calc_initial_quorum(1, None), 1); - assert_eq!(calc_initial_quorum(2, None), 2); - assert_eq!(calc_initial_quorum(3, None), 2); - assert_eq!(calc_initial_quorum(4, None), 3); - assert_eq!(calc_initial_quorum(5, None), 3); - assert_eq!(calc_initial_quorum(6, None), 4); - assert_eq!(calc_initial_quorum(7, None), 4); - assert_eq!(calc_initial_quorum(8, None), 5); - assert_eq!(calc_initial_quorum(9, None), 5); - assert_eq!(calc_initial_quorum(10, None), 6); - assert_eq!(calc_initial_quorum(11, None), 6); - assert_eq!(calc_initial_quorum(12, None), 7); - assert_eq!(calc_initial_quorum(13, None), 7); - assert_eq!(calc_initial_quorum(14, None), 8); - assert_eq!(calc_initial_quorum(15, None), 8); - assert_eq!(calc_initial_quorum(16, None), 9); - } - - #[test] - fn test_initial_quorum_is_custom() { - // smaller than the number of admins - assert_eq!(calc_initial_quorum(4, Some(1)), 1); - // half of the number of admins - assert_eq!(calc_initial_quorum(4, Some(2)), 2); - // equal to the number of admins - assert_eq!(calc_initial_quorum(4, Some(4)), 4); - // larger than the number of admins - assert_eq!(calc_initial_quorum(4, Some(5)), 4); - } - #[tokio::test] async fn test_initial_named_rules_with_correct_dependencies() { let id_1 = Uuid::new_v4().hyphenated().to_string(); @@ -1452,6 +1439,23 @@ mod tests { // Test duplicate UUIDs in accounts let account_id = Uuid::new_v4().hyphenated().to_string(); let empty_seed: AccountSeedDTO = [0; 16]; // Create a zero-filled array for the seed + + let allow = AllowDTO { + user_groups: vec![], + auth_scope: station_api::AuthScopeDTO::Authenticated, + users: vec![], + }; + + let rule = RequestPolicyRuleDTO::AutoApproved; + + let initial_permissions = InitAccountPermissionsInput { + read_permission: allow.clone(), + configs_permission: allow.clone(), + transfer_permission: allow.clone(), + configs_request_policy: Some(rule.clone()), + transfer_request_policy: Some(rule.clone()), + }; + let account_inputs = vec![ ( InitAccountInput { @@ -1461,7 +1465,7 @@ mod tests { assets: vec![], metadata: vec![], }, - None, + initial_permissions.clone(), ), ( InitAccountInput { @@ -1471,11 +1475,11 @@ mod tests { assets: vec![], metadata: vec![], }, - None, + initial_permissions.clone(), ), ]; - set_initial_accounts(account_inputs, &[], 1) + set_initial_accounts(account_inputs, &[]) .await .expect_err("Should have failed due to duplicate UUID in accounts"); } diff --git a/tests/integration/src/control_panel_tests.rs b/tests/integration/src/control_panel_tests.rs index d705cbe89..213dc03f8 100644 --- a/tests/integration/src/control_panel_tests.rs +++ b/tests/integration/src/control_panel_tests.rs @@ -643,17 +643,19 @@ fn deploy_station_with_insufficient_cycles() { let upgrader_wasm = get_canister_wasm("upgrader").to_vec(); let station_init_args = Encode!(&SystemInstallArg::Init(SystemInitArg { name: "Station".to_string(), - users: vec![UserInitInput { - identities: vec![UserIdentityInput { - identity: WALLET_ADMIN_USER, + initial_config: station_api::InitialConfig::WithAllDefaults { + users: vec![UserInitInput { + identities: vec![UserIdentityInput { + identity: WALLET_ADMIN_USER, + }], + name: "station-admin".to_string(), + groups: None, + id: None, + status: None, }], - name: "station-admin".to_string(), - groups: None, - id: None, - status: None, - }], - quorum: Some(1), - entries: None, + admin_quorum: 1, + operator_quorum: 1, + }, upgrader: station_api::SystemUpgraderInput::Deploy( station_api::DeploySystemUpgraderInput { wasm_module: upgrader_wasm, diff --git a/tests/integration/src/disaster_recovery_tests.rs b/tests/integration/src/disaster_recovery_tests.rs index 364224e25..21de05de4 100644 --- a/tests/integration/src/disaster_recovery_tests.rs +++ b/tests/integration/src/disaster_recovery_tests.rs @@ -503,42 +503,44 @@ fn test_disaster_recovery_flow_recreates_same_accounts() { module_extra_chunks: Some(module_extra_chunks), arg: Encode!(&station_api::SystemInstall::Init(station_api::SystemInit { name: "Station".to_string(), - users: vec![ - station_api::UserInitInput { - identities: vec![UserIdentityInput { - identity: WALLET_ADMIN_USER, - }], - name: "updated-admin-name".to_string(), - groups: None, - id: None, - status: None, - }, - station_api::UserInitInput { - identities: vec![UserIdentityInput { - identity: Principal::from_slice(&[95; 29]), - }], - name: "another-admin".to_string(), - groups: None, - id: None, - status: None, - }, - station_api::UserInitInput { - identities: vec![UserIdentityInput { - identity: Principal::from_slice(&[97; 29]), - }], - name: "yet-another-admin".to_string(), - groups: None, - id: None, - status: None, - }, - ], + fallback_controller: None, upgrader: station_api::SystemUpgraderInput::Id(upgrader_id), - quorum: None, - entries: Some(station_api::InitialEntries::WithDefaultPolicies { + initial_config: station_api::InitialConfig::WithDefaultPolicies { + users: vec![ + station_api::UserInitInput { + identities: vec![UserIdentityInput { + identity: WALLET_ADMIN_USER, + }], + name: "updated-admin-name".to_string(), + groups: None, + id: None, + status: None, + }, + station_api::UserInitInput { + identities: vec![UserIdentityInput { + identity: Principal::from_slice(&[95; 29]), + }], + name: "another-admin".to_string(), + groups: None, + id: None, + status: None, + }, + station_api::UserInitInput { + identities: vec![UserIdentityInput { + identity: Principal::from_slice(&[97; 29]), + }], + name: "yet-another-admin".to_string(), + groups: None, + id: None, + status: None, + }, + ], accounts: init_accounts_input, assets: vec![init_assets_input], - }), + admin_quorum: 1, + operator_quorum: 1, + }, })) .unwrap(), install_mode: upgrader_api::InstallMode::Reinstall, @@ -703,19 +705,21 @@ fn test_disaster_recovery_flow_reuses_same_upgrader() { module_extra_chunks: Some(module_extra_chunks), arg: Encode!(&station_api::SystemInstall::Init(station_api::SystemInit { name: "Station".to_string(), - users: vec![station_api::UserInitInput { - identities: vec![UserIdentityInput { - identity: WALLET_ADMIN_USER, - }], - name: "updated-admin-name".to_string(), - groups: None, - id: None, - status: None, - }], fallback_controller: Some(fallback_controller), upgrader: station_api::SystemUpgraderInput::Id(upgrader_id), - quorum: None, - entries: None, + initial_config: station_api::InitialConfig::WithAllDefaults { + users: vec![station_api::UserInitInput { + identities: vec![UserIdentityInput { + identity: WALLET_ADMIN_USER, + }], + name: "updated-admin-name".to_string(), + groups: None, + id: None, + status: None, + }], + admin_quorum: 1, + operator_quorum: 1 + }, })) .unwrap(), install_mode: upgrader_api::InstallMode::Reinstall, @@ -932,9 +936,11 @@ fn test_disaster_recovery_failing() { }, ), name: "Station".to_string(), - users: vec![], - quorum: None, - entries: None, + initial_config: station_api::InitialConfig::WithAllDefaults { + users: vec![], + admin_quorum: 1, + operator_quorum: 1, + }, }); // install with intentionally bad arg to fail diff --git a/tests/integration/src/install_tests.rs b/tests/integration/src/install_tests.rs index 80b61ba96..95554f41e 100644 --- a/tests/integration/src/install_tests.rs +++ b/tests/integration/src/install_tests.rs @@ -4,11 +4,12 @@ use candid::{Encode, Principal}; use pocket_ic::PocketIc; use rstest::rstest; use station_api::{ - AccountResourceActionDTO, AllowDTO, AuthScopeDTO, InitAccountInput, InitAssetInput, - InitNamedRuleInput, InitPermissionInput, InitRequestPolicyInput, InitUserGroupInput, - MetadataDTO, PermissionResourceActionDTO, RequestPolicyRuleDTO, RequestSpecifierDTO, - ResourceActionDTO, ResourceDTO, ResourceIdDTO, SystemInit, SystemInstall, UserIdentityInput, - UserInitInput, UserResourceActionDTO, UserStatusDTO, UuidDTO, + AccountResourceActionDTO, AllowDTO, AuthScopeDTO, DisasterRecoveryCommitteeDTO, + InitAccountInput, InitAssetInput, InitNamedRuleInput, InitPermissionInput, + InitRequestPolicyInput, InitUserGroupInput, MetadataDTO, PermissionResourceActionDTO, + RequestPolicyRuleDTO, RequestSpecifierDTO, ResourceActionDTO, ResourceDTO, ResourceIdDTO, + SystemInit, SystemInstall, UserIdentityInput, UserInitInput, UserResourceActionDTO, + UserStatusDTO, UuidDTO, }; use uuid::Uuid; @@ -107,7 +108,6 @@ fn install_with_default_policies() { let station_init_args = SystemInstall::Init(SystemInit { name: "Station".to_string(), - users: users.clone(), upgrader: station_api::SystemUpgraderInput::Deploy( station_api::DeploySystemUpgraderInput { wasm_module: upgrader_wasm, @@ -115,11 +115,13 @@ fn install_with_default_policies() { }, ), fallback_controller: Some(controller), - quorum: None, - entries: Some(station_api::InitialEntries::WithDefaultPolicies { + initial_config: station_api::InitialConfig::WithDefaultPolicies { + users: users.clone(), + admin_quorum: 1, + operator_quorum: 1, accounts: accounts.clone(), assets: assets.clone(), - }), + }, }); env.install_canister( canister_id, @@ -351,7 +353,6 @@ fn install_with_all_entries() { let station_init_args = SystemInstall::Init(SystemInit { name: "Station".to_string(), - users: users.clone(), upgrader: station_api::SystemUpgraderInput::Deploy( station_api::DeploySystemUpgraderInput { wasm_module: upgrader_wasm, @@ -359,15 +360,19 @@ fn install_with_all_entries() { }, ), fallback_controller: Some(controller), - quorum: None, - entries: Some(station_api::InitialEntries::Complete { + initial_config: station_api::InitialConfig::Complete { + users: users.clone(), accounts: accounts.clone(), assets: assets.clone(), permissions: permissions.clone(), request_policies: request_policies.clone(), user_groups: user_groups.clone(), named_rules: named_rules.clone(), - }), + disaster_recovery_committee: Some(DisasterRecoveryCommitteeDTO { + quorum: 1, + user_group_id: custom_user_group_id, + }), + }, }); env.install_canister( canister_id, @@ -474,7 +479,6 @@ fn install_with_all_defaults() { let station_init_args = SystemInstall::Init(SystemInit { name: "Station".to_string(), - users: users.clone(), upgrader: station_api::SystemUpgraderInput::Deploy( station_api::DeploySystemUpgraderInput { wasm_module: upgrader_wasm, @@ -482,8 +486,11 @@ fn install_with_all_defaults() { }, ), fallback_controller: Some(controller), - quorum: None, - entries: None, + initial_config: station_api::InitialConfig::WithAllDefaults { + users: users.clone(), + admin_quorum: 1, + operator_quorum: 1, + }, }); env.install_canister( canister_id, @@ -515,20 +522,39 @@ fn install_with_all_defaults() { #[rstest] #[should_panic] -#[case::empty_entries(station_api::InitialEntries::Complete { +#[case::empty_entries(station_api::InitialConfig::Complete { + users: vec![UserInitInput { + identities: vec![UserIdentityInput { + identity: WALLET_ADMIN_USER, + }], + name: "station-admin".to_string(), + groups: Some(vec![ADMIN_GROUP_ID.hyphenated().to_string()]), + id: None, + status: None, + }], accounts: vec![], assets: vec![], permissions: vec![], request_policies: vec![], user_groups: vec![], // no user groups yet user is referencing the ADMIN group named_rules: vec![], + disaster_recovery_committee: None, })] #[should_panic] #[case::circular_named_rules({ let id_1 = Uuid::new_v4().hyphenated().to_string(); let id_2 = Uuid::new_v4().hyphenated().to_string(); - station_api::InitialEntries::Complete { + station_api::InitialConfig::Complete { + users: vec![UserInitInput { + identities: vec![UserIdentityInput { + identity: WALLET_ADMIN_USER, + }], + name: "station-admin".to_string(), + groups: Some(vec![ADMIN_GROUP_ID.hyphenated().to_string()]), + id: None, + status: None, + }], accounts: vec![], assets: vec![], permissions: vec![], @@ -554,12 +580,27 @@ fn install_with_all_defaults() { rule: RequestPolicyRuleDTO::NamedRule(id_1.clone()), }, ], + disaster_recovery_committee: Some(DisasterRecoveryCommitteeDTO { + quorum: 1, + user_group_id: ADMIN_GROUP_ID.hyphenated().to_string(), + }), } })] #[should_panic] #[case::non_existent_asset_id({ let id_1 = Uuid::new_v4().hyphenated().to_string(); - station_api::InitialEntries::WithDefaultPolicies { + station_api::InitialConfig::WithDefaultPolicies { + users: vec![UserInitInput { + identities: vec![UserIdentityInput { + identity: WALLET_ADMIN_USER, + }], + name: "station-admin".to_string(), + groups: Some(vec![ADMIN_GROUP_ID.hyphenated().to_string()]), + id: None, + status: None, + }], + admin_quorum: 1, + operator_quorum: 1, accounts: vec![InitAccountInput { name: "account".to_string(), metadata: vec![], @@ -575,7 +616,16 @@ fn install_with_all_defaults() { #[should_panic] #[case::non_existent_policy_id({ let id_1 = Uuid::new_v4().hyphenated().to_string(); - station_api::InitialEntries::Complete { + station_api::InitialConfig::Complete { + users:vec![UserInitInput { + identities: vec![UserIdentityInput { + identity: WALLET_ADMIN_USER, + }], + name: "station-admin".to_string(), + groups: Some(vec![ADMIN_GROUP_ID.hyphenated().to_string()]), + id: None, + status: None, + }], accounts: vec![], assets: vec![], permissions: vec![], @@ -593,9 +643,13 @@ fn install_with_all_defaults() { }, ], named_rules: vec![], + disaster_recovery_committee: Some(DisasterRecoveryCommitteeDTO { + quorum: 1, + user_group_id: ADMIN_GROUP_ID.hyphenated().to_string(), + }), } })] -fn install_with_bad_input(#[case] bad_input: station_api::InitialEntries) { +fn install_with_bad_input(#[case] bad_input: station_api::InitialConfig) { let TestEnv { env, controller, .. } = setup_new_env(); @@ -609,19 +663,8 @@ fn install_with_bad_input(#[case] bad_input: station_api::InitialEntries) { let station_wasm = get_canister_wasm("station").to_vec(); let upgrader_wasm = get_canister_wasm("upgrader").to_vec(); - let users = vec![UserInitInput { - identities: vec![UserIdentityInput { - identity: WALLET_ADMIN_USER, - }], - name: "station-admin".to_string(), - groups: Some(vec![ADMIN_GROUP_ID.hyphenated().to_string()]), - id: None, - status: None, - }]; - let station_init_args = SystemInstall::Init(SystemInit { name: "Station".to_string(), - users: users.clone(), upgrader: station_api::SystemUpgraderInput::Deploy( station_api::DeploySystemUpgraderInput { wasm_module: upgrader_wasm.clone(), @@ -629,8 +672,7 @@ fn install_with_bad_input(#[case] bad_input: station_api::InitialEntries) { }, ), fallback_controller: Some(controller), - quorum: None, - entries: Some(bad_input), + initial_config: bad_input, }); env.install_canister( canister_id, diff --git a/tests/integration/src/setup.rs b/tests/integration/src/setup.rs index 0e8446afd..25d8b974e 100644 --- a/tests/integration/src/setup.rs +++ b/tests/integration/src/setup.rs @@ -306,15 +306,7 @@ fn install_canisters( let station_init_args = SystemInstallArg::Init(SystemInitArg { name: "Station".to_string(), - users: vec![UserInitInput { - identities: vec![UserIdentityInput { - identity: WALLET_ADMIN_USER, - }], - name: "station-admin".to_string(), - groups: None, - id: None, - status: None, - }], + upgrader: station_api::SystemUpgraderInput::Deploy( station_api::DeploySystemUpgraderInput { wasm_module: upgrader_wasm, @@ -322,8 +314,19 @@ fn install_canisters( }, ), fallback_controller: config.fallback_controller, - quorum: None, - entries: None, + initial_config: station_api::InitialConfig::WithAllDefaults { + users: vec![UserInitInput { + identities: vec![UserIdentityInput { + identity: WALLET_ADMIN_USER, + }], + name: "station-admin".to_string(), + groups: None, + id: None, + status: None, + }], + admin_quorum: 1, + operator_quorum: 1, + }, }); env.install_canister( station, From e27b6429a00243917ab3defdba474f21173b913e Mon Sep 17 00:00:00 2001 From: olaszakos Date: Wed, 12 Mar 2025 15:19:25 +0100 Subject: [PATCH 29/33] fix spec --- apps/wallet/src/generated/station/station.did | 2 +- .../src/generated/station/station.did.d.ts | 2 +- .../src/generated/station/station.did.js | 32 +++++++++---------- core/station/api/spec.did | 2 +- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/apps/wallet/src/generated/station/station.did b/apps/wallet/src/generated/station/station.did index 37aeabe94..cc9a42005 100644 --- a/apps/wallet/src/generated/station/station.did +++ b/apps/wallet/src/generated/station/station.did @@ -2901,7 +2901,7 @@ type SystemInit = record { // An additional controller of the station and upgrader canisters (optional). fallback_controller : opt principal; // The initial configuration to apply. - entries: InitialConfig; + initial_config: InitialConfig; }; // The upgrade configuration for the canister. diff --git a/apps/wallet/src/generated/station/station.did.d.ts b/apps/wallet/src/generated/station/station.did.d.ts index d59934cb4..46f53ab4a 100644 --- a/apps/wallet/src/generated/station/station.did.d.ts +++ b/apps/wallet/src/generated/station/station.did.d.ts @@ -1443,9 +1443,9 @@ export type SystemInfoResult = { 'Ok' : { 'system' : SystemInfo } } | { 'Err' : Error }; export interface SystemInit { 'name' : string, + 'initial_config' : InitialConfig, 'fallback_controller' : [] | [Principal], 'upgrader' : SystemUpgraderInput, - 'entries' : InitialConfig, } export type SystemInstall = { 'Upgrade' : SystemUpgrade } | { 'Init' : SystemInit }; diff --git a/apps/wallet/src/generated/station/station.did.js b/apps/wallet/src/generated/station/station.did.js index 0b045df1f..2e883798a 100644 --- a/apps/wallet/src/generated/station/station.did.js +++ b/apps/wallet/src/generated/station/station.did.js @@ -2,13 +2,6 @@ export const idlFactory = ({ IDL }) => { const RequestPolicyRule = IDL.Rec(); const RequestPolicyRuleResult = IDL.Rec(); const SystemUpgrade = IDL.Record({ 'name' : IDL.Opt(IDL.Text) }); - const SystemUpgraderInput = IDL.Variant({ - 'Id' : IDL.Principal, - 'Deploy' : IDL.Record({ - 'initial_cycles' : IDL.Opt(IDL.Nat), - 'wasm_module' : IDL.Vec(IDL.Nat8), - }), - }); const UUID = IDL.Text; const AssetMetadata = IDL.Record({ 'key' : IDL.Text, 'value' : IDL.Text }); const InitAssetInput = IDL.Record({ @@ -255,11 +248,18 @@ export const idlFactory = ({ IDL }) => { 'named_rules' : IDL.Vec(InitNamedRuleInput), }), }); + const SystemUpgraderInput = IDL.Variant({ + 'Id' : IDL.Principal, + 'Deploy' : IDL.Record({ + 'initial_cycles' : IDL.Opt(IDL.Nat), + 'wasm_module' : IDL.Vec(IDL.Nat8), + }), + }); const SystemInit = IDL.Record({ 'name' : IDL.Text, + 'initial_config' : InitialConfig, 'fallback_controller' : IDL.Opt(IDL.Principal), 'upgrader' : SystemUpgraderInput, - 'entries' : InitialConfig, }); const SystemInstall = IDL.Variant({ 'Upgrade' : SystemUpgrade, @@ -1872,13 +1872,6 @@ export const idlFactory = ({ IDL }) => { export const init = ({ IDL }) => { const RequestPolicyRule = IDL.Rec(); const SystemUpgrade = IDL.Record({ 'name' : IDL.Opt(IDL.Text) }); - const SystemUpgraderInput = IDL.Variant({ - 'Id' : IDL.Principal, - 'Deploy' : IDL.Record({ - 'initial_cycles' : IDL.Opt(IDL.Nat), - 'wasm_module' : IDL.Vec(IDL.Nat8), - }), - }); const UUID = IDL.Text; const AssetMetadata = IDL.Record({ 'key' : IDL.Text, 'value' : IDL.Text }); const InitAssetInput = IDL.Record({ @@ -2125,11 +2118,18 @@ export const init = ({ IDL }) => { 'named_rules' : IDL.Vec(InitNamedRuleInput), }), }); + const SystemUpgraderInput = IDL.Variant({ + 'Id' : IDL.Principal, + 'Deploy' : IDL.Record({ + 'initial_cycles' : IDL.Opt(IDL.Nat), + 'wasm_module' : IDL.Vec(IDL.Nat8), + }), + }); const SystemInit = IDL.Record({ 'name' : IDL.Text, + 'initial_config' : InitialConfig, 'fallback_controller' : IDL.Opt(IDL.Principal), 'upgrader' : SystemUpgraderInput, - 'entries' : InitialConfig, }); const SystemInstall = IDL.Variant({ 'Upgrade' : SystemUpgrade, diff --git a/core/station/api/spec.did b/core/station/api/spec.did index 37aeabe94..cc9a42005 100644 --- a/core/station/api/spec.did +++ b/core/station/api/spec.did @@ -2901,7 +2901,7 @@ type SystemInit = record { // An additional controller of the station and upgrader canisters (optional). fallback_controller : opt principal; // The initial configuration to apply. - entries: InitialConfig; + initial_config: InitialConfig; }; // The upgrade configuration for the canister. From a4a8447a0bc4ae1ee6f83bb775fdd0af2aa6bacd Mon Sep 17 00:00:00 2001 From: olaszakos Date: Wed, 12 Mar 2025 15:35:43 +0100 Subject: [PATCH 30/33] fix more lints --- core/station/impl/src/services/system.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/core/station/impl/src/services/system.rs b/core/station/impl/src/services/system.rs index 2ea936c4d..b8833b9b5 100644 --- a/core/station/impl/src/services/system.rs +++ b/core/station/impl/src/services/system.rs @@ -288,7 +288,7 @@ impl SystemService { // calculates the initial quorum based on the number of admins and the provided quorum - match &init.entries { + match &init.initial_config { InitialConfig::WithAllDefaults { .. } => {} InitialConfig::WithDefaultPolicies { accounts, @@ -327,7 +327,7 @@ impl SystemService { .iter() .map(|account| (account.clone(), default_permissions_policies.clone())) .collect(), - &assets, + assets, ) .await?; } @@ -346,7 +346,7 @@ impl SystemService { ) }) .collect(), - &assets, + assets, ) .await?; } @@ -358,7 +358,7 @@ impl SystemService { install_canister_post_process_finish(system_info); - match init.entries { + match init.initial_config { InitialConfig::WithAllDefaults { admin_quorum, .. } | InitialConfig::WithDefaultPolicies { admin_quorum, .. } => { SystemService::set_disaster_recovery_committee(Some( @@ -927,11 +927,11 @@ mod init_canister_sync_handlers { default_groups: &[UUID], ) -> Result<(), ApiError> { if users.is_empty() { - return Err(SystemError::NoUsersSpecified)?; + Err(SystemError::NoUsersSpecified)?; } if users.len() > u16::MAX as usize { - return Err(SystemError::TooManyUsersSpecified { + Err(SystemError::TooManyUsersSpecified { max: u16::MAX as usize, })?; } From 7892ffbf73cb604933251fb7ce6e4b4ff75be6ae Mon Sep 17 00:00:00 2001 From: olaszakos Date: Wed, 12 Mar 2025 15:48:57 +0100 Subject: [PATCH 31/33] add more docs to InitialConfig --- apps/wallet/src/generated/station/station.did | 20 +++++++++++++++---- core/station/api/spec.did | 20 +++++++++++++++---- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/apps/wallet/src/generated/station/station.did b/apps/wallet/src/generated/station/station.did index cc9a42005..ee3e849d0 100644 --- a/apps/wallet/src/generated/station/station.did +++ b/apps/wallet/src/generated/station/station.did @@ -2847,10 +2847,22 @@ type InitNamedRuleInput = record { rule : RequestPolicyRule; }; -// The initial configuration for the station. +// The initial configuration for the station. +// +// Unless the `Complete` variant is used, the station will be initialized with default user +// groups, named rules (aka. approval rules), request policies, permissions, and assets. +// +// The default user groups for the station will be: +// - `Admin` with the UUID "00000000-0000-4000-8000-000000000000" +// - `Operator` with the UUID "00000000-0000-4000-8000-000000000001" +// +// The default named rules for the station will be: +// - `Admin approval` with a specified admin quorum +// - `Operator approval` with a specified operator quorum +// type InitialConfig = variant { - // Initialize the station with default policies, permissions, and assets. - // This does not create an initial account. + // Initialize the station with default user groups, named rules, policies, permissions, and assets. + // This does not create an initial account. WithAllDefaults : record { // The initial users to create. users : vec InitUserInput; @@ -2859,7 +2871,7 @@ type InitialConfig = variant { // The initial operator quorum in the operator level approval rule. operator_quorum : nat16; }; - // Initialize the station with default policies, accounts and assets. + // Initialize the station with default user groups, named rules, policies, permissions. WithDefaultPolicies : record { // The initial users to create. users : vec InitUserInput; diff --git a/core/station/api/spec.did b/core/station/api/spec.did index cc9a42005..ee3e849d0 100644 --- a/core/station/api/spec.did +++ b/core/station/api/spec.did @@ -2847,10 +2847,22 @@ type InitNamedRuleInput = record { rule : RequestPolicyRule; }; -// The initial configuration for the station. +// The initial configuration for the station. +// +// Unless the `Complete` variant is used, the station will be initialized with default user +// groups, named rules (aka. approval rules), request policies, permissions, and assets. +// +// The default user groups for the station will be: +// - `Admin` with the UUID "00000000-0000-4000-8000-000000000000" +// - `Operator` with the UUID "00000000-0000-4000-8000-000000000001" +// +// The default named rules for the station will be: +// - `Admin approval` with a specified admin quorum +// - `Operator approval` with a specified operator quorum +// type InitialConfig = variant { - // Initialize the station with default policies, permissions, and assets. - // This does not create an initial account. + // Initialize the station with default user groups, named rules, policies, permissions, and assets. + // This does not create an initial account. WithAllDefaults : record { // The initial users to create. users : vec InitUserInput; @@ -2859,7 +2871,7 @@ type InitialConfig = variant { // The initial operator quorum in the operator level approval rule. operator_quorum : nat16; }; - // Initialize the station with default policies, accounts and assets. + // Initialize the station with default user groups, named rules, policies, permissions. WithDefaultPolicies : record { // The initial users to create. users : vec InitUserInput; From b599453d2a25aca893e2e027a789b17cd166e986 Mon Sep 17 00:00:00 2001 From: olaszakos Date: Wed, 12 Mar 2025 18:22:44 +0100 Subject: [PATCH 32/33] fix initial asset not adding assets --- core/station/impl/src/services/system.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/station/impl/src/services/system.rs b/core/station/impl/src/services/system.rs index b8833b9b5..fed690a66 100644 --- a/core/station/impl/src/services/system.rs +++ b/core/station/impl/src/services/system.rs @@ -1053,7 +1053,7 @@ mod init_canister_sync_handlers { let input = AddAccountOperationInput { name: account.name, - assets: vec![], + assets, metadata: account.metadata.into(), transfer_request_policy: permissions .transfer_request_policy @@ -1081,6 +1081,8 @@ mod init_canister_sync_handlers { .create_account(new_account, with_account_id) .await .map_err(|e| format!("Failed to add account: {:?}", e))?; + + print("account created"); } Ok(()) From fbcdf8b12490c606cde49c2a4bdebb16019b0626 Mon Sep 17 00:00:00 2001 From: olaszakos Date: Wed, 12 Mar 2025 19:18:34 +0100 Subject: [PATCH 33/33] fix dr test quorum --- tests/integration/src/disaster_recovery_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/src/disaster_recovery_tests.rs b/tests/integration/src/disaster_recovery_tests.rs index 21de05de4..f167f5ee5 100644 --- a/tests/integration/src/disaster_recovery_tests.rs +++ b/tests/integration/src/disaster_recovery_tests.rs @@ -538,7 +538,7 @@ fn test_disaster_recovery_flow_recreates_same_accounts() { ], accounts: init_accounts_input, assets: vec![init_assets_input], - admin_quorum: 1, + admin_quorum: 2, operator_quorum: 1, }, }))