-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathadmin.go
More file actions
130 lines (126 loc) · 3.67 KB
/
Copy pathadmin.go
File metadata and controls
130 lines (126 loc) · 3.67 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
package main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
"wanctl/internal/config"
)
// cmdAdmin manages admission invites against the relay's admin API. It is an
// operator tool: it authenticates with WANCTL_ADMIN_SECRET, the same secret
// the portal holds, so it works before any user exists — which is exactly when
// invites are needed.
func cmdAdmin(args []string) error {
if len(args) < 1 {
return fmt.Errorf("usage: wanctl admin invite [--github LOGIN] | invites | invite-revoke ID")
}
switch args[0] {
case "invite":
fs := flag.NewFlagSet("admin invite", flag.ExitOnError)
github := fs.String("github", "", "pre-approve this GitHub login instead of minting a code")
fs.Parse(args[1:])
body := map[string]string{}
if *github != "" {
body["github_login"] = *github
}
var out struct {
ID int `json:"id"`
Code string `json:"code"`
GitHubLogin string `json:"github_login"`
}
if err := adminCall("POST", "/admin/invites", body, &out); err != nil {
return err
}
if out.Code != "" {
fmt.Printf("invite #%d created — share this code (shown only once):\n%s\n", out.ID, out.Code)
} else {
fmt.Printf("invite #%d created — GitHub user %q can now sign in\n", out.ID, out.GitHubLogin)
}
return nil
case "invites":
var invites []struct {
ID int `json:"id"`
GitHubLogin string `json:"github_login"`
CreatedAt time.Time `json:"created_at"`
UsedAt *time.Time `json:"used_at"`
UsedByNamespace string `json:"used_by_namespace"`
HasCode bool `json:"has_code"`
}
if err := adminCall("GET", "/admin/invites", nil, &invites); err != nil {
return err
}
if len(invites) == 0 {
fmt.Println("no invites")
return nil
}
for _, in := range invites {
kind := "code"
if in.GitHubLogin != "" {
kind = "github:" + in.GitHubLogin
}
state := "unused"
if in.UsedAt != nil {
state = "used by " + in.UsedByNamespace + " at " + in.UsedAt.Format(time.RFC3339)
}
fmt.Printf("#%-4d %-30s %s (created %s)\n", in.ID, kind, state, in.CreatedAt.Format(time.RFC3339))
}
return nil
case "invite-revoke":
if len(args) != 2 {
return fmt.Errorf("usage: wanctl admin invite-revoke ID")
}
id, err := strconv.Atoi(args[1])
if err != nil {
return fmt.Errorf("invite id must be a number, got %q", args[1])
}
if err := adminCall("POST", "/admin/invites/revoke", map[string]int{"id": id}, nil); err != nil {
return err
}
fmt.Printf("invite #%d revoked\n", id)
return nil
default:
return fmt.Errorf("unknown admin subcommand %q (want invite | invites | invite-revoke)", args[0])
}
}
func adminCall(method, path string, body, out any) error {
relay, err := config.Relay()
if err != nil {
return err
}
secret := os.Getenv("WANCTL_ADMIN_SECRET")
if secret == "" {
return fmt.Errorf("WANCTL_ADMIN_SECRET is not set (the relay's admin secret)")
}
var rdr io.Reader
if body != nil {
b, _ := json.Marshal(body)
rdr = bytes.NewReader(b)
}
req, err := http.NewRequest(method, strings.TrimRight(relay, "/")+path, rdr)
if err != nil {
return err
}
req.Header.Set("X-Admin-Secret", secret)
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
resp, err := (&http.Client{Timeout: 15 * time.Second}).Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
b, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
return fmt.Errorf("relay admin %s %s: status %d: %s", method, path, resp.StatusCode, strings.TrimSpace(string(b)))
}
if out != nil {
return json.NewDecoder(resp.Body).Decode(out)
}
return nil
}