// The `carlos instances door-key` verb family (instance-moves P7, #175): the // CLI front over the console's three door-key doors // (internal/console/apidoorkeys.go) — mint, list and revoke the app-scoped // credential that authenticates an app's own processes on a box to the // edge-local door (a unix socket), whatever box the instance lives on. API // mode only, the same as `carlos instances create/list/delete`: a door key // is a console-authorized write, the console's database is the authority, // and there is no store-mode equivalent. Modelled on runInstancesDelete // (instances_cli.go) for the verb shape and mail_cli.go's emailDeclare/ // emailStatus for the wire calls. package main import ( "context" "encoding/json" "flag" "fmt" "io" "log" "net/http" "strings" "time" ) const doorKeyUsage = `usage: carlos instances door-key create --app [--label ] carlos instances door-key list --app carlos instances door-key revoke --app --account account (sqid or name; default: your only account, or the configured/chosen one) --account-id account sqid, matched as a sqid ONLY — supersedes --account --console console to act through (default: the only logged-in one) Signed in only (carlos auth login). A door key is written through a console, which keeps it in its own database. There is no direct-bucket form. create mints a new key for the app and prints it once. Store it now: it cannot be shown again, and list only ever shows its id and label. list shows the app's live keys: id, label and when each was created. revoke takes an id from list and stops that key working. ` // ---- wire types ------------------------------------------------------------- // apiDoorKeyMint is handleAPIDoorKeyMint's 201 reply // (internal/console/apidoorkeys.go): the raw key, shown here and nowhere // else — the console keeps only its digest from this point on. type apiDoorKeyMint struct { Key string `json:"key"` ID string `json:"id"` Account string `json:"account"` App string `json:"app"` } // apiDoorKeyEntry is one row of handleAPIDoorKeyList's reply — never the key // itself, which the console does not keep in a recoverable form. type apiDoorKeyEntry struct { ID string `json:"id"` Label string `json:"label"` CreatedAt time.Time `json:"created_at"` } // apiDoorKeyRevokeReply is handleAPIDoorKeyRevoke's 200 reply. type apiDoorKeyRevokeReply struct { Revoked string `json:"revoked"` } // ---- apiSession wire calls --------------------------------------------------- func (s *apiSession) doorKeyMint(ctx context.Context, a apiAcct, app, label string) (apiDoorKeyMint, error) { in := struct { Label string `json:"label,omitempty"` }{label} status, body, err := s.postJSON(ctx, apiPath(a, app, "/door-keys"), in) if err != nil { return apiDoorKeyMint{}, err } if status != http.StatusCreated { return apiDoorKeyMint{}, apiErrorFromBody(status, body) } var out apiDoorKeyMint if err := json.Unmarshal(body, &out); err != nil { return apiDoorKeyMint{}, fmt.Errorf("door-key create: unreadable response: %w", err) } return out, nil } func (s *apiSession) doorKeyList(ctx context.Context, a apiAcct, app string) ([]apiDoorKeyEntry, error) { status, body, err := s.getJSON(ctx, apiPath(a, app, "/door-keys")) if err != nil { return nil, err } if status != http.StatusOK { return nil, apiErrorFromBody(status, body) } var res struct { Keys []apiDoorKeyEntry `json:"keys"` } if err := json.Unmarshal(body, &res); err != nil { return nil, fmt.Errorf("door-key list: unreadable response: %w", err) } return res.Keys, nil } func (s *apiSession) doorKeyRevoke(ctx context.Context, a apiAcct, app, id string) (string, error) { status, body, err := s.postJSON(ctx, apiPath(a, app, "/door-keys/"+pathEscape(id)+"/revoke"), struct{}{}) if err != nil { return "", err } if status != http.StatusOK { return "", apiErrorFromBody(status, body) } var out apiDoorKeyRevokeReply if err := json.Unmarshal(body, &out); err != nil { return "", fmt.Errorf("door-key revoke: unreadable response: %w", err) } return out.Revoked, nil } // ---- verbs -------------------------------------------------------------- // runInstancesDoorKey dispatches the door-key sub-verbs — create/list/revoke // are positional words, not flags, and must never reach fs.Parse, the same // shape runInstances and runDomains use for their own sub-verbs. func runInstancesDoorKey(args []string) { if len(args) > 0 { switch args[0] { case "create": runInstancesDoorKeyCreate(args[1:]) return case "list": runInstancesDoorKeyList(args[1:]) return case "revoke": runInstancesDoorKeyRevoke(args[1:]) return } } log.Fatal(doorKeyUsage) } func runInstancesDoorKeyCreate(args []string) { fs := flag.NewFlagSet("instances door-key create", flag.ExitOnError) app := appFlag(fs, "app to mint a door key for") label := fs.String("label", "", "short note stored with the key and shown by list (default: none)") consoleFlag := consoleOption(fs, "console to create through (default: the only logged-in one)") acctRaw := fs.String("account", "", "account (sqid or name) the app belongs to (default: your only account, or the configured/chosen one)") acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes --account") fs.Usage = flagUsage(fs, doorKeyUsage) fs.Parse(args) sess := requireAPISession("door-key create", *consoleFlag) printUsing(stdout, sess) acct := resolvedAccount(sess, acctRaw, acctIDRaw) app2 := resolvedApp(sess, acct, *app, doorKeyUsage) res, err := sess.doorKeyMint(context.Background(), acct, app2, strings.TrimSpace(*label)) if err != nil { log.Fatalf("instances door-key create: %v", err) } printDoorKeyCreate(stdout, res) } // printDoorKeyCreate prints the raw key on its own line, so it is the one // thing a script capturing this command's stdout has to parse out, then a // warning that this is the only time it will ever be shown — the console // keeps only its digest from here on, the same rule email credentials // create follows for a standalone credential's password. func printDoorKeyCreate(w io.Writer, res apiDoorKeyMint) { fmt.Fprintln(w, res.Key) warnf(w, "shown once. store it now: it cannot be shown again") } func runInstancesDoorKeyList(args []string) { fs := flag.NewFlagSet("instances door-key list", flag.ExitOnError) app := appFlag(fs, "app whose door keys to list") consoleFlag := consoleOption(fs, "console to list through (default: the only logged-in one)") acctRaw := fs.String("account", "", "account (sqid or name) the app belongs to (default: your only account, or the configured/chosen one)") acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes --account") fs.Usage = flagUsage(fs, doorKeyUsage) fs.Parse(args) sess := requireAPISession("door-key list", *consoleFlag) printUsing(stdout, sess) acct := resolvedAccount(sess, acctRaw, acctIDRaw) app2 := resolvedApp(sess, acct, *app, doorKeyUsage) keys, err := sess.doorKeyList(context.Background(), acct, app2) if err != nil { log.Fatalf("instances door-key list: %v", err) } printDoorKeyList(stdout, keys) } // printDoorKeyList never prints a key — the list reply this reads from does // not carry one (apiDoorKeyEntry has no such field at all), so there is // nothing here to leak by construction. func printDoorKeyList(w io.Writer, keys []apiDoorKeyEntry) { if len(keys) == 0 { mutedf(w, "no door keys") return } t := newTable(w) fmt.Fprintf(t, "ID\tLABEL\tCREATED\n") for _, k := range keys { label := k.Label if label == "" { label = "-" } fmt.Fprintf(t, "%s\t%s\t%s\n", k.ID, label, k.CreatedAt.Format(time.RFC3339)) } t.Flush() } func runInstancesDoorKeyRevoke(args []string) { fs := flag.NewFlagSet("instances door-key revoke", flag.ExitOnError) app := appFlag(fs, "app the door key belongs to") consoleFlag := consoleOption(fs, "console to revoke through (default: the only logged-in one)") acctRaw := fs.String("account", "", "account (sqid or name) the app belongs to (default: your only account, or the configured/chosen one)") acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes --account") fs.Usage = flagUsage(fs, doorKeyUsage) fs.Parse(args) id := strings.TrimSpace(fs.Arg(0)) if id == "" || fs.NArg() > 1 { log.Fatal(doorKeyUsage) } sess := requireAPISession("door-key revoke", *consoleFlag) printUsing(stdout, sess) acct := resolvedAccount(sess, acctRaw, acctIDRaw) app2 := resolvedApp(sess, acct, *app, doorKeyUsage) revoked, err := sess.doorKeyRevoke(context.Background(), acct, app2, id) if err != nil { log.Fatalf("instances door-key revoke: %v", err) } fmt.Fprintf(stdout, "%s %s\n", colorFor(stdout).ok("revoked"), revoked) }