Skip to content

[front] Resolve model-tier group overrides from live user membership - #32925

Open
tdraier wants to merge 2 commits into
mainfrom
migrate-allowed-tiers-off-groupmodelids
Open

tdraier wants to merge 2 commits into
mainfrom
migrate-allowed-tiers-off-groupmodelids

Conversation

@tdraier

@tdraier tdraier commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Description

allowed_tiers was the last consumer of Authenticator.groupModelIds() (the cached _groupModelIds field) — it used it to find the caller's tier-override groups. It now resolves that membership live from the user via GroupResource.dangerouslyListUserGroupsForAuth, and the now-dead groupModelIds() accessor is removed.

Why not governance grants: GroupPermissions deliberately holds no group ids and flattens sources, so it can't express the tier resolver's user > highest group > workspace precedence (a flat union would over-grant). Tier overrides are fundamentally a group-membership feature, so the migration replaces the field read with a membership query rather than a governance lookup.

Behavior change (intentional): group-based models_tier overrides now apply only to auths backed by a workspace-member user. Auths with no user — API keys, system, internal, poke — get no group override and fall back to the user/workspace tiers; previously a key/superuser could inherit a group override via its construction-time group set. Agent-driven runs are unaffected (they bypass this path via getAgentAllowedTierNamesOverride). The user/workspace sources and precedence are unchanged. Documented via the new models-tier-group-override-follows-user-membership code contract.

Tests

  • lib/model_tiers/allowed_tiers.test.ts — 11/11 pass, including a new case asserting a no-user internal auth (carrying all workspace groups) does not inherit a group override.
  • Existing user-membership precedence tests (group-over-workspace, user-over-group) still pass unchanged.
  • tsgo clean on changed files.

Risk

Low. The group-tier-override path is user-facing (agent runs use the override bypass). The only behavior delta is non-user auths no longer inheriting group overrides, which is the intended semantics.

Deploy Plan

Standard.

🤖 Generated with Claude Code

@tdraier

tdraier commented Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

This change is part of the following stack:

Change managed by git-spice.

@vercel

vercel Bot commented Sep 21, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Actions Updated
playground Ignored Ignored Preview Sep 22, 2026 2:48pm UTC
storybook Ignored Ignored Preview Sep 22, 2026 2:48pm UTC

Request Review

@github-actions

github-actions Bot commented Sep 21, 2026

Copy link
Copy Markdown
Contributor
Fails
🚫

front/lib/auth.ts (Authenticator) has been modified. This code runs on front-sse pods as well.

Please add the sse-ack label to acknowledge that a front-sse deploy is required alongside the front deploy.

Generated by 🚫 dangerJS against 0e84963

tdraier and others added 2 commits September 22, 2026 16:48
`allowed_tiers` was the last consumer of `Authenticator.groupModelIds()` (and
thus of the cached `_groupModelIds` field) for its group-override source. It
now resolves the caller's tier-override group memberships live from the user
via `GroupResource.dangerouslyListUserGroupsForAuth`, and drops the now-dead
`groupModelIds()` accessor.

Behavior: group-based `models_tier` overrides are a per-user feature, so they
now apply only to auths backed by a workspace-member user. Auths with no user
(API keys, system, internal, poke) get no group override and fall back to the
user/workspace tiers — previously a key/superuser could inherit a group's
override via its construction-time group set. Agent-driven runs are unaffected;
they bypass this path via getAgentAllowedTierNamesOverride. The user and
workspace tier sources, and the user > group > workspace precedence, are
unchanged.

