package web import ( "errors" "log/slog" "net/http" "time" "tug/internal/platform/sshkey" "tug/internal/transport/web/text" ) const ( SSHKeyNewPath = "/ssh-keys/new" sshKeyRemovePath = "/ssh-keys/{key}/remove" ) var sshKeyRefusals = []struct { err error key text.Key }{ {sshkey.ErrNotOneLine, text.SSHKeyProblemNotOneLine}, {sshkey.ErrNotAKey, text.SSHKeyProblemNotAKey}, {sshkey.ErrOptions, text.SSHKeyProblemOptions}, {sshkey.ErrCertificate, text.SSHKeyProblemCertificate}, {sshkey.ErrKeyType, text.SSHKeyProblemType}, {sshkey.ErrShortRSA, text.SSHKeyProblemShortRSA}, {sshkey.ErrLabel, text.SSHKeyProblemLabel}, {sshkey.ErrOwned, text.SSHKeyProblemOwned}, {sshkey.ErrTaken, text.SSHKeyProblemTaken}, {sshkey.ErrLimit, text.SSHKeyProblemLimit}, } type SSHKey struct { ID string Fingerprint string Type string Label string AddedAt time.Time } type sshKeyRow struct { ID string Fingerprint Ident Type Ident Label string Added string } type sshKeyList struct { Rows []sshKeyRow Remove string Add string Full bool } type sshKeyForm struct { Action string Cancel string Key string Label string Message text.Key } func sshKeyRefusal(err error) (text.Key, bool) { for _, r := range sshKeyRefusals { if errors.Is(err, r.err) { return r.key, true } } return "", false } func sshKeyID(r *http.Request) (string, bool) { id := r.PathValue("key") return id, reAdminID.MatchString(id) } func newSSHKeyList(keys []SSHKey, remove, add string) sshKeyList { l := sshKeyList{Remove: remove, Add: add, Full: len(keys) >= sshkey.MaxPerAccount} for _, k := range keys { l.Rows = append(l.Rows, sshKeyRow{ID: k.ID, Fingerprint: Ident(k.Fingerprint), Type: Ident(k.Type), Label: k.Label, Added: stamp(k.AddedAt)}) } return l } func echoedSSHKeyForm(action, cancel, key, label string, message text.Key) sshKeyForm { return sshKeyForm{Action: action, Cancel: cancel, Key: echoed(key, echoSSHKeyBytes), Label: echoed(label, echoDisplayNameBytes), Message: message} } func findSSHKey(keys []SSHKey, id string) (SSHKey, bool) { for _, k := range keys { if k.ID == id { return k, true } } return SSHKey{}, false } func sshKeyRemoveConfirm(k SSHKey, action, cancel string) Confirm { return Confirm{Title: text.SSHKeyRemoveTitle, Body: text.SSHKeyRemoveBody, Button: text.SSHKeyRemoveButton, Subject: Ident(k.Fingerprint), Action: action, Cancel: cancel} } func (k *Kit) sshKeyDone(w http.ResponseWriter, r *http.Request, key text.Key, fingerprint, path string) { if err := k.SetFlash(w, key, Ident(fingerprint)); err != nil { slog.Error("ssh key flash not set", "err", err) } k.SeeOther(w, r, path) } func (a *portal) sshKeyRoutes() []Route { return []Route{ {Method: http.MethodGet, Path: SSHKeyNewPath, Handler: a.kit.RequireSudo(http.HandlerFunc(a.sshKeyNew))}, {Method: http.MethodPost, Path: SSHKeyNewPath, Handler: a.kit.RequireSudo(http.HandlerFunc(a.addSSHKey))}, {Method: http.MethodGet, Path: sshKeyRemovePath, Handler: http.HandlerFunc(a.sshKeyRemoveShow)}, {Method: http.MethodPost, Path: sshKeyRemovePath, Handler: http.HandlerFunc(a.removeSSHKey)}, } } func (a *portal) sshKeyNew(w http.ResponseWriter, r *http.Request) { if _, ok := a.kit.principal(w, r); ok { a.render(w, r, http.StatusOK, "account-ssh-key-new", sshKeyForm{Action: SSHKeyNewPath, Cancel: "/"}) } } func (a *portal) addSSHKey(w http.ResponseWriter, r *http.Request) { p, ok := a.kit.principal(w, r) if !ok { return } line, label := r.PostFormValue("key"), r.PostFormValue("label") added, err := a.b.AddSSHKey(r.Context(), p, line, label) if key, ok := sshKeyRefusal(err); ok { a.render(w, r, http.StatusOK, "account-ssh-key-new", echoedSSHKeyForm(SSHKeyNewPath, "/", line, label, key)) return } if err != nil { a.failed(w, r, "add ssh key", err) return } a.kit.sshKeyDone(w, r, text.FlashSSHKeyAdded, added.Fingerprint, "/") } func (a *portal) sshKeyRemoveShow(w http.ResponseWriter, r *http.Request) { p, ok := a.kit.principal(w, r) if !ok { return } keyID, ok := sshKeyID(r) if !ok { a.kit.ErrorPage(w, r, http.StatusNotFound) return } keys, err := a.b.SSHKeys(r.Context(), p) if err != nil { a.failed(w, r, "ssh keys", err) return } k, ok := findSSHKey(keys, keyID) if !ok { a.kit.ErrorPage(w, r, http.StatusNotFound) return } a.kit.RenderConfirm(w, r, sshKeyRemoveConfirm(k, "/ssh-keys/"+k.ID+"/remove", "/")) } func (a *portal) removeSSHKey(w http.ResponseWriter, r *http.Request) { p, ok := a.kit.principal(w, r) if !ok { return } keyID, ok := sshKeyID(r) if !ok { a.kit.ErrorPage(w, r, http.StatusNotFound) return } removed, err := a.b.RemoveSSHKey(r.Context(), p, keyID) if err != nil { a.failed(w, r, "remove ssh key", err) return } a.kit.sshKeyDone(w, r, text.FlashSSHKeyRemoved, removed.Fingerprint, "/") } func (a *admin) sshKeyRoutes(sudo func(method, path string, h http.HandlerFunc) Route) []Route { return []Route{ sudo(http.MethodGet, "/users/{id}"+SSHKeyNewPath, a.sshKeyNew), sudo(http.MethodPost, "/users/{id}"+SSHKeyNewPath, a.addSSHKey), sudo(http.MethodGet, "/users/{id}"+sshKeyRemovePath, a.sshKeyRemoveShow), sudo(http.MethodPost, "/users/{id}"+sshKeyRemovePath, a.removeSSHKey), } } type adminSSHKeyNewData struct { Viewer Ident ID Ident Username Ident sshKeyForm } func (a *admin) renderSSHKeyNew(w http.ResponseWriter, r *http.Request, p Principal, id string, u AdminUser, f sshKeyForm) { a.render(w, r, http.StatusOK, "admin-user-ssh-key-new", adminSSHKeyNewData{Viewer: p.Username, ID: Ident(id), Username: Ident(u.Username), sshKeyForm: f}) } func (a *admin) sshKeyNew(w http.ResponseWriter, r *http.Request) { if p, id, u, ok := a.loadUser(w, r); ok { a.renderSSHKeyNew(w, r, p, id, u, sshKeyForm{Action: "/users/" + id + SSHKeyNewPath, Cancel: "/users/" + id}) } } func (a *admin) addSSHKey(w http.ResponseWriter, r *http.Request) { p, id, u, ok := a.loadUser(w, r) if !ok { return } line, label := r.PostFormValue("key"), r.PostFormValue("label") added, err := a.b.AddSSHKey(r.Context(), p.UserID, id, line, label) key, refused := sshKeyRefusal(err) if !refused { key, refused = adminProblemKey(err) } if refused { a.renderSSHKeyNew(w, r, p, id, u, echoedSSHKeyForm("/users/"+id+SSHKeyNewPath, "/users/"+id, line, label, key)) return } if err != nil { a.failed(w, r, "add ssh key", err) return } a.kit.sshKeyDone(w, r, text.FlashSSHKeyAdded, added.Fingerprint, "/users/"+id) } func (a *admin) sshKeyRemoveShow(w http.ResponseWriter, r *http.Request) { _, id, u, ok := a.loadUser(w, r) if !ok { return } keyID, ok := sshKeyID(r) if !ok { a.kit.ErrorPage(w, r, http.StatusNotFound) return } k, ok := findSSHKey(u.SSHKeys, keyID) if !ok { a.kit.ErrorPage(w, r, http.StatusNotFound) return } a.kit.RenderConfirm(w, r, sshKeyRemoveConfirm(k, "/users/"+id+"/ssh-keys/"+k.ID+"/remove", "/users/"+id)) } func (a *admin) removeSSHKey(w http.ResponseWriter, r *http.Request) { p, ok := a.kit.principal(w, r) if !ok { return } id, ok := a.id(w, r) if !ok { return } keyID, ok := sshKeyID(r) if !ok { a.kit.ErrorPage(w, r, http.StatusNotFound) return } removed, err := a.b.RemoveSSHKey(r.Context(), p.UserID, id, keyID) if err != nil { a.failed(w, r, "remove ssh key", err) return } a.kit.sshKeyDone(w, r, text.FlashSSHKeyRemoved, removed.Fingerprint, "/users/"+id) }