Skip to content

feat: lk impersonate (mint/use/revoke buyer-scoped Access Token) — LIN-5921#4

Merged
djalmaaraujo merged 17 commits into
mainfrom
djalma/lk-impersonate
Jul 14, 2026
Merged

feat: lk impersonate (mint/use/revoke buyer-scoped Access Token) — LIN-5921#4
djalmaaraujo merged 17 commits into
mainfrom
djalma/lk-impersonate

Conversation

@djalmaaraujo

@djalmaaraujo djalmaaraujo commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

Linear: LIN-5995


O quê

lk impersonate <email|user_id> impersona o usuário @linkana de um buyer cunhando um Access Token real no backend, e o CLI passa a agir como aquele user nos comandos SRM. Comandos: impersonate <ref>, impersonate stop, impersonate status.

PR par no backend: linkanalabs/linkana#13683 (endpoint POST/DELETE /impersonation).

Modelo de segurança (núcleo)

Auth de duas camadas por origin: token original (login) + contexto de impersonação (token cunhado + alvo + expiry). Regras de resolução de credencial:

  • Contexto de impersonação ativo → usa o token impersonado.
  • Precedência: impersonação ativa > LK_TOKEN > PAT do keychain. LK_TOKEN só sobrescreve o token original; não desativa impersonação (fecha footgun onde LK_TOKEN silenciava a impersonação).
  • Sem fallback silencioso: contexto expirado (relógio local) ou 401 server-side → erro duro, nunca cai pro buyer original. Saídas: lk impersonate stop ou re-impersonar.

whoami / auth status mostram as duas camadas; segredo (lkn_...) nunca sai em stdout/JSON (só persistido no storage: keychain + fallback arquivo 0600).

Camadas

  • internal/client: Post/Delete + StartImpersonation/StopImpersonation (param target).
  • internal/auth: contexto de impersonação por origin (chave namespaced; lê o store ignorando LK_TOKEN).
  • internal/commands: authedClient (4-return) + resolveAPI/unauthorizedErr pegajosos; comandos impersonate *; visibilidade em whoami/auth status.
  • CLAUDE.md: seções de impersonação/buyer-scope + ponteiros pro repo backend (../linkana).

Testes & gates

make test (-race) verde; make cover total 95.0% (gate ≥95%); make lint (golangci-lint v2) 0 issues. TDD em todas as tasks.

Processo

Implementado via subagent-driven-development: 7 tasks, review (spec+qualidade) por task + review final whole-branch (opus) nos 2 repos. Achados corrigidos: footgun LK_TOKEN, JSON inválido em auth status ao impersonar, revogação do token anterior ao re-impersonar, e endurecimento de testes.

🤖 Generated with Claude Code


Summary by cubic

Adds lk impersonate to mint, use, and revoke buyer‑scoped Access Tokens so the CLI can act as a buyer user for SRM commands. Implements a strict two‑layer auth model with sticky impersonation and no silent fallback (LIN-5921).

  • New Features

    • Commands: lk impersonate <email|user_id>, lk impersonate stop, lk impersonate status.
    • Integrates with backend POST/DELETE /impersonation.json; client adds StartImpersonation/StopImpersonation.
    • Persists a per-origin impersonation context in internal/auth; secret token is never printed.
    • Credential precedence: impersonation > LK_TOKEN > keychain PAT; expired/401 is a hard error.
    • whoami shows an impersonation banner on stderr; auth status includes target, buyer, expiry, impersonator, and “authenticated” reflects impersonation state.
    • All SRM commands resolve credentials via the impersonation-aware path and surface clear 401 errors.
  • Bug Fixes

    • Prevent LK_TOKEN from masking an active impersonation by reading stored creds directly in auth (loadStored).
    • Ensure valid JSON across modes: auth status (auto/piped stays JSON), impersonate status --format json returns null when no context, and impersonate stop emits {"stopped":...}.
    • Print warnings to stderr if impersonation context load or impersonator identity lookup fails; status commands still exit 0.
    • Revoke any prior impersonation before starting a new one; warn on revoke failure.
    • TTL handling: round sub-second TTL up (avoid ttl_seconds=0) and reject negative TTL.
    • auth status “authenticated” now derives from impersonation: non-expired = true; expired = false (no fallback).
    • Repo hygiene: add CLAUDE.local.md banning specs/plans commits; remove planning specs.

Written for commit 6305199. Summary will update on new commits.

Review in cubic

LK_TOKEN is an env override for the *original* token only. Previously,
LoadImpersonation delegated to Load(), which checks LK_TOKEN first —
so any non-empty LK_TOKEN silently masked an active stored impersonation
context (returning nil with no error or warning).

Extract a private loadStored() helper in store.go that reads the
keychain/file store directly, bypassing the env override. Load() keeps
its existing behaviour (LK_TOKEN wins for original token lookups).
LoadImpersonation() now uses loadStored() and drops the now-unnecessary
src == SourceEnv guard.

New tests (TDD, RED first):
- TestLoadImpersonationNotMaskedByEnvToken (internal/auth)
- TestResolveAPIImpersonationWinsOverLKToken (internal/commands)
Implements `lk impersonate <ref>`, `lk impersonate stop`, and
`lk impersonate status` (TDD, 15 command tests + 3 client coverage tests,
total coverage 95.2% ≥ 95% gate). Secret token is never emitted in JSON
or styled output.
whoami emits a ⚠ banner to stderr (keeping stdout clean for the JSON
contract) when an impersonation context is active. auth status appends
an impersonation block (target email, buyer, expiry, impersonator,
state ativa/EXPIRADA) after the normal auth output.
Assign fmt.Fprintf return values to _, _ in auth.go and whoami.go
(errcheck). Rename unused http.Request param to _ in visibility test
(revive unused-parameter). All three files are from Task C5.
…mpersonate

- Guard impersonation human-readable line in `auth status` behind
  `formatFlag != JSON` so stdout remains valid JSON when impersonating
  (Finding 1: CRITICAL — was appending plain text after JSON object)
- Best-effort revoke prior impersonation token before minting a new one
  in runImpersonateStart; print stderr warning on revoke failure
  (Finding 2)
- Tighten whoami banner test: assert banner on errOut, absent from out
  (Finding 3)
- Remove stale plan comment from impersonation_resolve_test.go (Finding 4)
@linear-code

linear-code Bot commented Jun 22, 2026

Copy link
Copy Markdown
LIN-5921 CLI: expor /srm/suppliers via JSON (format.json + jbuilder) + comandos supplier

Começar a expor os controllers SSR existentes via JSON pra CLI consumir, seguindo a receita Rails-way (sem API nova).

Receita (acordada)

  1. respond_to por action que de fato precisa de JSON. Explícito, só onde precisa.
  2. Templates jbuilder por formato (index.json.jbuilder, show.json.jbuilder).
  3. DRY via partial jbuilder (_supplier.json.jbuilder) — reuso é no partial, não em base controller. A "repetição" do respond_to é proposital (explícito, idiomático).
  4. jbuilder (2.15.0) já no Gemfile, coexiste com Phlex (formato escolhe o renderer). SsrController tem respond_to :html (linha 17) mas o block-form respond_to ignora isso. layout false ok (jbuilder não usa layout).
  5. Paginação: expor meta no jbuilder se precisar (json.pagination { json.(pagy, :count, :page, :pages) }).
  6. Não usar serializer pra view — jbuilder substitui. Serializers svix/vaas seguem (integrações externas).

