Skip to content

Allowed IP ranges: an administrator can exclude their own address unwarned, and clearing the field does not take effect until a restart #30203

Closed

Description

@objectstack-fleet

epic:#26676#1 · zhuangjianguo · session_cdb7a717def74e719988f089fbaa18ff

From a real-user walkthrough of Setup, filed on the maintainer's word: 「以上都同意,立卡」 (2026-11-05). Measured on main b7075d9908, with the console in dev mode, unless a line says otherwise. Test app scaffolded with create-objectstack@18.3.0.

What a customer sees

A company wants sign-in only from the office. The administrator, working from home, enters the office range and saves. Nothing says their own address is outside it. They find out on the next sign-in. If they still have a page open and clear the field, the page says saved, and sign-in keeps being refused until someone restarts the server. A business administrator cannot do that and has no way to tell why it did not work.

Measured

  • allowed_ip_ranges = 203.0.113.0/24 from ::1: toast 「设置已保存」, no warning, the current address is not shown. POST /api/v1/auth/sign-in/email then answers 403 IP_NOT_ALLOWED.
  • The open session keeps working, so the field can still be edited.
  • Field cleared and saved: toast 「设置已保存」, the stored value is empty, and sign-in still answers 403 on three tries over 12 seconds. /api/v1/auth/two-factor/enable was refused the same way.
  • After a server restart: sign-in 200.

Likely cause, from reading the code

packages/plugins/plugin-auth/src/auth-plugin.ts:2062 applies a setting to the running config only when isExplicit(key), and :2292 does so for this key. A cleared value appears to read as default, so the old list stays in force. If so, other settings with an empty or restored-default state may behave the same; 恢复默认 was not measured.

Expected

  • The field shows the caller's current address. A list that excludes it is refused or asks for explicit confirmation.
  • Clearing applies at once, as saving a value does.
  • The documentation says how to recover when locked out. The seat found no environment variable for this key in content/docs/deployment/environment-variables.mdx.

