Skip to content

Commit 7d7babb

Browse files
docs(permissions): state the allowed_ip_ranges save refusal, the live clear and the env recovery (#30382)
<!-- claude-projects-attribution: {"github_login":"marchtian"} --> _Requested by **田騏** · [project thread](https://claude.ai/code/project/chan_01VmuUhXU9SSUzqJYyXSmYSi?thread=cmsg_01VmuUhXU9SSUzqJYyXSmYSiNjyvCdXYygFqntMNR7S2dW)_ Fixes #30314 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:认证文档的 `allowed_ip_ranges` 一行加了一句话并链到一个短小节,写明三件事:保存一份不匹配自己当前地址的列表会被拒(400 `SETTINGS_VALIDATION`,提示里写明你的地址和救援办法);清空字段立即生效、回到部署自己声明的列表;已被锁在外面时把 `OS_AUTH_ALLOWED_IP_RANGES` 设为空值并重启。 - 为什么改:PR #30309 已经上线了这套行为,但文档一个字没提;管理员读文档时不知道保存会被拒、也不知道被锁后怎么进来。 - 风险与回滚:只改一页文档,不动代码;回滚即撤本 PR。 - 先看:`content/docs/permissions/authentication.mdx` 的 Network 表格那一行,以及紧跟在“How enforcement works”引用块后面的新小节 “Saving, clearing and lockout recovery”。 - 两分钟验证:`pnpm check:doc-anchors`,预期 exit 0(行内链接指向的新小节锚点存在)。 - 需要你做的:无,席位落地。 ## What changes `content/docs/permissions/authentication.mdx` only. The `allowed_ip_ranges` row gains one sentence that links a new short subsection, **Saving, clearing and lockout recovery**, under **Network**. Each statement was read from the code merged by PR #30309 (at `fdad1786e8`), not from its description: - **The save refusal.** The settings save route `PUT /api/settings/:namespace` (`packages/services/service-settings/src/settings-routes.ts:320`, called by the console at `ui/apps/console/src/pages/settings/api.ts:172`) hands the request's peer address and headers to `setMany` (`settings-routes.ts:354-368`). The write predicate registered for `auth.allowed_ip_ranges` (`packages/plugins/plugin-auth/src/auth-plugin.ts:2551-2556`) resolves the caller's address with the auth manager's own `resolveClientAddress` and matches it with the gate's own `ipMatchesRange` (`packages/plugins/plugin-auth/src/allowed-ip-ranges.ts:148-152`). A refusal becomes `SettingsValidationError` (`packages/services/service-settings/src/settings-service.ts:2722`), answered `400 SETTINGS_VALIDATION` with `details.fields` (`settings-routes.ts:397`), field code `invalid_value` and `constraint.clientAddress`. The message is the copy at `allowed-ip-ranges.ts:85-123`, in the caller's language, ending with the recovery sentence (`allowed-ip-ranges.ts:93`). - **Clearing.** A cleared value resolves with source `default`; `applySettings` then puts back the deployment's declared list, or none (`auth-plugin.ts:2328-2334`). That list is captured after the `auth:configure` hook (`auth-plugin.ts:750`, `:792`). The pass re-runs on every auth settings change (`auth-plugin.ts:2569`), so no restart. A clear is never refused (`allowed-ip-ranges.ts:145`). - **Recovery.** `effectiveEnvOverride` treats any string as an override, the empty string included (`settings-service.ts:1378-1379`); the key has no default, so `coerceEnvValue` keeps `''` (`settings-service.ts:3843-3857`, `packages/services/service-settings/src/manifests/auth.manifest.ts:366-372`). The value is then `source: 'env'`, so `isExplicit` holds and `parseAllowedIpRanges('')` gives no list (`auth-plugin.ts:2094`, `:2328-2329`). An override in force also locks the key against writes (`settings-service.ts:2406-2408`, answered `409 SETTINGS_LOCKED` at `settings-routes.ts:378-379`). Where the code says more than, or something other than, the card's Wanted lines: 1. **Wanted 1 says "leaves out" the caller's address; the code refuses a list in which no entry *matches* it.** `ipMatchesRange` matches IPv4 CIDR and exact addresses only, so the page says "no entry matches". It also states two cases the Wanted line does not: when the server cannot establish the caller's address, any non-empty list is refused (`allowed-ip-ranges.ts:152-156`); and only a change is judged, so saving the list already in force again is not refused (`settings-service.ts:2695`). Writes inside the server carry no request and are not judged (`allowed-ip-ranges.ts:143`). 2. **The door is the settings API, not the console alone.** The check sits on `PUT /api/settings/auth`. The console calls it, and so can any HTTP client, which gets the same refusal. 3. **Wanted 3's recovery is not a permanent fix by itself.** While `OS_AUTH_ALLOWED_IP_RANGES` is set, the stored list is kept but not used, and a save to the field is refused with `409 SETTINGS_LOCKED`. When the variable is removed, the stored list applies again at the next restart. The page states this as it is. ## Tests None: a docs-only change. No code, test or i18n file is touched. No changeset: `content/docs/` ships in no published package, so this PR carries `skip-changeset`. ## Acceptance notes - **Placement.** The order asked for the subsection right after the table. The table is followed by the hardening section's own "How enforcement works" blockquote, which is about password expiry and MFA. A heading placed before it would have put that blockquote under the new heading. So the subsection sits right after the blockquote, still under **Network**, and the row links to it. - **No console path to clear a stored list while the env override is set.** Follows from point 3 above: an admin who recovers with the empty variable cannot then clear the stored list in the console, because the key is locked. Removing the variable brings the list back. Named for the seat; not filed. - **"Changes take effect immediately" in the section intro.** The hardening intro says every toggle applies without a restart. PR #30309's acceptance notes measured that clearing `lockout_threshold` or `max_concurrent_sessions_per_user` keeps the last stored value until a restart. That family is the seat's (named there); this PR does not touch the intro. - **`check:liveness` is red on this tree, from `main`.** `pnpm --filter @objectstack/spec run check:liveness` exits 1: the ledger entry `query/searchFields` cites `packages/objectql/src/engine.ts`, which no longer names the key after f672e0d moved its docblocks out. This diff does not touch that file or the ledger. ## Verification <details><summary>Derived gates: 45 derived, 45 run, 44 exit 0, 1 exit 1 (from main)</summary> - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`: 45 commands for the one changed path, at head `e4fc7bccf3`. - Each ran with its exit code captured before any pipe. 44 exit 0. - First pass, 5 refused with `PREREQUISITE NOT MET` (exit 3: `check:doc-formula-expressions`, `check:doc-security-posture`, `check:skill-examples`, `check:docs-transcript-drift`) or a missing `packages/spec/json-schema` (`check:docs`, exit 1). After building `@objectstack/spec`, `@objectstack/formula`, `@objectstack/lint`, and the `@objectstack/client` / `@objectstack/client-react` closures under the verify lock (`VERDICT command-exit 0` both times), all five re-ran to exit 0. - `pnpm --filter @objectstack/spec run check:liveness`: exit 1, the `main` finding above. - `--ran` reconciliation: "45 derived famil(ies) accounted for — 45 run, 0 NOT-MEASURED". - `git merge origin/main`: already up to date (`origin/main` = `fdad1786e8`). - NOT MEASURED here, CI's own: the `Build Docs` and `Plan Test Core shards` jobs the paths schedule. </details> 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
1 parent dbf53ef commit 7d7babb

1 file changed

Lines changed: 34 additions & 1 deletion

File tree

‎content/docs/permissions/authentication.mdx‎

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -619,7 +619,7 @@ often before the account-level lock engages.
619619

620620
| Setting | Effect |
621621
|---|---|
622-
| `allowed_ip_ranges` | Restrict authenticated access to one or more IPv4 CIDR ranges (or exact IPs). The address judged is the one the server establishes, by the same rule as the inbound rate limiter: `X-Forwarded-For` / `X-Real-IP` only when the stack declares `server.trustProxy`, the connection's peer address otherwise. Behind a reverse proxy, declare `server.trustProxy`, or the allow-list judges the proxy's own address. A request whose address cannot be established is refused with `403 IP_NOT_ALLOWED`, like one outside the ranges. `last_login_ip` records the same address. |
622+
| `allowed_ip_ranges` | Restrict authenticated access to one or more IPv4 CIDR ranges (or exact IPs). The address judged is the one the server establishes, by the same rule as the inbound rate limiter: `X-Forwarded-For` / `X-Real-IP` only when the stack declares `server.trustProxy`, the connection's peer address otherwise. Behind a reverse proxy, declare `server.trustProxy`, or the allow-list judges the proxy's own address. A request whose address cannot be established is refused with `403 IP_NOT_ALLOWED`, like one outside the ranges. `last_login_ip` records the same address. A save that leaves out your own address is refused, and clearing the field applies at once: see [Saving, clearing and lockout recovery](#saving-clearing-and-lockout-recovery). |
623623

624624
> **How enforcement works.** Password expiry (an administrator-forced password
625625
> change included) and enforced MFA share a single *session-validation gate*.
@@ -650,6 +650,39 @@ often before the account-level lock engages.
650650
> can't read or change data, or administer the platform, until they fix the
651651
> issue.
652652
653+
#### Saving, clearing and lockout recovery
654+
655+
These rules apply to `allowed_ip_ranges`.
656+
657+
- **A save that would lock you out is refused.** When you save the field, the server
658+
checks the new list against the address your own request comes from. It finds that
659+
address by the same rule as the gate (`server.trustProxy` decides whether the
660+
forwarded headers count). If no entry matches it, the save is refused with
661+
`400 SETTINGS_VALIDATION` and nothing is stored. The check sits on the settings
662+
API save, `PUT /api/settings/auth`, which **Setup → Authentication** calls, so any
663+
HTTP client gets the same answer. The message names your address, says the list
664+
would refuse your own next sign-in, and asks you to add your address or its range,
665+
or to save from an address the list matches. It ends with the recovery step below.
666+
It is written in your language (English, Chinese, Japanese or Spanish). The field
667+
error in `details.fields` also carries your address as `constraint.clientAddress`.
668+
If the server cannot establish your address at all, any non-empty list is refused,
669+
because the gate would refuse you too. Only a change is checked: saving the list
670+
already in force again is not refused. Writes made inside the server (boot, seed,
671+
migrations) carry no request and are not checked.
672+
- **Clearing takes effect at once.** Empty the field and save. No restart is needed:
673+
the gate goes back to the list the deployment itself declares (`allowedIpRanges` in
674+
its auth configuration, for example set by an `auth:configure` handler), or to no
675+
restriction when it declares none. A clear is never refused.
676+
- **If you are already locked out**, start the server with
677+
`OS_AUTH_ALLOWED_IP_RANGES` set to an empty value (`OS_AUTH_ALLOWED_IP_RANGES=`).
678+
Environment variables are read at startup, so a restart is needed. An empty value
679+
is not the same as an unset variable: any value that is set, the empty string
680+
included, is an env override in force. It wins over the stored list, and an empty
681+
list means no restriction. While the variable is set, the stored list is kept but
682+
not used, and the field is locked: a save to it is refused with
683+
`409 SETTINGS_LOCKED`. Remove the variable, and the stored list applies again at
684+
the next restart.
685+
653686
---
654687

655688
## Advanced Features

0 commit comments

Comments
 (0)