Skip to content

fix(authz): keep management credential issuance authenticated when login is disabled - #17064

Merged
diegosouzapw merged 3 commits into
release/v3.8.52from
fix/sec-open-mode-mgmt-credential-20261018
Oct 18, 2026
Merged

diegosouzapw merged 3 commits into
release/v3.8.52from
fix/sec-open-mode-mgmt-credential-20261018

Conversation

@diegosouzapw

Copy link
Copy Markdown
Collaborator

Summary

With requireLogin=false, the management tier lets anonymous callers through, but the
always-protected tier (credential exports, log export, shutdown, …) still demands a credential.
Several management routes handed exactly such a credential to an anonymous caller:

Call Before (login disabled, anonymous) After
POST /api/keys with a manage / admin scope 201, key returned 401
PATCH /api/keys/{id} granting manage / admin 200, scope granted 401
POST /api/keys/{id}/regenerate on a management key 200, new secret returned 401
GET /api/keys/{id}/reveal on a management key (reveal enabled) 200, secret returned 401
POST /api/cli/tokens (read / write / admin) 200, oma_live_… returned 401
POST /api/cli/tokens admin with a manage-scoped key 200 403 (same ceiling as with login on)

Each of those calls now needs the same credential it needs with login on. A new helper,
requireCredentialForManagementScopedKey() in src/lib/api/requireManagementAuth.ts, gates the
four key paths only when the key carries manage / admin (the hasManageScope set the
management auth itself uses). POST /api/cli/tokens uses the existing alwaysRequireAuth option on
both requireManagementAuth and resolveOperatorCredential.

Unchanged: inference-only keys are still created, edited, regenerated and revealed anonymously
in open mode; the dashboard session and the local CLI keep doing all of the above; with login on,
nothing changes.

Residual (no code change): keys or tokens minted anonymously before this fix carry no issuer
marker, so they stay valid — operators running with login disabled can review and revoke them in
the API Manager and under Settings → Access Tokens.

Refactor needed by the complexity ratchet: in POST /api/keys the create-time permission
flags moved into createPermissionOverrides() — the same payload under the same conditions. The
gate pushed the handler past the max-lines limit, and the flag spread was most of its cyclomatic
weight; the handler now sits below both limits (4 violations in the touched files vs 5 on the base).
A route-level test pins the flags end to end and passes against the base route as well.

Tests

  • New tests/unit/open-mode-management-credential-mint.test.ts — 16 cases; 9 RED on the base
    (the six rows above, by scope), 7 regression guards for the unchanged paths (inference-only keys,
    session and local CLI, create-time permission flags).
  • tests/unit/cli-tokens-scope-escalation.test.ts: the case "with login disabled the instance is
    open and admin can be minted" pinned the old behaviour (it passed on the base, 10/10). It now
    asserts the new contract — anonymous 401 with nothing persisted, the dashboard session still
    mints admin — and checks that open mode is actually in effect.
  • Sibling sweep: every unit/integration test touching requireManagementAuth, /api/keys*,
    /api/cli/tokens, access tokens or the operator credential (101 files) — see the validation
    section.

Docs

docs/architecture/AUTHZ_GUIDE.md and docs/guides/REMOTE-MODE.md stated that login disabled
lets anyone mint an admin CLI token (and issue a sync token); both now describe the verified
behaviour. The i18n mirrors need a refresh once the translation backend is back —
REMOTE-MODE.md was already out of date in .i18n-state.json on the base
(remote-token-privilege-locales is part of the inherited red set).

Validation

  • New test file: 16/16.
  • Sibling sweep (101 files): unit 1174/1174 (92 files); integration 80/80 (9 files; the 6 live contract-journey steps skip without RUN_CONTRACT_INT=1).
  • Prettier and ESLint (with the suppressions file) clean; complexity ratchet OK; file-size OK;
    check:openapi-security-tiers PASS; check:doc-links, check:fabricated-docs and
    check:docs-symbols PASS.
  • Typecheck of the touched files: 0 errors (1 inherited in tests/helpers/managementSession.ts,
    outside the diff).
  • Expected CI reds are the inherited base-red set (i18n drift, upstream-error-call-log-16558,
    mutation-test-coverage, …), identical on unrelated PRs.

⚠️ base-red inherited: #15306

diegosouzapw and others added 3 commits October 18, 2026 11:08
…ller when login is disabled

With requireLogin=false an anonymous caller could create or grant a manage/admin-scoped API
key, regenerate or reveal one, and mint CLI access tokens at any scope, and a manage-scoped
key could mint an admin token. All of those credentials pass the always-protected tier.
Inference-only keys and the session / local-CLI paths are pinned as unchanged.

Co-authored-by: gsmarcil <116014240+gsmarcil@users.noreply.github.com>
…gin is disabled

With requireLogin=false the management tier is open, but a manage/admin-scoped API key and a CLI
access token both pass the always-protected tier. Creating a key with a management scope,
granting that scope, regenerating or revealing such a key, and minting CLI access tokens at any
scope now need the same credential as with login on (requireCredentialForManagementScopedKey()
and alwaysRequireAuth on /api/cli/tokens). Inference-only keys and the session / local-CLI paths
are unchanged. AUTHZ_GUIDE and REMOTE-MODE no longer say login disabled lets anyone mint.

Co-authored-by: gsmarcil <116014240+gsmarcil@users.noreply.github.com>
…/keys

The scope gate pushed the handler past the max-lines ratchet; the flag spread was most of its
cyclomatic weight. createPermissionOverrides() builds the same payload under the same
conditions, and a route-level test pins the flags (and the no-flag defaults) end to end.
@github-actions

Copy link
Copy Markdown
Contributor

CI Coverage Report

  • Coverage job: skipped
  • PR test policy: success

Coverage artifact was not available for this run.

@diegosouzapw
diegosouzapw merged commit a016744 into release/v3.8.52 Oct 18, 2026
36 of 51 checks passed
@diegosouzapw
diegosouzapw deleted the fix/sec-open-mode-mgmt-credential-20261018 branch October 18, 2026 18:04
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