Skip to content

[Bug Report] CubeOps store layer swallows every database error, so outages surface as "invalid credentials" #1381

Description

@dwin-gharibi

Three CubeOps store getters turn any database error into ("", nil) whenever the scanned value is
empty, so a database outage is reported to the operator as 401 invalid credentials instead of a
500.

Environment

  • CubeSandbox version / commit: 5960c56 (master)
  • Host OS and kernel version: any
  • KVM info (modinfo kvm): n/a — control-plane only
  • Deployment mode: single-node / cluster
  • Relevant component: CubeOps

Steps to Reproduce

  1. Bring up CubeOps against MySQL/Postgres and confirm you can log in.
  2. Stop the database (or block the port) so reads fail:
    docker stop <mysql-container>
  3. curl -si -X POST http://<cubeops>/api/v1/auth/login -d '{"username":"admin","password":"admin"}'

Expected Behavior

500 with the underlying database error, so the operator can see the real fault. The comment in
AuthService.Login states exactly this intent: "Genuine infrastructure errors (DB down, etc.) are
still returned verbatim so the operator can diagnose them."

Actual Behavior

401 {"error":"invalid credentials"}. The operator chases a phantom credentials problem while the
real fault is the database.

Reproduced with verbatim copies of both functions:

DB unreachable  -> pwd="" err=<nil>  (the connection error is DISCARDED)
user not found  -> pwd="" err=<nil>
ctx canceled    -> pwd="" err=<nil>  (discarded)

Login: err==nil, so BOTH branches of `if err != nil` are skipped -> DEAD CODE
Login: -> ErrInvalidCredentials (401) even though the DATABASE IS DOWN

Additional Context

The pattern appears three times:

  • CubeOps/internal/store/setting.go:24GetSystemSetting
  • CubeOps/internal/store/setting.go:59GetSetting
  • CubeOps/internal/store/setting.go:111GetUserPassword
if errors.Is(err, sql.ErrNoRows) || val == "" {
    return "", nil          // row missing, value empty, AND any DB error
}
return val, err

val is the zero value whenever the query fails, so val == "" is true for a genuine error too.

Because of this, both branches of if err != nil in AuthService.Login
(internal/service/auth.go:82-91) are unreachable — control falls straight through to
VerifyPassword("", password). ChangePassword (:179-185) has the same shape.

There is also a contract mismatch that let this survive: the service-layer UserStore fake in
internal/service/auth_test.go:23-29 returns ("", errors.New("user not found")) for an unknown
user, i.e. it models not-found as an error, while the real store returns ("", nil). The tests
therefore passed against a contract production never implemented.

staticcheck does not flag this (the assignment is used), and go vet does not either — it needs a
reader to notice the shadow-free-but-dead branch.

Related: #1 (empty JWT signing secret) is the same root cause reaching a different consumer —
GetOrCreateSystemSetting reads back through GetSystemSetting, so a transient read error yields an
empty JWT secret with no error.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions