R-193: the recovery screen — unlocking, and only unlocking (v0.200.0)
A customer whose machine was rebuilt had everything needed to get their data back and no way to find out: the only route was a command line. This is the screen that closes that. IT UNLOCKS, AND ONLY UNLOCKS (operator ruling). It explains, takes the recovery code, opens the repository and shows what is in there — apps, dates, sizes. It restores nothing: restore is already per-app and lives in the backups area, and a screen that unlocks and then offers to overwrite is two decisions wearing one button. ONE CORE, TWO CALLERS. RecoverInstallCore is split out of RecoverAndInstall; the CLI wrapper keeps its exit codes and printed lines byte-identical, and the handler drives the same function. Two implementations of the one operation that can permanently lose a customer's data would drift, and only one would be tested. Asserted from source on both sides by AST. THREE WAYS OUT, none a dismiss button: recover; 'most nem' (the full page stops interrupting, the backups-area entry point stays PERMANENTLY, bound to the offer and never to the postpone flag); and 'I do not want the old data' — confirmed TWICE and reaching the SHIPPED move-aside, which sets aside and never deletes. THE CODE IS HANDLED NO MORE LOOSELY THAN ON THE COMMAND LINE: POST body only, never logged, never persisted, never echoed, cleared on every path, no-store, autocomplete off. No lockout — the code is a ten-word phrase, and locking a customer out of their own data for a typo is worse than anything it prevents. TWO DEFECTS THE TESTS CAUGHT, both fixed: an UNCLAIMED (legacy-open) box would have been shown the page, because RequireAuth passes such a box through; and the inventory nil-dereferenced when no off-site target was configured, which is exactly the pristine rebuilt shape.
This commit is contained in:
@@ -1241,6 +1241,44 @@ func (m *Manager) needsOffsiteCredential(t *settings.OffboxTarget) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// OffsiteRecoveryOffer reports whether the customer should be OFFERED the recovery screen (R-193):
|
||||
// the hub is holding a sealed recovery package for this box, and this box cannot open what that
|
||||
// package protects.
|
||||
//
|
||||
// TWO FACTS, BOTH REQUIRED — and this is the whole correctness of the screen:
|
||||
//
|
||||
// 1. **the hub holds a sealed package** (the ACK's identity_blob_present, cached in settings). Without
|
||||
// it there is nothing to recover, and a box that never had off-site backups must never be greeted
|
||||
// by a recovery screen for data it never had. Dropping this condition is the plausible wrong fix.
|
||||
// 2. **this box cannot open the history that package protects** — see the two shapes below.
|
||||
//
|
||||
// ⚠ WHY SHAPE (b) EXISTS, recorded because the task specified only shape (a) and the difference is
|
||||
// load-bearing. The literal reading of "the data area is fresh" is *no repository password on disk*,
|
||||
// which is true of a rebuilt box — but only until it re-applies its off-site target, because
|
||||
// `WriteOffboxSecrets` AUTO-GENERATES a repository password when none is present. That is precisely
|
||||
// R-193's orphaning mechanism, and since hub v0.96.0's credential self-heal the re-apply happens by
|
||||
// itself within ~15–30 minutes. So shape (a) alone would make this screen appear only inside a
|
||||
// half-hour window that closes on its own, and the customer who logs in the next morning — the actual
|
||||
// customer — would never see it. Shape (b) is the state they are in: a repository password exists, but
|
||||
// it is a NEW one and the inherited history cannot be opened with it, which the box has already
|
||||
// measured and recorded as `RepoState == "orphaned"`.
|
||||
//
|
||||
// Shape (b) also happens to be the state the existing move-aside requires (`ResetOrphanedRepo` refuses
|
||||
// unless orphaned), which is what lets "I do not want the old data" reach the shipped handler rather
|
||||
// than needing a new one.
|
||||
//
|
||||
// Scenario B still holds exactly: a healthy box has its own password and is not orphaned; a box that
|
||||
// never had off-site backups fails fact 1; an unclaimed box never reaches an authenticated page.
|
||||
func (m *Manager) OffsiteRecoveryOffer() bool {
|
||||
if m.settings == nil || !m.settings.GetHubEscrowIdentityPresent() {
|
||||
return false // the hub holds nothing for us — nothing to recover
|
||||
}
|
||||
if _, ok := m.OffboxRepoPasswordHash(); !ok {
|
||||
return true // (a) no repository password at all — the pristine rebuilt box
|
||||
}
|
||||
return m.OffboxOrphaned() // (b) a password exists but the inherited history will not open under it
|
||||
}
|
||||
|
||||
// OffboxReportStatus returns the offsite summary for the hub report.
|
||||
//
|
||||
// nil = nothing to say (not configured, and nothing to ask for) — the hub's checker treats absence as
|
||||
|
||||
Reference in New Issue
Block a user