Skip to content

fix(machine): report unavailable protections instead of failing - #1441

Open
clement-tourriere wants to merge 6 commits into
mainfrom
ctourriere/machine-unavailable-protections
Open

clement-tourriere wants to merge 6 commits into
mainfrom
ctourriere/machine-unavailable-protections

Conversation

@clement-tourriere

Copy link
Copy Markdown
Member

Splits out the three uncontested commits of #1410 (@amascia-gg, authorship preserved) and rebases them onto current main. Nothing here changes DEFAULT_SCOPES — the feat(auth): request the honeytokens:write scope by default commit stays on #1410, where the design question raised in review is still open.

The problem

ggshield auth login requests scan, honeytokens:check, endpoints:send, ai-discover:send — not honeytokens:write. So a token from a plain login can never plant a honeytoken, and ggshield machine setup ends its run with a raw endpoint-deployments API error (403): … per target and exit code 1. ggshield machine doctor fails the same way on the missing scope.

Neither is a broken machine. honeytokens:write is granted only on a plan that offers honeytokens, and only to a Manager; ai-discover:send is likewise plan-gated. A member's laptop is correctly set up and still reports red — and under MDM that gates a rollout on something no machine can fix.

What changes

  • machine setup checks the token's scopes before planting. Without honeytokens:write it reports the protection as unavailable and moves on, instead of running a doomed API call and failing the whole setup. Scopes it cannot read still go through plant, which surfaces the real error.
  • machine doctor grows an optional check: printed with its fix and a yellow !, excluded from the exit code. Applied to honeytokens:write and ai-discover:send — both plan-gated, and neither is needed by a protection that is already in place (honeytoken planting skips without its scope, and ai discover is a separate command). scan stays fatal, and endpoints:send stays fatal when the machine_scan plugin is installed: installed-but-cannot-upload is a real break.
  • honeytoken plant answers a 403 with the three prerequisites (honeytoken module enabled, Manager access level, honeytokens:write on the token) instead of the raw API body, which never said which one was missing.
  • auth login --scopes now falls through to a fresh login when the current token lacks a requested scope. Before, it reused the still-valid token and reported "already authenticated" without ever asking for it — which made every ↳ fix: line in doctor that says "re-run auth login" a no-op. The doctor fixes now say auth login --scopes <scope>.

Testing

  • Full unit suite: 2717 passed, 4 skipped. black / isort / flake8 clean, import-linter both contracts KEPT.
  • The only conflict from the rebase was a test import line (Mock vs MagicMock).

Notes for review

The reconcile call answers 403 when the honeytoken module is disabled, the
user is below Manager, or the token lacks `honeytokens:write`. Printing the
raw API body left the reader guessing which one it was, and reading it as a
workspace entitlement problem when a token scope was missing.

Report the three prerequisites instead, matching what `honeytoken create`
already prints. This is the shared path, so a direct run and the root
fan-out get the message too, not only `machine setup`.
`auth login` reuses any still-valid token, so `--scopes` was a no-op for
anyone already authenticated: the command printed "already authenticated"
and exited without ever requesting the scope. Every message telling a user
to run `auth login --scopes <scope>` was therefore dead advice unless they
knew to log out first.

Compare the requested scopes against the ones the token carries and fall
through to a fresh login when one is missing. Only `--scopes` triggers the
lookup, so a plain login keeps costing no extra round trip, and an
unreadable scope list keeps the token rather than forcing a needless
re-login. Comparison is on raw strings, since pygitguardian's `TokenScope`
enum does not know every scope ggshield requests.
`machine setup` plants a honeytoken by default, so a token without
`honeytokens:write` ended the run on a raw 403 and a non-zero exit. Doctor
did the same for `honeytokens:write` and `ai-discover:send`. Both are gated
on the plan, so no action on the machine can turn them green, and failing on
them gates an MDM rollout on something out of the fleet's reach.

