mod log;
mod role;
mod subnet;
pub use log::*;
pub use role::*;
pub use subnet::*;
use crate::{
InternalError, InternalErrorOrigin,
cdk::candid::Principal,
ids::{CanisterRole, SubnetRole},
};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
use thiserror::Error as ThisError;
#[derive(Debug, ThisError)]
pub enum ConfigSchemaError {
#[error("validation error: {0}")]
ValidationError(String),
}
#[cfg(any(not(target_arch = "wasm32"), test))]
pub const NAME_MAX_BYTES: usize = 40;
impl From<ConfigSchemaError> for InternalError {
fn from(err: ConfigSchemaError) -> Self {
Self::domain(InternalErrorOrigin::Config, err.to_string())
}
}
#[cfg(any(not(target_arch = "wasm32"), test))]
pub trait Validate {
fn validate(&self) -> Result<(), ConfigSchemaError>;
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ConfigModel {
#[serde(default)]
pub fleet: Option<FleetConfig>,
#[serde(default)]
pub controllers: Vec<Principal>,
#[serde(default)]
pub standards: Option<Standards>,
#[serde(default)]
pub log: LogConfig,
#[serde(default)]
pub auth: AuthConfig,
#[serde(default)]
pub app: AppConfig,
#[serde(default)]
pub app_index: BTreeSet<CanisterRole>,
#[serde(default)]
pub roles: BTreeMap<CanisterRole, RoleDeclaration>,
#[serde(default)]
pub subnets: BTreeMap<SubnetRole, SubnetConfig>,
}
impl ConfigModel {
#[must_use]
pub fn get_subnet(&self, role: &SubnetRole) -> Option<SubnetConfig> {
self.subnets.get(role).cloned()
}
#[must_use]
pub fn fleet_name(&self) -> Option<&str> {
self.fleet.as_ref().and_then(|fleet| fleet.name.as_deref())
}
#[must_use]
pub fn fleet_role_ref(&self, role: &CanisterRole) -> Option<FleetRoleRefV1> {
self.fleet_name()
.map(|fleet| FleetRoleRefV1::new(fleet, role.clone()))
}
#[must_use]
pub fn declares_role(&self, role: &CanisterRole) -> bool {
self.roles.contains_key(role)
}
#[must_use]
pub fn attached_roles(&self) -> BTreeSet<CanisterRole> {
let mut attached = BTreeSet::new();
let mut pending = Vec::new();
for subnet in self.subnets.values() {
for role in subnet.canisters.keys() {
if attached.insert(role.clone()) {
pending.push(role.clone());
}
}
}
while let Some(role) = pending.pop() {
for subnet in self.subnets.values() {
let Some(canister) = subnet.canisters.get(&role) else {
continue;
};
for child in canister.role_bearing_child_roles() {
if attached.insert(child.clone()) {
pending.push(child.clone());
}
}
}
}
attached
}
#[must_use]
pub fn attached_fleet_roles(&self) -> BTreeSet<FleetRoleRefV1> {
let Some(fleet) = self.fleet_name() else {
return BTreeSet::new();
};
self.attached_roles()
.into_iter()
.map(|role| FleetRoleRefV1::new(fleet, role))
.collect()
}
#[cfg(test)]
#[must_use]
pub fn test_default() -> Self {
let mut cfg = Self::default();
let mut prime = SubnetConfig::default();
prime.canisters.insert(
CanisterRole::ROOT,
CanisterConfig {
kind: CanisterKind::Root,
initial_cycles: crate::cdk::types::Cycles::new(0),
topup: None,
randomness: RandomnessConfig::default(),
scaling: None,
sharding: None,
directory: None,
auth: CanisterAuthConfig::default(),
standards: StandardsCanisterConfig::default(),
metrics: MetricsCanisterConfig::default(),
},
);
cfg.fleet = Some(FleetConfig {
name: Some("test".to_string()),
});
cfg.roles.insert(
CanisterRole::ROOT,
RoleDeclaration {
kind: RoleDeclarationKind::Root,
package: "root".to_string(),
},
);
cfg.subnets.insert(SubnetRole::PRIME, prime);
cfg
}
#[must_use]
pub fn is_whitelisted(&self, principal: &Principal) -> bool {
self.app
.whitelist
.as_ref()
.is_none_or(|w| w.principals.contains(&principal.to_string()))
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct FleetConfig {
#[serde(default)]
pub name: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct AppConfig {
#[serde(default)]
pub init_mode: AppInitMode,
#[serde(default)]
pub whitelist: Option<Whitelist>,
}
impl Default for AppConfig {
fn default() -> Self {
Self {
init_mode: AppInitMode::Enabled,
whitelist: None,
}
}
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum AppInitMode {
#[default]
Enabled,
Readonly,
Disabled,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct AuthConfig {
#[serde(default)]
pub delegated_tokens: DelegatedTokenConfig,
#[serde(default)]
pub role_attestation: RoleAttestationConfig,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct DelegatedTokenConfig {
#[serde(default = "default_delegated_tokens_enabled")]
pub enabled: bool,
#[serde(default = "default_delegated_tokens_ecdsa_key_name")]
pub ecdsa_key_name: String,
#[serde(default)]
pub max_ttl_secs: Option<u64>,
}
const fn default_delegated_tokens_enabled() -> bool {
true
}
fn default_delegated_tokens_ecdsa_key_name() -> String {
"key_1".to_string()
}
impl Default for DelegatedTokenConfig {
fn default() -> Self {
Self {
enabled: default_delegated_tokens_enabled(),
ecdsa_key_name: default_delegated_tokens_ecdsa_key_name(),
max_ttl_secs: None,
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct RoleAttestationConfig {
#[serde(default = "default_role_attestation_ecdsa_key_name")]
pub ecdsa_key_name: String,
#[serde(default = "default_role_attestation_max_ttl_secs")]
pub max_ttl_secs: u64,
#[serde(default)]
pub min_accepted_epoch_by_role: BTreeMap<String, u64>,
}
fn default_role_attestation_ecdsa_key_name() -> String {
"key_1".to_string()
}
const fn default_role_attestation_max_ttl_secs() -> u64 {
900
}
impl Default for RoleAttestationConfig {
fn default() -> Self {
Self {
ecdsa_key_name: default_role_attestation_ecdsa_key_name(),
max_ttl_secs: default_role_attestation_max_ttl_secs(),
min_accepted_epoch_by_role: BTreeMap::new(),
}
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Whitelist {
#[serde(default)]
pub principals: BTreeSet<String>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Standards {
#[serde(default)]
pub icrc21: bool,
#[serde(default)]
pub icrc103: bool,
}
#[cfg(test)]
mod tests;