Background
PR #459 (Phase A.2 Item 2) fixed a UTC drift in the RBAC FSM apply path: nextProposerTimestamp() on the proposer side and every time.Unix(0, entry.{Created,Updated}AtUnixNano) in internal/auth/cluster_rbac_apply.go now use .UTC() so SQLite text-encoded created_at values round-trip byte-identically under the WHERE created_at = ? readback in CreateRole / CreateMeasurementPermission.
That fix was narrowly scoped to RBAC. The same shape exists elsewhere in the codebase and should be audited.
Scope
Confirmed-needs-audit sites
The Phase A token applier path uses the same time.Unix(0, ...) shape without .UTC():
internal/auth/cluster_apply.go:132 — createdAt := time.Unix(0, entry.CreatedAtUnixNano)
internal/auth/cluster_apply.go:135 — expiresAt = time.Unix(0, entry.ExpiresAtUnixNano)
internal/auth/cluster_apply.go:204 — expiresAt = time.Unix(0, entry.ExpiresAtUnixNano)
There is no current WHERE created_at = ? or WHERE expires_at = ? equality readback paired with these inserts, so it doesn't round-trip-break today — but it's the same latent bug shape that bit PR #459, and any future readback added against these columns would inherit the timezone-mismatch risk.
Broader audit (lower priority)
Walk the codebase for proposer/applier-side timestamp pairs that pass through SQLite text serialisation:
- Grep for
time.Unix(0, and time.Now() in internal/auth/, internal/cluster/raft/, internal/database/, internal/audit/, and anywhere else timestamps land in SQLite.
- For each site that inserts a
time.Time into SQLite, confirm whether any read path uses WHERE <col> = ? equality. If yes, both sides must be UTC.
- For sites that only insert and never equality-match, normalise to UTC anyway for log/metric/diagnostic consistency.
Suggested approach
- Add
.UTC() to all time.Unix(0, ...) sites in internal/auth/cluster_apply.go (3 sites confirmed above) as a no-behaviour-change defensive normalisation.
- Run a broader grep + reviewer-agent sweep to enumerate every timestamp insert/readback pair across the auth, audit, and cluster packages.
- Add a unit test (or extend an existing one) that runs with
TZ=America/Argentina/Buenos_Aires set, exercising every Create/Update/Delete RBAC + token path. A round-trip-UTC bug only manifests in non-UTC environments; CI normally runs UTC so we won't catch regressions otherwise.
Why this should be tracked separately
PR #459's scope was the cascade-on-delete soft cap. The UTC fix landed inside that PR because Gemini's High finding on nextProposerTimestamp exposed it. Doing the broader sweep in the same PR would expand the diff well past Item 2's scope and make review harder. Better as its own focused PR.
References
Acceptance criteria
Background
PR #459 (Phase A.2 Item 2) fixed a UTC drift in the RBAC FSM apply path:
nextProposerTimestamp()on the proposer side and everytime.Unix(0, entry.{Created,Updated}AtUnixNano)ininternal/auth/cluster_rbac_apply.gonow use.UTC()so SQLite text-encodedcreated_atvalues round-trip byte-identically under theWHERE created_at = ?readback inCreateRole/CreateMeasurementPermission.That fix was narrowly scoped to RBAC. The same shape exists elsewhere in the codebase and should be audited.
Scope
Confirmed-needs-audit sites
The Phase A token applier path uses the same
time.Unix(0, ...)shape without.UTC():internal/auth/cluster_apply.go:132—createdAt := time.Unix(0, entry.CreatedAtUnixNano)internal/auth/cluster_apply.go:135—expiresAt = time.Unix(0, entry.ExpiresAtUnixNano)internal/auth/cluster_apply.go:204—expiresAt = time.Unix(0, entry.ExpiresAtUnixNano)There is no current
WHERE created_at = ?orWHERE expires_at = ?equality readback paired with these inserts, so it doesn't round-trip-break today — but it's the same latent bug shape that bit PR #459, and any future readback added against these columns would inherit the timezone-mismatch risk.Broader audit (lower priority)
Walk the codebase for proposer/applier-side timestamp pairs that pass through SQLite text serialisation:
time.Unix(0,andtime.Now()ininternal/auth/,internal/cluster/raft/,internal/database/,internal/audit/, and anywhere else timestamps land in SQLite.time.Timeinto SQLite, confirm whether any read path usesWHERE <col> = ?equality. If yes, both sides must be UTC.Suggested approach
.UTC()to alltime.Unix(0, ...)sites ininternal/auth/cluster_apply.go(3 sites confirmed above) as a no-behaviour-change defensive normalisation.TZ=America/Argentina/Buenos_Airesset, exercising every Create/Update/Delete RBAC + token path. A round-trip-UTC bug only manifests in non-UTC environments; CI normally runs UTC so we won't catch regressions otherwise.Why this should be tracked separately
PR #459's scope was the cascade-on-delete soft cap. The UTC fix landed inside that PR because Gemini's High finding on
nextProposerTimestampexposed it. Doing the broader sweep in the same PR would expand the diff well past Item 2's scope and make review harder. Better as its own focused PR.References
feedback_utc_must_match_on_both_sides_of_readbackmemory entry (project convention)Acceptance criteria
time.Unix(0, ...)sites ininternal/auth/cluster_apply.gonormalised to.UTC().time.Unix(0, ...)+time.Now()sites that land in SQLite; each documented as "audited, UTC-safe" or "fixed".TZto catch regressions.