Setup now checks the scope up front and skips planting with a message naming
the command that grants it. Doctor gains an optional check state, rendered
`!`, that still prints its fix but does not fail the run. `endpoints:send`
stays required: installing the `machine_scan` plugin is an explicit opt-in,
so a token that cannot upload endpoint data really is misconfigured.
@clement-tourriere
clement-tourriere requested a review from a team as a code owner August 27, 2026 09:36
@codecov

codecov Bot commented Aug 27, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.18182% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 94.00%. Comparing base (efc667f) to head (e6ba557).
⚠️ Report is 16 commits behind head on main.

Files with missing lines Patch % Lines
ggshield/cmd/honeytoken/plant.py 75.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1441      +/-   ##
==========================================
+ Coverage   93.99%   94.00%   +0.01%     
==========================================
  Files         200      201       +1     
  Lines       12660    12803     +143     
==========================================
+ Hits        11900    12036     +136     
- Misses        760      767       +7     
Flag Coverage Δ
unittests 94.00% <98.18%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@amascia-gg

Copy link
Copy Markdown
Member

Thanks for taking this over!

Splitting the fourth commit out removed the only feedback that a requested scope was refused,
and I had not spotted that. _warn_missing_scopes (login.py:29) only diffs against
DEFAULT_SCOPES, which no longer contains honeytokens:write. So for a non-Manager:

  1. machine setup prints skipped: ... run ggshield auth login --scopes honeytokens:write
  2. they run it, and commit 2 now forces a real re-login, minting a new PAT (the old one is
    not revoked, we only do that on logout --revoke)
  3. _warn_missing_scopes compares against DEFAULT_SCOPES only, so nothing is printed. They
    see "Success! You are now authenticated."
  4. machine doctor still shows ! with the same instruction, so they try again

Every attempt leaves another token behind, with no explanation and no way to succeed. On
#1410 the fourth commit hid this, so it only shows up in this split. Two small changes:

  • _warn_missing_scopes: diff against DEFAULT_SCOPES + extra_scopes so a refused
    --scopes is reported
  • the skip and doctor fix text: stop pointing non-Managers at a scope only a Manager can get,
    and say the role or plan cannot grant it instead

amascia-gg and others added 2 commits September 4, 2026 16:25
The backend grants the subset of requested scopes the member is eligible
for and drops the rest, so what a login actually obtained is only knowable
from the token afterwards. ggshield compared it against the hardcoded
`DEFAULT_SCOPES`, which left two cases silent.

A scope requested with `--scopes` and refused was never mentioned: the
command printed nothing but success, while `machine doctor` kept advising
the same flag. Compare against the effective requested set instead, the
defaults plus whatever `--scopes` added, which is also the set sent in the
authorize URL.

A reused token was not looked at at all, since the early return skips the
report entirely. One minted before a scope joined the defaults therefore
stayed short until it expired, visible only in `machine doctor`. Report on
that path too, with different words: nothing was refused there, the token
was simply never asked for the scope, so say that and name the command that
requests it. It costs one `/v1/api_tokens/self` call on an interactive,
infrequent command.

Warning and not re-authenticating is deliberate: forcing a login whenever a
default scope is missing would mint a token on every run for any workspace
whose plan cannot grant one of them.

Refs: NHI-1991
…-login-reports-refused-scopes

fix(auth): report the scopes login did not get
@linear

linear Bot commented Sep 7, 2026

Copy link
Copy Markdown

NHI-1924

…at --scopes

The `machine setup` skip and the `machine doctor` fix lines for
`honeytokens:write` and `ai-discover:send` told every member to run
`ggshield auth login --scopes <scope>`. The server grants what the plan and
role allow and drops the rest, so for a member who is not a Manager, or whose
plan does not offer honeytokens, that loop can never succeed: each attempt
forces a real re-login that mints another token, and doctor keeps printing the
same instruction.

Lead with who can get the scope instead. The command stays, conditioned on
being that person, since a Manager whose token came from a plain login still
needs to know how to request it; the alternative is spelled out too, so a
non-Manager reads that the plan or role cannot grant it and that nothing on
the machine needs fixing. `scan` and `endpoints:send` keep their
unconditional fix: missing them is a real break, not an unavailable feature.

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.

2 participants