Skip to content

fix(couchbase): persist json:"-" fields; guard nil password in login#646

Open
lakhansamani wants to merge 3 commits into
mainfrom
fix/couchbase-secret-json-tag-persistence
Open

fix(couchbase): persist json:"-" fields; guard nil password in login#646
lakhansamani wants to merge 3 commits into
mainfrom
fix/couchbase-secret-json-tag-persistence

Conversation

@lakhansamani

Copy link
Copy Markdown
Contributor

Summary

Couchbase is the only storage provider that (de)serializes entire schema structs through encoding/json — every other provider persists via a different tag (gorm column tags / bson / cql / dynamo), independent of json. User.Password carries json:"-" so it never leaks into API/log JSON output, but that same tag meant Couchbase's Insert/Upsert (write) and N1QL row decode (read) both silently dropped it — basic-auth password hashes have never actually been persisted on Couchbase.

Root cause (confirmed against gocb v2.6.4 in the module cache)

  • Write: Collection.Insert/Upsert → default JSON transcoder → json.Marshal → honors json:"-".
  • Read: N1QL QueryResult.Row/Onejson.Unmarshal → same.

Only User.Password is affected today (grepped every json:"-" field in internal/storage/schemas/) — it's the sole field with a dash tag in the codebase currently on main.

Fix

Two helpers in internal/storage/db/couchbase/shared.go:

  • structToDocument(v) — marshal normally, then re-add any json:"-" field under its bson tag key before writing.
  • decodeDocument(data, dest) — unmarshal normally, then populate json:"-" fields from their bson key.

Chose bson over cql as the source-of-truth tag because Couchbase's existing N1QL queries already select columns by json-tag-derived names (e.g. _id), and bson tags are byte-identical to json tags everywhere except the dashed fields — so this is provably shape-preserving for the 13 unaffected entities, and only adds back the previously-dropped key for User.

  • Write helper applied to all 14 Insert/Upsert call sites (no-op for entities without a json:"-" field).
  • Read helper applied only to User's 4 read methods (the only entity currently affected).

Bonus fix: nil-password panic guard in internal/service/login.go

While investigating, found bcrypt.CompareHashAndPassword([]byte(*user.Password), ...) dereferences user.Password unconditionally. A basic_auth user with no persisted hash (exactly what this bug produced on Couchbase) would nil-pointer-panic on login instead of failing cleanly. Now treated the same as a wrong password, reusing the existing dummy-bcrypt timing-equalization helper. Added a regression test.

Data-integrity note for existing Couchbase deployments

This fix does not retroactively recover already-missing password hashes — pre-existing Couchbase basic-auth users have no stored hash and will need to go through forgot-password/reset once this ships. New signups and resets persist correctly going forward. Social-login users are unaffected.

Test plan

  • go build ./..., go vet ./... clean
  • make lint-go — 0 issues
  • New unit tests for the couchbase helpers (shared_test.go) — round-trip, shape-preservation, nil-clear semantics
  • New regression test in internal/integration_tests/login_test.go for the nil-password panic guard
  • make test-sqlite (full suite) — pass
  • make test-couchbase (real Couchbase via Docker) — pass, including User_Operations asserting the password round-trips

Couchbase (de)serializes whole schema structs via encoding/json (gocb
default transcoder), which honors json:"-". User.Password is tagged
json:"-" for API safety, so it was silently dropped on both write and
read — password auth never worked on Couchbase, and no migration existed.

- add structToDocument / decodeDocument helpers that re-add json:"-"
  fields under their bson key; route all Insert/Upsert through the write
  helper and User reads through the read helper
- assert password round-trips in the cross-DB storage provider test
  (would have caught this) plus unit tests for the helpers
CompareHashAndPassword dereferenced user.Password unconditionally.
A basic_auth user with no persisted hash (e.g. a pre-fix Couchbase
record, since that provider silently dropped json:"-" fields on
write) would nil-pointer-panic instead of failing the login cleanly.
Treats a nil hash the same as a wrong password, using the existing
dummy-bcrypt timing-equalisation helper.
log.Fatal calls os.Exit(1), so one malformed row would crash the
whole server on an admin list call. Flagged independently by both
review passes on this PR since it now also covers decodeDocument
failures.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant