package webserver import ( "net/http" "strings" "github.com/go-chi/chi/v5" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) func (ws *WebServer) handleSSHCA(w http.ResponseWriter, r *http.Request) { info := tokenInfoFromContext(r.Context()) token := extractCookie(r) mountName, err := ws.findSSHCAMount(r, token) if err != nil { http.Redirect(w, r, "/dashboard", http.StatusFound) return } data := ws.baseData(r, info) data["MountName"] = mountName if pubkey, err := ws.vault.GetSSHCAPublicKey(r.Context(), mountName); err == nil { data["CAPublicKey"] = pubkey.PublicKey } if profiles, err := ws.vault.ListSSHCAProfiles(r.Context(), token, mountName); err == nil { data["Profiles"] = profiles } if certs, err := ws.vault.ListSSHCACerts(r.Context(), token, mountName); err == nil { for i := range certs { certs[i].IssuedBy = ws.resolveUser(certs[i].IssuedBy) } data["Certs"] = certs } ws.renderTemplate(w, "sshca.html", data) } func (ws *WebServer) handleSSHCASignUser(w http.ResponseWriter, r *http.Request) { info := tokenInfoFromContext(r.Context()) token := extractCookie(r) mountName, err := ws.findSSHCAMount(r, token) if err != nil { http.Error(w, "no SSH CA engine mounted", http.StatusNotFound) return } r.Body = http.MaxBytesReader(w, r.Body, 1<<20) _ = r.ParseForm() publicKey := strings.TrimSpace(r.FormValue("public_key")) if publicKey == "" { ws.renderSSHCAWithError(w, r, mountName, info, "Public key is required") return } var principals []string for _, line := range strings.Split(r.FormValue("principals"), "\n") { if v := strings.TrimSpace(line); v != "" { principals = append(principals, v) } } req := SSHCASignRequest{ PublicKey: publicKey, Principals: principals, Profile: r.FormValue("profile"), TTL: r.FormValue("ttl"), } result, err := ws.vault.SSHCASignUser(r.Context(), token, mountName, req) if err != nil { ws.renderSSHCAWithError(w, r, mountName, info, grpcMessage(err)) return } ws.renderSSHCAWithResult(w, r, mountName, info, "SignUserResult", result) } func (ws *WebServer) handleSSHCASignHost(w http.ResponseWriter, r *http.Request) { info := tokenInfoFromContext(r.Context()) token := extractCookie(r) mountName, err := ws.findSSHCAMount(r, token) if err != nil { http.Error(w, "no SSH CA engine mounted", http.StatusNotFound) return } r.Body = http.MaxBytesReader(w, r.Body, 1<<20) _ = r.ParseForm() publicKey := strings.TrimSpace(r.FormValue("public_key")) if publicKey == "" { ws.renderSSHCAWithError(w, r, mountName, info, "Public key is required") return } hostname := strings.TrimSpace(r.FormValue("hostname")) if hostname == "" { ws.renderSSHCAWithError(w, r, mountName, info, "Hostname is required") return } req := SSHCASignRequest{ PublicKey: publicKey, Principals: []string{hostname}, TTL: r.FormValue("ttl"), } result, err := ws.vault.SSHCASignHost(r.Context(), token, mountName, req) if err != nil { ws.renderSSHCAWithError(w, r, mountName, info, grpcMessage(err)) return } ws.renderSSHCAWithResult(w, r, mountName, info, "SignHostResult", result) } func (ws *WebServer) handleSSHCACertDetail(w http.ResponseWriter, r *http.Request) { info := tokenInfoFromContext(r.Context()) token := extractCookie(r) mountName, err := ws.findSSHCAMount(r, token) if err != nil { http.Error(w, "no SSH CA engine mounted", http.StatusNotFound) return } serial := chi.URLParam(r, "serial") cert, err := ws.vault.GetSSHCACert(r.Context(), token, mountName, serial) if err != nil { st, _ := status.FromError(err) if st.Code() == codes.NotFound { http.Error(w, "certificate not found", http.StatusNotFound) return } http.Error(w, grpcMessage(err), http.StatusInternalServerError) return } cert.IssuedBy = ws.resolveUser(cert.IssuedBy) cert.RevokedBy = ws.resolveUser(cert.RevokedBy) data := ws.baseData(r, info) data["MountName"] = mountName data["Cert"] = cert ws.renderTemplate(w, "sshca_cert_detail.html", data) } func (ws *WebServer) handleSSHCACertRevoke(w http.ResponseWriter, r *http.Request) { info := tokenInfoFromContext(r.Context()) if !info.IsAdmin { http.Error(w, "forbidden", http.StatusForbidden) return } token := extractCookie(r) mountName, err := ws.findSSHCAMount(r, token) if err != nil { http.Error(w, "no SSH CA engine mounted", http.StatusNotFound) return } serial := chi.URLParam(r, "serial") if err := ws.vault.RevokeSSHCACert(r.Context(), token, mountName, serial); err != nil { http.Error(w, grpcMessage(err), http.StatusInternalServerError) return } http.Redirect(w, r, "/sshca/cert/"+serial, http.StatusSeeOther) } func (ws *WebServer) handleSSHCACertDelete(w http.ResponseWriter, r *http.Request) { info := tokenInfoFromContext(r.Context()) if !info.IsAdmin { http.Error(w, "forbidden", http.StatusForbidden) return } token := extractCookie(r) mountName, err := ws.findSSHCAMount(r, token) if err != nil { http.Error(w, "no SSH CA engine mounted", http.StatusNotFound) return } serial := chi.URLParam(r, "serial") if err := ws.vault.DeleteSSHCACert(r.Context(), token, mountName, serial); err != nil { http.Error(w, grpcMessage(err), http.StatusInternalServerError) return } http.Redirect(w, r, "/sshca", http.StatusSeeOther) } func (ws *WebServer) handleSSHCACreateProfile(w http.ResponseWriter, r *http.Request) { info := tokenInfoFromContext(r.Context()) if !info.IsAdmin { http.Error(w, "forbidden", http.StatusForbidden) return } token := extractCookie(r) mountName, err := ws.findSSHCAMount(r, token) if err != nil { http.Error(w, "no SSH CA engine mounted", http.StatusNotFound) return } r.Body = http.MaxBytesReader(w, r.Body, 1<<20) _ = r.ParseForm() name := strings.TrimSpace(r.FormValue("name")) if name == "" { ws.renderSSHCAWithError(w, r, mountName, info, "Profile name is required") return } var principals []string for _, line := range strings.Split(r.FormValue("allowed_principals"), "\n") { if v := strings.TrimSpace(line); v != "" { principals = append(principals, v) } } extensions := map[string]string{} for _, ext := range r.Form["extensions"] { extensions[ext] = "" } criticalOptions := map[string]string{} if v := strings.TrimSpace(r.FormValue("force_command")); v != "" { criticalOptions["force-command"] = v } if v := strings.TrimSpace(r.FormValue("source_address")); v != "" { criticalOptions["source-address"] = v } req := SSHCAProfileRequest{ Name: name, CriticalOptions: criticalOptions, Extensions: extensions, MaxTTL: r.FormValue("max_ttl"), AllowedPrincipals: principals, } if err := ws.vault.CreateSSHCAProfile(r.Context(), token, mountName, req); err != nil { ws.renderSSHCAWithError(w, r, mountName, info, grpcMessage(err)) return } http.Redirect(w, r, "/sshca/profile/"+name, http.StatusFound) } func (ws *WebServer) handleSSHCAProfileDetail(w http.ResponseWriter, r *http.Request) { info := tokenInfoFromContext(r.Context()) token := extractCookie(r) mountName, err := ws.findSSHCAMount(r, token) if err != nil { http.Error(w, "no SSH CA engine mounted", http.StatusNotFound) return } name := chi.URLParam(r, "name") profile, err := ws.vault.GetSSHCAProfile(r.Context(), token, mountName, name) if err != nil { st, _ := status.FromError(err) if st.Code() == codes.NotFound { http.Error(w, "profile not found", http.StatusNotFound) return } http.Error(w, grpcMessage(err), http.StatusInternalServerError) return } data := ws.baseData(r, info) data["MountName"] = mountName data["Profile"] = profile ws.renderTemplate(w, "sshca_profile_detail.html", data) } func (ws *WebServer) handleSSHCAUpdateProfile(w http.ResponseWriter, r *http.Request) { info := tokenInfoFromContext(r.Context()) if !info.IsAdmin { http.Error(w, "forbidden", http.StatusForbidden) return } token := extractCookie(r) mountName, err := ws.findSSHCAMount(r, token) if err != nil { http.Error(w, "no SSH CA engine mounted", http.StatusNotFound) return } name := chi.URLParam(r, "name") r.Body = http.MaxBytesReader(w, r.Body, 1<<20) _ = r.ParseForm() var principals []string for _, line := range strings.Split(r.FormValue("allowed_principals"), "\n") { if v := strings.TrimSpace(line); v != "" { principals = append(principals, v) } } extensions := map[string]string{} for _, ext := range r.Form["extensions"] { extensions[ext] = "" } criticalOptions := map[string]string{} if v := strings.TrimSpace(r.FormValue("force_command")); v != "" { criticalOptions["force-command"] = v } if v := strings.TrimSpace(r.FormValue("source_address")); v != "" { criticalOptions["source-address"] = v } req := SSHCAProfileRequest{ CriticalOptions: criticalOptions, Extensions: extensions, MaxTTL: r.FormValue("max_ttl"), AllowedPrincipals: principals, } if err := ws.vault.UpdateSSHCAProfile(r.Context(), token, mountName, name, req); err != nil { http.Error(w, grpcMessage(err), http.StatusInternalServerError) return } http.Redirect(w, r, "/sshca/profile/"+name, http.StatusSeeOther) } func (ws *WebServer) handleSSHCADeleteProfile(w http.ResponseWriter, r *http.Request) { info := tokenInfoFromContext(r.Context()) if !info.IsAdmin { http.Error(w, "forbidden", http.StatusForbidden) return } token := extractCookie(r) mountName, err := ws.findSSHCAMount(r, token) if err != nil { http.Error(w, "no SSH CA engine mounted", http.StatusNotFound) return } name := chi.URLParam(r, "name") if err := ws.vault.DeleteSSHCAProfile(r.Context(), token, mountName, name); err != nil { http.Error(w, grpcMessage(err), http.StatusInternalServerError) return } http.Redirect(w, r, "/sshca", http.StatusSeeOther) } func (ws *WebServer) renderSSHCAWithError(w http.ResponseWriter, r *http.Request, mountName string, info *TokenInfo, errMsg string) { token := extractCookie(r) data := ws.baseData(r, info) data["MountName"] = mountName data["Error"] = errMsg if pubkey, err := ws.vault.GetSSHCAPublicKey(r.Context(), mountName); err == nil { data["CAPublicKey"] = pubkey.PublicKey } if profiles, err := ws.vault.ListSSHCAProfiles(r.Context(), token, mountName); err == nil { data["Profiles"] = profiles } if certs, err := ws.vault.ListSSHCACerts(r.Context(), token, mountName); err == nil { data["Certs"] = certs } ws.renderTemplate(w, "sshca.html", data) } func (ws *WebServer) renderSSHCAWithResult(w http.ResponseWriter, r *http.Request, mountName string, info *TokenInfo, resultKey string, result *SSHCASignResult) { token := extractCookie(r) data := ws.baseData(r, info) data["MountName"] = mountName data[resultKey] = result if pubkey, err := ws.vault.GetSSHCAPublicKey(r.Context(), mountName); err == nil { data["CAPublicKey"] = pubkey.PublicKey } if profiles, err := ws.vault.ListSSHCAProfiles(r.Context(), token, mountName); err == nil { data["Profiles"] = profiles } if certs, err := ws.vault.ListSSHCACerts(r.Context(), token, mountName); err == nil { data["Certs"] = certs } ws.renderTemplate(w, "sshca.html", data) }