diff --git a/src/backend/backend.did b/src/backend/backend.did index 8d566338ae..ca3aab0c4b 100644 --- a/src/backend/backend.did +++ b/src/backend/backend.did @@ -10,6 +10,7 @@ type AddUserCredentialRequest = record { current_user_version : opt nat64; credential_spec : CredentialSpec; }; +type ApiEnabled = variant { ReadOnly; Enabled; Disabled }; type Arg = variant { Upgrade; Init : InitArg }; type ArgumentValue = variant { Int : int32; String : text }; type CanisterStatusResultV2 = record { @@ -25,6 +26,7 @@ type CanisterStatusResultV2 = record { }; type CanisterStatusType = variant { stopped; stopping; running }; type Config = record { + api : opt Guards; ecdsa_key_name : text; allowed_callers : vec principal; supported_credentials : opt vec SupportedCredential; @@ -48,6 +50,7 @@ type DefiniteCanisterSettingsArgs = record { compute_allocation : nat; }; type GetUserProfileError = variant { NotFound }; +type Guards = record { user_data : ApiEnabled; threshold_key : ApiEnabled }; type HttpRequest = record { url : text; method : text; @@ -61,6 +64,7 @@ type HttpResponse = record { }; type IcrcToken = record { ledger_id : principal; index_id : opt principal }; type InitArg = record { + api : opt Guards; ecdsa_key_name : text; allowed_callers : vec principal; supported_credentials : opt vec SupportedCredential; diff --git a/src/backend/src/guards.rs b/src/backend/src/guards.rs index 6b9153d6a2..ea523e6ed0 100644 --- a/src/backend/src/guards.rs +++ b/src/backend/src/guards.rs @@ -19,3 +19,48 @@ pub fn caller_is_allowed() -> Result<(), String> { Err("Caller is not allowed.".to_string()) } } + +/// User data writes are locked during and after a migration away to another canister. +pub fn may_write_user_data() -> Result<(), String> { + caller_is_not_anonymous()?; + if read_config(|s| s.api.unwrap_or_default().user_data.writable()) { + Ok(()) + } else { + Err("User data is in read only mode due to a migration.".to_string()) + } +} + +/// User data writes are locked during and after a migration away to another canister. +pub fn may_read_user_data() -> Result<(), String> { + caller_is_not_anonymous()?; + if read_config(|s| s.api.unwrap_or_default().user_data.readable()) { + Ok(()) + } else { + Err("User data cannot be read at this time due to a migration.".to_string()) + } +} + +/// Is getting threshold public keys is enabled? +pub fn may_read_threshold_keys() -> Result<(), String> { + caller_is_not_anonymous()?; + if read_config(|s| s.api.unwrap_or_default().threshold_key.readable()) { + Ok(()) + } else { + Err("Reading threshold keys is disabled.".to_string()) + } +} +/// Caller is allowed AND reading threshold keys is enabled. +pub fn caller_is_allowed_and_may_read_threshold_keys() -> Result<(), String> { + caller_is_allowed()?; + may_read_threshold_keys() +} + +/// Is signing with threshold keys is enabled? +pub fn may_threshold_sign() -> Result<(), String> { + caller_is_not_anonymous()?; + if read_config(|s| s.api.unwrap_or_default().threshold_key.writable()) { + Ok(()) + } else { + Err("Threshold signing is disabled.".to_string()) + } +} diff --git a/src/backend/src/lib.rs b/src/backend/src/lib.rs index ed0b1825a5..a2e173c742 100644 --- a/src/backend/src/lib.rs +++ b/src/backend/src/lib.rs @@ -1,5 +1,8 @@ use crate::assertions::{assert_token_enabled_is_some, assert_token_symbol_length}; -use crate::guards::{caller_is_allowed, caller_is_not_anonymous}; +use crate::guards::{ + caller_is_allowed, caller_is_allowed_and_may_read_threshold_keys, may_read_threshold_keys, + may_read_user_data, may_threshold_sign, may_write_user_data, +}; use crate::token::{add_to_user_token, remove_from_user_token}; use candid::{Nat, Principal}; use config::find_credential_config; @@ -216,13 +219,13 @@ fn parse_eth_address(address: &str) -> [u8; 20] { } /// Returns the Ethereum address of the caller. -#[update(guard = "caller_is_not_anonymous")] +#[update(guard = "may_read_threshold_keys")] async fn caller_eth_address() -> String { pubkey_bytes_to_address(&ecdsa_pubkey_of(&ic_cdk::caller()).await) } -/// Returns the Ethereum address of the specified . -#[update(guard = "caller_is_allowed")] +/// Returns the Ethereum address of the specified user. +#[update(guard = "caller_is_allowed_and_may_read_threshold_keys")] async fn eth_address_of(p: Principal) -> String { if p == Principal::anonymous() { ic_cdk::trap("Anonymous principal is not authorized"); @@ -261,7 +264,7 @@ async fn pubkey_and_signature(caller: &Principal, message_hash: Vec) -> (Vec } /// Computes a signature for an [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) transaction. -#[update(guard = "caller_is_not_anonymous")] +#[update(guard = "may_threshold_sign")] async fn sign_transaction(req: SignRequest) -> String { use ethers_core::types::transaction::eip1559::Eip1559TransactionRequest; use ethers_core::types::Signature; @@ -309,7 +312,7 @@ async fn sign_transaction(req: SignRequest) -> String { } /// Computes a signature for a hex-encoded message according to [EIP-191](https://eips.ethereum.org/EIPS/eip-191). -#[update(guard = "caller_is_not_anonymous")] +#[update(guard = "may_threshold_sign")] async fn personal_sign(plaintext: String) -> String { let caller = ic_cdk::caller(); @@ -334,7 +337,7 @@ async fn personal_sign(plaintext: String) -> String { } /// Computes a signature for a precomputed hash. -#[update(guard = "caller_is_not_anonymous")] +#[update(guard = "may_threshold_sign")] async fn sign_prehash(prehash: String) -> String { let caller = ic_cdk::caller(); @@ -349,7 +352,7 @@ async fn sign_prehash(prehash: String) -> String { format!("0x{}", hex::encode(&signature)) } -#[update(guard = "caller_is_not_anonymous")] +#[update(guard = "may_write_user_data")] #[allow(clippy::needless_pass_by_value)] fn set_user_token(token: UserToken) { assert_token_symbol_length(&token).unwrap_or_else(|e| ic_cdk::trap(&e)); @@ -366,7 +369,7 @@ fn set_user_token(token: UserToken) { mutate_state(|s| add_to_user_token(stored_principal, &mut s.user_token, &token, &find)); } -#[update(guard = "caller_is_not_anonymous")] +#[update(guard = "may_write_user_data")] fn set_many_user_tokens(tokens: Vec) { let stored_principal = StoredPrincipal(ic_cdk::caller()); @@ -385,7 +388,7 @@ fn set_many_user_tokens(tokens: Vec) { }); } -#[update(guard = "caller_is_not_anonymous")] +#[update(guard = "may_write_user_data")] #[allow(clippy::needless_pass_by_value)] fn remove_user_token(token_id: UserTokenId) { let addr = parse_eth_address(&token_id.contract_address); @@ -398,14 +401,14 @@ fn remove_user_token(token_id: UserTokenId) { mutate_state(|s| remove_from_user_token(stored_principal, &mut s.user_token, &find)); } -#[query(guard = "caller_is_not_anonymous")] +#[query(guard = "may_read_user_data")] fn list_user_tokens() -> Vec { let stored_principal = StoredPrincipal(ic_cdk::caller()); read_state(|s| s.user_token.get(&stored_principal).unwrap_or_default().0) } /// Add, remove or update custom token for the user. -#[update(guard = "caller_is_not_anonymous")] +#[update(guard = "may_write_user_data")] #[allow(clippy::needless_pass_by_value)] fn set_custom_token(token: CustomToken) { let stored_principal = StoredPrincipal(ic_cdk::caller()); @@ -417,7 +420,7 @@ fn set_custom_token(token: CustomToken) { mutate_state(|s| add_to_user_token(stored_principal, &mut s.custom_token, &token, &find)); } -#[update(guard = "caller_is_not_anonymous")] +#[update(guard = "may_write_user_data")] fn set_many_custom_tokens(tokens: Vec) { let stored_principal = StoredPrincipal(ic_cdk::caller()); @@ -432,13 +435,13 @@ fn set_many_custom_tokens(tokens: Vec) { }); } -#[query(guard = "caller_is_not_anonymous")] +#[query(guard = "may_read_user_data")] fn list_custom_tokens() -> Vec { let stored_principal = StoredPrincipal(ic_cdk::caller()); read_state(|s| s.custom_token.get(&stored_principal).unwrap_or_default().0) } -#[update(guard = "caller_is_not_anonymous")] +#[update(guard = "may_write_user_data")] #[allow(clippy::needless_pass_by_value)] fn add_user_credential(request: AddUserCredentialRequest) -> Result<(), AddUserCredentialError> { let user_principal = ic_cdk::caller(); @@ -474,7 +477,7 @@ fn add_user_credential(request: AddUserCredentialRequest) -> Result<(), AddUserC /// It create a new user profile for the caller. /// If the user has already a profile, it will return that profile. -#[update(guard = "caller_is_not_anonymous")] +#[update(guard = "may_write_user_data")] fn create_user_profile() -> UserProfile { let stored_principal = StoredPrincipal(ic_cdk::caller()); @@ -486,7 +489,7 @@ fn create_user_profile() -> UserProfile { }) } -#[query(guard = "caller_is_not_anonymous")] +#[query(guard = "may_read_user_data")] fn get_user_profile() -> Result { let stored_principal = StoredPrincipal(ic_cdk::caller()); diff --git a/src/backend/tests/it/upgrade/credentials_init_args.rs b/src/backend/tests/it/upgrade/credentials_init_args.rs index 8aac49dfcb..ee26ffca95 100644 --- a/src/backend/tests/it/upgrade/credentials_init_args.rs +++ b/src/backend/tests/it/upgrade/credentials_init_args.rs @@ -26,6 +26,7 @@ fn test_upgrade_credential_init_args() { allowed_callers: allowed_callers.clone(), ic_root_key_der: None, supported_credentials: None, + api: None, }); let encoded_updated_arg = encode_one(updated_arg).unwrap(); diff --git a/src/backend/tests/it/utils/pocketic.rs b/src/backend/tests/it/utils/pocketic.rs index d25344f739..03078a9897 100644 --- a/src/backend/tests/it/utils/pocketic.rs +++ b/src/backend/tests/it/utils/pocketic.rs @@ -108,6 +108,7 @@ pub(crate) fn init_arg() -> Arg { issuer_origin: ISSUER_ORIGIN.to_string(), credential_type: CredentialType::ProofOfUniqueness, }]), + api: None, }) } diff --git a/src/declarations/backend/backend.did b/src/declarations/backend/backend.did index 8d566338ae..ca3aab0c4b 100644 --- a/src/declarations/backend/backend.did +++ b/src/declarations/backend/backend.did @@ -10,6 +10,7 @@ type AddUserCredentialRequest = record { current_user_version : opt nat64; credential_spec : CredentialSpec; }; +type ApiEnabled = variant { ReadOnly; Enabled; Disabled }; type Arg = variant { Upgrade; Init : InitArg }; type ArgumentValue = variant { Int : int32; String : text }; type CanisterStatusResultV2 = record { @@ -25,6 +26,7 @@ type CanisterStatusResultV2 = record { }; type CanisterStatusType = variant { stopped; stopping; running }; type Config = record { + api : opt Guards; ecdsa_key_name : text; allowed_callers : vec principal; supported_credentials : opt vec SupportedCredential; @@ -48,6 +50,7 @@ type DefiniteCanisterSettingsArgs = record { compute_allocation : nat; }; type GetUserProfileError = variant { NotFound }; +type Guards = record { user_data : ApiEnabled; threshold_key : ApiEnabled }; type HttpRequest = record { url : text; method : text; @@ -61,6 +64,7 @@ type HttpResponse = record { }; type IcrcToken = record { ledger_id : principal; index_id : opt principal }; type InitArg = record { + api : opt Guards; ecdsa_key_name : text; allowed_callers : vec principal; supported_credentials : opt vec SupportedCredential; diff --git a/src/declarations/backend/backend.did.d.ts b/src/declarations/backend/backend.did.d.ts index 04cdf9c6c1..56c7e45114 100644 --- a/src/declarations/backend/backend.did.d.ts +++ b/src/declarations/backend/backend.did.d.ts @@ -13,6 +13,7 @@ export interface AddUserCredentialRequest { current_user_version: [] | [bigint]; credential_spec: CredentialSpec; } +export type ApiEnabled = { ReadOnly: null } | { Enabled: null } | { Disabled: null }; export type Arg = { Upgrade: null } | { Init: InitArg }; export type ArgumentValue = { Int: number } | { String: string }; export interface CanisterStatusResultV2 { @@ -28,6 +29,7 @@ export interface CanisterStatusResultV2 { } export type CanisterStatusType = { stopped: null } | { stopping: null } | { running: null }; export interface Config { + api: [] | [Guards]; ecdsa_key_name: string; allowed_callers: Array; supported_credentials: [] | [Array]; @@ -51,6 +53,10 @@ export interface DefiniteCanisterSettingsArgs { compute_allocation: bigint; } export type GetUserProfileError = { NotFound: null }; +export interface Guards { + user_data: ApiEnabled; + threshold_key: ApiEnabled; +} export interface HttpRequest { url: string; method: string; @@ -67,6 +73,7 @@ export interface IcrcToken { index_id: [] | [Principal]; } export interface InitArg { + api: [] | [Guards]; ecdsa_key_name: string; allowed_callers: Array; supported_credentials: [] | [Array]; diff --git a/src/declarations/backend/backend.factory.certified.did.js b/src/declarations/backend/backend.factory.certified.did.js index 96190825ec..6a1abebb87 100644 --- a/src/declarations/backend/backend.factory.certified.did.js +++ b/src/declarations/backend/backend.factory.certified.did.js @@ -1,5 +1,14 @@ // @ts-ignore export const idlFactory = ({ IDL }) => { + const ApiEnabled = IDL.Variant({ + ReadOnly: IDL.Null, + Enabled: IDL.Null, + Disabled: IDL.Null + }); + const Guards = IDL.Record({ + user_data: ApiEnabled, + threshold_key: ApiEnabled + }); const CredentialType = IDL.Variant({ ProofOfUniqueness: IDL.Null }); const SupportedCredential = IDL.Record({ ii_canister_id: IDL.Principal, @@ -9,6 +18,7 @@ export const idlFactory = ({ IDL }) => { credential_type: CredentialType }); const InitArg = IDL.Record({ + api: IDL.Opt(Guards), ecdsa_key_name: IDL.Text, allowed_callers: IDL.Vec(IDL.Principal), supported_credentials: IDL.Opt(IDL.Vec(SupportedCredential)), @@ -37,6 +47,7 @@ export const idlFactory = ({ IDL }) => { Err: AddUserCredentialError }); const Config = IDL.Record({ + api: IDL.Opt(Guards), ecdsa_key_name: IDL.Text, allowed_callers: IDL.Vec(IDL.Principal), supported_credentials: IDL.Opt(IDL.Vec(SupportedCredential)), @@ -161,6 +172,15 @@ export const idlFactory = ({ IDL }) => { }; // @ts-ignore export const init = ({ IDL }) => { + const ApiEnabled = IDL.Variant({ + ReadOnly: IDL.Null, + Enabled: IDL.Null, + Disabled: IDL.Null + }); + const Guards = IDL.Record({ + user_data: ApiEnabled, + threshold_key: ApiEnabled + }); const CredentialType = IDL.Variant({ ProofOfUniqueness: IDL.Null }); const SupportedCredential = IDL.Record({ ii_canister_id: IDL.Principal, @@ -170,6 +190,7 @@ export const init = ({ IDL }) => { credential_type: CredentialType }); const InitArg = IDL.Record({ + api: IDL.Opt(Guards), ecdsa_key_name: IDL.Text, allowed_callers: IDL.Vec(IDL.Principal), supported_credentials: IDL.Opt(IDL.Vec(SupportedCredential)), diff --git a/src/declarations/backend/backend.factory.did.js b/src/declarations/backend/backend.factory.did.js index 0d577da4da..d0ff7ef90c 100644 --- a/src/declarations/backend/backend.factory.did.js +++ b/src/declarations/backend/backend.factory.did.js @@ -1,5 +1,14 @@ // @ts-ignore export const idlFactory = ({ IDL }) => { + const ApiEnabled = IDL.Variant({ + ReadOnly: IDL.Null, + Enabled: IDL.Null, + Disabled: IDL.Null + }); + const Guards = IDL.Record({ + user_data: ApiEnabled, + threshold_key: ApiEnabled + }); const CredentialType = IDL.Variant({ ProofOfUniqueness: IDL.Null }); const SupportedCredential = IDL.Record({ ii_canister_id: IDL.Principal, @@ -9,6 +18,7 @@ export const idlFactory = ({ IDL }) => { credential_type: CredentialType }); const InitArg = IDL.Record({ + api: IDL.Opt(Guards), ecdsa_key_name: IDL.Text, allowed_callers: IDL.Vec(IDL.Principal), supported_credentials: IDL.Opt(IDL.Vec(SupportedCredential)), @@ -37,6 +47,7 @@ export const idlFactory = ({ IDL }) => { Err: AddUserCredentialError }); const Config = IDL.Record({ + api: IDL.Opt(Guards), ecdsa_key_name: IDL.Text, allowed_callers: IDL.Vec(IDL.Principal), supported_credentials: IDL.Opt(IDL.Vec(SupportedCredential)), @@ -161,6 +172,15 @@ export const idlFactory = ({ IDL }) => { }; // @ts-ignore export const init = ({ IDL }) => { + const ApiEnabled = IDL.Variant({ + ReadOnly: IDL.Null, + Enabled: IDL.Null, + Disabled: IDL.Null + }); + const Guards = IDL.Record({ + user_data: ApiEnabled, + threshold_key: ApiEnabled + }); const CredentialType = IDL.Variant({ ProofOfUniqueness: IDL.Null }); const SupportedCredential = IDL.Record({ ii_canister_id: IDL.Principal, @@ -170,6 +190,7 @@ export const init = ({ IDL }) => { credential_type: CredentialType }); const InitArg = IDL.Record({ + api: IDL.Opt(Guards), ecdsa_key_name: IDL.Text, allowed_callers: IDL.Vec(IDL.Principal), supported_credentials: IDL.Opt(IDL.Vec(SupportedCredential)), diff --git a/src/shared/src/impls.rs b/src/shared/src/impls.rs index 5767dcbf68..a00c9da25e 100644 --- a/src/shared/src/impls.rs +++ b/src/shared/src/impls.rs @@ -46,6 +46,7 @@ impl From for Config { allowed_callers, supported_credentials, ic_root_key_der, + api, } = arg; let ic_root_key_raw = match extract_raw_root_pk_from_der( &ic_root_key_der.unwrap_or_else(|| IC_ROOT_PK_DER.to_vec()), @@ -58,6 +59,7 @@ impl From for Config { allowed_callers, supported_credentials, ic_root_key_raw: Some(ic_root_key_raw), + api, } } } diff --git a/src/shared/src/types.rs b/src/shared/src/types.rs index 793322858d..9cc9561f0f 100644 --- a/src/shared/src/types.rs +++ b/src/shared/src/types.rs @@ -24,6 +24,56 @@ pub struct InitArg { pub supported_credentials: Option>, /// Root of trust for checking canister signatures. pub ic_root_key_der: Option>, + /// Enables or disables APIs + pub api: Option, +} + +#[derive(CandidType, Deserialize, Eq, PartialEq, Debug, Copy, Clone)] +#[repr(u8)] +pub enum ApiEnabled { + Enabled, + ReadOnly, + Disabled, +} +impl Default for ApiEnabled { + fn default() -> Self { + Self::Enabled + } +} +impl ApiEnabled { + #[must_use] + pub fn readable(&self) -> bool { + matches!(self, Self::Enabled | Self::ReadOnly) + } + #[must_use] + pub fn writable(&self) -> bool { + matches!(self, Self::Enabled) + } +} +#[test] +fn test_api_enabled() { + assert_eq!(ApiEnabled::Enabled.readable(), true); + assert_eq!(ApiEnabled::Enabled.writable(), true); + assert_eq!(ApiEnabled::ReadOnly.readable(), true); + assert_eq!(ApiEnabled::ReadOnly.writable(), false); + assert_eq!(ApiEnabled::Disabled.readable(), false); + assert_eq!(ApiEnabled::Disabled.writable(), false); +} + +#[derive(CandidType, Deserialize, Default, Copy, Clone, Debug, PartialEq, Eq)] +pub struct Guards { + pub threshold_key: ApiEnabled, + pub user_data: ApiEnabled, +} +#[test] +fn guards_default() { + assert_eq!( + Guards::default(), + Guards { + threshold_key: ApiEnabled::Enabled, + user_data: ApiEnabled::Enabled, + } + ); } #[derive(CandidType, Deserialize)] @@ -40,6 +90,8 @@ pub struct Config { pub supported_credentials: Option>, /// Root of trust for checking canister signatures. pub ic_root_key_raw: Option>, + /// Enables or disables APIs + pub api: Option, } pub mod transaction {