diff --git a/core/dr-sync-nextcloud/src/provider.rs b/core/dr-sync-nextcloud/src/provider.rs index 7b1a503..adbb702 100644 --- a/core/dr-sync-nextcloud/src/provider.rs +++ b/core/dr-sync-nextcloud/src/provider.rs @@ -96,6 +96,18 @@ impl BackendProvider for NextcloudProvider { } } + /// TRACES: NFR-SEC-3 + /// An `http://` account written before sign-in kept the address the user + /// typed: it was stored as the server reported itself, and behind a proxy + /// without `overwriteprotocol` that is `http`. The client refuses to send + /// to it now, so it is upgraded here rather than left to fail. The + /// namespace ignores the scheme, so the catalog stays where it is. + fn upgrade_endpoint(&self, stored: &str) -> Option { + stored + .strip_prefix("http://") + .map(|rest| format!("https://{rest}")) + } + fn connect(&self, conn: &Connection) -> Result, RemoteError> { let creds = Self::credentials(conn)?; Ok(Box::new(NextcloudBackend::new( @@ -132,6 +144,18 @@ mod tests { assert!(p.normalise_endpoint(" ").is_err()); } + /// TRACES: NFR-SEC-3 + #[test] + fn a_stored_http_endpoint_is_upgraded_and_nothing_else_is_touched() { + let p = NextcloudProvider; + assert_eq!( + p.upgrade_endpoint("http://cloud.example/nextcloud") + .as_deref(), + Some("https://cloud.example/nextcloud") + ); + assert_eq!(p.upgrade_endpoint("https://cloud.example"), None); + } + #[test] fn the_account_keeps_the_dav_user_id_apart_from_the_login() { // A login can be an email address while the user id is something diff --git a/core/dr-sync/src/account.rs b/core/dr-sync/src/account.rs index 699d2d1..d870554 100644 --- a/core/dr-sync/src/account.rs +++ b/core/dr-sync/src/account.rs @@ -29,7 +29,7 @@ use dr_plat::{SecretError, SecretRef, SecretStore}; use dr_types::{Format, FormatFilter}; use serde::{Deserialize, Serialize}; -use crate::RemoteError; +use crate::{BackendRegistry, RemoteError}; /// The connector every account had before there was a choice. /// @@ -510,6 +510,95 @@ impl AccountStore { written } + /// TRACES: NFR-SEC-3 + /// Rewrite the endpoints an older build stored in a form this one would + /// not, asking each account's connector + /// ([`upgrade_endpoint`](crate::BackendProvider::upgrade_endpoint)). + /// + /// Per account and best effort: one that cannot be moved — its keyring + /// locked, say — is logged and left as it was, and is tried again on the + /// next launch, rather than stopping the others or the launch. Returns + /// the accounts it rewrote. + pub fn upgrade_endpoints(&self, registry: &BackendRegistry) -> Vec { + let mut upgraded = Vec::new(); + for account in self.list() { + let Ok(provider) = registry.for_account(&account) else { + continue; + }; + let Some(endpoint) = provider.upgrade_endpoint(&account.endpoint) else { + continue; + }; + if endpoint == account.endpoint { + continue; + } + match self.move_endpoint(&account, &endpoint) { + Ok(moved) => { + log::info!("account {} moved to {endpoint}", account.describe()); + upgraded.push(moved); + } + Err(e) => log::warn!( + "account {} could not be moved to {endpoint}: {e}", + account.describe() + ), + } + } + upgraded + } + + /// Move an account to a new endpoint, taking its credential with it. + /// + /// The endpoint is half of two keys, and both have to be dealt with. The + /// credential is filed under it ([`Account::secret_ref`]), so rewriting + /// the record alone would strand the app password under the old key and + /// sign the user out. And it feeds [`Account::namespace`], so a rewrite + /// that changed the namespace would abandon the catalog and everything + /// beside it; that is refused outright rather than left to the caller. + /// + /// Ordered so an interruption at any step leaves something that works: + /// the credential is copied before the record names the new key, and the + /// old copy is deleted only once nothing names the old one. + fn move_endpoint(&self, account: &Account, endpoint: &str) -> Result { + let mut moved = account.clone(); + moved.endpoint = endpoint.to_string(); + if moved.namespace() != account.namespace() { + return Err(AccountError::WouldMoveData { + from: account.namespace(), + to: moved.namespace(), + }); + } + + let mut config = self.read_config(); + // Already there — the user signed in again at the new address. That + // record and its credential are the newer, so the old one just goes. + let duplicate = config.sessions.iter().any(|a| a.is_same_as(&moved)); + + let old_ref = account.secret_ref(); + let secret = match self.secrets.retrieve(&old_ref) { + Ok(s) => Some(s), + Err(SecretError::NotFound) => None, + Err(e) => return Err(e.into()), + }; + if let (Some(s), false) = (&secret, duplicate) { + self.secrets.store(&moved.secret_ref(), s)?; + } + + if duplicate { + config.sessions.retain(|a| !a.is_same_as(account)); + } else { + // In place, not removed and pushed: the last record is the one + // the next launch resumes. + for a in config.sessions.iter_mut().filter(|a| a.is_same_as(account)) { + *a = moved.clone(); + } + } + self.write_config(&config)?; + + if secret.is_some() { + self.secrets.delete(&old_ref)?; + } + Ok(moved) + } + fn read_config(&self) -> ConfigFile { std::fs::read_to_string(&self.config_path) .ok() @@ -545,6 +634,11 @@ pub enum AccountError { #[error(transparent)] Remote(#[from] RemoteError), + + /// A change that would give an account a different local data directory, + /// leaving its catalog and caches behind under the old one. + #[error("moving the account would leave its local data behind ({from} → {to})")] + WouldMoveData { from: String, to: String }, } #[cfg(test)] @@ -574,6 +668,134 @@ mod tests { d } + /// A connector that upgrades `http://` endpoints the way Nextcloud's does, + /// and every other one not at all. + struct Upgrading(&'static str); + impl crate::BackendProvider for Upgrading { + fn id(&self) -> &'static str { + self.0 + } + fn display_name(&self) -> &'static str { + "U" + } + fn endpoint_label(&self) -> &'static str { + "Server" + } + fn endpoint_placeholder(&self) -> &'static str { + "" + } + fn sign_in(&self) -> crate::SignIn { + crate::SignIn::Browser + } + fn normalise_endpoint(&self, i: &str) -> Result { + Ok(i.into()) + } + fn upgrade_endpoint(&self, stored: &str) -> Option { + stored + .strip_prefix("http://") + .map(|rest| format!("https://{rest}")) + } + fn connect(&self, _: &Connection) -> Result, RemoteError> { + Err(RemoteError::Unsupported("stub")) + } + } + + fn upgrading(id: &'static str) -> BackendRegistry { + let mut r = BackendRegistry::new(); + r.register(std::sync::Arc::new(Upgrading(id))); + r + } + + /// TRACES: NFR-SEC-3 + #[test] + fn an_http_account_is_upgraded_and_keeps_its_credential_and_data() { + let dir = tmpdir("upgrade"); + let store = store_in(&dir); + let old = + Account::new(LEGACY_BACKEND, "http://cloud.example").with_login("duncan", "duncan"); + store.save(&folder(), None).unwrap(); + store + .save(&old, Some(&Secret::new("secret-token"))) + .unwrap(); + + let moved = store.upgrade_endpoints(&upgrading(LEGACY_BACKEND)); + assert_eq!(moved.len(), 1); + + let now = store.current().expect("still the account resumed"); + assert_eq!(now.endpoint, "https://cloud.example"); + assert_eq!( + now.namespace(), + old.namespace(), + "the catalog directory must not move" + ); + assert_eq!( + store + .connection(&now, true) + .unwrap() + .require_secret() + .unwrap() + .expose(), + "secret-token", + "the credential moves with the account" + ); + assert!( + store.connection(&old, true).is_err(), + "nothing is left under the old key" + ); + assert_eq!(store.list().len(), 2, "the folder library is untouched"); + + // And a second launch has nothing to do. + assert!(store + .upgrade_endpoints(&upgrading(LEGACY_BACKEND)) + .is_empty()); + } + + /// TRACES: NFR-SEC-3 + #[test] + fn a_move_that_would_change_the_data_directory_is_refused() { + // A scheme change keeps the namespace, which is what makes the upgrade + // safe; a host change does not, and would give the library a new, + // empty data directory. The account is left exactly as it was. + let dir = tmpdir("upgrade-refused"); + let store = store_in(&dir); + let old = nextcloud(); + store + .save(&old, Some(&Secret::new("secret-token"))) + .unwrap(); + + assert!(matches!( + store.move_endpoint(&old, "https://elsewhere.example"), + Err(AccountError::WouldMoveData { .. }) + )); + assert_eq!(store.current().unwrap().endpoint, old.endpoint); + assert!(store.connection(&old, true).is_ok()); + } + + /// TRACES: NFR-SEC-3 + #[test] + fn an_upgrade_onto_an_existing_sign_in_keeps_the_newer_one() { + let dir = tmpdir("upgrade-duplicate"); + let store = store_in(&dir); + let old = + Account::new(LEGACY_BACKEND, "http://cloud.example").with_login("duncan", "duncan"); + let new = + Account::new(LEGACY_BACKEND, "https://cloud.example").with_login("duncan", "duncan"); + store.save(&old, Some(&Secret::new("stale"))).unwrap(); + store.save(&new, Some(&Secret::new("fresh"))).unwrap(); + + store.upgrade_endpoints(&upgrading(LEGACY_BACKEND)); + assert_eq!(store.list().len(), 1); + assert_eq!( + store + .connection(&new, true) + .unwrap() + .require_secret() + .unwrap() + .expose(), + "fresh" + ); + } + #[test] fn a_saved_account_survives_reopening() { let dir = tmpdir("survives"); diff --git a/core/dr-sync/src/provider.rs b/core/dr-sync/src/provider.rs index 1f3ab1a..13a8340 100644 --- a/core/dr-sync/src/provider.rs +++ b/core/dr-sync/src/provider.rs @@ -83,6 +83,20 @@ pub trait BackendProvider: Send + Sync { /// wrong rather than naming a type. fn normalise_endpoint(&self, input: &str) -> Result; + /// The form a *stored* endpoint should take now, where an older build + /// wrote one this build would not. + /// + /// Not [`normalise_endpoint`](Self::normalise_endpoint) run again: that + /// judges what a person typed, and may touch the world to do it — a folder + /// is canonicalised and must exist — so rerunning it on every launch would + /// fail a library whose disk is unplugged, or rename one whose path now + /// resolves differently. This is a pure rewrite of the string, and `None` + /// means leave it alone, which is the answer for almost every connector. + fn upgrade_endpoint(&self, stored: &str) -> Option { + let _ = stored; + None + } + /// Build an account from a normalised endpoint alone. /// /// Only meaningful for [`SignIn::EndpointOnly`]; a browser flow produces diff --git a/docs/dev/traceability.md b/docs/dev/traceability.md index 5cb35af..ef0b8f1 100644 --- a/docs/dev/traceability.md +++ b/docs/dev/traceability.md @@ -111,10 +111,10 @@ _None._ | FR-MRG-6 | [`core/dr-pipeline/src/sidecar.rs:1033`](../../core/dr-pipeline/src/sidecar.rs#L1033), [`core/dr-pipeline/src/sidecar.rs:113`](../../core/dr-pipeline/src/sidecar.rs#L113), [`core/dr-pipeline/src/sidecar.rs:123`](../../core/dr-pipeline/src/sidecar.rs#L123), [`core/dr-pipeline/src/sidecar.rs:2241`](../../core/dr-pipeline/src/sidecar.rs#L2241), [`core/dr-pipeline/src/sidecar.rs:841`](../../core/dr-pipeline/src/sidecar.rs#L841), [`ui/dr-ui/src/merge.rs:842`](../../ui/dr-ui/src/merge.rs#L842) | | FR-MRG-7 | [`ui/dr-ui/src/merge.rs:1`](../../ui/dr-ui/src/merge.rs#L1), [`ui/dr-ui/src/merge_ui.rs:1`](../../ui/dr-ui/src/merge_ui.rs#L1), [`ui/dr-ui/ui/app.slint:533`](../../ui/dr-ui/ui/app.slint#L533), [`ui/dr-ui/ui/merge.slint:1`](../../ui/dr-ui/ui/merge.slint#L1) | | FR-MRG-8 | [`core/dr-pano/src/xfeat.rs:1`](../../core/dr-pano/src/xfeat.rs#L1), [`core/dr-segment/examples/onnx_probe.rs:1`](../../core/dr-segment/examples/onnx_probe.rs#L1) | -| FR-NC-1 | [`core/dr-sync-nextcloud/src/auth.rs:206`](../../core/dr-sync-nextcloud/src/auth.rs#L206), [`core/dr-sync-nextcloud/src/auth.rs:49`](../../core/dr-sync-nextcloud/src/auth.rs#L49), [`core/dr-sync-nextcloud/src/provider.rs:1`](../../core/dr-sync-nextcloud/src/provider.rs#L1), [`core/dr-sync/src/account.rs:375`](../../core/dr-sync/src/account.rs#L375), [`ui/dr-ui/src/launch.rs:316`](../../ui/dr-ui/src/launch.rs#L316), [`ui/dr-ui/src/launch.rs:61`](../../ui/dr-ui/src/launch.rs#L61), [`ui/dr-ui/src/launch_ui.rs:446`](../../ui/dr-ui/src/launch_ui.rs#L446) | +| FR-NC-1 | [`core/dr-sync-nextcloud/src/auth.rs:206`](../../core/dr-sync-nextcloud/src/auth.rs#L206), [`core/dr-sync-nextcloud/src/auth.rs:49`](../../core/dr-sync-nextcloud/src/auth.rs#L49), [`core/dr-sync-nextcloud/src/provider.rs:1`](../../core/dr-sync-nextcloud/src/provider.rs#L1), [`core/dr-sync/src/account.rs:375`](../../core/dr-sync/src/account.rs#L375), [`ui/dr-ui/src/launch.rs:316`](../../ui/dr-ui/src/launch.rs#L316), [`ui/dr-ui/src/launch.rs:61`](../../ui/dr-ui/src/launch.rs#L61), [`ui/dr-ui/src/launch_ui.rs:450`](../../ui/dr-ui/src/launch_ui.rs#L450) | | FR-NC-10 | [`core/dr-sync/src/account.rs:240`](../../core/dr-sync/src/account.rs#L240), [`ui/dr-ui/src/export.rs:1`](../../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/lib.rs:682`](../../ui/dr-ui/src/lib.rs#L682), [`ui/dr-ui/src/library/paths.rs:69`](../../ui/dr-ui/src/library/paths.rs#L69), [`ui/dr-ui/src/library/sidecar.rs:100`](../../ui/dr-ui/src/library/sidecar.rs#L100), [`ui/dr-ui/src/library/sidecar.rs:427`](../../ui/dr-ui/src/library/sidecar.rs#L427), [`ui/dr-ui/src/library/thumbnails_fetch.rs:449`](../../ui/dr-ui/src/library/thumbnails_fetch.rs#L449), [`ui/dr-ui/src/library_ui/controller.rs:657`](../../ui/dr-ui/src/library_ui/controller.rs#L657), [`ui/dr-ui/src/library_ui/offline.rs:623`](../../ui/dr-ui/src/library_ui/offline.rs#L623), [`ui/dr-ui/src/library_ui/sync.rs:100`](../../ui/dr-ui/src/library_ui/sync.rs#L100), [`ui/dr-ui/src/sidecar_cache.rs:1`](../../ui/dr-ui/src/sidecar_cache.rs#L1) | | FR-NC-12 | [`core/dr-sync-folder/src/lib.rs:1`](../../core/dr-sync-folder/src/lib.rs#L1), [`core/dr-sync-nextcloud/src/lib.rs:1097`](../../core/dr-sync-nextcloud/src/lib.rs#L1097), [`core/dr-sync-nextcloud/src/lib.rs:40`](../../core/dr-sync-nextcloud/src/lib.rs#L40), [`core/dr-sync-nextcloud/src/provider.rs:1`](../../core/dr-sync-nextcloud/src/provider.rs#L1), [`core/dr-sync/src/account.rs:1`](../../core/dr-sync/src/account.rs#L1), [`core/dr-sync/src/account.rs:87`](../../core/dr-sync/src/account.rs#L87), [`core/dr-sync/src/lib.rs:218`](../../core/dr-sync/src/lib.rs#L218), [`core/dr-sync/src/lib.rs:51`](../../core/dr-sync/src/lib.rs#L51), [`core/dr-sync/src/provider.rs:120`](../../core/dr-sync/src/provider.rs#L120), [`core/dr-sync/src/provider.rs:1`](../../core/dr-sync/src/provider.rs#L1), [`core/dr-sync/src/provider.rs:53`](../../core/dr-sync/src/provider.rs#L53), [`core/dr-sync/src/reachability.rs:1`](../../core/dr-sync/src/reachability.rs#L1), [`ui/dr-ui/src/remote.rs:1`](../../ui/dr-ui/src/remote.rs#L1) | -| FR-NC-13 | [`core/dr-sync-folder/src/lib.rs:197`](../../core/dr-sync-folder/src/lib.rs#L197), [`core/dr-sync-folder/src/lib.rs:1`](../../core/dr-sync-folder/src/lib.rs#L1), [`core/dr-sync-folder/src/lib.rs:73`](../../core/dr-sync-folder/src/lib.rs#L73), [`core/dr-sync/src/provider.rs:120`](../../core/dr-sync/src/provider.rs#L120), [`ui/dr-ui/src/launch_ui.rs:345`](../../ui/dr-ui/src/launch_ui.rs#L345), [`ui/dr-ui/src/remote.rs:1`](../../ui/dr-ui/src/remote.rs#L1) | +| FR-NC-13 | [`core/dr-sync-folder/src/lib.rs:197`](../../core/dr-sync-folder/src/lib.rs#L197), [`core/dr-sync-folder/src/lib.rs:1`](../../core/dr-sync-folder/src/lib.rs#L1), [`core/dr-sync-folder/src/lib.rs:73`](../../core/dr-sync-folder/src/lib.rs#L73), [`core/dr-sync/src/provider.rs:120`](../../core/dr-sync/src/provider.rs#L120), [`ui/dr-ui/src/launch_ui.rs:349`](../../ui/dr-ui/src/launch_ui.rs#L349), [`ui/dr-ui/src/remote.rs:1`](../../ui/dr-ui/src/remote.rs#L1) | | FR-NC-2 | [`core/dr-sync/src/account.rs:1`](../../core/dr-sync/src/account.rs#L1), [`core/dr-sync/src/account.rs:318`](../../core/dr-sync/src/account.rs#L318), [`core/dr-sync/src/account.rs:375`](../../core/dr-sync/src/account.rs#L375), [`core/dr-sync/src/account.rs:59`](../../core/dr-sync/src/account.rs#L59), [`platform/dr-plat/src/secrets.rs:82`](../../platform/dr-plat/src/secrets.rs#L82) | | FR-NC-3 | [`core/dr-decode/src/locate.rs:1`](../../core/dr-decode/src/locate.rs#L1), [`core/dr-decode/src/preview.rs:148`](../../core/dr-decode/src/preview.rs#L148), [`core/dr-sync/src/capability.rs:85`](../../core/dr-sync/src/capability.rs#L85), [`core/dr-thumbs/src/lib.rs:1`](../../core/dr-thumbs/src/lib.rs#L1), [`core/dr-types/src/place.rs:1`](../../core/dr-types/src/place.rs#L1), [`ui/dr-ui/src/library/mod.rs:1`](../../ui/dr-ui/src/library/mod.rs#L1), [`ui/dr-ui/src/library/sweep.rs:557`](../../ui/dr-ui/src/library/sweep.rs#L557), [`ui/dr-ui/src/library_ui/grid.rs:458`](../../ui/dr-ui/src/library_ui/grid.rs#L458), [`ui/dr-ui/src/library_ui/mod.rs:1`](../../ui/dr-ui/src/library_ui/mod.rs#L1), [`ui/dr-ui/src/library_ui/sync.rs:385`](../../ui/dr-ui/src/library_ui/sync.rs#L385), [`ui/dr-ui/ui/library.slint:737`](../../ui/dr-ui/ui/library.slint#L737), [`ui/dr-ui/ui/settings.slint:362`](../../ui/dr-ui/ui/settings.slint#L362), [`ui/dr-ui/ui/settings.slint:74`](../../ui/dr-ui/ui/settings.slint#L74) | | FR-NC-4 | [`core/dr-sync-folder/src/lib.rs:197`](../../core/dr-sync-folder/src/lib.rs#L197), [`core/dr-sync-nextcloud/src/propfind.rs:103`](../../core/dr-sync-nextcloud/src/propfind.rs#L103), [`core/dr-sync-nextcloud/src/propfind.rs:51`](../../core/dr-sync-nextcloud/src/propfind.rs#L51), [`core/dr-sync/src/capability.rs:6`](../../core/dr-sync/src/capability.rs#L6), [`core/dr-sync/src/lib.rs:218`](../../core/dr-sync/src/lib.rs#L218), [`core/dr-sync/src/scan.rs:129`](../../core/dr-sync/src/scan.rs#L129), [`ui/dr-ui/src/launch.rs:61`](../../ui/dr-ui/src/launch.rs#L61) | @@ -137,7 +137,7 @@ _None._ | FR-PLAT-LIN-1 | [`core/dr-types/src/settings.rs:1`](../../core/dr-types/src/settings.rs#L1), [`platform/dr-plat/src/dirs.rs:1`](../../platform/dr-plat/src/dirs.rs#L1), [`platform/dr-plat/src/storage.rs:344`](../../platform/dr-plat/src/storage.rs#L344), [`ui/dr-ui/src/lib.rs:1386`](../../ui/dr-ui/src/lib.rs#L1386), [`ui/dr-ui/src/preset_store.rs:1`](../../ui/dr-ui/src/preset_store.rs#L1), [`ui/dr-ui/src/settings_store.rs:1`](../../ui/dr-ui/src/settings_store.rs#L1) | | FR-PLAT-LIN-2 | [`platform/dr-plat/src/display.rs:1`](../../platform/dr-plat/src/display.rs#L1), [`platform/dr-plat/src/display/wayland.rs:1`](../../platform/dr-plat/src/display/wayland.rs#L1), [`platform/dr-plat/src/display/x11.rs:1`](../../platform/dr-plat/src/display/x11.rs#L1) | | FR-PLAT-WIN-1 | [`platform/dr-plat/src/dirs.rs:1`](../../platform/dr-plat/src/dirs.rs#L1) | -| FR-PLAT-WIN-2 | [`apps/darkroom-desktop/build.rs:1`](../../apps/darkroom-desktop/build.rs#L1), [`apps/darkroom-desktop/src/main.rs:6`](../../apps/darkroom-desktop/src/main.rs#L6), [`ui/dr-ui/src/launch_ui.rs:861`](../../ui/dr-ui/src/launch_ui.rs#L861) | +| FR-PLAT-WIN-2 | [`apps/darkroom-desktop/build.rs:1`](../../apps/darkroom-desktop/build.rs#L1), [`apps/darkroom-desktop/src/main.rs:6`](../../apps/darkroom-desktop/src/main.rs#L6), [`ui/dr-ui/src/launch_ui.rs:865`](../../ui/dr-ui/src/launch_ui.rs#L865) | | FR-PLAT-WIN-3 | [`apps/darkroom-desktop/src/main.rs:19`](../../apps/darkroom-desktop/src/main.rs#L19) | | FR-RAW-1 | [`core/dr-decode/src/lib.rs:259`](../../core/dr-decode/src/lib.rs#L259), [`core/dr-types/src/lib.rs:132`](../../core/dr-types/src/lib.rs#L132), [`core/dr-types/src/lib.rs:203`](../../core/dr-types/src/lib.rs#L203) | | FR-RAW-3 | [`core/dr-decode/src/lib.rs:155`](../../core/dr-decode/src/lib.rs#L155), [`core/dr-decode/src/lib.rs:546`](../../core/dr-decode/src/lib.rs#L546), [`core/dr-decode/src/locate.rs:1435`](../../core/dr-decode/src/locate.rs#L1435) | @@ -182,7 +182,7 @@ _None._ | NFR-RES-4 | [`core/dr-catalog/src/cache.rs:1`](../../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/face_shard.rs:1`](../../core/dr-catalog/src/face_shard.rs#L1), [`core/dr-catalog/src/schema.rs:1182`](../../core/dr-catalog/src/schema.rs#L1182), [`core/dr-gpu/src/lib.rs:95`](../../core/dr-gpu/src/lib.rs#L95), [`core/dr-thumbs/src/codec.rs:1`](../../core/dr-thumbs/src/codec.rs#L1), [`core/dr-thumbs/src/lib.rs:1`](../../core/dr-thumbs/src/lib.rs#L1), [`core/dr-thumbs/src/lib.rs:393`](../../core/dr-thumbs/src/lib.rs#L393) | | NFR-SEC-1 | [`core/dr-decode/src/error.rs:1`](../../core/dr-decode/src/error.rs#L1), [`core/dr-decode/src/error.rs:30`](../../core/dr-decode/src/error.rs#L30) | | NFR-SEC-2 | [`core/dr-sync/src/account.rs:318`](../../core/dr-sync/src/account.rs#L318), [`platform/dr-plat/src/crash.rs:1`](../../platform/dr-plat/src/crash.rs#L1), [`platform/dr-plat/src/diagnostics.rs:1`](../../platform/dr-plat/src/diagnostics.rs#L1), [`platform/dr-plat/src/diagnostics/bundle.rs:1`](../../platform/dr-plat/src/diagnostics/bundle.rs#L1), [`platform/dr-plat/src/diagnostics/redact.rs:1`](../../platform/dr-plat/src/diagnostics/redact.rs#L1), [`platform/dr-plat/src/secrets.rs:82`](../../platform/dr-plat/src/secrets.rs#L82) | -| NFR-SEC-3 | [`core/dr-sync-nextcloud/src/auth.rs:174`](../../core/dr-sync-nextcloud/src/auth.rs#L174), [`core/dr-sync-nextcloud/src/auth.rs:244`](../../core/dr-sync-nextcloud/src/auth.rs#L244), [`core/dr-sync-nextcloud/src/auth.rs:274`](../../core/dr-sync-nextcloud/src/auth.rs#L274), [`core/dr-sync-nextcloud/src/auth.rs:89`](../../core/dr-sync-nextcloud/src/auth.rs#L89), [`core/dr-sync-nextcloud/src/lib.rs:1035`](../../core/dr-sync-nextcloud/src/lib.rs#L1035), [`core/dr-sync-nextcloud/src/lib.rs:1`](../../core/dr-sync-nextcloud/src/lib.rs#L1), [`core/dr-sync-nextcloud/src/lib.rs:726`](../../core/dr-sync-nextcloud/src/lib.rs#L726), [`core/dr-sync-nextcloud/src/provider.rs:147`](../../core/dr-sync-nextcloud/src/provider.rs#L147), [`core/dr-sync-nextcloud/src/provider.rs:1`](../../core/dr-sync-nextcloud/src/provider.rs#L1), [`core/dr-sync-nextcloud/src/provider.rs:99`](../../core/dr-sync-nextcloud/src/provider.rs#L99), [`core/dr-sync/src/account.rs:513`](../../core/dr-sync/src/account.rs#L513), [`core/dr-sync/src/account.rs:709`](../../core/dr-sync/src/account.rs#L709), [`core/dr-sync/src/account.rs:753`](../../core/dr-sync/src/account.rs#L753), [`core/dr-sync/src/account.rs:774`](../../core/dr-sync/src/account.rs#L774), [`ui/dr-ui/src/launch_ui.rs:1018`](../../ui/dr-ui/src/launch_ui.rs#L1018), [`ui/dr-ui/src/launch_ui.rs:833`](../../ui/dr-ui/src/launch_ui.rs#L833) | +| NFR-SEC-3 | [`core/dr-sync-nextcloud/src/auth.rs:174`](../../core/dr-sync-nextcloud/src/auth.rs#L174), [`core/dr-sync-nextcloud/src/auth.rs:244`](../../core/dr-sync-nextcloud/src/auth.rs#L244), [`core/dr-sync-nextcloud/src/auth.rs:274`](../../core/dr-sync-nextcloud/src/auth.rs#L274), [`core/dr-sync-nextcloud/src/auth.rs:89`](../../core/dr-sync-nextcloud/src/auth.rs#L89), [`core/dr-sync-nextcloud/src/lib.rs:1035`](../../core/dr-sync-nextcloud/src/lib.rs#L1035), [`core/dr-sync-nextcloud/src/lib.rs:1`](../../core/dr-sync-nextcloud/src/lib.rs#L1), [`core/dr-sync-nextcloud/src/lib.rs:726`](../../core/dr-sync-nextcloud/src/lib.rs#L726), [`core/dr-sync-nextcloud/src/provider.rs:147`](../../core/dr-sync-nextcloud/src/provider.rs#L147), [`core/dr-sync-nextcloud/src/provider.rs:1`](../../core/dr-sync-nextcloud/src/provider.rs#L1), [`core/dr-sync-nextcloud/src/provider.rs:99`](../../core/dr-sync-nextcloud/src/provider.rs#L99), [`core/dr-sync/src/account.rs:513`](../../core/dr-sync/src/account.rs#L513), [`core/dr-sync/src/account.rs:709`](../../core/dr-sync/src/account.rs#L709), [`core/dr-sync/src/account.rs:753`](../../core/dr-sync/src/account.rs#L753), [`core/dr-sync/src/account.rs:774`](../../core/dr-sync/src/account.rs#L774), [`ui/dr-ui/src/launch_ui.rs:1022`](../../ui/dr-ui/src/launch_ui.rs#L1022), [`ui/dr-ui/src/launch_ui.rs:837`](../../ui/dr-ui/src/launch_ui.rs#L837) | | NFR-SEC-4 | [`platform/dr-plat/src/diagnostics/bundle.rs:1`](../../platform/dr-plat/src/diagnostics/bundle.rs#L1) | | NFR-SEC-5 | [`core/dr-catalog/src/faces.rs:1`](../../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:1011`](../../core/dr-catalog/src/schema.rs#L1011), [`platform/dr-plat/src/diagnostics/bundle.rs:1`](../../platform/dr-plat/src/diagnostics/bundle.rs#L1), [`ui/dr-ui/src/faces.rs:1`](../../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/identity.rs:1`](../../ui/dr-ui/src/identity.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../../ui/dr-ui/src/identity_ui.rs#L1), [`ui/dr-ui/ui/identity.slint:1`](../../ui/dr-ui/ui/identity.slint#L1) | | R3 | [`core/dr-export/src/lib.rs:1`](../../core/dr-export/src/lib.rs#L1), [`core/dr-pipeline/src/lib.rs:1`](../../core/dr-pipeline/src/lib.rs#L1) | diff --git a/ui/dr-ui/src/launch_ui.rs b/ui/dr-ui/src/launch_ui.rs index e8d0e22..fc38a31 100644 --- a/ui/dr-ui/src/launch_ui.rs +++ b/ui/dr-ui/src/launch_ui.rs @@ -29,6 +29,10 @@ pub struct LaunchController { impl LaunchController { pub fn new() -> Rc { let store = AccountStore::open(Box::new(PlatformSecretStore::new())); + // Before anything reads an account: an endpoint an older build stored + // as `http://` is refused by the client, so resuming it unrewritten + // would fail the library it names (NFR-SEC-3). + store.upgrade_endpoints(crate::remote::registry()); let model = LaunchModel::from_store(&store); Rc::new(Self { model: RefCell::new(model),