Adds a code contract and a test covering the non-user case.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rename `_groupModelIds` -> `_requestedGroupModelIds` and persist it ONLY for
auths whose group scope can't be re-derived from the auth's own identity:
- sandbox tokens (narrowed to the workload's spaces from token claims that
  aren't retained),
- the `fromKey` `requestedGroupIds` override (a system key acting as a user),
- internal/system auths (no user, no key).

`null` means "derive on refresh". `refresh()` now consolidates all sources:
explicit scope -> reuse it; user auth -> live membership; API-key auth ->
re-derive from the key (`KeyResource.fetchByWorkspaceAndId` + the key's groups,
workspace-scoped so a foreign-workspace key yields none). So user and plain
API-key auths no longer carry a stored group snapshot — the user-group snapshot
is gone, and keys refresh from `_key` alone.

Serialization keeps the field name `groupIds` but makes it nullable to
distinguish "derive" (null) from an explicit empty scope ([]); user/plain-key
auths serialize null and recompute on rehydrate.

Behavior-preserving for `_permissions`; adds a key-refresh test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@tdraier
tdraier force-pushed the migrate-allowed-tiers-off-groupmodelids branch from 86b6d69 to 0e84963 Compare September 22, 2026 14:48
@zeropath-ai

zeropath-ai Bot commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

🟡 1 security issue detected below the blocking threshold. Reviewed everything up to 0e84963.

The following issues were found:

  • Issue 1: Broken Access Control
    • Location: front/lib/auth.ts:1836-1841
    • Score: MEDIUM (54.0)
    • Description: fromJSON interprets any non-null serialized groupIds as an explicit, immutable scope. Older or otherwise serialized API-key auth snapshots contain the key's group set, and after this PR refresh() trusts that set instead of re-deriving groups from the current key. Consequently, revoking a key's group (or deleting the key) does not remove the group's permissions from an in-flight serialized authenticator, allowing continued access to workspace/space resources until the snapshot expires.

Evidence: The PR changes rehydration to set requestedGroupModelIds directly from authType.groupIds at lines 1839-1841. refresh() then takes the explicit-scope branch at lines 388-390 and never calls groupModelIdsFromKey(); the latter is only reached when the field is null (lines 405-406). fromKey() serializes plain API-key auth with requestedGroupModelIds: null (1083-1086), but pre-existing persisted/in-flight snapshots with the old full group list are explicitly treated as valid by the new comment at 1836-1838.

Security Overview
Detected Code Changes
Change Type Relevant files
Refactor ► front/lib/auth.ts
    Rename/replace groupModelIds with _requestedGroupModelIds and introduce related behavior
Enhancement ► front/lib/auth.ts
    Support explicit group scope persistence via requestedGroupModelIds and refresh logic
► front/lib/auth.ts
    Add groupModelIdsFromKey() helper to derive groups from API keys
► front/lib/auth.ts
    Add listPrincipalGroupModelIds() API to list groups for principal (user or key)
Enhancement ► front/lib/auth.fromjson.test.ts
    Add test exercising re-derivation of API key group scope on refresh without persisting it
Enhancement ► front/lib/model_tiers/allowed_tiers.ts
    Update to compute model tier overrides from principal groups via new listModelTierOverrideGroupModelIds()
Enhancement ► front/lib/model_tiers/allowed_tiers.test.ts
    New tests for ignoring group overrides for non-user auth and for API key group-based overrides
Enhancement ► front/lib/model_tiers/allowed_tiers.ts
    Rename usage of groupModelIds() to listPrincipalGroupModelIds() in tier override resolution logic
Bug Fix / Enhancement ► front-api/routes/w/[wId]/skills/[sId]/editors.test.ts
    Remove empty groupModelIds in setup (adjust to new scope handling)
Enhancement ► front/lib/actions/mcp_internal_actions/mcp_servers_metadata.test.ts
    Update mock Authenticator to use _requestedGroupModelIds (renamed field) in tests

Comment thread front/lib/auth.ts
Comment on lines +1836 to +1841
// `null` = no explicit scope (derive on refresh). A serialized list rehydrates the explicit
// scope. (Old payloads that stored the full group set land here as an explicit scope too, which
// is correct: refresh reuses it instead of re-deriving until the payload cycles out.)
const requestedGroupModelIds = authType.groupIds
? removeNulls(authType.groupIds.map((sId) => getResourceIdFromSId(sId)))
: null;

@zeropath-ai zeropath-ai Bot Sep 22, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unauthorized Access via In-Flight Authenticator Rehydration in front/lib/auth.ts (Severity: MEDIUM)

Impact: revoking a key’s group or deleting the key may not remove access for in-flight authenticators, allowing continued workspace/space access until the snapshot expires. The code path rehydrates explicit scopes from legacy serialized groupIds in fromJSON, causing refresh() to trust the old in-snapshot group set instead of deriving from the current key, which leads to stale permissions persisting across key changes.
View details in ZeroPath

Automatic patch generation was not possible for this finding.

View reasoning


💬 Reply @ZeroPath false-positive because … or @ZeroPath accepted-risk because … to triage this finding, or ask it any question.

All commands

This branch has not been deployed

No deployments
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