diff --git a/apps/wallet/src/generated/station/station.did b/apps/wallet/src/generated/station/station.did index 320ac82cd..4bb55e461 100644 --- a/apps/wallet/src/generated/station/station.did +++ b/apps/wallet/src/generated/station/station.did @@ -2287,8 +2287,10 @@ type ExternalCanisterCallerMethodsPrivileges = record { // The caller privileges for the external canister. type ExternalCanisterCallerPrivileges = record { - // The external canister retource id. + // The external canister entry id. id : UUID; + // The canister id. + canister_id : principal; // Wether or not the caller can edit the external canister. can_change : bool; // The list of methods that the caller can call on the external canister. @@ -2305,14 +2307,24 @@ type GetExternalCanisterResult = variant { Err : Error; }; +// The input type for sorting the results of listing external canisters. +type ListExternalCanistersSortInput = variant { + // Sort by the name of the external canister. + Name : SortByDirection; +}; + // Input type for listing external canisters with the given filters. type ListExternalCanistersInput = record { // The principal id of the external canister to search for. canister_ids : opt vec principal; // The labels to use for filtering the external canisters. labels : opt vec text; + // The current state of the external canisters to use for filtering (e.g. `Active`, `Archived`). + states : opt vec ExternalCanisterState; // The pagination parameters. paginate : opt PaginationInput; + // The sort parameters. + sort_by : opt ListExternalCanistersSortInput; }; // Result type for listing external canisters. diff --git a/apps/wallet/src/generated/station/station.did.d.ts b/apps/wallet/src/generated/station/station.did.d.ts index 02c6f10c2..0a830d9b7 100644 --- a/apps/wallet/src/generated/station/station.did.d.ts +++ b/apps/wallet/src/generated/station/station.did.d.ts @@ -400,6 +400,7 @@ export interface ExternalCanisterCallerMethodsPrivileges { export interface ExternalCanisterCallerPrivileges { 'id' : UUID, 'can_change' : boolean, + 'canister_id' : Principal, 'can_call' : Array, } export interface ExternalCanisterChangeRequestPolicyRule { @@ -571,6 +572,8 @@ export type ListAddressBookEntriesResult = { } | { 'Err' : Error }; export interface ListExternalCanistersInput { + 'sort_by' : [] | [ListExternalCanistersSortInput], + 'states' : [] | [Array], 'canister_ids' : [] | [Array], 'labels' : [] | [Array], 'paginate' : [] | [PaginationInput], @@ -584,6 +587,7 @@ export type ListExternalCanistersResult = { } } | { 'Err' : Error }; +export type ListExternalCanistersSortInput = { 'Name' : SortByDirection }; export interface ListNotificationsInput { 'status' : [] | [NotificationStatus], 'to_dt' : [] | [TimestampRFC3339], diff --git a/apps/wallet/src/generated/station/station.did.js b/apps/wallet/src/generated/station/station.did.js index f6e246bbf..4c3260473 100644 --- a/apps/wallet/src/generated/station/station.did.js +++ b/apps/wallet/src/generated/station/station.did.js @@ -755,6 +755,7 @@ export const idlFactory = ({ IDL }) => { const ExternalCanisterCallerPrivileges = IDL.Record({ 'id' : UUID, 'can_change' : IDL.Bool, + 'canister_id' : IDL.Principal, 'can_call' : IDL.Vec(ExternalCanisterCallerMethodsPrivileges), }); const ExternalCanister = IDL.Record({ @@ -986,7 +987,13 @@ export const idlFactory = ({ IDL }) => { }), 'Err' : Error, }); + const SortByDirection = IDL.Variant({ 'Asc' : IDL.Null, 'Desc' : IDL.Null }); + const ListExternalCanistersSortInput = IDL.Variant({ + 'Name' : SortByDirection, + }); const ListExternalCanistersInput = IDL.Record({ + 'sort_by' : IDL.Opt(ListExternalCanistersSortInput), + 'states' : IDL.Opt(IDL.Vec(ExternalCanisterState)), 'canister_ids' : IDL.Opt(IDL.Vec(IDL.Principal)), 'labels' : IDL.Opt(IDL.Vec(IDL.Text)), 'paginate' : IDL.Opt(PaginationInput), @@ -1100,7 +1107,6 @@ export const idlFactory = ({ IDL }) => { }), 'Err' : Error, }); - const SortByDirection = IDL.Variant({ 'Asc' : IDL.Null, 'Desc' : IDL.Null }); const ListRequestsSortBy = IDL.Variant({ 'ExpirationDt' : SortByDirection, 'LastModificationDt' : SortByDirection, diff --git a/core/station/api/spec.did b/core/station/api/spec.did index 320ac82cd..4bb55e461 100644 --- a/core/station/api/spec.did +++ b/core/station/api/spec.did @@ -2287,8 +2287,10 @@ type ExternalCanisterCallerMethodsPrivileges = record { // The caller privileges for the external canister. type ExternalCanisterCallerPrivileges = record { - // The external canister retource id. + // The external canister entry id. id : UUID; + // The canister id. + canister_id : principal; // Wether or not the caller can edit the external canister. can_change : bool; // The list of methods that the caller can call on the external canister. @@ -2305,14 +2307,24 @@ type GetExternalCanisterResult = variant { Err : Error; }; +// The input type for sorting the results of listing external canisters. +type ListExternalCanistersSortInput = variant { + // Sort by the name of the external canister. + Name : SortByDirection; +}; + // Input type for listing external canisters with the given filters. type ListExternalCanistersInput = record { // The principal id of the external canister to search for. canister_ids : opt vec principal; // The labels to use for filtering the external canisters. labels : opt vec text; + // The current state of the external canisters to use for filtering (e.g. `Active`, `Archived`). + states : opt vec ExternalCanisterState; // The pagination parameters. paginate : opt PaginationInput; + // The sort parameters. + sort_by : opt ListExternalCanistersSortInput; }; // Result type for listing external canisters. diff --git a/core/station/api/src/external_canister.rs b/core/station/api/src/external_canister.rs index 69bf335ac..b9c8c76ed 100644 --- a/core/station/api/src/external_canister.rs +++ b/core/station/api/src/external_canister.rs @@ -1,6 +1,6 @@ use crate::{ AllowDTO, CanisterInstallMode, PaginationInput, RequestPolicyRuleDTO, Sha256HashDTO, - TimestampRfc3339, UuidDTO, ValidationMethodResourceTargetDTO, + SortDirection, TimestampRfc3339, UuidDTO, ValidationMethodResourceTargetDTO, }; use candid::{CandidType, Deserialize, Nat, Principal}; @@ -193,6 +193,7 @@ pub struct ExternalCanisterCallerMethodPrivilegesDTO { #[derive(CandidType, serde::Serialize, Deserialize, Debug, Clone)] pub struct ExternalCanisterCallerPrivilegesDTO { pub id: UuidDTO, + pub canister_id: Principal, pub can_change: bool, pub can_call: Vec, } @@ -203,11 +204,18 @@ pub struct GetExternalCanisterResponse { pub privileges: ExternalCanisterCallerPrivilegesDTO, } +#[derive(CandidType, serde::Serialize, Deserialize, Debug, Clone)] +pub enum ListExternalCanistersSortInput { + Name(SortDirection), +} + #[derive(CandidType, serde::Serialize, Deserialize, Debug, Clone)] pub struct ListExternalCanistersInput { pub canister_ids: Option>, pub labels: Option>, + pub states: Option>, pub paginate: Option, + pub sort_by: Option, } #[derive(CandidType, serde::Serialize, Deserialize, Debug, Clone)] @@ -220,13 +228,13 @@ pub struct ListExternalCanistersResponse { #[derive(CandidType, serde::Serialize, Deserialize, Debug, Clone)] pub struct GetExternalCanisterFiltersInputWithName { - prefix: Option, + pub prefix: Option, } #[derive(CandidType, serde::Serialize, Deserialize, Debug, Clone)] pub struct GetExternalCanisterFiltersInput { - with_name: Option, - with_labels: Option, + pub with_name: Option, + pub with_labels: Option, } #[derive(CandidType, serde::Serialize, Deserialize, Debug, Clone)] diff --git a/core/station/impl/results.yml b/core/station/impl/results.yml index 53a4ad5aa..5f056f69a 100644 --- a/core/station/impl/results.yml +++ b/core/station/impl/results.yml @@ -5,11 +5,17 @@ benches: heap_increase: 0 stable_memory_increase: 0 scopes: {} + list_external_canisters_with_all_statuses: + total: + instructions: 3462083192 + heap_increase: 19 + stable_memory_increase: 0 + scopes: {} repository_batch_insert_100_requests: total: instructions: 490157820 heap_increase: 0 - stable_memory_increase: 241 + stable_memory_increase: 240 scopes: {} repository_filter_all_request_ids_by_default_filters: total: @@ -25,13 +31,13 @@ benches: scopes: {} service_filter_all_requests_with_creation_time_filters: total: - instructions: 1151272167 + instructions: 1151272168 heap_increase: 0 stable_memory_increase: 16 scopes: {} service_filter_all_requests_with_default_filters: total: - instructions: 6015172658 + instructions: 6015172659 heap_increase: 3 stable_memory_increase: 16 scopes: {} diff --git a/core/station/impl/src/controllers/external_canister.rs b/core/station/impl/src/controllers/external_canister.rs index 2c0a62d6b..09ae6d20e 100644 --- a/core/station/impl/src/controllers/external_canister.rs +++ b/core/station/impl/src/controllers/external_canister.rs @@ -11,8 +11,9 @@ use lazy_static::lazy_static; use orbit_essentials::api::ApiResult; use orbit_essentials::with_middleware; use station_api::{ - GetExternalCanisterFiltersInput, GetExternalCanisterFiltersResponse, GetExternalCanisterInput, - GetExternalCanisterResponse, ListExternalCanistersInput, ListExternalCanistersResponse, + ExternalCanisterCallerPrivilegesDTO, GetExternalCanisterFiltersInput, + GetExternalCanisterFiltersResponse, GetExternalCanisterInput, GetExternalCanisterResponse, + ListExternalCanistersInput, ListExternalCanistersResponse, }; use std::sync::Arc; @@ -69,22 +70,86 @@ impl ExternalCanisterController { &self, input: GetExternalCanisterInput, ) -> ApiResult { - unimplemented!("get_external_canister") + let ctx = call_context(); + let external_canister = self + .canister_service + .get_external_canister_by_canister_id(&input.canister_id)?; + let external_canister_policies = self + .canister_service + .get_external_canister_request_policies(&external_canister.canister_id); + let external_canister_permissions = self + .canister_service + .get_external_canister_permissions(&external_canister.canister_id); + let caller_privileges = self + .canister_service + .get_caller_privileges_for_external_canister( + &external_canister.id, + &external_canister.canister_id, + &ctx, + ); + + Ok(GetExternalCanisterResponse { + canister: external_canister + .into_dto(external_canister_permissions, external_canister_policies), + privileges: caller_privileges.into(), + }) } #[with_middleware(guard = authorize(&call_context(), &[Resource::ExternalCanister(ExternalCanisterResourceAction::List)]))] async fn list_external_canisters( &self, - _input: ListExternalCanistersInput, + input: ListExternalCanistersInput, ) -> ApiResult { - unimplemented!("list_external_canisters") + let ctx = call_context(); + let result = self.canister_service.list_external_canisters(input, &ctx)?; + + let mut privileges = Vec::new(); + for external_canister in &result.items { + let caller_privileges = self + .canister_service + .get_caller_privileges_for_external_canister( + &external_canister.id, + &external_canister.canister_id, + &ctx, + ); + + privileges.push(ExternalCanisterCallerPrivilegesDTO::from(caller_privileges)); + } + + Ok(ListExternalCanistersResponse { + canisters: result + .items + .into_iter() + .map(|external_canister| { + let policies = self + .canister_service + .get_external_canister_permissions(&external_canister.canister_id); + let permissions = self + .canister_service + .get_external_canister_request_policies(&external_canister.canister_id); + + external_canister.into_dto(policies, permissions) + }) + .collect(), + next_offset: result.next_offset, + total: result.total, + privileges, + }) } #[with_middleware(guard = authorize(&call_context(), &[Resource::ExternalCanister(ExternalCanisterResourceAction::List)]))] async fn get_external_canister_filters( &self, - _input: GetExternalCanisterFiltersInput, + input: GetExternalCanisterFiltersInput, ) -> ApiResult { - unimplemented!("get_external_canister_filters") + let ctx = call_context(); + let filters = self + .canister_service + .available_external_canisters_filters(input, &ctx); + + Ok(GetExternalCanisterFiltersResponse { + names: filters.names, + labels: filters.labels, + }) } } diff --git a/core/station/impl/src/controllers/system.rs b/core/station/impl/src/controllers/system.rs index 8de0f137d..60171a27e 100644 --- a/core/station/impl/src/controllers/system.rs +++ b/core/station/impl/src/controllers/system.rs @@ -30,9 +30,19 @@ async fn initialize(input: Option) { #[cfg(all(feature = "canbench", not(test)))] #[ic_cdk_macros::init] pub async fn mock_init() { + use crate::core::write_system_info; + use crate::models::SystemInfo; + use candid::Principal; + // Initialize the random number generator with a fixed seed to ensure deterministic // results across runs of the benchmarks. orbit_essentials::utils::initialize_rng_from_seed([0u8; 32]); + + // Initialize the system info. + let mut system = SystemInfo::default(); + system.set_upgrader_canister_id(Principal::from_slice(&[25; 29])); + + write_system_info(system); } #[post_upgrade] diff --git a/core/station/impl/src/core/mod.rs b/core/station/impl/src/core/mod.rs index 4b873a179..a07090702 100644 --- a/core/station/impl/src/core/mod.rs +++ b/core/station/impl/src/core/mod.rs @@ -50,7 +50,7 @@ pub mod test_utils { pub const UPGRADER_CANISTER_ID: [u8; 29] = [25; 29]; pub fn init_canister_system() -> SystemInfo { - let mut system = SystemInfo::default(); + let mut system: SystemInfo = SystemInfo::default(); system .set_upgrader_canister_id(Principal::from_slice(self::UPGRADER_CANISTER_ID.as_slice())); diff --git a/core/station/impl/src/core/utils.rs b/core/station/impl/src/core/utils.rs index fdbfdb863..60312ce18 100644 --- a/core/station/impl/src/core/utils.rs +++ b/core/station/impl/src/core/utils.rs @@ -1,3 +1,5 @@ +use candid::Principal; + use super::authorization::Authorization; use super::CallContext; use crate::errors::PaginationError; @@ -113,6 +115,10 @@ pub(crate) fn format_unique_string(text: &str) -> String { deunicode::deunicode(text).to_lowercase().replace(' ', "") } +/// The minimum principal value that can be used. +pub const MIN_PRINCIPAL: Principal = Principal::from_slice(&[0; 29]); +pub const MAX_PRINCIPAL: Principal = Principal::from_slice(&[255; 29]); + #[cfg(test)] mod tests { use super::*; diff --git a/core/station/impl/src/mappers/external_canister.rs b/core/station/impl/src/mappers/external_canister.rs index 2e522f38e..b7f8cf3dc 100644 --- a/core/station/impl/src/mappers/external_canister.rs +++ b/core/station/impl/src/mappers/external_canister.rs @@ -3,11 +3,16 @@ use crate::{ models::{ ConfigureExternalCanisterOperationInput, ConfigureExternalCanisterOperationKind, ConfigureExternalCanisterSettingsInput, CreateExternalCanisterOperationInput, - DefiniteCanisterSettingsInput, ExternalCanister, ExternalCanisterState, + DefiniteCanisterSettingsInput, ExternalCanister, ExternalCanisterCallerMethodsPrivileges, + ExternalCanisterCallerPrivileges, ExternalCanisterPermissions, + ExternalCanisterRequestPolicies, ExternalCanisterState, }, + repositories::ExternalCanisterWhereClauseSort, }; use candid::Principal; use ic_cdk::api::management_canister::main::CanisterSettings; +use orbit_essentials::{repository::SortDirection, utils::timestamp_to_rfc3339}; +use station_api::ExternalCanisterDTO; use uuid::Uuid; #[derive(Default, Clone, Debug)] @@ -31,6 +36,62 @@ impl ExternalCanisterMapper { } } +impl ExternalCanister { + pub fn into_dto( + self, + permissions: ExternalCanisterPermissions, + policies: ExternalCanisterRequestPolicies, + ) -> ExternalCanisterDTO { + ExternalCanisterDTO { + id: Uuid::from_bytes(self.id).hyphenated().to_string(), + canister_id: self.canister_id, + name: self.name, + description: self.description, + labels: self.labels, + state: self.state.into(), + permissions: permissions.into(), + request_policies: policies.into(), + created_at: timestamp_to_rfc3339(&self.created_at), + modified_at: self.modified_at.map(|ts| timestamp_to_rfc3339(&ts)), + } + } +} + +impl From for station_api::ExternalCanisterCallerPrivilegesDTO { + fn from(privileges: ExternalCanisterCallerPrivileges) -> Self { + station_api::ExternalCanisterCallerPrivilegesDTO { + id: Uuid::from_bytes(privileges.id).hyphenated().to_string(), + canister_id: privileges.canister_id, + can_change: privileges.can_change, + can_call: privileges.can_call.into_iter().map(Into::into).collect(), + } + } +} + +impl From for ExternalCanisterWhereClauseSort { + fn from(input: station_api::ListExternalCanistersSortInput) -> Self { + match input { + station_api::ListExternalCanistersSortInput::Name(direction) => { + ExternalCanisterWhereClauseSort::Name(match direction { + station_api::SortDirection::Asc => SortDirection::Ascending, + station_api::SortDirection::Desc => SortDirection::Descending, + }) + } + } + } +} + +impl From + for station_api::ExternalCanisterCallerMethodPrivilegesDTO +{ + fn from(privileges: ExternalCanisterCallerMethodsPrivileges) -> Self { + station_api::ExternalCanisterCallerMethodPrivilegesDTO { + validation_method: privileges.validation_method.into(), + execution_method: privileges.execution_method, + } + } +} + impl From for CanisterSettings { fn from(input: DefiniteCanisterSettingsInput) -> Self { CanisterSettings { diff --git a/core/station/impl/src/models/external_canister.rs b/core/station/impl/src/models/external_canister.rs index 82f48710f..8b5feebde 100644 --- a/core/station/impl/src/models/external_canister.rs +++ b/core/station/impl/src/models/external_canister.rs @@ -1,5 +1,8 @@ use super::resource::ValidationMethodResourceTarget; -use super::ConfigureExternalCanisterSettingsInput; +use super::{ + ConfigureExternalCanisterSettingsInput, ExternalCanisterPermissionsInput, + ExternalCanisterRequestPoliciesInput, +}; use crate::errors::ExternalCanisterError; use candid::Principal; use orbit_essentials::storable; @@ -7,6 +10,7 @@ use orbit_essentials::{ model::{ModelValidator, ModelValidatorResult}, types::{Timestamp, UUID}, }; +use station_api::GetExternalCanisterFiltersResponse; use std::collections::BTreeSet; use std::hash::Hash; @@ -38,6 +42,9 @@ pub struct ExternalCanister { pub modified_at: Option, } +pub type ExternalCanisterPermissions = ExternalCanisterPermissionsInput; +pub type ExternalCanisterRequestPolicies = ExternalCanisterRequestPoliciesInput; + #[storable] #[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct ExternalCanisterKey { @@ -60,10 +67,13 @@ pub struct ExternalCanisterCallerMethodsPrivileges { #[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct ExternalCanisterCallerPrivileges { pub id: UUID, + pub canister_id: Principal, pub can_change: bool, pub can_call: Vec, } +pub type ExternalCanisterAvailableFilters = GetExternalCanisterFiltersResponse; + impl ExternalCanister { pub const MAX_NAME_LENGTH: usize = 100; pub const MAX_LABEL_LENGTH: usize = 50; @@ -182,7 +192,7 @@ impl ModelValidator for ExternalCanister { } } -#[cfg(test)] +#[cfg(any(test, feature = "canbench"))] pub mod external_canister_test_utils { use super::*; use crate::core::ic_cdk::next_time; diff --git a/core/station/impl/src/models/indexes/external_canister_index.rs b/core/station/impl/src/models/indexes/external_canister_index.rs index ddaa90fde..b4c9d2514 100644 --- a/core/station/impl/src/models/indexes/external_canister_index.rs +++ b/core/station/impl/src/models/indexes/external_canister_index.rs @@ -13,13 +13,18 @@ pub struct ExternalCanisterIndex { pub index: ExternalCanisterIndexKind, /// The external canister id, which is a UUID. pub external_canister_entry_id: ExternalCanisterId, + /// The canister id of the external canister, also, it helps avoid an extra lookup for permission checks. + pub canister_id: Principal, } #[storable] #[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum ExternalCanisterIndexKind { + // Used to check if a canister id already exists. CanisterId(Principal), + // Used to check if a name already exists and to list all names to facilitate searching. Name(String), + // Used to list all labels to facilitate searching/ Label(String), } @@ -29,6 +34,7 @@ impl ExternalCanister { ExternalCanisterIndex { index: ExternalCanisterIndexKind::Name(format_unique_string(self.name.as_str())), external_canister_entry_id: self.id, + canister_id: self.canister_id, } } @@ -39,6 +45,7 @@ impl ExternalCanister { .map(|label| ExternalCanisterIndex { index: ExternalCanisterIndexKind::Label(format_unique_string(label.as_str())), external_canister_entry_id: self.id, + canister_id: self.canister_id, }) .collect() } @@ -48,12 +55,14 @@ impl ExternalCanister { ExternalCanisterIndex { index: ExternalCanisterIndexKind::CanisterId(self.canister_id), external_canister_entry_id: self.id, + canister_id: self.canister_id, } } /// Converts the external canister to indexes to facilitate searching. pub fn indexes(&self) -> Vec { let mut indexes = vec![self.to_index_by_name(), self.to_index_by_canister_id()]; + indexes.extend(self.to_index_by_labels()); indexes @@ -77,6 +86,7 @@ mod tests { let model = ExternalCanisterIndex { index: ExternalCanisterIndexKind::CanisterId(Principal::anonymous()), external_canister_entry_id: [u8::MAX; 16], + canister_id: Principal::anonymous(), }; let serialized_model = model.to_bytes(); diff --git a/core/station/impl/src/models/request_operation.rs b/core/station/impl/src/models/request_operation.rs index cd07af9a6..c06b84688 100644 --- a/core/station/impl/src/models/request_operation.rs +++ b/core/station/impl/src/models/request_operation.rs @@ -434,7 +434,7 @@ pub struct DefiniteCanisterSettingsInput { } #[storable] -#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Default)] pub struct ConfigureExternalCanisterSettingsInput { pub name: Option, pub description: Option, diff --git a/core/station/impl/src/models/user_group.rs b/core/station/impl/src/models/user_group.rs index 5acece675..c4d13e7a4 100644 --- a/core/station/impl/src/models/user_group.rs +++ b/core/station/impl/src/models/user_group.rs @@ -160,7 +160,7 @@ mod tests { } } -#[cfg(test)] +#[cfg(any(test, feature = "canbench"))] pub mod user_group_test_utils { use super::*; use orbit_essentials::repository::Repository; diff --git a/core/station/impl/src/repositories/external_canister.rs b/core/station/impl/src/repositories/external_canister.rs index 8dc6da97f..36ceddf4b 100644 --- a/core/station/impl/src/repositories/external_canister.rs +++ b/core/station/impl/src/repositories/external_canister.rs @@ -5,15 +5,15 @@ use crate::{ indexes::external_canister_index::{ ExternalCanisterIndexCriteria, ExternalCanisterIndexKind, }, - ExternalCanister, ExternalCanisterId, ExternalCanisterKey, + ExternalCanister, ExternalCanisterId, ExternalCanisterKey, ExternalCanisterState, }, }; use candid::Principal; use ic_stable_structures::{memory_manager::VirtualMemory, StableBTreeMap}; use lazy_static::lazy_static; -use orbit_essentials::repository::IndexRepository; +use orbit_essentials::repository::{IndexRepository, SortDirection}; use orbit_essentials::repository::{RefreshIndexMode, Repository}; -use std::{cell::RefCell, sync::Arc}; +use std::{cell::RefCell, collections::HashSet, sync::Arc}; thread_local! { /// The memory reference to the external canister repository. @@ -77,14 +77,8 @@ impl ExternalCanisterRepository { /// Returns an external canister by its name if it exists. pub fn find_by_name(&self, name: &str) -> Option { let name = format_unique_string(name); - let found = self - .indexes - .find_by_criteria(ExternalCanisterIndexCriteria { - from: ExternalCanisterIndexKind::Name(name.to_string()), - to: ExternalCanisterIndexKind::Name(name.to_string()), - }); - found.into_iter().next() + self.indexes.find_by_name(&name) } /// Returns an external canister by its canister id if it exists. @@ -118,6 +112,90 @@ impl ExternalCanisterRepository { self.find_by_canister_id(canister_id) .map_or(true, |existing_id| skip_id == Some(existing_id)) } + + /// Finds all the labels of the external canisters, which are unique. + pub fn find_all_labels(&self) -> Vec { + self.indexes.find_all_labels() + } + + /// Finds the names of the external canisters that start with the given prefix. + pub fn find_names_by_prefix( + &self, + prefix: &str, + ) -> Vec<(String, ExternalCanisterId, Principal)> { + self.indexes.find_names_by_prefix(prefix) + } + + /// Finds external canisters based on the provided where clause. + pub fn find_canister_ids_where( + &self, + where_clause: ExternalCanisterWhereClause, + ) -> Vec { + let filter_by_labels: HashSet = where_clause.labels.into_iter().collect(); + let filter_by_canister_ids: HashSet = + where_clause.canister_ids.into_iter().collect(); + let filter_by_states: HashSet = + where_clause.states.into_iter().collect(); + + let mut found_ids = self + .list() + .into_iter() + .filter_map(|entry| { + if !filter_by_labels.is_empty() + && !entry + .labels + .iter() + .any(|label| filter_by_labels.contains(label)) + { + return None; + } + + if !filter_by_canister_ids.is_empty() + && !filter_by_canister_ids.contains(&entry.canister_id) + { + return None; + } + + if !filter_by_states.is_empty() && !filter_by_states.contains(&entry.state) { + return None; + } + + Some((entry.name, entry.canister_id)) + }) + .collect::>(); + + let sort_by = match where_clause.sort_by { + Some(sort_by) => sort_by, + None => ExternalCanisterWhereClauseSort::Name(SortDirection::Ascending), + }; + + match sort_by { + ExternalCanisterWhereClauseSort::Name(direction) => { + found_ids.sort_by(|(name_a, _), (name_b, _)| match direction { + SortDirection::Ascending => name_a.cmp(name_b), + SortDirection::Descending => name_b.cmp(name_a), + }); + } + } + + found_ids + .into_iter() + .map(|(_, canister_id)| canister_id) + .collect() + } +} + +#[derive(Debug, Clone)] +pub enum ExternalCanisterWhereClauseSort { + Name(SortDirection), +} + +#[derive(Debug, Clone)] +pub struct ExternalCanisterWhereClause { + pub canister_ids: Vec, + pub labels: Vec, + pub states: Vec, + pub sort_by: Option, } #[cfg(test)] diff --git a/core/station/impl/src/repositories/indexes/external_canister_index.rs b/core/station/impl/src/repositories/indexes/external_canister_index.rs index ea06deaf5..51a93fd95 100644 --- a/core/station/impl/src/repositories/indexes/external_canister_index.rs +++ b/core/station/impl/src/repositories/indexes/external_canister_index.rs @@ -1,5 +1,8 @@ use crate::{ - core::{with_memory_manager, Memory, EXTERNAL_CANISTER_INDEX_MEMORY_ID}, + core::{ + utils::{MAX_PRINCIPAL, MIN_PRINCIPAL}, + with_memory_manager, Memory, EXTERNAL_CANISTER_INDEX_MEMORY_ID, + }, models::{ indexes::external_canister_index::{ ExternalCanisterIndex, ExternalCanisterIndexCriteria, ExternalCanisterIndexKind, @@ -7,6 +10,7 @@ use crate::{ ExternalCanisterId, }, }; +use candid::Principal; use ic_stable_structures::{memory_manager::VirtualMemory, StableBTreeMap}; use orbit_essentials::repository::IndexRepository; use std::{cell::RefCell, collections::HashSet}; @@ -45,10 +49,12 @@ impl IndexRepository let start_key = ExternalCanisterIndex { index: criteria.from, external_canister_entry_id: [u8::MIN; 16], + canister_id: MIN_PRINCIPAL, }; let end_key = ExternalCanisterIndex { index: criteria.to, external_canister_entry_id: [u8::MAX; 16], + canister_id: MAX_PRINCIPAL, }; db.borrow() @@ -60,6 +66,8 @@ impl IndexRepository } impl ExternalCanisterIndexRepository { + pub const MAX_NAME_PREFIX_LIST_LIMIT: usize = 250; + pub fn len(&self) -> u64 { DB.with(|m| m.borrow().len()) } @@ -69,24 +77,56 @@ impl ExternalCanisterIndexRepository { } /// Finds the names of the external canisters that start with the given prefix. - pub fn find_names_by_prefix(&self, prefix: &str) -> Vec { + /// + /// Returns a list of names, external canister ids, and their canister ids. + pub fn find_names_by_prefix( + &self, + prefix: &str, + ) -> Vec<(String, ExternalCanisterId, Principal)> { DB.with(|db| { db.borrow() .range((ExternalCanisterIndex { index: ExternalCanisterIndexKind::Name(prefix.to_string()), external_canister_entry_id: [u8::MIN; 16], + canister_id: MIN_PRINCIPAL, })..) - .take_while(|(index, _)| { - matches!(&index.index, ExternalCanisterIndexKind::Name(name) if name.starts_with(prefix)) - }) + .take_while(|(index, _)| matches!(&index.index, ExternalCanisterIndexKind::Name(name) if name.starts_with(prefix))) .filter_map(|(index, _)| match &index.index { - ExternalCanisterIndexKind::Name(name) => Some(name.clone()), + ExternalCanisterIndexKind::Name(name) => Some((name.clone(), index.external_canister_entry_id, index.canister_id)), _ => None, }) .collect() }) } + /// Finds the external canister that matches the given name. + pub fn find_by_name(&self, search_name: &str) -> Option { + DB.with(|db| { + db.borrow() + .range( + (ExternalCanisterIndex { + index: ExternalCanisterIndexKind::Name(search_name.to_string()), + external_canister_entry_id: [u8::MIN; 16], + canister_id: MIN_PRINCIPAL, + })..=(ExternalCanisterIndex { + index: ExternalCanisterIndexKind::Name(search_name.to_string()), + external_canister_entry_id: [u8::MAX; 16], + canister_id: MAX_PRINCIPAL, + }), + ) + .filter_map(|(index, _)| match &index.index { + ExternalCanisterIndexKind::Name(name) => match name == search_name { + true => Some(index.external_canister_entry_id), + false => None, + }, + _ => None, + }) + .collect::>() + .first() + .cloned() + }) + } + /// Finds all the labels of the external canisters, which are unique. pub fn find_all_labels(&self) -> Vec { DB.with(|db| { @@ -95,6 +135,7 @@ impl ExternalCanisterIndexRepository { (ExternalCanisterIndex { index: ExternalCanisterIndexKind::Label(String::new()), external_canister_entry_id: [u8::MIN; 16], + canister_id: MIN_PRINCIPAL, }).., ) .take_while(|(index, _)| { @@ -128,6 +169,7 @@ mod tests { let index = ExternalCanisterIndex { index: ExternalCanisterIndexKind::Name("test".to_string()), external_canister_entry_id: [1; 16], + canister_id: Principal::from_slice(&[1; 29]), }; assert!(!repository.exists(&index)); @@ -146,17 +188,16 @@ mod tests { repository.insert(ExternalCanisterIndex { index: ExternalCanisterIndexKind::Name(format!("test-{}", i)), external_canister_entry_id: [i; 16], + canister_id: Principal::from_slice(&[i; 29]), }); } - let result = repository.find_by_criteria(ExternalCanisterIndexCriteria { - from: ExternalCanisterIndexKind::Name("test-5".to_string()), - to: ExternalCanisterIndexKind::Name("test-5".to_string()), - }); + for i in 0..10 { + let result = repository.find_by_name(&format!("test-{}", i)); - assert!(!result.is_empty()); - assert_eq!(result.len(), 1); - assert!(result.contains(&[5; 16])); + assert!(result.is_some()); + assert_eq!(result.unwrap(), [i; 16]); + } } #[test] @@ -166,6 +207,7 @@ mod tests { repository.insert(ExternalCanisterIndex { index: ExternalCanisterIndexKind::CanisterId(Principal::from_slice(&[i; 29])), external_canister_entry_id: [i; 16], + canister_id: Principal::from_slice(&[i; 29]), }); } @@ -179,27 +221,6 @@ mod tests { assert!(result.contains(&[5; 16])); } - #[test] - fn test_find_by_labels() { - let repository = ExternalCanisterIndexRepository::default(); - for i in 0..10 { - repository.insert(ExternalCanisterIndex { - index: ExternalCanisterIndexKind::Label(format!("label-{}", i)), - external_canister_entry_id: [i; 16], - }); - } - - let result = repository.find_by_criteria(ExternalCanisterIndexCriteria { - from: ExternalCanisterIndexKind::Label("label-5".to_string()), - to: ExternalCanisterIndexKind::Label("label-6".to_string()), - }); - - assert!(!result.is_empty()); - assert_eq!(result.len(), 2); - assert!(result.contains(&[5; 16])); - assert!(result.contains(&[6; 16])); - } - #[test] fn test_find_by_name_prefix() { let repository = ExternalCanisterIndexRepository::default(); @@ -210,6 +231,7 @@ mod tests { repository.insert(ExternalCanisterIndex { index: ExternalCanisterIndexKind::Name(index_name.clone()), external_canister_entry_id: [i; 16], + canister_id: Principal::from_slice(&[i; 29]), }); if index_name.starts_with(search_prefix) { @@ -217,7 +239,11 @@ mod tests { } } - let result = repository.find_names_by_prefix("test2"); + let result = repository + .find_names_by_prefix("test2") + .into_iter() + .map(|(name, _, _)| name) + .collect::>(); assert!(!result.is_empty()); assert_eq!(result.len(), expected_results.len()); @@ -233,6 +259,7 @@ mod tests { repository.insert(ExternalCanisterIndex { index: ExternalCanisterIndexKind::Label(format!("label-{}", i)), external_canister_entry_id: [i; 16], + canister_id: Principal::from_slice(&[i; 29]), }); } @@ -240,6 +267,7 @@ mod tests { repository.insert(ExternalCanisterIndex { index: ExternalCanisterIndexKind::Label(format!("label-{}", i)), external_canister_entry_id: [i + 20; 16], + canister_id: Principal::from_slice(&[i + 20; 29]), }); } diff --git a/core/station/impl/src/services/external_canister.rs b/core/station/impl/src/services/external_canister.rs index 04fe81e54..8526691bc 100644 --- a/core/station/impl/src/services/external_canister.rs +++ b/core/station/impl/src/services/external_canister.rs @@ -1,7 +1,10 @@ use super::permission::{PermissionService, PERMISSION_SERVICE}; use super::request_policy::{RequestPolicyService, REQUEST_POLICY_SERVICE}; +use crate::core::authorization::Authorization; use crate::core::ic_cdk::api::print; +use crate::core::utils::{retain_accessible_resources, PaginatedData}; use crate::core::validation::EnsureExternalCanister; +use crate::core::CallContext; use crate::errors::ExternalCanisterError; use crate::mappers::ExternalCanisterMapper; use crate::models::request_specifier::RequestSpecifier; @@ -14,13 +17,16 @@ use crate::models::{ AddRequestPolicyOperationInput, CanisterMethod, ConfigureExternalCanisterSettingsInput, CreateExternalCanisterOperationInput, CreateExternalCanisterOperationKind, DefiniteCanisterSettingsInput, EditPermissionOperationInput, EditRequestPolicyOperationInput, - ExternalCanister, ExternalCanisterId, ExternalCanisterPermissionsInput, - ExternalCanisterRequestPoliciesInput, + ExternalCanister, ExternalCanisterAvailableFilters, ExternalCanisterCallPermission, + ExternalCanisterCallRequestPolicyRuleInput, ExternalCanisterCallerMethodsPrivileges, + ExternalCanisterCallerPrivileges, ExternalCanisterChangeRequestPolicyRuleInput, + ExternalCanisterId, ExternalCanisterPermissions, ExternalCanisterPermissionsInput, + ExternalCanisterRequestPolicies, ExternalCanisterRequestPoliciesInput, RequestPolicy, }; use crate::repositories::permission::{PermissionRepository, PERMISSION_REPOSITORY}; use crate::repositories::{ - ExternalCanisterRepository, RequestPolicyRepository, EXTERNAL_CANISTER_REPOSITORY, - REQUEST_POLICY_REPOSITORY, + ExternalCanisterRepository, ExternalCanisterWhereClause, RequestPolicyRepository, + EXTERNAL_CANISTER_REPOSITORY, REQUEST_POLICY_REPOSITORY, }; use candid::{Encode, Principal}; use ic_cdk::api::call::call_raw; @@ -31,8 +37,13 @@ use ic_cdk::api::management_canister::main::{ use lazy_static::lazy_static; use orbit_essentials::api::ServiceResult; use orbit_essentials::model::ModelValidator; +use orbit_essentials::pagination::{paginated_items, PaginatedItemsArgs}; use orbit_essentials::repository::Repository; use orbit_essentials::types::UUID; +use station_api::{ + GetExternalCanisterFiltersInput, GetExternalCanisterFiltersResponseNameEntry, + ListExternalCanistersInput, +}; use std::collections::HashSet; use std::sync::Arc; use uuid::Uuid; @@ -60,6 +71,9 @@ pub struct ExternalCanisterService { } impl ExternalCanisterService { + const DEFAULT_LIST_LIMIT: u16 = 25; + const MAX_LIST_LIMIT: u16 = 250; + pub fn new( external_canister_repository: Arc, permission_service: Arc, @@ -106,6 +120,290 @@ impl ExternalCanisterService { self.get_external_canister(&recource_id) } + /// Returns all request policies of the external canister by its canister id. + /// + /// The policies are grouped by the type of request they are for: + /// + /// - `calls`: Policies for calling the external canister. + /// - `change`: Policies for changing the external canister. + pub fn get_external_canister_request_policies( + &self, + canister_id: &Principal, + ) -> ExternalCanisterRequestPolicies { + let policies = self + .request_policy_repository + .find_external_canister_policies(canister_id) + .iter() + .filter_map(|policy_id| self.request_policy_repository.get(policy_id)) + .collect::>(); + + let calls = policies + .iter() + .filter_map(|policy| match &policy.specifier { + RequestSpecifier::CallExternalCanister(target) => match target { + CallExternalCanisterResourceTarget { + execution_method: + ExecutionMethodResourceTarget::ExecutionMethod(CanisterMethod { + canister_id: target_canister_id, + method_name, + }), + validation_method, + } if *target_canister_id == *canister_id => { + Some(ExternalCanisterCallRequestPolicyRuleInput { + policy_id: Some(policy.id), + execution_method: method_name.clone(), + validation_method: validation_method.clone(), + rule: policy.rule.clone(), + }) + } + _ => None, + }, + _ => None, + }) + .collect::>(); + + let change = policies + .iter() + .filter_map(|policy| match &policy.specifier { + RequestSpecifier::ChangeExternalCanister(target) => match target { + ChangeExternalCanisterResourceTarget::Canister(target_canister_id) + if *target_canister_id == *canister_id => + { + Some(ExternalCanisterChangeRequestPolicyRuleInput { + policy_id: Some(policy.id), + rule: policy.rule.clone(), + }) + } + _ => None, + }, + _ => None, + }) + .collect::>(); + + ExternalCanisterRequestPolicies { calls, change } + } + + /// Returns the permissions of the external canister by its canister id. + /// + /// The permissions are grouped by the type of action they are for: + /// + /// - `read`: Permissions for reading the external canister. + /// - `change`: Permissions for changing the external canister. + /// - `calls`: Permissions for calling the external canister. + pub fn get_external_canister_permissions( + &self, + canister_id: &Principal, + ) -> ExternalCanisterPermissions { + let read_permission = self + .permission_service + .get_permission(&Resource::ExternalCanister( + ExternalCanisterResourceAction::Read(ReadExternalCanisterResourceTarget::Canister( + *canister_id, + )), + )); + + let change_permission = + self.permission_service + .get_permission(&Resource::ExternalCanister( + ExternalCanisterResourceAction::Change( + ChangeExternalCanisterResourceTarget::Canister(*canister_id), + ), + )); + + ExternalCanisterPermissions { + read: read_permission.allow, + change: change_permission.allow, + calls: self.find_external_canister_call_permissions(canister_id), + } + } + + fn find_external_canister_call_permissions( + &self, + canister_id: &Principal, + ) -> Vec { + self.permission_repository + .find_external_canister_call_permissions(canister_id) + .iter() + .filter_map(|permission| match &permission.resource { + Resource::ExternalCanister(ExternalCanisterResourceAction::Call(target)) => { + match &target { + CallExternalCanisterResourceTarget { + execution_method: + ExecutionMethodResourceTarget::ExecutionMethod(CanisterMethod { + canister_id: target_canister_id, + method_name, + }), + validation_method, + } if *target_canister_id == *canister_id => { + Some(ExternalCanisterCallPermission { + allow: permission.allow.clone(), + execution_method: method_name.clone(), + validation_method: validation_method.clone(), + }) + } + _ => None, + } + } + _ => None, + }) + .collect() + } + + /// Returns the permissions of the caller for the external canister. + pub fn get_caller_privileges_for_external_canister( + &self, + entry_id: &UUID, + canister_id: &Principal, + ctx: &CallContext, + ) -> ExternalCanisterCallerPrivileges { + ExternalCanisterCallerPrivileges { + id: *entry_id, + canister_id: *canister_id, + can_change: Authorization::is_allowed( + ctx, + &Resource::ExternalCanister(ExternalCanisterResourceAction::Change( + ChangeExternalCanisterResourceTarget::Canister(*canister_id), + )), + ), + can_call: self + .find_external_canister_call_permissions(canister_id) + .iter() + .filter_map(|p| { + let can_call = Authorization::is_allowed( + ctx, + &Resource::ExternalCanister(ExternalCanisterResourceAction::Call( + CallExternalCanisterResourceTarget { + execution_method: ExecutionMethodResourceTarget::ExecutionMethod( + CanisterMethod { + canister_id: *canister_id, + method_name: p.execution_method.clone(), + }, + ), + validation_method: p.validation_method.clone(), + }, + )), + ); + + match can_call { + true => Some(ExternalCanisterCallerMethodsPrivileges { + validation_method: p.validation_method.clone(), + execution_method: p.execution_method.clone(), + }), + false => None, + } + }) + .collect(), + } + } + + /// Lists all external canisters that match the given filters. + /// + /// Filters can contain: + /// + /// - `canister_ids`: A list of canister ids to filter by. + /// - `labels`: A list of labels to filter by. + /// - `paginate`: Pagination settings. + /// + pub fn list_external_canisters( + &self, + input: ListExternalCanistersInput, + ctx: &CallContext, + ) -> ServiceResult> { + let mut found_ids = self.external_canister_repository.find_canister_ids_where( + ExternalCanisterWhereClause { + canister_ids: input.canister_ids.clone().unwrap_or_default(), + labels: input.labels.clone().unwrap_or_default(), + states: input + .states + .map(|states| states.into_iter().map(Into::into).collect()) + .unwrap_or_default(), + sort_by: input.sort_by.clone().map(Into::into), + }, + ); + + // filter out requests that the caller does not have access to read + retain_accessible_resources(ctx, &mut found_ids, |id| { + Resource::ExternalCanister(ExternalCanisterResourceAction::Read( + ReadExternalCanisterResourceTarget::Canister(*id), + )) + }); + + let paginated_ids = paginated_items(PaginatedItemsArgs { + offset: input.paginate.to_owned().and_then(|p| p.offset), + limit: input.paginate.and_then(|p| p.limit), + default_limit: Some(Self::DEFAULT_LIST_LIMIT), + max_limit: Some(Self::MAX_LIST_LIMIT), + items: &found_ids, + })?; + + Ok(PaginatedData { + total: paginated_ids.total, + next_offset: paginated_ids.next_offset, + items: paginated_ids + .items + .into_iter() + .flat_map(|id| match self.get_external_canister_by_canister_id(&id) { + Ok(entry) => Some(entry), + Err(error) => { + print(format!( + "Failed to get external canister entry {}: {:?}", + id.to_text(), + error + )); + None + } + }) + .collect::>(), + }) + } + + /// Lists the available information that facilitates filtering external canisters. + /// + /// These helpers can contain: + /// + /// - `name`: The available names of existing external canisters. + /// - `labels`: The available labels of existing external canisters. + pub fn available_external_canisters_filters( + &self, + input: GetExternalCanisterFiltersInput, + ctx: &CallContext, + ) -> ExternalCanisterAvailableFilters { + let mut names = input.with_name.as_ref().map(|name| { + self.external_canister_repository + .find_names_by_prefix(name.prefix.clone().unwrap_or_default().as_str()) + .iter() + .map( + |(name, _, canister_id)| GetExternalCanisterFiltersResponseNameEntry { + name: name.clone(), + canister_id: *canister_id, + }, + ) + .collect::>() + }); + + // filter out names that the caller does not have access to read + if let Some(ref mut names) = &mut names { + retain_accessible_resources(ctx, names, |entry| { + Resource::ExternalCanister(ExternalCanisterResourceAction::Read( + ReadExternalCanisterResourceTarget::Canister(entry.canister_id), + )) + }); + + names.truncate(Self::MAX_LIST_LIMIT as usize) + } + + let labels = match input.with_labels { + Some(true) => Some(self.external_canister_repository.find_all_labels()), + _ => None, + }; + + ExternalCanisterAvailableFilters { names, labels } + } + + /// Creates a new external canister. + /// + /// Optionally, the caller can provide the initial cycles to deposit into the canister, if not provided, + /// the default value will be used. pub async fn create_canister( &self, cycles: Option, @@ -126,6 +424,9 @@ impl ExternalCanisterService { Ok(canister_id) } + /// Calls the management canister to get the status of the canister with the given id. + /// + /// The station needs to be a controller of the target canister. pub async fn canister_status( &self, input: CanisterIdRecord, @@ -144,6 +445,7 @@ impl ExternalCanisterService { Ok(canister_status_response) } + /// Calls the target canister with the given method, argument, and cycles. pub async fn call_external_canister( &self, canister_id: Principal, @@ -634,7 +936,8 @@ mod tests { use crate::{ core::test_utils, models::{ - permission::Allow, resource::ValidationMethodResourceTarget, + permission::{Allow, AuthScope}, + resource::ValidationMethodResourceTarget, CreateExternalCanisterOperationKindAddExisting, ExternalCanisterCallPermission, ExternalCanisterCallRequestPolicyRuleInput, ExternalCanisterChangeRequestPolicyRuleInput, ExternalCanisterPermissionsInput, @@ -1005,4 +1308,231 @@ mod tests { assert!(call_permission.is_empty()); } + + #[tokio::test] + async fn finds_all_call_permissions() { + setup(); + for i in 0..2 { + let _ = EXTERNAL_CANISTER_SERVICE + .add_external_canister(CreateExternalCanisterOperationInput { + name: format!("test{}", i), + description: None, + labels: None, + permissions: ExternalCanisterPermissionsInput { + read: Allow::authenticated(), + change: Allow::authenticated(), + calls: vec![ + ExternalCanisterCallPermission { + allow: Allow::authenticated(), + execution_method: "test".to_string(), + validation_method: ValidationMethodResourceTarget::No, + }, + ExternalCanisterCallPermission { + allow: Allow::authenticated(), + execution_method: "test".to_string(), + validation_method: ValidationMethodResourceTarget::ValidationMethod( + CanisterMethod { + canister_id: Principal::from_slice(&[i; 29]), + method_name: "validate_test".to_string(), + }, + ), + }, + ], + }, + request_policies: ExternalCanisterRequestPoliciesInput { + change: Vec::new(), + calls: Vec::new(), + }, + kind: CreateExternalCanisterOperationKind::AddExisting( + CreateExternalCanisterOperationKindAddExisting { + canister_id: Principal::from_slice(&[i; 29]), + }, + ), + }) + .await + .unwrap(); + } + + let permissions = EXTERNAL_CANISTER_SERVICE + .get_external_canister_permissions(&Principal::from_slice(&[1; 29])); + + assert_eq!(permissions.read.auth_scope, AuthScope::Authenticated); + assert_eq!(permissions.change.auth_scope, AuthScope::Authenticated); + assert_eq!(permissions.calls.len(), 2); + } + + #[tokio::test] + async fn finds_request_policies_of_external_canister() { + setup(); + for i in 0..2 { + let _ = EXTERNAL_CANISTER_SERVICE + .add_external_canister(CreateExternalCanisterOperationInput { + name: format!("test{}", i), + description: None, + labels: None, + permissions: ExternalCanisterPermissionsInput { + read: Allow::authenticated(), + change: Allow::authenticated(), + calls: Vec::new(), + }, + request_policies: ExternalCanisterRequestPoliciesInput { + change: vec![ + ExternalCanisterChangeRequestPolicyRuleInput { + policy_id: None, + rule: RequestPolicyRule::AutoApproved, + }, + ExternalCanisterChangeRequestPolicyRuleInput { + policy_id: None, + rule: RequestPolicyRule::AutoApproved, + }, + ], + calls: vec![ExternalCanisterCallRequestPolicyRuleInput { + policy_id: None, + execution_method: "test".to_string(), + validation_method: ValidationMethodResourceTarget::No, + rule: RequestPolicyRule::AutoApproved, + }], + }, + kind: CreateExternalCanisterOperationKind::AddExisting( + CreateExternalCanisterOperationKindAddExisting { + canister_id: Principal::from_slice(&[i; 29]), + }, + ), + }) + .await + .unwrap(); + } + + let policies = EXTERNAL_CANISTER_SERVICE + .get_external_canister_request_policies(&Principal::from_slice(&[1; 29])); + + assert_eq!(policies.calls.len(), 1); + assert_eq!(policies.change.len(), 2); + } +} + +#[cfg(feature = "canbench")] +mod benchs { + use super::*; + use crate::{models::ExternalCanisterState, services::user_service_test_utils::add_users}; + use canbench_rs::{bench, BenchResult}; + use external_canister_test_utils::add_test_external_canisters; + + #[bench(raw)] + fn list_external_canisters_with_all_statuses() -> BenchResult { + // creates 20 admin users with 5 groups assigned + let admins = add_users(20, 5); + // and 100 employees with 10 groups assigned + let _ = add_users(100, 10); + + let first_admin = admins.first().expect("Unexpected admin not set"); + let first_admin_identity = first_admin + .identities + .first() + .expect("Unexpected admin identity not available"); + + let first_admin = first_admin.clone(); + let caller_identity = first_admin_identity.clone(); + + // these should only be accessible to admins + add_test_external_canisters( + 500, // adds 500 external canisters managed by the station + 10, // with 10 individual method calls each + ExternalCanisterState::Active, + Some(first_admin.groups.clone()), + ); + + // these are accessible by any employee + add_test_external_canisters( + 1500, // adds 1500 external canisters managed by the station + 5, // with 5 individual method calls each + ExternalCanisterState::Active, + None, + ); + + // also adds 1000 archived external canisters + add_test_external_canisters( + 1000, // adds 1000 external canisters managed by the station + 5, // with 5 individual method calls each + ExternalCanisterState::Archived, + None, + ); + + canbench_rs::bench_fn(|| { + let result = EXTERNAL_CANISTER_SERVICE + .list_external_canisters( + ListExternalCanistersInput { + canister_ids: None, + labels: None, + states: Some(vec![ + station_api::ExternalCanisterStateDTO::Active, + station_api::ExternalCanisterStateDTO::Archived, + ]), + paginate: Some(station_api::PaginationInput { + limit: Some(25), + offset: None, + }), + sort_by: Some(station_api::ListExternalCanistersSortInput::Name( + station_api::SortDirection::Asc, + )), + }, + &CallContext::new(caller_identity), + ) + .expect("Unexpected failed search of external canisters"); + + if result.total != 3000 { + panic!( + "Unexpected total count of external canisters, expected 3000, got {}", + result.total + ); + } + }) + } +} + +#[cfg(feature = "canbench")] +mod external_canister_test_utils { + use super::*; + use crate::models::{ + external_canister_test_utils::mock_external_canister, permission::Allow, + ExternalCanisterState, + }; + + pub fn add_test_external_canisters( + canisters_count: usize, + calls_count: usize, + state: ExternalCanisterState, + allow_user_groups: Option>, + ) { + let allow = match allow_user_groups { + Some(groups) => Allow::user_groups(groups), + None => Allow::authenticated(), + }; + + for _ in 0..canisters_count { + let mut external_canister = mock_external_canister(); + external_canister.state = state.clone(); + let calls = (0..calls_count) + .map(|i| ExternalCanisterCallPermission { + allow: Allow::authenticated(), + execution_method: format!("exec_method_{}", i), + validation_method: ValidationMethodResourceTarget::No, + }) + .collect::>(); + + EXTERNAL_CANISTER_REPOSITORY + .insert(external_canister.to_key(), external_canister.clone()); + + let mut input = ConfigureExternalCanisterSettingsInput::default(); + input.permissions = Some(ExternalCanisterPermissionsInput { + calls, + read: allow.clone(), + change: allow.clone(), + }); + + EXTERNAL_CANISTER_SERVICE + .edit_external_canister(&external_canister.id, input) + .expect("Unexpected error while configuring external canister"); + } + } } diff --git a/core/station/impl/src/services/user.rs b/core/station/impl/src/services/user.rs index 1e64465f6..759e8a41a 100644 --- a/core/station/impl/src/services/user.rs +++ b/core/station/impl/src/services/user.rs @@ -578,3 +578,32 @@ mod tests { assert!(privileges.contains(&UserPrivilege::AddUser)); } } + +#[cfg(any(test, feature = "canbench"))] +pub mod user_service_test_utils { + use super::*; + use crate::models::user_group_test_utils::add_group; + + pub fn add_users(users_count: u8, groups_count: u8) -> Vec { + let mut groups = Vec::new(); + let mut users = Vec::new(); + for _ in 0..groups_count { + let group_name = Uuid::new_v4().to_string(); + groups.push(add_group(&group_name)); + } + + for _ in 0..users_count { + let user_id = Uuid::new_v4(); + let input = AddUserOperationInput { + identities: vec![Principal::from_slice(user_id.as_bytes())], + groups: groups.iter().map(|g| g.id).collect(), + status: UserStatus::Active, + name: user_id.to_string(), + }; + + users.push(USER_SERVICE.add_user(input).unwrap()); + } + + users + } +}