Skip to content

fix(cms): LUMIBASE_REQUIRE_SETUP_TOKEN locks the instance out — the token is never printed #470

Description

@khuepm

Summary

Setting LUMIBASE_REQUIRE_SETUP_TOKEN=true makes an instance impossible to set up: /setup/complete demands a token, but the CMS never prints one. There is no way out except turning the flag off and recreating the container.

The mint-and-print helper exists and is unit-tested. It is simply never called at startup.

Found while implementing #332 (PR #468): the starter initially enabled this flag believing it was the safer choice.

Reproduce

docker run -e LUMIBASE_REQUIRE_SETUP_TOKEN=true -e DATABASE_URL=... \
  ghcr.io/khuepm/lumibase-cms

docker logs <container> | grep SETUP_TOKEN
# (nothing)

curl -s -H 'x-lumi-site: __default__' http://localhost:1989/api/v1/setup/state
# {"state":"uninitialized","requiresSetupToken":true}

curl -s -X POST -H 'content-type: application/json' -H 'x-lumi-site: __default__' \
  -d '{"account":{...},"adminPath":"admin-a7f3c1"}' \
  http://localhost:1989/api/v1/setup/complete
# {"errors":[{"code":"SETUP_TOKEN_REQUIRED"}]}

A closed loop: requiresSetupToken: true, complete refused, and no way to obtain the token.

Cause

printSetupTokenIfRequired (apps/cms/src/modules/setup/setup-token.ts:148) does the right thing: generates a token, stores its sha256 in system_state.setup_token_hash, prints one [lumibase-cms] SETUP_TOKEN=<token> line (:199), and is idempotent within a process.

But grepping the repo returns three hits, all inside that same file — two doc-comment mentions and the declaration:

$ git grep -n "printSetupTokenIfRequired" -- '*.ts' | grep -v __tests__
apps/cms/src/modules/setup/setup-token.ts:16:  *   - {@link printSetupTokenIfRequired} encapsulates the startup-side
apps/cms/src/modules/setup/setup-token.ts:115: * if `printSetupTokenIfRequired` is called multiple times (e.g. test
apps/cms/src/modules/setup/setup-token.ts:148:export async function printSetupTokenIfRequired(

Neither serve.ts nor index.ts calls it. The reading side — verifySetupToken, the requiresSetupToken branch of /setup/state, the SETUP_TOKEN_REQUIRED error — is fully wired. So the system checks a token that nothing produces.

How it slipped through

The helper's own doc comment cites Req 2.6, and the requirement is explicit (.kiro/specs/admin-setup-wizard/requirements.md:63):

WHERE biến môi trường LUMIBASE_REQUIRE_SETUP_TOKEN=true được set, THE CMS SHALL sinh Setup_Token ngẫu nhiên … lúc startup khi System_State là uninitialized và in token vào stdout đúng một lần.

(Where LUMIBASE_REQUIRE_SETUP_TOKEN=true is set, the CMS shall generate a random Setup_Token … at startup while System_State is uninitialized, and print it to stdout exactly once.)

The unit tests call the helper directly, so they stay green; nothing asserts that startup calls it. This is the classic gap between "the function is correct" and "the function is wired in".

Impact

  • The flag is a trap: an operator enables it to be safer and locks themselves out instead.
  • The broken state only shows up after the container is running and someone tries to set up.
  • In practice an exposed instance therefore cannot be protected the way the docs describe: the only way to complete setup is to leave the flag off, which means whoever reaches the port first claims the admin account.

Suggested fix (not implemented)

  1. Call printSetupTokenIfRequired from apps/cms/src/serve.ts during startup (after the DB is available, before serving requests).
  2. Decide the Cloudflare story: Workers has no "startup" in this sense, so either mint lazily on the first /setup/* touch, or document the flag as Node/Docker-only.
  3. A regression test should go through the startup path rather than calling the helper directly — that is exactly where this slipped.

Verified on

main @ 6a20441a, and on the published image ghcr.io/khuepm/lumibase-cms@sha256:3f125caa… (revision 683a0270).

Related: the #332 starter deliberately does not enable this flag and says why in its README; revisit that once this closes.


🇻🇳 Tóm tắt tiếng Việt

Vấn đề. Đặt LUMIBASE_REQUIRE_SETUP_TOKEN=true làm instance không thể setup được nữa: /setup/complete đòi token, nhưng CMS không bao giờ in ra token nào. Không có đường phục hồi ngoài tắt cờ rồi tạo lại container.

Nguyên nhân. printSetupTokenIfRequired (setup-token.ts:148) làm đúng việc — sinh token, lưu hash, in một dòng [lumibase-cms] SETUP_TOKEN=<token> (:199). Nhưng grep toàn repo chỉ ra 3 kết quả, đều trong chính file đó: hai dòng doc comment và một dòng khai báo. serve.tsindex.ts đều không gọi. Trong khi phía đọc token (verifySetupToken, nhánh requiresSetupToken, mã lỗi SETUP_TOKEN_REQUIRED) đã nối đầy đủ — nên hệ thống kiểm tra một token mà không có gì sinh ra nó.

Vì sao lọt. Requirement ghi tường minh (admin-setup-wizard/requirements.md:63): sinh token lúc startup và in đúng một lần. Unit test gọi thẳng hàm nên vẫn xanh; không có test nào khẳng định startup gọi nó — khoảng trống điển hình giữa "hàm đúng" và "hàm được nối vào".

Hệ quả. Cờ này là bẫy: bật để an toàn hơn thì tự khoá mình ra ngoài. Thực tế khiến một instance expose không thể được bảo vệ theo cách tài liệu mô tả — muốn setup được thì phải để cờ tắt, tức ai tới cổng trước thì chiếm tài khoản admin.

Hướng sửa (gợi ý): (1) gọi printSetupTokenIfRequired trong serve.ts lúc khởi động; (2) chốt cách xử lý cho Cloudflare (Workers không có "startup" — hoặc mint lazy ở lần chạm /setup/* đầu, hoặc ghi rõ cờ chỉ dành cho Node/Docker); (3) test hồi quy phải đi qua đường khởi động, không gọi thẳng hàm.

Kiểm chứng trên main @ 6a20441a và image đã phát hành sha256:3f125caa….

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions