diff --git a/CHANGELOG.md b/CHANGELOG.md index d804de51c..46d6a2b11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,8 +13,10 @@ air-gapped signing * `icp canister status` and `icp canister settings show` now list principals one per line under their label rather than comma-separated on the label's line: `log viewer:` / `status viewer: ` for the allowed viewers of a visibility setting, `controller: ` for a canister's controllers. This applies to `log_visibility` and to the controller list as well, so scripts matching those lines need updating. Both lists are sorted, which the controller list previously was not. * `icp canister settings update` no longer lets a relative viewer edit silently revoke public access. `--add-log-viewer` / `--remove-log-viewer` and their `--*-status-viewer` counterparts are now rejected while that setting is `public`, which carries no allowed-viewers list for them to be relative to — previously they started one from empty, so adding a viewer to a public canister took access away from everyone else. The error points at `--set-*-viewer`, which states the new list outright, and at `--*-visibility controllers`, which revokes public access on its own. Both remain allowed and now warn about what they cost: replacing a `public` policy with a list, and removing the last viewer. This is breaking for the pre-existing log flags. * This raises the minimum replica version: reading a canister's status now requires one that reports `status_visibility`, and against an older replica `canister_status` fails to decode — which affects `icp deploy`, `icp canister status`, and `icp canister settings show`/`sync`, not just the new setting. Every mainnet subnet reports it. A `managed` network resolves the launcher to `latest` unless it pins `version:`, so only a pinned launcher older than `15.0.0-2026-08-13-03-55` is affected; raise the pin to that version or later. +* feat: a new `snapshot_visibility` canister setting controls who may read the canister's snapshots — the `list_canister_snapshots`, `read_canister_snapshot_metadata`, and `read_canister_snapshot_data` endpoints behind `icp canister snapshot list` and `icp canister snapshot download`. It takes the same forms as `log_visibility` and `status_visibility` — `controllers` (the default), `public`, or `{ allowed_viewers: [...] }` — in a manifest's `settings:` block or with `icp canister settings update --snapshot-visibility / --add-snapshot-viewer / --remove-snapshot-viewer / --set-snapshot-viewer`, and follows the same rule that a relative viewer edit is refused while the setting is `public`. `icp canister status` and `icp canister settings show` now report it, it is applied at creation, and `icp canister settings sync` keeps it in sync. Taking, restoring, and deleting snapshots remains controller-only; the setting governs reading alone. See the [canister settings reference](docs/reference/canister-settings.md#snapshot_visibility). * feat: `script` build steps now receive `ICP_CLI_ENVIRONMENT`, the name of the environment the canisters are being built for, so a build can vary by environment the way a sync step already could. * feat: `icp completions ` prints a shell completion script for `bash`, `zsh`, `fish`, `powershell`, or `elvish` to stdout. See the [installation guide](docs/guides/installation.md#shell-completions) for where to put it. +* fix(sync-plugin): Wasmtime is updated 47.0.3 → 47.0.4, patching two advisories in the runtime that executes `plugin` sync steps: a WASI filesystem sandbox escape when paths or symlinks carry trailing slashes ([RUSTSEC-2026-0269](https://rustsec.org/advisories/RUSTSEC-2026-0269)), and a guest-controlled host heap allocation through WASIp3 streams ([RUSTSEC-2026-0268](https://rustsec.org/advisories/RUSTSEC-2026-0268)). The escape is the one that bears on this crate: plugins run against directories preopened under the canister directory, and containing a symlink that escapes a preopen is the runtime's job rather than something the declared-path checks can cover. Lockfile-only — no manifest, API, or minimum-toolchain change. * fix: canister settings from the manifest are no longer silently discarded when a canister is created through the legacy management-canister fallback (a CloudEngine subnet with no registered engine operator). That path went through `ic-utils`, which encodes `create_canister`'s argument as a bare `canister_settings` record rather than the `record { settings : opt canister_settings; ... }` the interface spec defines, so the replica read no settings at all and created the canister with defaults. `icp deploy` masked this by syncing settings afterwards; `icp canister create` does not, and left the canister unconfigured. * fix: `icp canister logs` output formats are corrected. `--json` now emits machine-readable JSON and the default emits the human-readable lines (the two were swapped), and `--follow --json` emits newline-delimited JSON, one record per line, streamed as each record arrives. This is breaking for scripts: parsing the default output as JSON now requires `--json`, and consumers of `--follow --json` must read one JSON object per line. diff --git a/Cargo.lock b/Cargo.lock index f208b3697..f85aa4674 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1046,9 +1046,9 @@ dependencies = [ [[package]] name = "cap-fs-ext" -version = "4.0.2" +version = "4.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d78e5a3368ae89b7cb68186411452b4b9fac8b41be9c19bf3f47c2d2c8e36e6b" +checksum = "56ff379b70af8e08307a8f65e7040c7301cb4a572538ade16b4984f0da77847f" dependencies = [ "cap-primitives", "cap-std", @@ -1058,9 +1058,9 @@ dependencies = [ [[package]] name = "cap-primitives" -version = "4.0.2" +version = "4.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdadbd7c002d3a484b35243669abdae85a0ebaded5a61117169dc3400f9a7ff0" +checksum = "8b5f74729fd2f44701d1a8eb47e906cdb3ccd9ec0f02baad85a744b791940b18" dependencies = [ "ambient-authority", "fs-set-times", @@ -1076,9 +1076,9 @@ dependencies = [ [[package]] name = "cap-std" -version = "4.0.2" +version = "4.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7281235d6e96d3544ca18bba9049be92f4190f8d923e3caef1b5f66cfa752608" +checksum = "c1ec78e242cfa2cfe276807ac2ecc00315a6c97786977414bcd1c3963b6c91b8" dependencies = [ "cap-primitives", "io-extras", @@ -1443,27 +1443,27 @@ dependencies = [ [[package]] name = "cranelift-assembler-x64" -version = "0.134.3" +version = "0.134.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d552bd33b7a56dc70aeb1e1c960e51a218fa0db50f23873b500a310379450b2d" +checksum = "16c273ba1bfc3a1cb27cb4f83df27134f3cb638a61f5ed79ab22cf86eb13da0d" dependencies = [ "cranelift-assembler-x64-meta", ] [[package]] name = "cranelift-assembler-x64-meta" -version = "0.134.3" +version = "0.134.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "078e80e4c222279e3330f6aa1a256ca77ddf156d4453166a9f09defedc4594dd" +checksum = "cbf8909326a466739e83ffe11e74c0c8c6b4bfa911b612aece915daa746cff58" dependencies = [ "cranelift-srcgen", ] [[package]] name = "cranelift-bforest" -version = "0.134.3" +version = "0.134.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "820ce15d4ad3562d613c31a67b6d0434d403e7091a68d1349903842f7d31737e" +checksum = "3cb26b06b54d8b2f8cdf4d440a32fc3b3b91c71c162333a69a70a66fa265fc45" dependencies = [ "cranelift-entity", "wasmtime-internal-core", @@ -1471,9 +1471,9 @@ dependencies = [ [[package]] name = "cranelift-bitset" -version = "0.134.3" +version = "0.134.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61bca563d4b86d285928d9e27f97f27039bb33a0fc524fa130d7d0c106bf8ab3" +checksum = "7ce4e57dbb7f73d08011808a19787c3a03c3e7854b2a479ed0b788915fb671be" dependencies = [ "serde", "serde_derive", @@ -1482,9 +1482,9 @@ dependencies = [ [[package]] name = "cranelift-codegen" -version = "0.134.3" +version = "0.134.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709f4b7c0fb57d952658d5b5c07fbdc4149acd7b7f0de9678ae754b9c981949a" +checksum = "74b0a8968f33d0bc13bc86fe70dd3947e93acc9f358ed9848e9988448972a9be" dependencies = [ "bumpalo", "cranelift-assembler-x64", @@ -1513,9 +1513,9 @@ dependencies = [ [[package]] name = "cranelift-codegen-meta" -version = "0.134.3" +version = "0.134.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "903dc8915af62aad1d9d0f39a5968d33fa80b9aa899ed6f56e47f40ca4512e1e" +checksum = "f69e8e2fe9deb48ef7b8d0c61612f8c1e2277d0421c201e30a4c844a4ebac698" dependencies = [ "cranelift-assembler-x64-meta", "cranelift-codegen-shared", @@ -1526,24 +1526,24 @@ dependencies = [ [[package]] name = "cranelift-codegen-shared" -version = "0.134.3" +version = "0.134.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0522d74c227e49f3fd49ab055311486ae4f09083262b66705bed676952491469" +checksum = "ad4db1e87a65e9c5a832e3ede160830d4e34df938ac38b5604819f8825687e73" [[package]] name = "cranelift-control" -version = "0.134.3" +version = "0.134.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66560ea1c5cef72e170b18e46d263dba2d3169c9d39e8cafdab2173c6362cc1a" +checksum = "9248cd37bb6ec460981f57ead368e32b1eca7a207ad50e3e9c7adc3b161f20e9" dependencies = [ "arbitrary", ] [[package]] name = "cranelift-entity" -version = "0.134.3" +version = "0.134.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62ef5b17cc814d27e96b66a5b46da0e4ce2b8ac55a6d478048bb99d04b05526" +checksum = "26b8bc91c0a3530d132acbf118965dabe9a084029eb0fa74b64760360dec3316" dependencies = [ "cranelift-bitset", "serde", @@ -1553,9 +1553,9 @@ dependencies = [ [[package]] name = "cranelift-frontend" -version = "0.134.3" +version = "0.134.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a87e0aaa39dbf70693b348a221e45904111704ee8f9fef140498471005f9842d" +checksum = "5086f2ebc084a98b387e522680f62ef65a512444d8382151e03088716d3a1714" dependencies = [ "cranelift-codegen", "hashbrown 0.17.1", @@ -1566,15 +1566,15 @@ dependencies = [ [[package]] name = "cranelift-isle" -version = "0.134.3" +version = "0.134.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf79003ebfa1eed5e87f3b84446ad5236f540268289960e14f387dc9b28e40b7" +checksum = "b6a658670f779afc2df083be201ebe54805f69b7b9ffa8dab04142b72ed751c9" [[package]] name = "cranelift-native" -version = "0.134.3" +version = "0.134.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05bf4f235743c81e67ee4db617c5a4a0b65d58d3f0cfc575ee0f1a4e0cd58273" +checksum = "16a41083b1fd953debd6f32d21fcda765b258f323ce907fdd6b4715f2769c478" dependencies = [ "cranelift-codegen", "libc", @@ -1583,9 +1583,9 @@ dependencies = [ [[package]] name = "cranelift-srcgen" -version = "0.134.3" +version = "0.134.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6977c2a71ab1e0d1e62f966b411a498aa04c4dce47d93d52f8a360a06058922" +checksum = "244d9cd0759b0a3ccdd587cafb13434f9ea43b5c9d07701d277daf5cd0f7b7c1" [[package]] name = "crc32fast" @@ -5700,9 +5700,9 @@ dependencies = [ [[package]] name = "pulley-interpreter" -version = "47.0.3" +version = "47.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc5c8c21ea032e4efbdf2d067dc45171779dbe0c8ecf20ef4a57efa7474f2b0a" +checksum = "ed2bd641cb6a3ec5bffe60405568933163e0ea619da0943e9d2a417ceaa7fe82" dependencies = [ "cranelift-bitset", "log", @@ -5712,9 +5712,9 @@ dependencies = [ [[package]] name = "pulley-macros" -version = "47.0.3" +version = "47.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f10925455d5dde962e3604eade797ba5488644c8ee14a44191e2f2b58980574" +checksum = "d3fd4024d1e559eaf5579bc8ad84a2b5915f34b47acc13a06ffba7dac401b166" dependencies = [ "proc-macro2", "quote", @@ -7986,9 +7986,9 @@ dependencies = [ [[package]] name = "wasmtime" -version = "47.0.3" +version = "47.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c80ca6098e0d4d06886d91d7f2cc3cb6623eb583c4c0ab3c89cbfb6098c8586c" +checksum = "4cdaf5b8af5713146e3a62d940c71e3eb2ad1ebacfa6b55f0c8509a5b4b87718" dependencies = [ "addr2line", "async-trait", @@ -8039,9 +8039,9 @@ dependencies = [ [[package]] name = "wasmtime-environ" -version = "47.0.3" +version = "47.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134f9d136d29c76f6c1b4c9b468e97a2efc38c7d49fd188e39b64870b2fea701" +checksum = "58793271eab7ec26bab99ca497131f8b37702e7dc39e2f2d18293e3e915b4964" dependencies = [ "anyhow", "cpp_demangle", @@ -8070,9 +8070,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-cache" -version = "47.0.3" +version = "47.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65db2eb1bfc5371a3b4107dbf539d0b93d05016fc29625b1648146b47db02071" +checksum = "ede02ba77442cab88a2ddd5d16373e8bc450b55e05de8be039b024987a53d5a7" dependencies = [ "base64", "directories-next", @@ -8090,9 +8090,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-component-macro" -version = "47.0.3" +version = "47.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bf7b91fed3fc34781d57f9c6654ea2513bf0a99f7b0b0523c00de1e6efebe1c" +checksum = "c5bba4eff4804620bae04d7793308d97a0c5a8a97ea19e635452518f84560f84" dependencies = [ "anyhow", "proc-macro2", @@ -8105,15 +8105,15 @@ dependencies = [ [[package]] name = "wasmtime-internal-component-util" -version = "47.0.3" +version = "47.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc8a678149885cae00289f806fbe74c7863084cf74cace0b8dc73602279400e1" +checksum = "3d7032ff6b4d7a45e05be1cb32ec1756df5f2669f08296dbe2c61c3789dead2c" [[package]] name = "wasmtime-internal-core" -version = "47.0.3" +version = "47.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0092c4b9d070ac5e278b6d0db10f5e71214190f0347ca57502b4692f796321" +checksum = "f41d3b2cb9c3de7b696690af070d408b5e357011832f8b8bb7fbb315dbb620ed" dependencies = [ "anyhow", "hashbrown 0.17.1", @@ -8123,9 +8123,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-cranelift" -version = "47.0.3" +version = "47.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6851ebc9e03cab23d9821d2b2505d380bdfe38f35ccec945247b05274d85c98b" +checksum = "1d5cda5b90247978dd2ba6ca4e309508da19e9244445671d2d0e6a34e5c3b847" dependencies = [ "cfg-if", "cranelift-codegen", @@ -8150,9 +8150,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-fiber" -version = "47.0.3" +version = "47.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b26da6d5f60d4c438da70bba3553fe810a840533a64156be287dca2081c6991" +checksum = "4d126366176553e8f304e8ac2078a4cc37134fe8f19d4d4695a09c5a2f577f10" dependencies = [ "cc", "cfg-if", @@ -8165,9 +8165,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-jit-debug" -version = "47.0.3" +version = "47.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed621ba25d7bf78b7edd7b4749abbb27c2e5cdba836c9504a424ee74b6c23149" +checksum = "c1d28ff131bc40e11d84bc3a9fa0d264506439ffbbc89f3562fbb529d1caa617" dependencies = [ "cc", "object 0.39.1", @@ -8177,9 +8177,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-jit-icache-coherence" -version = "47.0.3" +version = "47.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5684ba160951baad06a725696f3c590e2fb0e8067c2aebee27bf7f9259058e85" +checksum = "4a52de8b6afbfcd618073f592c1450cb661574e7061f3f4a44f1f7ec0c7f7909" dependencies = [ "cfg-if", "libc", @@ -8189,9 +8189,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-unwinder" -version = "47.0.3" +version = "47.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112eead527bffa8ff0646a11fb4339a9d52ddd1da2b9a6fce4aff84c815dd94f" +checksum = "5524fbaaf3d2134dd6229164d7fc81e443cce06d1e0576524d5f4e86e747cb13" dependencies = [ "cfg-if", "cranelift-codegen", @@ -8202,9 +8202,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-versioned-export-macros" -version = "47.0.3" +version = "47.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "153592e0bed824fc13c6696203fa1bd7bd20eb355316475e39a55f081ef80eca" +checksum = "e93bb9ae112a80618510f938275a5d88637b656e7d0a021fc906f52206c708ad" dependencies = [ "proc-macro2", "quote", @@ -8213,9 +8213,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-wit-bindgen" -version = "47.0.3" +version = "47.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c456ad6e81e0f46abfeca43687d18ea15e289c395b262ea6e0023e2682e088be" +checksum = "bfdbdd9d6fab1ecb67a7dc189e32c9e341444945fb709437bf8aab25d9a88459" dependencies = [ "anyhow", "bitflags 2.13.0", @@ -8226,9 +8226,9 @@ dependencies = [ [[package]] name = "wasmtime-wasi" -version = "47.0.3" +version = "47.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c1cf60cd6a565213af7074b4aad7bb5e110e3c6c6aae54425aa0500a0e15e86" +checksum = "b373095a6dc8382da56882ad16d7ea16771dd6927dd480b1ce3fe7c4b816d2cf" dependencies = [ "async-trait", "bitflags 2.13.0", @@ -8252,9 +8252,9 @@ dependencies = [ [[package]] name = "wasmtime-wasi-io" -version = "47.0.3" +version = "47.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fb08e1d7755aaa467ce14080b7b01e33c914a920f6000696f1e42e40ea6387e" +checksum = "51b3c9b39b1a1b3029f80e614623e182f165133dfaca8610f156450343f080a4" dependencies = [ "async-trait", "bytes", @@ -8331,9 +8331,9 @@ checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" [[package]] name = "wiggle" -version = "47.0.3" +version = "47.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89091642df051b84a7e00bdff75e985e507f2298384e477cc9310b0ddd79f004" +checksum = "8c08bd1d66a10c7d4deaeb746cb1c5b5c62fb631907f18dfe058ecba71c99002" dependencies = [ "bitflags 2.13.0", "thiserror 2.0.18", @@ -8345,9 +8345,9 @@ dependencies = [ [[package]] name = "wiggle-generate" -version = "47.0.3" +version = "47.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12247f46f31bb5e7593947f9bba07317d0c1978e0d02250b979dd08f56f46e9f" +checksum = "19eb81a4ceda41cc6fa2adc8e865473fc2e85630149ed7f5d95356b0010f0ed3" dependencies = [ "heck", "proc-macro2", @@ -8359,9 +8359,9 @@ dependencies = [ [[package]] name = "wiggle-macro" -version = "47.0.3" +version = "47.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e556a0880c9506fdcc662b1e226c7ced692f4977fed5b1ed40a3f3f0a48356f2" +checksum = "c0e0d7c6d28846ffa438e421976734a2057f13a7e20af9f2a8607a6714e1e358" dependencies = [ "proc-macro2", "quote", diff --git a/crates/icp-cli/src/commands/canister/create.rs b/crates/icp-cli/src/commands/canister/create.rs index 380225540..f406718d9 100644 --- a/crates/icp-cli/src/commands/canister/create.rs +++ b/crates/icp-cli/src/commands/canister/create.rs @@ -164,6 +164,11 @@ impl CreateArgs { .map(|c| Nat::from(c.get())), // TODO This should be configurable from the CLI log_visibility: default.settings.log_visibility.clone().map(|v| v.0.into()), + snapshot_visibility: default + .settings + .snapshot_visibility + .clone() + .map(|v| v.0.into()), status_visibility: default .settings .status_visibility @@ -259,6 +264,7 @@ impl CreateArgs { .map(|c| Nat::from(c.get())), // TODO This should be configurable from the CLI log_visibility: None, + snapshot_visibility: None, status_visibility: None, memory_allocation: self .settings diff --git a/crates/icp-cli/src/commands/canister/settings/show.rs b/crates/icp-cli/src/commands/canister/settings/show.rs index 8f17b4e60..e405ed6a3 100644 --- a/crates/icp-cli/src/commands/canister/settings/show.rs +++ b/crates/icp-cli/src/commands/canister/settings/show.rs @@ -98,6 +98,12 @@ fn build_output(s: &DefiniteCanisterSettings) -> String { format_visibility(&s.log_visibility.clone().into(), "log viewer", "") ) .unwrap(); + writeln!( + &mut buf, + "Snapshot visibility: {}", + format_visibility(&s.snapshot_visibility.clone().into(), "snapshot viewer", "") + ) + .unwrap(); writeln!( &mut buf, "Status visibility: {}", diff --git a/crates/icp-cli/src/commands/canister/settings/update.rs b/crates/icp-cli/src/commands/canister/settings/update.rs index f708fc0b6..50d7d2599 100644 --- a/crates/icp-cli/src/commands/canister/settings/update.rs +++ b/crates/icp-cli/src/commands/canister/settings/update.rs @@ -95,6 +95,56 @@ impl LogVisibilityOpt { } } +#[derive(Clone, Debug, Default, Args)] +pub(crate) struct SnapshotVisibilityOpt { + /// Set snapshot visibility to a fixed policy [possible values: controllers, public]. + /// Conflicts with --add-snapshot-viewer, --remove-snapshot-viewer, and --set-snapshot-viewer. + /// Use --add-snapshot-viewer / --set-snapshot-viewer to grant access to specific principals instead. + #[arg( + long, + value_parser = visibility_parser, + conflicts_with("add_snapshot_viewer"), + conflicts_with("remove_snapshot_viewer"), + conflicts_with("set_snapshot_viewer"), + )] + snapshot_visibility: Option, + + /// Add a principal to the allowed snapshot viewers list. + /// + /// Rejected while snapshot visibility is public, which has no viewers list to + /// add to; use --set-snapshot-viewer to replace the public policy with a list. + #[arg(long, action = ArgAction::Append, conflicts_with("set_snapshot_viewer"))] + add_snapshot_viewer: Option>, + + /// Remove a principal from the allowed snapshot viewers list. + /// + /// Rejected while snapshot visibility is public, which has no viewers list to + /// remove from; use --snapshot-visibility controllers to revoke public access. + #[arg(long, action = ArgAction::Append, conflicts_with("set_snapshot_viewer"))] + remove_snapshot_viewer: Option>, + + /// Replace the allowed snapshot viewers list with the specified principals + #[arg(long, action = ArgAction::Append)] + set_snapshot_viewer: Option>, +} + +impl SnapshotVisibilityOpt { + fn flags(&self) -> VisibilityFlags<'_> { + VisibilityFlags { + label: "Snapshot visibility", + stem: "snapshot", + fixed: self.snapshot_visibility.as_ref(), + add: self.add_snapshot_viewer.as_deref(), + remove: self.remove_snapshot_viewer.as_deref(), + set: self.set_snapshot_viewer.as_deref(), + } + } + + pub(crate) fn require_current_settings(&self) -> bool { + self.flags().require_current_settings() + } +} + #[derive(Clone, Debug, Default, Args)] pub(crate) struct StatusVisibilityOpt { /// Set status visibility to a fixed policy [possible values: controllers, public]. @@ -325,6 +375,9 @@ pub(crate) struct UpdateArgs { #[command(flatten)] log_visibility: Option, + #[command(flatten)] + snapshot_visibility: Option, + #[command(flatten)] status_visibility: Option, @@ -418,7 +471,8 @@ pub(crate) async fn exec(ctx: &Context, args: &UpdateArgs) -> Result<(), anyhow: } } - // Handle log and status visibility. + // Handle log, snapshot and status visibility. + let log_visibility = args .log_visibility .as_ref() @@ -429,6 +483,16 @@ pub(crate) async fn exec(ctx: &Context, args: &UpdateArgs) -> Result<(), anyhow: resolve_visibility(opt.flags(), current) }) .transpose()?; + let snapshot_visibility = args + .snapshot_visibility + .as_ref() + .map(|opt| { + let current = current_status + .as_ref() + .map(|status| Visibility::from(status.settings.snapshot_visibility.clone())); + resolve_visibility(opt.flags(), current) + }) + .transpose()?; let status_visibility = args .status_visibility .as_ref() @@ -489,6 +553,11 @@ pub(crate) async fn exec(ctx: &Context, args: &UpdateArgs) -> Result<(), anyhow: "Log visibility is already set in icp.yaml; this new value will be overridden on next settings sync" ); } + if snapshot_visibility.is_some() && configured_settings.snapshot_visibility.is_some() { + warn!( + "Snapshot visibility is already set in icp.yaml; this new value will be overridden on next settings sync" + ); + } if status_visibility.is_some() && configured_settings.status_visibility.is_some() { warn!( "Status visibility is already set in icp.yaml; this new value will be overridden on next settings sync" @@ -511,10 +580,10 @@ pub(crate) async fn exec(ctx: &Context, args: &UpdateArgs) -> Result<(), anyhow: .map(|m| Nat::from(m.get())), log_memory_limit: args.log_memory_limit.as_ref().map(|m| Nat::from(m.get())), log_visibility: log_visibility.map(Into::into), + snapshot_visibility: snapshot_visibility.map(Into::into), status_visibility: status_visibility.map(Into::into), environment_variables, - // Not exposed as flags yet; `None` leaves them unchanged. - snapshot_visibility: None, + // Not exposed as a flag yet; `None` leaves it unchanged. minimum_incoming_canister_call_cycles: None, }; @@ -572,6 +641,12 @@ fn require_current_settings(args: &UpdateArgs) -> bool { return true; } + if let Some(snapshot_visibility) = &args.snapshot_visibility + && snapshot_visibility.require_current_settings() + { + return true; + } + if let Some(status_visibility) = &args.status_visibility && status_visibility.require_current_settings() { @@ -875,6 +950,109 @@ mod tests { ); } + /// The three groups are copies of one another, so the hazard is a + /// mis-wired `flags()` quietly driving another setting: a group must read + /// its own flags and name itself. + #[test] + fn each_group_reads_its_own_flags() { + let viewers = vec![alice()]; + + let log = LogVisibilityOpt { + log_visibility: Some(Visibility::Public), + add_log_viewer: Some(viewers.clone()), + remove_log_viewer: Some(viewers.clone()), + set_log_viewer: Some(viewers.clone()), + }; + let snapshot = SnapshotVisibilityOpt { + snapshot_visibility: Some(Visibility::Public), + add_snapshot_viewer: Some(viewers.clone()), + remove_snapshot_viewer: Some(viewers.clone()), + set_snapshot_viewer: Some(viewers.clone()), + }; + let status = StatusVisibilityOpt { + status_visibility: Some(Visibility::Public), + add_status_viewer: Some(viewers.clone()), + remove_status_viewer: Some(viewers.clone()), + set_status_viewer: Some(viewers.clone()), + }; + + for (flags, label, stem) in [ + (log.flags(), "Log visibility", "log"), + (snapshot.flags(), "Snapshot visibility", "snapshot"), + (status.flags(), "Status visibility", "status"), + ] { + assert_eq!(flags.label, label); + assert_eq!(flags.stem, stem); + assert_eq!(flags.fixed, Some(&Visibility::Public)); + assert_eq!(flags.add, Some(&viewers[..])); + assert_eq!(flags.remove, Some(&viewers[..])); + assert_eq!(flags.set, Some(&viewers[..])); + } + + // And an empty group drives nothing, whichever setting it belongs to. + for flags in [ + LogVisibilityOpt::default().flags(), + SnapshotVisibilityOpt::default().flags(), + StatusVisibilityOpt::default().flags(), + ] { + assert_eq!(flags.fixed, None); + assert!(!flags.require_current_settings()); + } + } + + /// The error a group raises names the group's own flags, so a refusal + /// points at the setting the caller was actually editing. + #[test] + fn the_public_refusal_names_the_settings_own_flags() { + let viewers = vec![alice()]; + + for (opt_flags, label, stem) in [ + ( + LogVisibilityOpt { + add_log_viewer: Some(viewers.clone()), + ..<_>::default() + } + .flags(), + "Log visibility", + "log", + ), + ( + SnapshotVisibilityOpt { + add_snapshot_viewer: Some(viewers.clone()), + ..<_>::default() + } + .flags(), + "Snapshot visibility", + "snapshot", + ), + ( + StatusVisibilityOpt { + add_status_viewer: Some(viewers.clone()), + ..<_>::default() + } + .flags(), + "Status visibility", + "status", + ), + ] { + let err = opt_flags + .resolve(Some(&Visibility::Public)) + .unwrap_err() + .to_string(); + + assert!( + err.contains(&format!("{label} is currently public")), + "{err}" + ); + assert!(err.contains(&format!("--add-{stem}-viewer")), "{err}"); + assert!(err.contains(&format!("--set-{stem}-viewer")), "{err}"); + assert!( + err.contains(&format!("--{stem}-visibility controllers")), + "{err}" + ); + } + } + /// Only a fixed policy stands on its own; every viewer edit is resolved /// against the current one, so it has to be fetched. #[test] diff --git a/crates/icp-cli/src/commands/canister/status.rs b/crates/icp-cli/src/commands/canister/status.rs index 05415c878..5eca81c4e 100644 --- a/crates/icp-cli/src/commands/canister/status.rs +++ b/crates/icp-cli/src/commands/canister/status.rs @@ -377,6 +377,7 @@ struct SerializableCanisterSettings { wasm_memory_threshold: String, log_memory_limit: String, log_visibility: SerializableVisibility, + snapshot_visibility: SerializableVisibility, status_visibility: SerializableVisibility, environment_variables: Vec, } @@ -436,6 +437,9 @@ impl SerializableCanisterSettings { wasm_memory_threshold: settings.wasm_memory_threshold.to_string(), log_memory_limit: settings.log_memory_limit.to_string(), log_visibility: SerializableVisibility(settings.log_visibility.clone().into()), + snapshot_visibility: SerializableVisibility( + settings.snapshot_visibility.clone().into(), + ), status_visibility: SerializableVisibility(settings.status_visibility.clone().into()), environment_variables: settings.environment_variables.clone(), } @@ -546,6 +550,11 @@ fn build_output(result: &SerializableCanisterStatusResult) -> Result assert_cmd::assert::Assert { + let mut all = vec![ + "canister", + "settings", + "update", + "my-canister", + "--environment", + "random-environment", + ]; + all.extend_from_slice(args); + ctx.icp() + .current_dir(project_dir) + .args(all) + .assert() + .success() + } + + fn update_fails( + ctx: &TestContext, + project_dir: &Path, + args: &[&str], + ) -> assert_cmd::assert::Assert { + let mut all = vec![ + "canister", + "settings", + "update", + "my-canister", + "--environment", + "random-environment", + ]; + all.extend_from_slice(args); + ctx.icp() + .current_dir(project_dir) + .args(all) + .assert() + .failure() + } + + fn confirm(ctx: &TestContext, project_dir: &Path) -> assert_cmd::assert::Assert { + ctx.icp() + .current_dir(project_dir) + .args([ + "canister", + "settings", + "show", + "my-canister", + "--environment", + "random-environment", + ]) + .assert() + .success() + } + + /// `snapshot list` as alice, who is not a controller. The call is one of the + /// three the setting gates, so it either returns a list or is rejected. + fn snapshots_as_alice(ctx: &TestContext, project_dir: &Path) -> assert_cmd::assert::Assert { + ctx.icp() + .current_dir(project_dir) + .args([ + "canister", + "snapshot", + "list", + "my-canister", + "--identity", + "alice", + "--environment", + "random-environment", + ]) + .assert() + } + + // The default is controllers, reported alongside the other two settings. + confirm(&ctx, &project_dir).stdout( + contains("Snapshot visibility: Controllers") + .and(contains("Log visibility: Controllers")) + .and(contains("Status visibility: Controllers")), + ); + snapshots_as_alice(&ctx, &project_dir).failure(); + + // --add-snapshot-viewer grants it to alice, relative to the current list. + update( + &ctx, + &project_dir, + &["--add-snapshot-viewer", principal_alice.as_str()], + ); + confirm(&ctx, &project_dir).stdout( + contains("Snapshot visibility: Allowed viewers").and(contains(principal_alice.as_str())), + ); + snapshots_as_alice(&ctx, &project_dir).success(); + + // Add and remove in one call, again relative to the current list. Alice + // loses access. + update( + &ctx, + &project_dir, + &[ + "--add-snapshot-viewer", + principal_bob.as_str(), + "--remove-snapshot-viewer", + principal_alice.as_str(), + ], + ); + confirm(&ctx, &project_dir).stdout( + contains("Snapshot visibility: Allowed viewers") + .and(contains(principal_bob.as_str())) + .and(contains(principal_alice.as_str()).not()), + ); + snapshots_as_alice(&ctx, &project_dir).failure(); + + // --set-snapshot-viewer replaces the list outright. + update( + &ctx, + &project_dir, + &["--set-snapshot-viewer", principal_alice.as_str()], + ); + confirm(&ctx, &project_dir).stdout( + contains("Snapshot visibility: Allowed viewers") + .and(contains(principal_alice.as_str())) + .and(contains(principal_bob.as_str()).not()), + ); + snapshots_as_alice(&ctx, &project_dir).success(); + + // Public grants it to everyone, and leaves the other two settings alone. + update(&ctx, &project_dir, &["--snapshot-visibility", "public"]); + confirm(&ctx, &project_dir).stdout( + contains("Snapshot visibility: Public") + .and(contains("Log visibility: Controllers")) + .and(contains("Status visibility: Controllers")), + ); + snapshots_as_alice(&ctx, &project_dir).success(); + + // A relative viewer edit has no list to be relative to while public, and is + // refused rather than silently revoking public access. + update_fails( + &ctx, + &project_dir, + &["--add-snapshot-viewer", principal_bob.as_str()], + ) + .stderr(contains( + "Snapshot visibility is currently public, so there is no allowed viewers list for --add-snapshot-viewer to edit", + )); + update_fails( + &ctx, + &project_dir, + &["--remove-snapshot-viewer", principal_bob.as_str()], + ) + .stderr(contains("--remove-snapshot-viewer")); + // Refused, so the canister is untouched and alice still lists snapshots. + snapshots_as_alice(&ctx, &project_dir).success(); + + // Stating the list outright is allowed, and warns about what it revokes. + update( + &ctx, + &project_dir, + &["--set-snapshot-viewer", principal_bob.as_str()], + ) + .stderr(contains( + "Snapshot visibility is currently public; listing allowed viewers revokes access for everyone else", + )); + snapshots_as_alice(&ctx, &project_dir).failure(); + + // Removing the last viewer leaves the controllers alone with it. + update( + &ctx, + &project_dir, + &["--remove-snapshot-viewer", principal_bob.as_str()], + ) + .stderr(contains( + "Snapshot visibility is left with no allowed viewers; only the controllers keep access", + )); + + // Revoking it outright. + update( + &ctx, + &project_dir, + &["--snapshot-visibility", "controllers"], + ); + snapshots_as_alice(&ctx, &project_dir).failure(); + + // An update naming no visibility group must leave all three alone, rather + // than resetting them to an empty allowed-viewers list. + update( + &ctx, + &project_dir, + &["--set-snapshot-viewer", principal_alice.as_str()], + ); + update(&ctx, &project_dir, &["--freezing-threshold", "7d"]); + confirm(&ctx, &project_dir).stdout( + contains("Snapshot visibility: Allowed viewers") + .and(contains(principal_alice.as_str())) + .and(contains("Log visibility: Controllers")) + .and(contains("Status visibility: Controllers")), + ); +} + #[tokio::test] async fn canister_settings_sync_through_proxy() { let ctx = TestContext::new(); @@ -1874,6 +2159,7 @@ async fn canister_settings_show() { .and(contains(r#""wasm_memory_threshold""#)) .and(contains(r#""log_memory_limit""#)) .and(contains(r#""log_visibility""#)) + .and(contains(r#""snapshot_visibility""#)) .and(contains(r#""status_visibility""#)) .and(contains(r#""environment_variables""#)), ); diff --git a/crates/icp/src/canister/mod.rs b/crates/icp/src/canister/mod.rs index 065f7e0fd..1c7487ecb 100644 --- a/crates/icp/src/canister/mod.rs +++ b/crates/icp/src/canister/mod.rs @@ -18,7 +18,7 @@ pub mod visibility; mod script; pub mod wasm; -pub use visibility::{LogVisibilityDef, StatusVisibilityDef, Visibility}; +pub use visibility::{LogVisibilityDef, SnapshotVisibilityDef, StatusVisibilityDef, Visibility}; /// A reference to a controller: either an explicit principal or a canister name in this project. /// @@ -132,6 +132,10 @@ pub struct Settings { #[serde(skip_serializing_if = "Option::is_none")] pub log_visibility: Option, + /// Controls who can read the canister's snapshots. + #[serde(skip_serializing_if = "Option::is_none")] + pub snapshot_visibility: Option, + /// Controls who can read the canister's status. #[serde(skip_serializing_if = "Option::is_none")] pub status_visibility: Option, @@ -189,6 +193,7 @@ impl From for ManifestSettings { fn from(settings: Settings) -> Self { let Settings { log_visibility, + snapshot_visibility, status_visibility, compute_allocation, memory_allocation, @@ -203,6 +208,7 @@ impl From for ManifestSettings { Self { log_visibility, + snapshot_visibility, status_visibility, compute_allocation, memory_allocation, @@ -228,6 +234,7 @@ impl From for CanisterSettings { controllers: None, reserved_cycles_limit: settings.reserved_cycles_limit.map(|c| Nat::from(c.get())), log_visibility: settings.log_visibility.map(|v| v.0.into()), + snapshot_visibility: settings.snapshot_visibility.map(|v| v.0.into()), status_visibility: settings.status_visibility.map(|v| v.0.into()), memory_allocation: settings.memory_allocation.map(|m| Nat::from(m.get())), compute_allocation: settings.compute_allocation.map(Nat::from), diff --git a/crates/icp/src/canister/visibility.rs b/crates/icp/src/canister/visibility.rs index bc756c8c9..6d27662aa 100644 --- a/crates/icp/src/canister/visibility.rs +++ b/crates/icp/src/canister/visibility.rs @@ -1,7 +1,7 @@ use std::fmt; use candid::Principal; -use ic_management_canister_types::{LogVisibility, StatusVisibility}; +use ic_management_canister_types::{LogVisibility, SnapshotVisibility, StatusVisibility}; use serde::{Deserialize, Serialize, Serializer, de}; /// Who may read a visibility-gated part of a canister. @@ -121,6 +121,7 @@ macro_rules! candid_conversions { } candid_conversions!(LogVisibility); +candid_conversions!(SnapshotVisibility); candid_conversions!(StatusVisibility); fn visibility_schema(description: &str, subject: &str, controllers: &str) -> schemars::Schema { @@ -204,6 +205,15 @@ visibility_setting!( controllers = "only the canister's controllers can read the logs", ); +visibility_setting!( + SnapshotVisibilityDef, + setting = "snapshot_visibility", + schema = "SnapshotVisibility", + description = "Controls who can read the canister's snapshots.", + subject = "read the snapshots", + controllers = "only the canister's controllers can read the snapshots", +); + visibility_setting!( StatusVisibilityDef, setting = "status_visibility", @@ -245,6 +255,11 @@ allowed_viewers: .to_string(); assert!(err.contains("unknown log_visibility value"), "{err}"); + let err = serde_yaml::from_str::("invalid") + .unwrap_err() + .to_string(); + assert!(err.contains("unknown snapshot_visibility value"), "{err}"); + let err = serde_yaml::from_str::("invalid") .unwrap_err() .to_string(); @@ -286,6 +301,9 @@ allowed_viewers: let log: LogVisibility = value.clone().into(); assert_eq!(Visibility::from(log), value); + let snapshot: SnapshotVisibility = value.clone().into(); + assert_eq!(Visibility::from(snapshot), value); + let status: StatusVisibility = value.clone().into(); assert_eq!(Visibility::from(status), value); } diff --git a/crates/icp/src/operations/settings.rs b/crates/icp/src/operations/settings.rs index 6dc690fbf..69a6d925e 100644 --- a/crates/icp/src/operations/settings.rs +++ b/crates/icp/src/operations/settings.rs @@ -86,6 +86,7 @@ pub async fn sync_settings( .context(FetchCurrentSettingsSnafu { canister: *cid })?; let &Settings { ref log_visibility, + ref snapshot_visibility, ref status_visibility, compute_allocation, ref memory_allocation, @@ -100,8 +101,11 @@ pub async fn sync_settings( let current_settings = status.settings; let desired_log_visibility = log_visibility.clone().map(Visibility::from); + let desired_snapshot_visibility = snapshot_visibility.clone().map(Visibility::from); let desired_status_visibility = status_visibility.clone().map(Visibility::from); let current_log_visibility = Visibility::from(current_settings.log_visibility.clone()); + let current_snapshot_visibility = + Visibility::from(current_settings.snapshot_visibility.clone()); let current_status_visibility = Visibility::from(current_settings.status_visibility.clone()); let environment_variable_setting = @@ -152,6 +156,9 @@ pub async fn sync_settings( if desired_log_visibility .as_ref() .is_none_or(|s| visibility_eq(s, ¤t_log_visibility)) + && desired_snapshot_visibility + .as_ref() + .is_none_or(|s| visibility_eq(s, ¤t_snapshot_visibility)) && desired_status_visibility .as_ref() .is_none_or(|s| visibility_eq(s, ¤t_status_visibility)) @@ -190,6 +197,7 @@ pub async fn sync_settings( let settings = CanisterSettings { log_visibility: desired_log_visibility.map(Into::into), + snapshot_visibility: desired_snapshot_visibility.map(Into::into), status_visibility: desired_status_visibility.map(Into::into), compute_allocation: compute_allocation.map(Nat::from), memory_allocation: memory_allocation.as_ref().map(|m| Nat::from(m.get())), @@ -200,8 +208,7 @@ pub async fn sync_settings( log_memory_limit: log_memory_limit.as_ref().map(|m| Nat::from(m.get())), environment_variables: environment_variable_setting, controllers: controllers_setting, - // Not configurable from the manifest yet; `None` leaves them unchanged. - snapshot_visibility: None, + // Not configurable from the manifest yet; `None` leaves it unchanged. minimum_incoming_canister_call_cycles: None, }; diff --git a/crates/icp/src/project.rs b/crates/icp/src/project.rs index b24dd0a4d..cf18ea9b7 100644 --- a/crates/icp/src/project.rs +++ b/crates/icp/src/project.rs @@ -241,6 +241,7 @@ fn resolve_manifest_settings( ) -> Result<(Settings, BTreeMap), ConsolidateManifestError> { let ManifestSettings { log_visibility, + snapshot_visibility, status_visibility, compute_allocation, memory_allocation, @@ -281,6 +282,7 @@ fn resolve_manifest_settings( let settings = Settings { log_visibility: log_visibility.clone(), + snapshot_visibility: snapshot_visibility.clone(), status_visibility: status_visibility.clone(), compute_allocation: *compute_allocation, memory_allocation: memory_allocation.clone(), diff --git a/docs/reference/canister-settings.md b/docs/reference/canister-settings.md index 7e7cf387e..e4d54a10c 100644 --- a/docs/reference/canister-settings.md +++ b/docs/reference/canister-settings.md @@ -159,6 +159,50 @@ settings: - "2vxsx-fae" ``` +### snapshot_visibility + +Controls who can read the canister's snapshots — the `list_canister_snapshots`, +`read_canister_snapshot_metadata`, and `read_canister_snapshot_data` endpoints of +the management canister, which back `icp canister snapshot list` and +`icp canister snapshot download`. + +| Property | Value | +|----------|-------| +| Type | String or Object | +| Values | `controllers`, `public`, or `allowed_viewers` object | +| Default | `controllers` | + +```yaml +# Only controllers can read the snapshots (default) +settings: + snapshot_visibility: controllers + +# Anyone can read the snapshots +settings: + snapshot_visibility: public + +# Specific principals can read the snapshots, in addition to the controllers +settings: + snapshot_visibility: + allowed_viewers: + - "aaaaa-aa" + - "2vxsx-fae" +``` + +Reading a snapshot exposes the canister's full state — WASM module, WASM memory, +stable memory, and chunk store. Granting snapshot access is therefore closer to +granting a state dump than to granting the read-only report +[`status_visibility`](#status_visibility) covers. + +Like [`log_visibility`](#log_visibility) and unlike +[`status_visibility`](#status_visibility), `controllers` here is exhaustive: +there are no always-allowed callers on top of it. + +Taking, restoring, and deleting snapshots stays controller-only whatever this is +set to; the setting governs reading alone. + +The replica accepts at most 10 principals in `allowed_viewers`. + ### status_visibility Controls who can read the canister's status through the management canister's diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 672927c05..d8839988e 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -539,6 +539,14 @@ Change a canister's settings to specified values Rejected while log visibility is public, which has no viewers list to remove from; use --log-visibility controllers to revoke public access. * `--set-log-viewer ` — Replace the allowed log viewers list with the specified principals +* `--snapshot-visibility ` — Set snapshot visibility to a fixed policy [possible values: controllers, public]. Conflicts with --add-snapshot-viewer, --remove-snapshot-viewer, and --set-snapshot-viewer. Use --add-snapshot-viewer / --set-snapshot-viewer to grant access to specific principals instead +* `--add-snapshot-viewer ` — Add a principal to the allowed snapshot viewers list. + + Rejected while snapshot visibility is public, which has no viewers list to add to; use --set-snapshot-viewer to replace the public policy with a list. +* `--remove-snapshot-viewer ` — Remove a principal from the allowed snapshot viewers list. + + Rejected while snapshot visibility is public, which has no viewers list to remove from; use --snapshot-visibility controllers to revoke public access. +* `--set-snapshot-viewer ` — Replace the allowed snapshot viewers list with the specified principals * `--status-visibility ` — Set status visibility to a fixed policy [possible values: controllers, public]. Conflicts with --add-status-viewer, --remove-status-viewer, and --set-status-viewer. Use --add-status-viewer / --set-status-viewer to grant access to specific principals instead * `--add-status-viewer ` — Add a principal to the allowed status viewers list. diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index ad75cf50a..390d25cbc 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -388,6 +388,7 @@ settings: wasm_memory_limit: 1gib wasm_memory_threshold: 512mib log_visibility: controllers + snapshot_visibility: controllers status_visibility: controllers environment_variables: KEY: "value" diff --git a/docs/schemas/canister-yaml-schema.json b/docs/schemas/canister-yaml-schema.json index e05c42f1e..e9c6e5923 100644 --- a/docs/schemas/canister-yaml-schema.json +++ b/docs/schemas/canister-yaml-schema.json @@ -438,6 +438,17 @@ ], "description": "Upper limit on cycles reserved for future resource payments.\nMemory allocations that would push the reserved balance above this limit will fail.\nSupports suffixes in YAML: k, m, b, t (e.g. \"4t\" or \"4.3t\")." }, + "snapshot_visibility": { + "anyOf": [ + { + "$ref": "#/$defs/SnapshotVisibility" + }, + { + "type": "null" + } + ], + "description": "Controls who can read the canister's snapshots." + }, "status_visibility": { "anyOf": [ { @@ -474,6 +485,37 @@ }, "type": "object" }, + "SnapshotVisibility": { + "description": "Controls who can read the canister's snapshots.", + "oneOf": [ + { + "description": "'controllers' (only the canister's controllers can read the snapshots) or 'public' (anyone can read the snapshots)", + "enum": [ + "controllers", + "public" + ], + "type": "string" + }, + { + "additionalProperties": false, + "description": "Specific principals that can read the snapshots", + "properties": { + "allowed_viewers": { + "description": "Principal IDs that can read the snapshots, in addition to the controllers", + "items": { + "description": "A principal ID", + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "allowed_viewers" + ], + "type": "object" + } + ] + }, "StatusVisibility": { "description": "Controls who can read the canister's status.", "oneOf": [ diff --git a/docs/schemas/environment-yaml-schema.json b/docs/schemas/environment-yaml-schema.json index 96c8ee048..29053addd 100644 --- a/docs/schemas/environment-yaml-schema.json +++ b/docs/schemas/environment-yaml-schema.json @@ -244,6 +244,17 @@ ], "description": "Upper limit on cycles reserved for future resource payments.\nMemory allocations that would push the reserved balance above this limit will fail.\nSupports suffixes in YAML: k, m, b, t (e.g. \"4t\" or \"4.3t\")." }, + "snapshot_visibility": { + "anyOf": [ + { + "$ref": "#/$defs/SnapshotVisibility" + }, + { + "type": "null" + } + ], + "description": "Controls who can read the canister's snapshots." + }, "status_visibility": { "anyOf": [ { @@ -280,6 +291,37 @@ }, "type": "object" }, + "SnapshotVisibility": { + "description": "Controls who can read the canister's snapshots.", + "oneOf": [ + { + "description": "'controllers' (only the canister's controllers can read the snapshots) or 'public' (anyone can read the snapshots)", + "enum": [ + "controllers", + "public" + ], + "type": "string" + }, + { + "additionalProperties": false, + "description": "Specific principals that can read the snapshots", + "properties": { + "allowed_viewers": { + "description": "Principal IDs that can read the snapshots, in addition to the controllers", + "items": { + "description": "A principal ID", + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "allowed_viewers" + ], + "type": "object" + } + ] + }, "StatusVisibility": { "description": "Controls who can read the canister's status.", "oneOf": [ diff --git a/docs/schemas/icp-yaml-schema.json b/docs/schemas/icp-yaml-schema.json index 213804feb..ded4a3485 100644 --- a/docs/schemas/icp-yaml-schema.json +++ b/docs/schemas/icp-yaml-schema.json @@ -970,6 +970,17 @@ ], "description": "Upper limit on cycles reserved for future resource payments.\nMemory allocations that would push the reserved balance above this limit will fail.\nSupports suffixes in YAML: k, m, b, t (e.g. \"4t\" or \"4.3t\")." }, + "snapshot_visibility": { + "anyOf": [ + { + "$ref": "#/$defs/SnapshotVisibility" + }, + { + "type": "null" + } + ], + "description": "Controls who can read the canister's snapshots." + }, "status_visibility": { "anyOf": [ { @@ -1006,6 +1017,37 @@ }, "type": "object" }, + "SnapshotVisibility": { + "description": "Controls who can read the canister's snapshots.", + "oneOf": [ + { + "description": "'controllers' (only the canister's controllers can read the snapshots) or 'public' (anyone can read the snapshots)", + "enum": [ + "controllers", + "public" + ], + "type": "string" + }, + { + "additionalProperties": false, + "description": "Specific principals that can read the snapshots", + "properties": { + "allowed_viewers": { + "description": "Principal IDs that can read the snapshots, in addition to the controllers", + "items": { + "description": "A principal ID", + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "allowed_viewers" + ], + "type": "object" + } + ] + }, "StatusVisibility": { "description": "Controls who can read the canister's status.", "oneOf": [