Skip to content

Scope auth login credentials to chosen roles - #143

Open
leet-c1 wants to merge 4 commits into
mainfrom
feat/login-scoped-roles
Open

leet-c1 wants to merge 4 commits into
mainfrom
feat/login-scoped-roles

Conversation

@leet-c1

@leet-c1 leet-c1 commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Summary

c1i auth login can now scope the personal client it mints to chosen roles (IGA-4372), with no server change.

  • First browser login to a tenant, in a terminal: before the device code, login asks whether the credential gets all of your roles (Enter) or only roles you choose.
  • --choose-roles: after browser approval, a menu of live roles. Pick one or more, or 0 for full permissions. A blank answer re-prompts, so a stray Enter can't widen the scope.
  • --scoped-role <role> (repeatable): for scripts, by ID or by name (basic-user, Basic User, system:user). IDs alone pass straight to the server (unknown → 404 plus a hint). With any name, every value is resolved against the tenant's roles: exact ID first, then the role's name, then its display name. A miss or an ambiguous name is a usage error that lists the choices, before your credential is created.
  • --display-name names the credential.
  • Login always states the resulting scope. On re-login it names the previous credential, which stays unrevoked.
  • New c1i roles list / c1i roles get.

Why a temporary helper credential

Verified live: the device-flow access token may only create a personal client. Introspect, roles, users and listing personal clients all return 403. PersonalClientService.Update silently ignores scopedRoles. So the menu works like this:

  1. Create an unscoped helper credential, c1i login role lookup (temporary).
  2. Read the roles with it.
  3. Delete it on every exit path, including Ctrl-C.
  4. Create the scoped credential with the device token.

If the delete fails, login warns with the helper's id.

Role set

The menu offers the union of C1.ai's two web pickers:

  • Personal-client page (profile/personal-clients/list/create.tsx): API-only roles, Basic User, Read-Only Administrator.
  • MCP consent page (profile/ai-connections/approve.tsx): the roles you hold, or every role for a super or read-only administrator.

Other changes

  • Post-login verification is now GET /api/v1/auth/introspect, not POST /api/v1/search/users. A narrowly scoped credential gets 403 on user search (verified live), which used to make login reject and delete it.
  • client.NewWithCredentials: the REST client for credentials that are never stored. Its token is reused in memory and never cached on disk.
  • keychain.StoredClientID: the stored client id, ignoring env credentials.

Review

Three independent adversarial reviews (design, code, help/docs; DRY included), then two re-review rounds on the fixes. Main issues found and fixed:

  • The first version mirrored the wrong web picker.
  • A blank Enter silently gave full access.
  • The prompt appeared on every login.
  • Ctrl-C at the menu hung, and a second Ctrl-C leaked the helper credential.
  • The helper minted a token per request.
  • Wrong error labels and exit codes.
  • Help text overclaimed.

Test plan

  • go build, go vet, golangci-lint (0 issues), gosec, gitleaks, go mod tidy (clean tree)
  • go test -count=1 -shuffle=on ×3
  • Mutation checks: removing the helper delete, accepting blank input, dropping pagination, ignoring ctx cancel, or dropping first-login gating each fails a test
  • Live on the lab tenant (pty-driven, approved in browser):
    • First login prompts, then a live menu of 11 roles. I picked Read-Only Administrator: the helper was deleted, the credential came back scoped, reads worked, and an app create got 403 Apps.Create.
    • Re-login didn't prompt, inherited all roles, and named the previous credential.
    • --scoped-role with a bogus id, no TTY: 404 plus hint, exit 4, nothing stored.
    • --scoped-role basic-user, no TTY: credential scoped to Basic User, helper deleted. basic-usr: exit 2 listing all roles, nothing stored.
    • Ctrl-C at the menu: exits immediately and the helper is deleted.
    • All test credentials cleaned up.

Follow-ups that need C1 server changes (EPD Triage)

  • EPD-3231: role picker on the device-approval page (MCP-DCR parity), plus fixing its "full access" wording.
  • EPD-3232: PersonalClientService.Update silently drops scopedRoles, and Create skips the delegation check.
  • EPD-3233: let the device-flow token read assignable roles, so CLIs don't need the temporary credential.
  • EPD-3234: admin-set default and maximum roles for CLI logins ("safe by default").

🤖 Generated with Claude Code

c1i auth login can restrict the personal client it mints. The first
interactive browser login to a tenant asks whether to keep all roles or
choose; --choose-roles shows a menu of live roles after approval (one or
more, or 0 for full permissions), and --scoped-role passes ids through for
scripts. --display-name names the credential. Login states the resulting
scope and names the credential a re-login leaves unrevoked.

The device-flow token may only create a personal client, so the menu reads
roles with a short-lived helper credential that is deleted on every exit
path, including Ctrl-C, before the scoped one is created. The menu offers
the union of C1.ai's personal-client and MCP-consent role pickers.

Also: c1i roles list/get; verify new logins with auth/introspect, which a
narrowly scoped credential can still call; client.NewWithCredentials (an
in-memory-reused, never-cached bearer); keychain.StoredClientID; every login
prompt now honors Ctrl-C.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@linear-code

linear-code Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

leet-c1 and others added 3 commits October 2, 2026 16:28
A value that isn't a role id is matched by name, display name or a
normalized form (basic-user, Basic User, system:user) against the tenant's
roles, read with the same temporary credential as the menu. A miss or an
ambiguous name is a usage error listing the choices, before the credential
is created; ids alone still pass straight to the server.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Match in tiers (id, then name, then display name) so a display name can't
make a role's own name ambiguous; label duplicate display names with ids in
errors and the scope line; dedupe bare ids; show the 404 hint only when ids
went to the server unchecked; document the id format and mixed-input check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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