Primeiro endpoint: /srm/suppliers

  • indexGET /srm/suppliers.json (lista suppliers do buyer logado).
  • show (panel) → GET /srm/suppliers/:id/panel.json (Srm::Suppliers::PanelsController#show).
  • _supplier.json.jbuilder compartilhado por index e show.

Rotas existentes de supplier (config/routes/srm.rb): resources :suppliers, except: %i[edit show update] → index/new/create/destroy; show é o panel. Nested: supplier_categories, supplier_requests, supplier_events, block, notify, supplier_connections_enabling.

CLI (lk)

Mapear 1-1 os REST methods. Nesta fatia: lk supplier list + lk supplier show <id>.

Dependência

Pra testar o fluxo CLI→PAT→JSON ao vivo, precisa do auth (LIN-5916) mergeado — a Warden strategy que autentica o Bearer PAT vive lá. O endpoint JSON em si é independente (testável via sessão/sign_in). Branch desta issue sai da main, sem ligação com a branch de auth.

Review in Linear

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

5 issues found across 19 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread internal/commands/auth.go Outdated
Comment thread internal/commands/auth.go Outdated
Comment thread internal/client/impersonation.go Outdated
Comment thread internal/commands/impersonate.go Outdated
Comment thread internal/commands/impersonate.go Outdated
Specs em docs/superpowers/specs sao artefatos de planejamento local,
nao pertencem ao repo versionado.
… status; round sub-second TTL

Finding A: impersonate status --format json with no context now emits `null` (not
plain text) via impersonateStatusView.MarshalJSON(); styled still shows the human
notice. Adapted three existing tests to use --format styled for text assertions.

Finding B: impersonate stop now routes through impersonateStopView which emits
{"stopped":true,"target_email":"..."} or {"stopped":false} in JSON mode; styled
keeps the original human sentences.

Finding C: auth status --format json now includes statusImpersonation block inside
statusResult (omitempty) with target_email, buyer_id, expires_at, impersonator_email,
and expired bool. Token never appears in output.

Finding D: LoadImpersonation error in auth status now prints a warning to stderr
(aviso: ...) instead of being silently discarded; command still exits 0.

Finding E: StartImpersonation uses math.Ceil(ttl.Seconds()) so a 500ms TTL sends
ttl_seconds=1 instead of 0, preventing the backend from silently applying 24h default.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 issue found across 9 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread internal/commands/auth.go Outdated
…ays JSON)

The impersonation line was appended out-of-band guarded by the raw --format flag,
so `--format auto` piped to a non-TTY (which Render resolves to JSON) emitted a
JSON object followed by a human line — invalid JSON. Fold the line into
statusResult.Styled() and let output.Render own format resolution.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

2 issues found across 21 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread internal/client/impersonation.go
Comment thread internal/commands/auth.go
@djalmaaraujo
djalmaaraujo requested a review from cirdes June 30, 2026 12:31

@cirdes cirdes left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@djalmaaraujo, olha se o comentário do cubic faz sentido e depois pede revisão novamente!

djalmaaraujo added a commit that referenced this pull request Jun 30, 2026
…sonation

Addresses cubic review on PR #4:
- StartImpersonation rejects negative ttl (was sent as negative ttl_seconds or
  silently rounded to the backend default).
- auth status  now reflects active impersonation: true while a
  non-expired context is active (even without a base token), false when the
  context is expired (sticky, no fallback).
…sonation

Addresses cubic review on PR #4:
- StartImpersonation rejects negative ttl (was sent as a negative ttl_seconds
  or silently rounded to the backend default).
- auth status "authenticated" now reflects active impersonation: true while a
  non-expired context is active (even without a base token), false when the
  context is expired (sticky, no fallback to the original token).
@djalmaaraujo
djalmaaraujo force-pushed the djalma/lk-impersonate branch from e670b15 to 6305199 Compare June 30, 2026 21:51
@djalmaaraujo

Copy link
Copy Markdown
Collaborator Author

@cirdes os 2 pontos do cubic faziam sentido, corrigi ambos em 6305199:

  1. ttl negativoStartImpersonation agora rejeita ttl < 0 (antes ia como ttl_seconds negativo, ou era arredondado pro default do backend silenciosamente).
  2. auth statusauthenticated agora reflete a impersonação ativa: true enquanto o contexto não expirou (mesmo sem token base), false quando expirou (estado pegajoso, sem fallback pro token original).

Cobertos por teste; make test (-race), cover 95.3%, make lint limpo. Pode revisar de novo 🙏

@djalmaaraujo
djalmaaraujo requested a review from cirdes June 30, 2026 22:00
@djalmaaraujo
djalmaaraujo merged commit 1615580 into main Jul 14, 2026
4 checks passed
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.

2 participants