Fix two defects a production-clone dress rehearsal exposed
Streamed a clone of a production store into the smoke VM - 3.6 GB, 12,361 settings, 6 accounts across 9 domains - and migrated it 0.15.5 -> 0.16.14 with the tool's own phases. The migration succeeded. Two defects surfaced that no smaller instance could have shown, plus one finding worth recording. 1. Account roles broke on production-shaped names. v0.16 stores an account as a local part plus a domain reference: a v0.15 account named "[email protected]" becomes name "john" with a domainId. The generator passed the full address and the server rejected it outright ("Invalid email local part"), failing the apply. The smoke instance used bare usernames - alice, bob - and never exercised this. Fixed to use the local part. And because local parts are unique only within a domain - [email protected] and [email protected] both become "postmaster" - an ambiguous one is now refused with a warning rather than risking an upsert that grants Admin to the wrong account. Verified on the clone: the one admin came out with roles {"@type": "Admin"} and the other five accounts untouched. 2. Cutover's health check conflated liveness with credentials. A config fallback-admin does not survive the migration - v0.16's config is a store pointer, so the old [authentication.fallback-admin] block simply ceases to exist - so the credentials supplied for the pre-migration instance came back 401 on the migrated one, and the check reported the service as never having answered. It had answered; it was up and serving on all ten ports. Liveness and credentials are now separate: any response proves the service is up, and credentials that stopped working are a warning that names this cause. Also recorded: a failed apply leaves the store in bootstrap mode, where only Bootstrap objects are accessible. A half-applied plan is not a partially configured server but an unusable one. Timing, which is the other reason to rehearse: the recovery-mode conversion of that 3.6 GB store took 2 seconds. A migration window is dominated by waiting and verification, not data volume. No production data in this commit; fixtures use example.net and the shapes involved.
This commit is contained in:
@@ -111,3 +111,53 @@ func TestAccountRolesReportsRolesItCannotMap(t *testing.T) {
|
||||
t.Errorf("warnings = %v, want the unmappable role named", warnings)
|
||||
}
|
||||
}
|
||||
|
||||
// Found by a dress rehearsal against a clone of production, where accounts
|
||||
// are named by address. v0.16 stores an account as a local part plus a
|
||||
// domain reference, and rejects a full address outright ("Invalid email
|
||||
// local part"). The smoke instance used bare names and never exercised it.
|
||||
func TestAccountRolesUsesTheLocalPartOfAnEmailStyleName(t *testing.T) {
|
||||
ops, _, warnings, err := AccountRoleOperations([]backup.Principal{
|
||||
{Type: "individual", Name: "[email protected]", Emails: []string{"[email protected]"}, Roles: []string{"admin"}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(ops) != 1 {
|
||||
t.Fatalf("generated %d op(s), want 1", len(ops))
|
||||
}
|
||||
v := ops[0].Value["role-john"]
|
||||
if v == nil {
|
||||
t.Fatalf("operation not keyed by local part: %+v", ops[0].Value)
|
||||
}
|
||||
if v["name"] != "john" {
|
||||
t.Errorf("name = %v, want the local part \"john\" - the full address is rejected by the server", v["name"])
|
||||
}
|
||||
if len(warnings) != 0 {
|
||||
t.Errorf("unexpected warnings: %v", warnings)
|
||||
}
|
||||
}
|
||||
|
||||
// Local parts are unique only within a domain. Granting Admin to the wrong
|
||||
// account is worse than granting it to none.
|
||||
func TestAccountRolesRefusesAnAmbiguousLocalPart(t *testing.T) {
|
||||
ops, covered, warnings, err := AccountRoleOperations([]backup.Principal{
|
||||
{Type: "individual", Name: "[email protected]", Roles: []string{"admin"}},
|
||||
{Type: "individual", Name: "[email protected]", Roles: []string{"user"}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(ops) != 0 {
|
||||
t.Errorf("generated %d op(s) for an ambiguous local part, want 0 - it could land on the wrong account", len(ops))
|
||||
}
|
||||
if len(covered) != 0 {
|
||||
t.Errorf("covered = %v, want none", covered)
|
||||
}
|
||||
if len(warnings) != 1 || !strings.Contains(warnings[0], "share the local part") {
|
||||
t.Errorf("warnings = %v, want one explaining the ambiguity", warnings)
|
||||
}
|
||||
if !strings.Contains(warnings[0], "by hand") {
|
||||
t.Errorf("warning %q should tell the operator what to do instead", warnings[0])
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user