feat(login): send device_label so users can identify CLI sessions - #743
feat(login): send device_label so users can identify CLI sessions#743rsnodgrass wants to merge 2 commits into
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (7)
🚧 Files skipped from review as they are similar to previous changes (7)
📝 WalkthroughWalkthrough
ChangesDevice label login flow
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant ox_login
participant deviceLabel
participant pollToken
participant TokenEndpoint
ox_login->>pollToken: start token polling
pollToken->>deviceLabel: generate optional label
deviceLabel-->>pollToken: sanitized label or empty value
pollToken->>TokenEndpoint: submit token request
TokenEndpoint-->>pollToken: return token response
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
internal/auth/device_label_test.go (1)
68-80: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAssertions are skipped whenever the resolved part is empty — risk of a vacuous pass.
In
TestDeviceLabel_FallsBackWhenUsernameUnavailable(68-80) andTestDeviceLabel_HostileHostnameStillProducesASafeLabel(122-159), the meaningful checks are gated behindif got != ""/if part == "" { continue }. Ifos.Hostname()legitimately resolves empty in a sandbox, or a hostile input sanitizes fully to"", the test silently verifies nothing for that case and still passes. Also, theUSERNAME(Windows) fallback branch indevice_label.go(lines 74-76) has no dedicated test — onlyUSERis exercised directly.Consider making hostname/user resolution overridable (e.g. package-level
var lookupHostname = os.Hostname) so both the "host present" and "host absent" paths can be asserted deterministically instead of relying on ambient environment, and add a case that setsUSER=""/USERNAME=<value>to cover the Windows fallback.Also applies to: 122-159
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/auth/device_label_test.go` around lines 68 - 80, Update deviceLabel test coverage to avoid vacuous assertions: make hostname lookup injectable via a package-level lookupHostname variable used by deviceLabel, then stub it to test both non-empty and empty hostname outcomes explicitly. Remove conditional assertion skips in TestDeviceLabel_FallsBackWhenUsernameUnavailable and TestDeviceLabel_HostileHostnameStillProducesASafeLabel, asserting the required result contract for every case, and add a dedicated USER empty/USERNAME populated case covering the Windows fallback.Source: Learnings
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/adr/008-privacy.md`:
- Around line 358-369: Update the privacy comparison and surrounding explanation
to acknowledge that the credential label may include USER/USERNAME and therefore
potentially identify the local OS account. Replace the “None” machine-identifier
risk and “never who” claims with wording that accurately distinguishes machine
identification from possible account-name disclosure, while preserving the
distinction from anonymous telemetry.
---
Nitpick comments:
In `@internal/auth/device_label_test.go`:
- Around line 68-80: Update deviceLabel test coverage to avoid vacuous
assertions: make hostname lookup injectable via a package-level lookupHostname
variable used by deviceLabel, then stub it to test both non-empty and empty
hostname outcomes explicitly. Remove conditional assertion skips in
TestDeviceLabel_FallsBackWhenUsernameUnavailable and
TestDeviceLabel_HostileHostnameStillProducesASafeLabel, asserting the required
result contract for every case, and add a dedicated USER empty/USERNAME
populated case covering the Windows fallback.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: fe532751-8bae-435b-8205-3a10fbd45356
📒 Files selected for processing (7)
docs/adr/008-privacy.mddocs/debugging.mddocs/guides/ci-pipelines.mdinternal/auth/device_flow.gointernal/auth/device_flow_label_test.gointernal/auth/device_label.gointernal/auth/device_label_test.go
Co-Authored-By: SageOx <ox@sageox.ai>
…t name Co-Authored-By: SageOx <ox@sageox.ai>
2a94cea to
918455f
Compare
|
Fixed — you were right, and it mattered.
ADR-008 §6 now says plainly that the label identifies the machine and the account on it — for a single-user laptop, a person — and that it is not anonymous. The argument is narrowed to the thing that's actually true: the device-flow exchange is already authenticated, so the label adds which device without adding linkability the server didn't already have. The residual disclosure (the local account name itself) is named explicitly as the reason the opt-out and the first-DNS-label reduction exist. Same correction applied to the |
Backend check: safe to merge, but inert until sageox-mono changesVerified against No strict decoding — zero break risk. The handler does But the field goes nowhere. The route doesn't forward the client's body — it constructs its own internal request to Better Auth with an explicit So this PR is groundwork, not a working feature. It's safe to land, but nothing changes in Second thought on
|
Fixes #625 (CLI half).
/settings/securitylists every PAT minted byox loginwith nothing but timestamps. Someone with a laptop, a desktop, a devcontainer and a CI runner cannot tell which row to revoke. This sends a shortuser@hostlabel with the device-flow token exchange.One edit covers both login entry points — the package-level
Login()and(*AuthClient).Login()sharepollToken.SAGEOX_NO_DEVICE_LABEL. Per CLAUDE.md, any newSAGEOX_*var needs your review. I took the reversible defaults rather than blocking: opt-out (not opt-in), default labeluser@hostdegrading to host-only.DisallowUnknownFields/ Zod.strict()) would 400 and breakox loginoutright. Worth a yes from the mono owner before merge.Why
user@hostand not hostname-onlyHostname alone answers the question for a laptop fleet and fails exactly the cases that need it most — shared boxes, CI runners, devcontainers, and multi-account workstations. It's also the format users already read in GitHub SSH key labels, 1Password and
sshitself. Degrades gracefully: host-only → user-only → empty.Where the username comes from — and three sources deliberately rejected
The OS account name (
$USER, then$USERNAME). Explicitly not:signature.GetMachineID()identity.AttributionNameidentity.AttributionUsernameox loginfrom — non-deterministic, and more identifying than the account name.Privacy — meeting ADR-008 head-on
A reviewer will reach for "we never collect machine identifiers". That table sits under Telemetry, and the distinction is the scope of that rule, not an exception to it:
A stable machine id is banned in telemetry precisely because it de-anonymizes an otherwise anonymous stream. That hazard doesn't exist for metadata attached to a credential the user is knowingly creating. The label answers which machine, never who.
This PR ships the ADR amendment (new §6 + a summary-matrix row) rather than leaving the scoping to be inferred from section nesting.
Data minimization applied: first DNS label only (
laptop, notlaptop.corp.internal— the domain identifies an employer more than a machine); allowlisted to[A-Za-z0-9._-]; truncated to 32 (user) + 63 (host, the DNS label max, so no legal hostname is ever cut).Sanitization is an allowlist, not a denylist
A hostname is attacker-influenceable on a shared box, and the label is transmitted then rendered in a web UI. The allowed set is the intersection of what's safe in a GitLab PAT name, an HTTP header, a JSON string and a URL segment — so everything below
0x20,0x7F, and all non-ASCII is excluded by construction:a\r\nX-Evil: 1a-X-Evil-1"><script>alert(1)</script>script-alert-1-scriptryan@corpryan-corp— an@in a part is neutralized, so the single@in the output is always the composer'sTest Plan
TestPollToken_SendsDeviceLabel— decodes the actual request body; asserts the pre-existing fields are untouched and the label matches the shape contract.TestPollToken_OmitsDeviceLabelWhenOptedOut— asserts the key is absent, not present-and-empty. That's what provesomitempty: an empty string would still be a wire change, which an opt-out must not be.TestDeviceLabel_HostileHostnameStillProducesASafeLabel— property test over injection payloads.TestOptedOutOfDeviceLabel— pins truthiness, including thatSAGEOX_NO_DEVICE_LABEL=0means labels on, as it reads.internal/auth/env_naming_test.go(theOX_*guard) still passes;make lintclean.Follow-up, deliberately out of scope
Not sending the label on the earlier
/api/auth/device/coderequest. Showing "you are authorizing ryan@laptop" on the approval screen before consent is genuinely better security UX, but it needs a second backend change and an approval-screen design decision.Summary by CodeRabbit
New Features
Privacy
SAGEOX_NO_DEVICE_LABEL=1to prevent device labels from being sent during login.Tests