mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
The edge01 demo refresh copied `password_hash` straight from the source account, and the README beside it asserted "demo login works". That only held while the source account happened to use the password the project publishes. The source is the operator's own instance, so the moment that password was rotated the demo started rejecting the credentials the README hands to every visitor, and nothing anywhere noticed. Someone eventually filed it as a bug (#805) after being locked out of the shop window. The load now hands the row an explicit password: the same Argon2id literal the seed script writes, applied right after the copy, then read back and compared byte for byte. A mismatch aborts the run rather than waiting for a stranger to discover it at a login screen. The statement goes in on psql's stdin. Interpolating it into the ssh command string would let the remote shell expand the hash's `$argon2id`, `$v`, `$m`, `$t` and `$p` segments to empty strings and store something no password can ever match, which is the same outage wearing a different hat. A guard test pins that too, alongside pinning the literal to the seed script's copy so the two cannot drift apart again.
74 lines
2.9 KiB
TypeScript
74 lines
2.9 KiB
TypeScript
import { readFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
/**
|
|
* The demo password is published in the README so anyone can sign in to
|
|
* demo.healthlog.dev. Its Argon2id hash lives in two places that must agree:
|
|
*
|
|
* - scripts/seed-demo.ts writes it when a demo tenant is seeded
|
|
* - scripts/migrate-demo-edge01.sh overwrites it after a migration load
|
|
*
|
|
* Public issue #805: the migration used to carry the SOURCE account's hash
|
|
* across instead, so once the operator rotated their own demo password the
|
|
* credentials the README advertises stopped working, and nothing noticed until
|
|
* a visitor filed a bug. These assertions make the two copies move together.
|
|
*/
|
|
|
|
const REPO_ROOT = join(__dirname, "..", "..");
|
|
const SEED = join(REPO_ROOT, "scripts", "seed-demo.ts");
|
|
const MIGRATE = join(REPO_ROOT, "scripts", "migrate-demo-edge01.sh");
|
|
|
|
/** Every Argon2id hash literal in a file, in source order. */
|
|
function argon2Hashes(path: string): string[] {
|
|
const source = readFileSync(path, "utf8");
|
|
return source.match(/\$argon2id\$[^"'\s]+/g) ?? [];
|
|
}
|
|
|
|
describe("published demo password", () => {
|
|
it("is the same hash in the seed script and the edge01 migration", () => {
|
|
const seeded = argon2Hashes(SEED);
|
|
const migrated = argon2Hashes(MIGRATE);
|
|
|
|
// An empty match set would make every assertion below vacuously true —
|
|
// the failure mode this repo keeps rediscovering. Demand a hit first.
|
|
expect(seeded.length).toBeGreaterThan(0);
|
|
expect(migrated.length).toBeGreaterThan(0);
|
|
|
|
// Each file states the hash exactly once, so a stray second copy (the
|
|
// thing that lets the two drift again) is a failure in its own right.
|
|
expect(new Set(seeded).size).toBe(1);
|
|
expect(new Set(migrated).size).toBe(1);
|
|
|
|
expect(migrated[0]).toBe(seeded[0]);
|
|
});
|
|
|
|
it("is written by the migration rather than carried from the source", () => {
|
|
const script = readFileSync(MIGRATE, "utf8");
|
|
|
|
// The overwrite must exist...
|
|
expect(script).toMatch(
|
|
/UPDATE users SET password_hash = '\$\{?PUBLIC_DEMO_PASSWORD_HASH\}?'/,
|
|
);
|
|
|
|
// ...and it must be proven, not merely attempted. Without the check a
|
|
// mangled hash would reach a visitor's login screen instead of the script.
|
|
expect(script).toContain(
|
|
"ABORT/FAIL: demo password_hash on edge01 is not the published literal",
|
|
);
|
|
});
|
|
|
|
it("keeps the overwrite off the ssh command line", () => {
|
|
const script = readFileSync(MIGRATE, "utf8");
|
|
|
|
// The hash carries '$argon2id', '$v', '$m', '$t', '$p'. Interpolated into
|
|
// a double-quoted remote command those expand to empty strings and the
|
|
// stored hash matches nothing. It has to travel on stdin.
|
|
const sshWithHashInline = new RegExp(
|
|
String.raw`ssh\s+"\$DST_SSH"\s+"[^"]*PUBLIC_DEMO_PASSWORD_HASH`,
|
|
);
|
|
expect(script).not.toMatch(sshWithHashInline);
|
|
});
|
|
});
|