carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1// The `carlos instances door-key` verb family (instance-moves P7, #175): the
2// CLI front over the console's three door-key doors
3// (internal/console/apidoorkeys.go) — mint, list and revoke the app-scoped
4// credential that authenticates an app's own processes on a box to the
5// edge-local door (a unix socket), whatever box the instance lives on. API
6// mode only, the same as `carlos instances create/list/delete`: a door key
7// is a console-authorized write, the console's database is the authority,
8// and there is no store-mode equivalent. Modelled on runInstancesDelete
9// (instances_cli.go) for the verb shape and mail_cli.go's emailDeclare/
10// emailStatus for the wire calls.
11package main
12
13import (
14 "context"
15 "encoding/json"
16 "flag"
17 "fmt"
18 "io"
19 "log"
20 "net/http"
21 "strings"
22 "time"
23)
24
25const doorKeyUsage = `usage: carlos instances door-key create --app <a> [--label <text>]
26 carlos instances door-key list --app <a>
27 carlos instances door-key revoke --app <a> <id>
28
29 --account <a> account (sqid or name; default: your only account,
30 or the configured/chosen one)
31 --account-id <id> account sqid, matched as a sqid ONLY — supersedes
32 --account
33 --console <origin> console to act through (default: the only
34 logged-in one)
35
36Signed 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.
37
38create 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.
39
40list shows the app's live keys: id, label and when each was created.
41
42revoke takes an id from list and stops that key working.
43`
44
45// ---- wire types -------------------------------------------------------------
46
47// apiDoorKeyMint is handleAPIDoorKeyMint's 201 reply
48// (internal/console/apidoorkeys.go): the raw key, shown here and nowhere
49// else — the console keeps only its digest from this point on.
50type apiDoorKeyMint struct {
51 Key string `json:"key"`
52 ID string `json:"id"`
53 Account string `json:"account"`
54 App string `json:"app"`
55}
56
57// apiDoorKeyEntry is one row of handleAPIDoorKeyList's reply — never the key
58// itself, which the console does not keep in a recoverable form.
59type apiDoorKeyEntry struct {
60 ID string `json:"id"`
61 Label string `json:"label"`
62 CreatedAt time.Time `json:"created_at"`
63}
64
65// apiDoorKeyRevokeReply is handleAPIDoorKeyRevoke's 200 reply.
66type apiDoorKeyRevokeReply struct {
67 Revoked string `json:"revoked"`
68}
69
70// ---- apiSession wire calls ---------------------------------------------------
71
72func (s *apiSession) doorKeyMint(ctx context.Context, a apiAcct, app, label string) (apiDoorKeyMint, error) {
73 in := struct {
74 Label string `json:"label,omitempty"`
75 }{label}
76 status, body, err := s.postJSON(ctx, apiPath(a, app, "/door-keys"), in)
77 if err != nil {
78 return apiDoorKeyMint{}, err
79 }
80 if status != http.StatusCreated {
81 return apiDoorKeyMint{}, apiErrorFromBody(status, body)
82 }
83 var out apiDoorKeyMint
84 if err := json.Unmarshal(body, &out); err != nil {
85 return apiDoorKeyMint{}, fmt.Errorf("door-key create: unreadable response: %w", err)
86 }
87 return out, nil
88}
89
90func (s *apiSession) doorKeyList(ctx context.Context, a apiAcct, app string) ([]apiDoorKeyEntry, error) {
91 status, body, err := s.getJSON(ctx, apiPath(a, app, "/door-keys"))
92 if err != nil {
93 return nil, err
94 }
95 if status != http.StatusOK {
96 return nil, apiErrorFromBody(status, body)
97 }
98 var res struct {
99 Keys []apiDoorKeyEntry `json:"keys"`
100 }
101 if err := json.Unmarshal(body, &res); err != nil {
102 return nil, fmt.Errorf("door-key list: unreadable response: %w", err)
103 }
104 return res.Keys, nil
105}
106
107func (s *apiSession) doorKeyRevoke(ctx context.Context, a apiAcct, app, id string) (string, error) {
108 status, body, err := s.postJSON(ctx, apiPath(a, app, "/door-keys/"+pathEscape(id)+"/revoke"), struct{}{})
109 if err != nil {
110 return "", err
111 }
112 if status != http.StatusOK {
113 return "", apiErrorFromBody(status, body)
114 }
115 var out apiDoorKeyRevokeReply
116 if err := json.Unmarshal(body, &out); err != nil {
117 return "", fmt.Errorf("door-key revoke: unreadable response: %w", err)
118 }
119 return out.Revoked, nil
120}
121
122// ---- verbs --------------------------------------------------------------
123
124// runInstancesDoorKey dispatches the door-key sub-verbs — create/list/revoke
125// are positional words, not flags, and must never reach fs.Parse, the same
126// shape runInstances and runDomains use for their own sub-verbs.
127func runInstancesDoorKey(args []string) {
128 if len(args) > 0 {
129 switch args[0] {
130 case "create":
131 runInstancesDoorKeyCreate(args[1:])
132 return
133 case "list":
134 runInstancesDoorKeyList(args[1:])
135 return
136 case "revoke":
137 runInstancesDoorKeyRevoke(args[1:])
138 return
139 }
140 }
141 log.Fatal(doorKeyUsage)
142}
143
144func runInstancesDoorKeyCreate(args []string) {
145 fs := flag.NewFlagSet("instances door-key create", flag.ExitOnError)
146 app := appFlag(fs, "app to mint a door key for")
147 label := fs.String("label", "", "short note stored with the key and shown by list (default: none)")
148 consoleFlag := consoleOption(fs, "console to create through (default: the only logged-in one)")
149 acctRaw := fs.String("account", "", "account (sqid or name) the app belongs to (default: your only account, or the configured/chosen one)")
150 acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes --account")
151 fs.Usage = flagUsage(fs, doorKeyUsage)
152 fs.Parse(args)
153 sess := requireAPISession("door-key create", *consoleFlag)
154 printUsing(stdout, sess)
155 acct := resolvedAccount(sess, acctRaw, acctIDRaw)
156 app2 := resolvedApp(sess, acct, *app, doorKeyUsage)
157 res, err := sess.doorKeyMint(context.Background(), acct, app2, strings.TrimSpace(*label))
158 if err != nil {
159 log.Fatalf("instances door-key create: %v", err)
160 }
161 printDoorKeyCreate(stdout, res)
162}
163
164// printDoorKeyCreate prints the raw key on its own line, so it is the one
165// thing a script capturing this command's stdout has to parse out, then a
166// warning that this is the only time it will ever be shown — the console
167// keeps only its digest from here on, the same rule email credentials
168// create follows for a standalone credential's password.
169func printDoorKeyCreate(w io.Writer, res apiDoorKeyMint) {
170 fmt.Fprintln(w, res.Key)
171 warnf(w, "shown once. store it now: it cannot be shown again")
172}
173
174func runInstancesDoorKeyList(args []string) {
175 fs := flag.NewFlagSet("instances door-key list", flag.ExitOnError)
176 app := appFlag(fs, "app whose door keys to list")
177 consoleFlag := consoleOption(fs, "console to list through (default: the only logged-in one)")
178 acctRaw := fs.String("account", "", "account (sqid or name) the app belongs to (default: your only account, or the configured/chosen one)")
179 acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes --account")
180 fs.Usage = flagUsage(fs, doorKeyUsage)
181 fs.Parse(args)
182 sess := requireAPISession("door-key list", *consoleFlag)
183 printUsing(stdout, sess)
184 acct := resolvedAccount(sess, acctRaw, acctIDRaw)
185 app2 := resolvedApp(sess, acct, *app, doorKeyUsage)
186 keys, err := sess.doorKeyList(context.Background(), acct, app2)
187 if err != nil {
188 log.Fatalf("instances door-key list: %v", err)
189 }
190 printDoorKeyList(stdout, keys)
191}
192
193// printDoorKeyList never prints a key — the list reply this reads from does
194// not carry one (apiDoorKeyEntry has no such field at all), so there is
195// nothing here to leak by construction.
196func printDoorKeyList(w io.Writer, keys []apiDoorKeyEntry) {
197 if len(keys) == 0 {
198 mutedf(w, "no door keys")
199 return
200 }
201 t := newTable(w)
202 fmt.Fprintf(t, "ID\tLABEL\tCREATED\n")
203 for _, k := range keys {
204 label := k.Label
205 if label == "" {
206 label = "-"
207 }
208 fmt.Fprintf(t, "%s\t%s\t%s\n", k.ID, label, k.CreatedAt.Format(time.RFC3339))
209 }
210 t.Flush()
211}
212
213func runInstancesDoorKeyRevoke(args []string) {
214 fs := flag.NewFlagSet("instances door-key revoke", flag.ExitOnError)
215 app := appFlag(fs, "app the door key belongs to")
216 consoleFlag := consoleOption(fs, "console to revoke through (default: the only logged-in one)")
217 acctRaw := fs.String("account", "", "account (sqid or name) the app belongs to (default: your only account, or the configured/chosen one)")
218 acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes --account")
219 fs.Usage = flagUsage(fs, doorKeyUsage)
220 fs.Parse(args)
221 id := strings.TrimSpace(fs.Arg(0))
222 if id == "" || fs.NArg() > 1 {
223 log.Fatal(doorKeyUsage)
224 }
225 sess := requireAPISession("door-key revoke", *consoleFlag)
226 printUsing(stdout, sess)
227 acct := resolvedAccount(sess, acctRaw, acctIDRaw)
228 app2 := resolvedApp(sess, acct, *app, doorKeyUsage)
229 revoked, err := sess.doorKeyRevoke(context.Background(), acct, app2, id)
230 if err != nil {
231 log.Fatalf("instances door-key revoke: %v", err)
232 }
233 fmt.Fprintf(stdout, "%s %s\n", colorFor(stdout).ok("revoked"), revoked)
234}
235