Raw
  1package web
  2
  3import (
  4	"errors"
  5	"log/slog"
  6	"net/http"
  7	"time"
  8
  9	"tug/internal/platform/sshkey"
 10	"tug/internal/transport/web/text"
 11)
 12
 13const (
 14	SSHKeyNewPath    = "/ssh-keys/new"
 15	sshKeyRemovePath = "/ssh-keys/{key}/remove"
 16)
 17
 18var sshKeyRefusals = []struct {
 19	err error
 20	key text.Key
 21}{
 22	{sshkey.ErrNotOneLine, text.SSHKeyProblemNotOneLine},
 23	{sshkey.ErrNotAKey, text.SSHKeyProblemNotAKey},
 24	{sshkey.ErrOptions, text.SSHKeyProblemOptions},
 25	{sshkey.ErrCertificate, text.SSHKeyProblemCertificate},
 26	{sshkey.ErrKeyType, text.SSHKeyProblemType},
 27	{sshkey.ErrShortRSA, text.SSHKeyProblemShortRSA},
 28	{sshkey.ErrLabel, text.SSHKeyProblemLabel},
 29	{sshkey.ErrOwned, text.SSHKeyProblemOwned},
 30	{sshkey.ErrTaken, text.SSHKeyProblemTaken},
 31	{sshkey.ErrLimit, text.SSHKeyProblemLimit},
 32}
 33
 34type SSHKey struct {
 35	ID          string
 36	Fingerprint string
 37	Type        string
 38	Label       string
 39	AddedAt     time.Time
 40}
 41
 42type sshKeyRow struct {
 43	ID          string
 44	Fingerprint Ident
 45	Type        Ident
 46	Label       string
 47	Added       string
 48}
 49
 50type sshKeyList struct {
 51	Rows   []sshKeyRow
 52	Remove string
 53	Add    string
 54	Full   bool
 55}
 56
 57type sshKeyForm struct {
 58	Action  string
 59	Cancel  string
 60	Key     string
 61	Label   string
 62	Message text.Key
 63}
 64
 65func sshKeyRefusal(err error) (text.Key, bool) {
 66	for _, r := range sshKeyRefusals {
 67		if errors.Is(err, r.err) {
 68			return r.key, true
 69		}
 70	}
 71	return "", false
 72}
 73
 74func sshKeyID(r *http.Request) (string, bool) {
 75	id := r.PathValue("key")
 76	return id, reAdminID.MatchString(id)
 77}
 78
 79func newSSHKeyList(keys []SSHKey, remove, add string) sshKeyList {
 80	l := sshKeyList{Remove: remove, Add: add, Full: len(keys) >= sshkey.MaxPerAccount}
 81	for _, k := range keys {
 82		l.Rows = append(l.Rows, sshKeyRow{ID: k.ID, Fingerprint: Ident(k.Fingerprint), Type: Ident(k.Type), Label: k.Label, Added: stamp(k.AddedAt)})
 83	}
 84	return l
 85}
 86
 87func echoedSSHKeyForm(action, cancel, key, label string, message text.Key) sshKeyForm {
 88	return sshKeyForm{Action: action, Cancel: cancel, Key: echoed(key, echoSSHKeyBytes), Label: echoed(label, echoDisplayNameBytes), Message: message}
 89}
 90
 91func findSSHKey(keys []SSHKey, id string) (SSHKey, bool) {
 92	for _, k := range keys {
 93		if k.ID == id {
 94			return k, true
 95		}
 96	}
 97	return SSHKey{}, false
 98}
 99
100func sshKeyRemoveConfirm(k SSHKey, action, cancel string) Confirm {
101	return Confirm{Title: text.SSHKeyRemoveTitle, Body: text.SSHKeyRemoveBody, Button: text.SSHKeyRemoveButton, Subject: Ident(k.Fingerprint), Action: action, Cancel: cancel}
102}
103
104func (k *Kit) sshKeyDone(w http.ResponseWriter, r *http.Request, key text.Key, fingerprint, path string) {
105	if err := k.SetFlash(w, key, Ident(fingerprint)); err != nil {
106		slog.Error("ssh key flash not set", "err", err)
107	}
108	k.SeeOther(w, r, path)
109}
110
111func (a *portal) sshKeyRoutes() []Route {
112	return []Route{
113		{Method: http.MethodGet, Path: SSHKeyNewPath, Handler: a.kit.RequireSudo(http.HandlerFunc(a.sshKeyNew))},
114		{Method: http.MethodPost, Path: SSHKeyNewPath, Handler: a.kit.RequireSudo(http.HandlerFunc(a.addSSHKey))},
115		{Method: http.MethodGet, Path: sshKeyRemovePath, Handler: http.HandlerFunc(a.sshKeyRemoveShow)},
116		{Method: http.MethodPost, Path: sshKeyRemovePath, Handler: http.HandlerFunc(a.removeSSHKey)},
117	}
118}
119
120func (a *portal) sshKeyNew(w http.ResponseWriter, r *http.Request) {
121	if _, ok := a.kit.principal(w, r); ok {
122		a.render(w, r, http.StatusOK, "account-ssh-key-new", sshKeyForm{Action: SSHKeyNewPath, Cancel: "/"})
123	}
124}
125
126func (a *portal) addSSHKey(w http.ResponseWriter, r *http.Request) {
127	p, ok := a.kit.principal(w, r)
128	if !ok {
129		return
130	}
131	line, label := r.PostFormValue("key"), r.PostFormValue("label")
132	added, err := a.b.AddSSHKey(r.Context(), p, line, label)
133	if key, ok := sshKeyRefusal(err); ok {
134		a.render(w, r, http.StatusOK, "account-ssh-key-new", echoedSSHKeyForm(SSHKeyNewPath, "/", line, label, key))
135		return
136	}
137	if err != nil {
138		a.failed(w, r, "add ssh key", err)
139		return
140	}
141	a.kit.sshKeyDone(w, r, text.FlashSSHKeyAdded, added.Fingerprint, "/")
142}
143
144func (a *portal) sshKeyRemoveShow(w http.ResponseWriter, r *http.Request) {
145	p, ok := a.kit.principal(w, r)
146	if !ok {
147		return
148	}
149	keyID, ok := sshKeyID(r)
150	if !ok {
151		a.kit.ErrorPage(w, r, http.StatusNotFound)
152		return
153	}
154	keys, err := a.b.SSHKeys(r.Context(), p)
155	if err != nil {
156		a.failed(w, r, "ssh keys", err)
157		return
158	}
159	k, ok := findSSHKey(keys, keyID)
160	if !ok {
161		a.kit.ErrorPage(w, r, http.StatusNotFound)
162		return
163	}
164	a.kit.RenderConfirm(w, r, sshKeyRemoveConfirm(k, "/ssh-keys/"+k.ID+"/remove", "/"))
165}
166
167func (a *portal) removeSSHKey(w http.ResponseWriter, r *http.Request) {
168	p, ok := a.kit.principal(w, r)
169	if !ok {
170		return
171	}
172	keyID, ok := sshKeyID(r)
173	if !ok {
174		a.kit.ErrorPage(w, r, http.StatusNotFound)
175		return
176	}
177	removed, err := a.b.RemoveSSHKey(r.Context(), p, keyID)
178	if err != nil {
179		a.failed(w, r, "remove ssh key", err)
180		return
181	}
182	a.kit.sshKeyDone(w, r, text.FlashSSHKeyRemoved, removed.Fingerprint, "/")
183}
184
185func (a *admin) sshKeyRoutes(sudo func(method, path string, h http.HandlerFunc) Route) []Route {
186	return []Route{
187		sudo(http.MethodGet, "/users/{id}"+SSHKeyNewPath, a.sshKeyNew),
188		sudo(http.MethodPost, "/users/{id}"+SSHKeyNewPath, a.addSSHKey),
189		sudo(http.MethodGet, "/users/{id}"+sshKeyRemovePath, a.sshKeyRemoveShow),
190		sudo(http.MethodPost, "/users/{id}"+sshKeyRemovePath, a.removeSSHKey),
191	}
192}
193
194type adminSSHKeyNewData struct {
195	Viewer   Ident
196	ID       Ident
197	Username Ident
198	sshKeyForm
199}
200
201func (a *admin) renderSSHKeyNew(w http.ResponseWriter, r *http.Request, p Principal, id string, u AdminUser, f sshKeyForm) {
202	a.render(w, r, http.StatusOK, "admin-user-ssh-key-new", adminSSHKeyNewData{Viewer: p.Username, ID: Ident(id), Username: Ident(u.Username), sshKeyForm: f})
203}
204
205func (a *admin) sshKeyNew(w http.ResponseWriter, r *http.Request) {
206	if p, id, u, ok := a.loadUser(w, r); ok {
207		a.renderSSHKeyNew(w, r, p, id, u, sshKeyForm{Action: "/users/" + id + SSHKeyNewPath, Cancel: "/users/" + id})
208	}
209}
210
211func (a *admin) addSSHKey(w http.ResponseWriter, r *http.Request) {
212	p, id, u, ok := a.loadUser(w, r)
213	if !ok {
214		return
215	}
216	line, label := r.PostFormValue("key"), r.PostFormValue("label")
217	added, err := a.b.AddSSHKey(r.Context(), p.UserID, id, line, label)
218	key, refused := sshKeyRefusal(err)
219	if !refused {
220		key, refused = adminProblemKey(err)
221	}
222	if refused {
223		a.renderSSHKeyNew(w, r, p, id, u, echoedSSHKeyForm("/users/"+id+SSHKeyNewPath, "/users/"+id, line, label, key))
224		return
225	}
226	if err != nil {
227		a.failed(w, r, "add ssh key", err)
228		return
229	}
230	a.kit.sshKeyDone(w, r, text.FlashSSHKeyAdded, added.Fingerprint, "/users/"+id)
231}
232
233func (a *admin) sshKeyRemoveShow(w http.ResponseWriter, r *http.Request) {
234	_, id, u, ok := a.loadUser(w, r)
235	if !ok {
236		return
237	}
238	keyID, ok := sshKeyID(r)
239	if !ok {
240		a.kit.ErrorPage(w, r, http.StatusNotFound)
241		return
242	}
243	k, ok := findSSHKey(u.SSHKeys, keyID)
244	if !ok {
245		a.kit.ErrorPage(w, r, http.StatusNotFound)
246		return
247	}
248	a.kit.RenderConfirm(w, r, sshKeyRemoveConfirm(k, "/users/"+id+"/ssh-keys/"+k.ID+"/remove", "/users/"+id))
249}
250
251func (a *admin) removeSSHKey(w http.ResponseWriter, r *http.Request) {
252	p, ok := a.kit.principal(w, r)
253	if !ok {
254		return
255	}
256	id, ok := a.id(w, r)
257	if !ok {
258		return
259	}
260	keyID, ok := sshKeyID(r)
261	if !ok {
262		a.kit.ErrorPage(w, r, http.StatusNotFound)
263		return
264	}
265	removed, err := a.b.RemoveSSHKey(r.Context(), p.UserID, id, keyID)
266	if err != nil {
267		a.failed(w, r, "remove ssh key", err)
268		return
269	}
270	a.kit.sshKeyDone(w, r, text.FlashSSHKeyRemoved, removed.Fingerprint, "/users/"+id)
271}