Skip to content

fix(hub): derive entitlement from key listing, not resolved values (#127) - #1336

Open
ptone wants to merge 3 commits into
sn-metaauth-inv/extract-resolve-agent-secretsfrom
sn-metaauth-inv/entitlement-from-listing
Open

fix(hub): derive entitlement from key listing, not resolved values (#127)#1336
ptone wants to merge 3 commits into
sn-metaauth-inv/extract-resolve-agent-secretsfrom
sn-metaauth-inv/entitlement-from-listing

Conversation

@ptone

@ptone ptone commented Aug 29, 2026

Copy link
Copy Markdown
Owner

Summary

  • R7: Entitlement derived from ListSecrets/ListProgenySecrets (what exists),
    not from Resolve() output (what happened to decrypt). A secret that exists but
    fails to resolve is still entitled.
  • R8: computeEntitledSecretKeys is a single package-level function called by
    all five credential writers. No copy-forward anywhere, no special case.
  • R9: resolveAgentSecrets returns (forInjection, err) — entitled-keys return
    value removed. One source of truth, not two.
  • R10: Progeny secrets in the listing path go through authzService.CheckAccess
    with the same inputs as Resolve() (agent ID, project ID, ancestry, secret ID,
    ActionRead). Entitlement is not wider than resolution.
  • Fifth writer: handleAgentTokenRefresh now recomputes entitled keys.

Credential writer enumeration

Extent of search: grep -rn 'GenerateAgentToken\|RecordAgentCredential\|UpdateAgentCredentialEntitledKeys\|AgentCredential{' pkg/hub/ --include='*.go', excluding test files and interface/type declarations.

Writer File:Line Records entitled keys Source
buildCreateRequest httpdispatcher.go:452 Yes computeEntitledSecretKeys
DispatchAgentStart httpdispatcher.go:1983 Yes computeEntitledSecretKeys
DispatchAgentRestart httpdispatcher.go:2242 Yes computeEntitledSecretKeys
DispatchAgentResetAuth httpdispatcher.go:2358 Yes computeEntitledSecretKeys
handleAgentTokenRefresh handlers_agents_core.go:2789 Yes computeEntitledSecretKeys

All five call computeEntitledSecretKeys. Zero call sites derive entitlement from
resolveAgentSecrets output (R9 enforced by removing the return value).

Depends on

PR #1333 (refactor: extract resolveAgentSecrets, return JTI hash)

Test plan

  • go build ./... — clean
  • go vet ./... — clean
  • TestComputeEntitledSecretKeys_ListingNotResolution — the R7 distinguishing test
  • TestComputeEntitledSecretKeys_NilBackend
  • TestComputeEntitledSecretKeys_EmptyProject
  • TestComputeEntitledSecretKeys_ListingError
  • TestComputeEntitledSecretKeys_ExcludesInternal
  • TestComputeEntitledSecretKeys_MultiScope
  • TestResolveSecrets* (4 test files updated for new signature)
  • TestHTTPAgentDispatcher*, TestGenerateAgentToken, TestRefreshAgentToken

Scion Agent (sn-metaauth-inv) added 3 commits August 29, 2026 00:15
 R7/R8/R9/R10)

Entitlement was computed from Resolve()'s best-effort output: a secret
that failed to decrypt was silently excluded, permanently narrowing what
the agent could fetch. A transient storage fault became a durable,
invisible entitlement reduction.

Change 1 — computeEntitledSecretKeys (R7/R8):
New package-level function derives entitled keys from ListSecrets and
ListProgenySecrets (what exists and is scoped to the agent), not from
resolved values. One function, called by all five credential writers
(create, start, restart, resetAuth, refresh). No copy-forward anywhere.

Change 2 — resolveAgentSecrets returns (forInjection, err) (R9):
Remove the entitled-keys return value. Entitlement comes from
computeEntitledSecretKeys and from nowhere else. Two sources of truth
for one field is the defect, not the mismatch between them.

Change 3 — fifth writer: handleAgentTokenRefresh (R8):
Refresh now recomputes entitled keys from the listing via
computeEntitledSecretKeys, same as every other writer. No special case.

Authz scoping (R10): progeny secrets in computeEntitledSecretKeys go
through the same authzService.CheckAccess path that Resolve() uses
(same inputs: agent ID, project ID, ancestry, secret ID, ActionRead).
Add TestComputeEntitledSecretKeys_EqualsResolvedWhenAllSucceed: when
every secret resolves cleanly, the entitled set must equal the resolved
set. This test goes red if the two implementations (listing-based
entitlement and resolution-based injection) drift on scope selection,
internal-secret exclusion, or any other filtering rule.

Fix fakeSecretBackendForEntitlement.Resolve to exclude internal secrets,
matching real backend behavior (localbackend.go:173). The drift test
itself caught this gap.
… resolution (R11)

Two secrets with different keys but the same injection target:
- Resolution applies DeduplicateByTarget → one survives
- Entitlement does NOT apply that filter → both keys in entitled set

This divergence is intentional. Target dedup is injection mechanics (env
var collision), not authorization. The fetch-by-key channel has no target
collision, so there is nothing for target dedup to resolve.

Also makes the fake backend's Resolve() apply DeduplicateByTarget and
default Target/SecretType, matching localbackend.go behavior. The fake
was created in commit 5f2f52e as part of this PR — zero blast radius,
no pre-existing test uses it.
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