Activity

  1. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: first-touch audit · p2 · domain:server · pm:queue · 2026-11-05T02:57Z

    ⛔ Not a claim.

    • Read fresh. Allowed IP ranges can exclude the saving administrator's own address with no warning. Clearing the field does not take effect until a restart. A self-lockout with a recovery path through other admins or the environment, so p2.
    • Fix site: plugin-auth (auth-plugin.ts): read the setting live, and warn or refuse when the caller's address falls outside. The warning text is the domain:ui half.

    Generated by Claude Code

  2. added
    domain:serverDomain lane — seat card indexed by label:pm:seat
    pm:queueReady for the PM dispatch loop
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 5, 2026
  3. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL

    Claim: PM loop round 3
    Session: session_01Utp6Cbvcmv8YWFhFJwzjKL (seat) · dev thread session_01Utp6Ca3N2EsTAqg7jhabeL
    Account: os-bill
    Branch: claude/project-thread-2etnqx (harness-pinned thread branch; platform exception to claude/issue-<n>-<slug>)
    Worktree: objectstack-issue-30203
    Domain: domain:server
    Seat: domain:server#2
    File surface: packages/plugins/plugin-auth/src/ (auth-plugin.ts settings apply, the IP gate helper if reused, tests), possibly manifest.ts (help text), one changeset .changeset/30203-allowed-ip-ranges-live.md. ⛔ packages/spec/**, ui/**, apps/**, skills/**, docs/adr/**, content/docs/**, .claude/**. (stop on breach; explain in the report)
    Container & model: M, mode:thread, model: default tier (opus) — dispatch-gates --tier: no path-derived mandate, effort: high
    Clause-②: no — a fix to an existing settings apply plus a refused self-excluding save; the dev re-declares no (narrowing) if a list an admin could save before is now refused (then CONTRACT_REVIEW_TIER review), yes for a new published code or settings key
    Metadata-protocol: none
    Responsibility: applySettings in auth-plugin.ts skips a default-sourced (cleared) allowed_ip_ranges, so the running list survives until restart; the save door does not check the caller's own address | the IP gate itself enforces the running list | every administrator who sets or clears allowed IP ranges
    Thread-read: 6648444250
    Premise: holds — at bb86ff96 isExplicit (:2062) treats a default source as not explicit and allowed_ip_ranges (:2292) is applied only when explicit; the cleared-value source is the card's reading, re-measured by the dev
    Serial constraints cleared: this seat's #30202 edits the same applySettings at email_password_enabled (:2112) and #30201 may edit near mfa_required (:2210); disjoint branches, a change to isExplicit is announced to the seat first, each pulls main before PR and before landing. No sibling seat's card names plugin-auth.

    Order: project file server-seat2/dispatch-30203.md, delivered as the card thread's opening message.

  4. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-bill · session_01Utp6Ca3N2EsTAqg7jhabeL
    os-dev-report

    {
      "issue": 30203,
      "status": "done",
      "branch": "claude/project-thread-2etnqx",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/30309",
      "session": "session_01Utp6Ca3N2EsTAqg7jhabeL",
      "premise_still_valid": true,
      "summary": "Premise measured on the real SettingsService with the shipped auth manifest: the console sends an emptied field as null, and after that save (and after resetNamespace) allowed_ip_ranges resolves {value: null, source: 'default'}, which applySettings skipped, so the stored list stayed in force until a restart. Now a default-sourced value hands the gate back to the deployment's own declared allowedIpRanges (none, or the plugin options' list) in the same pass; isExplicit is unchanged. A console save whose list leaves out the saving request's address (resolved by AuthManager.resolveClientAddress under trustProxy, matched by ipMatchesRange) is refused by a settings write predicate: 400 SETTINGS_VALIDATION, invalid_value, constraint.clientAddress, localized message naming the address and the recovery (OS_AUTH_ALLOWED_IP_RANGES set empty, an existing env override, measured in force). service-settings carries the request to write predicates on setMany's options (SettingsWriteOptions.transport), not on the caller's context, so the door's identity pins hold. Scope choice: this key only; the clear-skips family is reported below.",
      "tests": "allowed-ip-ranges.pin.test.ts 9/9 (real AuthPlugin + real SettingsService + real settings routes). plugin-auth test: Test Files 221 passed (221), Tests 3885 passed | 10 skipped; service-settings test: Test Files 67 passed (67), Tests 1159 passed; both typecheck exit 0 (head 4a971127; the main merge 89ff3aa9 touched neither package). First service-settings run red on two door pins asserting toBe identity of ctx while transport rode the context; moved to options, both green unchanged. Ablations via scripts/ablation-replace.mjs (tests resolve both packages from src through the vitest alias, so no build in between): A clear branch off -> 3 failed | 6 passed; B predicate registration off -> 3 failed; C route transport renamed away -> 3 failed; D service forwarding of transport deleted -> 3 failed; each mutation landed (anchor 1 -> 0, blob changed), each restore blob equals HEAD, git diff HEAD empty after.",
      "gates": "head 89ff3aa9: dispatch-gates --commands derived 65; each run with exit captured before any pipe; --ran: '65 derived famil(ies) accounted for — 64 run, 1 NOT-MEASURED'. NOT MEASURED: pnpm check:dual-build-cjs-loads exit 3 (35 packages without dist; needs a whole-workspace build), left to CI. check-plugin-teardown-shape --self-test exit 3 on the shallow checkout, exit 0 ('48 cases pass') after fetching 621a4876. pnpm lint exit 0. CI check-runs: in_progress at report time.",
      "dogfood_battery": "89ff3aa9 · VERDICT command-exit 1 · Test Files 1 failed | 418 passed | 3 skipped (422) · Tests 1 failed | 3491 passed | 36 skipped (3528) · failure: test/permission-set-clone-door.dogfood.test.ts 'a platform set ... copies into its shipping package's pair' answers 422 INVALID_METADATA (member_default clone grants on sys_comment / sys_activity 'not defined in objects'); not this change (no permission, metadata or clone code in the diff); the grants arrived with #30222 (490d136f2)",
      "line_budget": "8 files, +507 / -12 vs merge base (dispatch-gates count); under the 3000 human-merge threshold",
      "files_changed": [
        ".changeset/30203-allowed-ip-ranges-live.md",
        "packages/plugins/plugin-auth/src/allowed-ip-ranges.ts",
        "packages/plugins/plugin-auth/src/allowed-ip-ranges.pin.test.ts",
        "packages/plugins/plugin-auth/src/auth-plugin.ts",
        "packages/services/service-settings/src/index.ts",
        "packages/services/service-settings/src/settings-routes.ts",
        "packages/services/service-settings/src/settings-service.ts",
        "packages/services/service-settings/src/settings-service.types.ts"
      ],
      "deviations": [
        "service-settings (4 files) is outside the order's expected landing: the save door had no way to carry the caller's address; it now carries the request on setMany's options. No packages/spec change, no new error code, no new settings key, no new env var.",
        "manifest help text (service-settings auth.manifest.ts and its three translations) not changed: the refusal text carries the explanation; changing the description would move the translation bundles.",
        "The full dogfood is red on one file from main (#30222), reported, not fixed here."
      ],
      "mcp_calls": "2 — mcp__github__issue_read (get, get_comments); no write tool",
      "api_writes": "2 relay writes — pr_create #30309 (fleet-write run 41975771563), os-dev-report comment on #30203 (post-stamped); plus git pushes to the designated branch",
      "open_questions": [],
      "security_findings": "none",
      "out_of_scope_findings": [
        "class: a · reach: PUT /api/settings/auth with a key set to null (the console's clear) or the namespace reset answers saved, while the running value keeps the last stored one until a restart; measured on the real AuthPlugin + SettingsService: lockout_threshold 3 and max_concurrent_sessions_per_user 2 stay in force after both are saved null · evidence: every if (isExplicit(key)) branch in auth-plugin.ts applySettings without a reset; the general fix must restore each key's deployment-declared value, across the refusable emailAndPassword pieces #30201/#30202 are editing · dedupe words: auth settings cleared default restart, restore default not applied, isExplicit default source",
        "class: a · reach: POST permission-set clone of member_default on a booted showcase answers 422 INVALID_METADATA ('grants on object sys_comment / sys_activity which is not defined in objects') where 200 is expected; packages/qa/dogfood/test/permission-set-clone-door.dogfood.test.ts red on main since #30222 (490d136f2) · dedupe words: member_default clone sys_comment, permission set clone door 422, sys_activity not defined",
        "carrier: seat (domain:ui) · the console field does not show the caller's current address before saving; the refusal carries it in constraint.clientAddress for the console to show · noted, not filed",
        "carrier: seat (docs) · content/docs/permissions/authentication.mdx does not state the save refusal or the OS_AUTH_ALLOWED_IP_RANGES recovery; content/docs is outside this card's surface · noted, not filed"
      ]
    }

    Report taken 2026-11-05T05:44Z.


    Generated by Claude Code

  5. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL

    Claim correction (supersedes the Clause-② line and widens the file surface of claim 6649086146; every other line stands)
    Clause-②: no (narrowing) — measured by the dev (report 6650755695): a save of allowed_ip_ranges that leaves the saving caller's own address outside the list is now refused, a value an admin could save before; clearing the list now takes effect at once (falls back to the deployment's declared list). Review owed: CONTRACT_REVIEW_TIER over the non-prose diff (narrowing face), which also judges whether the new optional SettingsWriteOptions.transport reachable from the @objectstack/service-settings exports is a published widening (then the line becomes yes (narrowing)).
    File surface (added): packages/services/service-settings/src/index.ts, settings-routes.ts, settings-service.ts, settings-service.types.ts (the route hands the caller's transport to the write predicate). Accepted by the seat, subject to the contract review. PR #30298 (card #30202) adds a batch field to the same predicate input in the same two files; whichever lands second merges main first.

  6. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL

    Landed · 2026-11-05T07:29Z

    Reading Value
    PR #30309 merged 2026-11-05T07:28Z, merge commit d1d90b70, reviewed head 8767d6aa (seat ACCEPT 6651982979)
    Contract review CONTRACT_REVIEW_TIER, narrowing face: FAIL 6650936040 (89ff3aa9, declaration), delta PASS 6651963356 (8767d6aa)
    #30203 closed by Fixes #30203
    Release @objectstack/plugin-auth and @objectstack/service-settings patch, Clause-②: yes (narrowing), BREAKING banner and ADR-0087 marker; next release

    State change: pm:dispatched removed. Follow-ups: #30312 (clearing other auth keys needs a restart), #30313 (console shows the caller's address), #30314 (docs), #30320 (IPv6 CIDR never matches).

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

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3security

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions