Repository navigation
feat(cli): campus auth status (#45) - #49
Merged
Merged
Conversation
…#45) Tokens carry exactly the scopes requested at login (no accumulation), but the credential store never recorded them: the only way to learn what a stored token could do was to hit a 403 and re-login. The store now records the granted scopes and authorizing principal at login (additive keys, written from the device grant response), and `campus auth status` reports them alongside the client id, endpoints and expiry — with a --json mode for scripts and agents. The default mode adds a lightweight authenticated validity check: GET <auth>/users/ with the bearer, classified by status code only (FORBIDDEN = valid-but-unprivileged, UNAUTHORIZED = rejected). The probe targets the endpoint that minted the token, mirroring logout's revoke-at-issuing-endpoint rule. --offline skips it. Network errors are a third, non-fatal state (warning, exit 0); a rejected token exits 1, as does not-logged-in.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
campus auth statusnow reports the stored token's identity — OAuth client, endpoints, principal, scopes, expiry — locally, and (unless--offline) adds a lightweight authenticated validity check.--jsonemits the same fields machine-readably for scripts and agents (#42).Acceptance checklist (#45)
campus auth statusprints client, server, principal, and scopes from the stored credential--jsonmachine-readable output--offlineskips any network validationWhat the store had vs what this adds
Prominent callout: the issue's premise that "#38 stores scopes" turned out not to hold. The #38 short-circuit only bypasses re-login when
--scopeis absent; it never reads scopes back, and the credential store held no scopes and no principal (access_token,refresh_token,token_auth_url,login_session_id,token_expires_atonly). The data does exist at login time — the device-grant token response carries bothscope(space-joined) anduser_id(verified in campusroutes/oauth.py).Smallest backward-compatible addition, so the contract is met end-to-end:
campus_cli/credentials.py:get/set_token_scopes()(space-joined, RFC 6749 scope syntax) andget/set_token_user_id(); both keys are cleared bydelete_token().campus_cli/auth/login.py: after token storage, recordstoken_data["scope"]andtoken_data["user_id"]. Purely additive — no flow, error, or success-path behavior changes; credentials stored before this simply lack the keys and status reports principal/scopes asunknown (not recorded; … until next login).campus auth refreshpreserves the recorded scopes (the refresh grant reissues exactly the granted scopes, RFC 6749 §6), so no change there.Design notes
Default probe — what and why. No whoami/tokeninfo route exists in the SDK's resource surface. I surveyed two candidates:
POST /auth/v1/root/{"token": …}— introspects the token and even returns client+user, but the route module is explicitly documented "to be used only by Campus backend services"; building a CLI feature on it is fragile.GET <auth>/users/with the bearer, classified by status code alone —200/403 FORBIDDEN= token authenticated (valid, just lacksusers:read, which is operator/admin-gated server-side),401 UNAUTHORIZED= rejected. This is the error-envelope distinction the issue suggested.The probe targets the endpoint that minted the token (
token_auth_url), falling back to the current target for pre-binding credentials — mirroring logout's revoke-at-issuing-endpoint rule.Raw
requestsinstead of the campus_python client: the SDK'sCampusRequestwrapsrequests.RequestExceptionaserrors.ServerError, conflating "could not reach server" with a real HTTP 500 — the three-state machine needs them apart. It also matches login.py's device-flow HTTP style.No-network state machine (documented in
--helpand the module docstring):authenticated: false(JSON)validinvalidcampus auth login) printedunreachableunknownnot_checked--offline--jsonkeys: the pre-existingauthenticated/expires_at/is_expired/can_refresh/auth_url/token_auth_url/endpoint_matchare kept verbatim (backward compatible); new keysclient_id,user_id,scopes(null= not recorded,[]= genuinely none),expires_in_seconds,token_validity,probe_url,probe_note. JSON is written viatyper.echo(repo convention: Rich wrapping must not corrupt machine output).Test / verify results
poetry run pytest -q→ 199 passed (baseline 175 + 24 new); no live-network tests — the probe is always mocked or pointed at a closed porttests/unit/test_status.py: probe status-code mapping (200/403/401/5xx/network), assembly (present/absent credential, scopes recorded vs not-recorded vs empty, expiry math), JSON vs human output, offline-vs-probe paths, exit codes, probe-target selectiontests/unit/test_login.py: login persistsscope/user_id; omits both when the response lacks them (older servers)tests/integration/test_cli.py: updated for the feat(cli):campus auth status— show stored token identity and scopes #45 contract — not-logged-in is now exit 1 in both modes (previously 0), and status tests mock the probe/store fieldspoetry run ruff check campus_cli/ tests/→ cleancampus auth status --help— renders the exit-code/state documentation--jsonprint the not-logged-in message cleanly, exit 1 in all three modes (default/--json/--offline)⚠ Token check … could not reach server … — validity unknown, exit 0;--offlineprintsToken check: skipped (--offline)Closes #45