Checkpoint: grpc auth fix, issuer list/detail, v2 protos, architecture docs
Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
@@ -12,6 +12,17 @@ import (
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// splitLines splits a newline-delimited string into non-empty trimmed lines.
|
||||
func splitLines(s string) []interface{} {
|
||||
var out []interface{}
|
||||
for _, line := range strings.Split(s, "\n") {
|
||||
if v := strings.TrimSpace(line); v != "" {
|
||||
out = append(out, v)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (ws *WebServer) registerRoutes(r chi.Router) {
|
||||
r.Handle("/static/*", http.StripPrefix("/static/", http.FileServer(http.FS(ws.staticFS))))
|
||||
|
||||
@@ -26,6 +37,8 @@ func (ws *WebServer) registerRoutes(r chi.Router) {
|
||||
r.Get("/", ws.requireAuth(ws.handlePKI))
|
||||
r.Post("/import-root", ws.requireAuth(ws.handleImportRoot))
|
||||
r.Post("/create-issuer", ws.requireAuth(ws.handleCreateIssuer))
|
||||
r.Post("/issue", ws.requireAuth(ws.handleIssueCert))
|
||||
r.Get("/issuer/{issuer}", ws.requireAuth(ws.handleIssuerDetail))
|
||||
r.Get("/{issuer}", ws.requireAuth(ws.handlePKIIssuer))
|
||||
})
|
||||
}
|
||||
@@ -394,7 +407,159 @@ func (ws *WebServer) handlePKIIssuer(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write(certPEM) //nolint:gosec
|
||||
}
|
||||
|
||||
func (ws *WebServer) handleIssuerDetail(w http.ResponseWriter, r *http.Request) {
|
||||
info := tokenInfoFromContext(r.Context())
|
||||
token := extractCookie(r)
|
||||
|
||||
mountName, err := ws.findCAMount(r, token)
|
||||
if err != nil {
|
||||
http.Error(w, "no CA engine mounted", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
issuerName := chi.URLParam(r, "issuer")
|
||||
|
||||
resp, err := ws.vault.EngineRequest(r.Context(), token, mountName, "list-certs", nil)
|
||||
if err != nil {
|
||||
http.Error(w, "failed to list certificates", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
nameFilter := strings.ToLower(r.URL.Query().Get("name"))
|
||||
sortBy := r.URL.Query().Get("sort")
|
||||
if sortBy == "" {
|
||||
sortBy = "cn"
|
||||
}
|
||||
|
||||
var certs []map[string]interface{}
|
||||
if raw, ok := resp["certs"]; ok {
|
||||
if list, ok := raw.([]interface{}); ok {
|
||||
for _, item := range list {
|
||||
if m, ok := item.(map[string]interface{}); ok {
|
||||
issuer, _ := m["issuer"].(string)
|
||||
if issuer != issuerName {
|
||||
continue
|
||||
}
|
||||
if nameFilter != "" {
|
||||
cn, _ := m["cn"].(string)
|
||||
if !strings.Contains(strings.ToLower(cn), nameFilter) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
certs = append(certs, m)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort: by expiry date or by common name (default).
|
||||
if sortBy == "expiry" {
|
||||
for i := 1; i < len(certs); i++ {
|
||||
for j := i; j > 0; j-- {
|
||||
a, _ := certs[j-1]["expires_at"].(string)
|
||||
b, _ := certs[j]["expires_at"].(string)
|
||||
if a > b {
|
||||
certs[j-1], certs[j] = certs[j], certs[j-1]
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for i := 1; i < len(certs); i++ {
|
||||
for j := i; j > 0; j-- {
|
||||
a, _ := certs[j-1]["cn"].(string)
|
||||
b, _ := certs[j]["cn"].(string)
|
||||
if strings.ToLower(a) > strings.ToLower(b) {
|
||||
certs[j-1], certs[j] = certs[j], certs[j-1]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"Username": info.Username,
|
||||
"IsAdmin": info.IsAdmin,
|
||||
"MountName": mountName,
|
||||
"IssuerName": issuerName,
|
||||
"Certs": certs,
|
||||
"NameFilter": r.URL.Query().Get("name"),
|
||||
"SortBy": sortBy,
|
||||
}
|
||||
|
||||
ws.renderTemplate(w, "issuer_detail.html", data)
|
||||
}
|
||||
|
||||
func (ws *WebServer) handleIssueCert(w http.ResponseWriter, r *http.Request) {
|
||||
info := tokenInfoFromContext(r.Context())
|
||||
token := extractCookie(r)
|
||||
|
||||
mountName, err := ws.findCAMount(r, token)
|
||||
if err != nil {
|
||||
http.Error(w, "no CA engine mounted", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
|
||||
_ = r.ParseForm()
|
||||
|
||||
commonName := r.FormValue("common_name")
|
||||
if commonName == "" {
|
||||
ws.renderPKIWithError(w, r, mountName, info, "Common name is required")
|
||||
return
|
||||
}
|
||||
issuer := r.FormValue("issuer")
|
||||
if issuer == "" {
|
||||
ws.renderPKIWithError(w, r, mountName, info, "Issuer is required")
|
||||
return
|
||||
}
|
||||
|
||||
reqData := map[string]interface{}{
|
||||
"common_name": commonName,
|
||||
"issuer": issuer,
|
||||
}
|
||||
if v := r.FormValue("profile"); v != "" {
|
||||
reqData["profile"] = v
|
||||
}
|
||||
if v := r.FormValue("ttl"); v != "" {
|
||||
reqData["ttl"] = v
|
||||
}
|
||||
if lines := splitLines(r.FormValue("dns_names")); len(lines) > 0 {
|
||||
reqData["dns_names"] = lines
|
||||
}
|
||||
if lines := splitLines(r.FormValue("ip_addresses")); len(lines) > 0 {
|
||||
reqData["ip_addresses"] = lines
|
||||
}
|
||||
|
||||
resp, err := ws.vault.EngineRequest(r.Context(), token, mountName, "issue", reqData)
|
||||
if err != nil {
|
||||
ws.renderPKIWithError(w, r, mountName, info, grpcMessage(err))
|
||||
return
|
||||
}
|
||||
|
||||
// Re-render the PKI page with the issued certificate displayed.
|
||||
data := map[string]interface{}{
|
||||
"Username": info.Username,
|
||||
"IsAdmin": info.IsAdmin,
|
||||
"MountName": mountName,
|
||||
"IssuedCert": resp,
|
||||
}
|
||||
if rootPEM, err := ws.vault.GetRootCert(r.Context(), mountName); err == nil && len(rootPEM) > 0 {
|
||||
if cert, err := parsePEMCert(rootPEM); err == nil {
|
||||
data["RootCN"] = cert.Subject.CommonName
|
||||
data["RootOrg"] = strings.Join(cert.Subject.Organization, ", ")
|
||||
data["RootNotBefore"] = cert.NotBefore.Format(time.RFC3339)
|
||||
data["RootNotAfter"] = cert.NotAfter.Format(time.RFC3339)
|
||||
data["RootExpired"] = time.Now().After(cert.NotAfter)
|
||||
data["HasRoot"] = true
|
||||
}
|
||||
}
|
||||
if issuerResp, err := ws.vault.EngineRequest(r.Context(), token, mountName, "list-issuers", nil); err == nil {
|
||||
data["Issuers"] = issuerResp["issuers"]
|
||||
}
|
||||
ws.renderTemplate(w, "pki.html", data)
|
||||
}
|
||||
|
||||
func (ws *WebServer) renderPKIWithError(w http.ResponseWriter, r *http.Request, mountName string, info *TokenInfo, errMsg string) {
|
||||
token := extractCookie(r)
|
||||
data := map[string]interface{}{
|
||||
"Username": info.Username,
|
||||
"IsAdmin": info.IsAdmin,
|
||||
@@ -412,6 +577,9 @@ func (ws *WebServer) renderPKIWithError(w http.ResponseWriter, r *http.Request,
|
||||
data["HasRoot"] = true
|
||||
}
|
||||
}
|
||||
if resp, err := ws.vault.EngineRequest(r.Context(), token, mountName, "list-issuers", nil); err == nil {
|
||||
data["Issuers"] = resp["issuers"]
|
||||
}
|
||||
|
||||
ws.renderTemplate(w, "pki.html", data)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user