Skip to content

🔑 feat: let estela-cli send people here to create its key - #304

Merged
erick-GeGe merged 7 commits into
mainfrom
feat/api-keys-cli
Sep 28, 2026
Merged

erick-GeGe merged 7 commits into
mainfrom
feat/api-keys-cli

Conversation

@erick-GeGe

Copy link
Copy Markdown
Contributor

Stacked on #303. GitHub will retarget this to main once that merges.
This is only the estela side — the estela-cli repo is untouched.

Why

estela login sends a username and password, gets the Django token back, and writes it to ~/.estela.yaml in plain text at mode 0664. That token is also the browser session, so it cannot be rotated without logging the person out.

It is also why the captcha had to be switched off: a captcha exists to tell humans from programs, and the CLI is a program. The captcha is not the bug — machines having no door of their own is.

This builds the door on estela's side. The CLI changes come later, in its own repo.

What

estela login will open /settings/apiKeys?cli=1, which opens the create form already filled in:

NEW KEY FOR ESTELA-CLI

  Name         estela-cli @ admin
  Permissions  read · data · run · manage
  Expires      After this estela's default period.

               [ Create ]

One click. It never creates the key on arrival — a link someone sends you must not mint a credential by itself, which is why GitHub prefills a PAT form rather than generating one.

After creating, the result modal adds the line to paste:

estela login estela_kJ8mN2pQxR7vT9wLmDf4hG6jK8sW1zY3

Two details worth naming:

  • The duration is omitted from that request, so the key inherits API_KEY_DEFAULT_DAYS. Change that setting and the CLI hands out the new period without either side being touched.
  • Nothing about the machine travels in the URL. The name is built from the session, so there is no untrusted input to validate.

A gap this flow would have fallen into

PrivateRoute redirected to /login without remembering where you were headed, and LoginPage always landed on /projects. Anyone not already signed in would open the CLI's link, log in, and arrive at the project list with the flow gone.

Both now carry a next, and safeNextPath accepts it only as a same-site path:

?next= lands on
(absent) /projects
/settings/apiKeys%3Fcli%3D1 /settings/apiKeys?cli=1
/projects/abc /projects/abc
//evil.com /projects
https://evil.com /projects

//host is rejected alongside absolute URLs because the browser reads it as another origin — without that check the parameter is an open redirect.

This helps any deep link, not just this one.

Still to come, in the estela-cli repo

estela set-host, estela login <key>, ESTELA_API_KEY for CI, ~/.estela.yaml at 0600, and logout leaving the key alone — expiry now puts a ceiling on it. Then the captcha can go back on.

Verified

yarn lint, tsc --noEmit and yarn build pass, and the redirect table above was checked against the guard.

@erick-GeGe
erick-GeGe changed the base branch from feat/api-keys to main September 28, 2026 17:56
`estela login` will open /settings/apiKeys?cli=1. That opens the create form
already filled in — named after the signed-in user, carrying the scopes the CLI
needs — and waits for a click. It never creates the key on arrival: a link
someone sends you must not mint a credential by itself, which is why GitHub
prefills a PAT form rather than generating one.

The duration is left out of the request on that path, so the key inherits
`API_KEY_DEFAULT_DAYS`. Change that setting and the CLI hands out the new
period without either side being touched.

Nothing about the machine travels in the URL — the name is built from the
session — so there is no untrusted input to validate.

Also fixes a gap the flow would have fallen into: PrivateRoute sent people to
/login without remembering where they were headed, and LoginPage always landed
on /projects, so anyone not signed in lost the flow on the way. Both now carry
a `next`, accepted only when it is a same-site path — "//host" reads as another
origin to the browser, so it is rejected along with absolute URLs. This helps
any deep link, not just this one.
An API key carries no username, so a CLI holding one has no way to say who it
is connected as — and "the key was accepted" is a weaker thing to tell someone
than "you are admin".

GET /api/auth/whoami answers with the username and email behind whatever
credential was used, key or session. It follows the shape already used by the
password-change action in this file: authentication declared on the action
rather than the viewset, since the rest of it is public.

estela set-token calls it to validate the key and name the account in one round
trip.
…piry

The deploys page still taught the password login, down to a `Password:` line.
It now shows set-host, the optional create-api-token, and set-token — the same
in both the demo and the bring-your-own-project column, which were identical
copies of each other.

The form arriving from ?cli=1 locked the expiry to whatever the server
defaults to, with nothing to change it. There is no reason a CLI key cannot
last a week if that is what someone wants, so it gets the same selector as
every other key, preselected at 90 days.

The prefilled name loses its spaces: `estela-cli@erick` rather than
`estela-cli @ erick`, which reads better next to a hostname and copies as one
word.
The key modal told people to run `estela login <key>`, which is not a command —
and putting a credential on a command line is the one shape worth avoiding: it
lands in shell history and shows up in the process list while it runs.

It now names `estela set-token`, which asks for the key without echoing it. The
copy button above already put the key on the clipboard, so there is nothing to
retype.
A key carries no visible permissions, so a program holding one cannot tell
whether it is about to be refused until it tries — which is how a read-only key
gets as far as uploading a project before the API turns it away.

whoami now returns the scopes of the key that authenticated the call, so a
client can say up front what it will and will not be able to do. The field is
absent for a session, which has no scopes to report.
A key with the `run` scope still gets nowhere if its owner is a viewer on the
project, and until now that produced DRF's default "You do not have permission
to perform this action" — indistinguishable from the key simply lacking the
scope, which is a different problem with a different fix.

IsAdminOrReadOnly now names what is actually in the way, so the two read apart:

  Your role on this project does not allow this action.
  This API key does not have the required scope.

Sessions get the clearer wording too; the old text never said what to change.
docs/api.yaml predated the whoami endpoint, so the schema and the generated TS
client were missing both the route and the WhoAmI definition — including its
scopes field, which is what tells a client what a key may do.
@erick-GeGe
erick-GeGe merged commit 76943fc into main Sep 28, 2026
1 check 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