Checkpoint: grpc auth fix, issuer list/detail, v2 protos, architecture docs
Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
@@ -412,7 +412,7 @@ func (e *CAEngine) handleImportRoot(ctx context.Context, req *engine.Request) (*
|
||||
return &engine.Response{
|
||||
Data: map[string]interface{}{
|
||||
"cn": newCert.Subject.CommonName,
|
||||
"expires_at": newCert.NotAfter,
|
||||
"expires_at": newCert.NotAfter.Format(time.RFC3339),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -639,7 +639,7 @@ func (e *CAEngine) handleListIssuers(_ context.Context, req *engine.Request) (*e
|
||||
return nil, ErrSealed
|
||||
}
|
||||
|
||||
names := make([]string, 0, len(e.issuers))
|
||||
names := make([]interface{}, 0, len(e.issuers))
|
||||
for name := range e.issuers {
|
||||
names = append(names, name)
|
||||
}
|
||||
@@ -795,7 +795,7 @@ func (e *CAEngine) handleIssue(ctx context.Context, req *engine.Request) (*engin
|
||||
"cn": cn,
|
||||
"sans": allSANs,
|
||||
"issued_by": req.CallerInfo.Username,
|
||||
"expires_at": leafCert.NotAfter,
|
||||
"expires_at": leafCert.NotAfter.Format(time.RFC3339),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -838,8 +838,8 @@ func (e *CAEngine) handleGetCert(ctx context.Context, req *engine.Request) (*eng
|
||||
"profile": record.Profile,
|
||||
"cert_pem": record.CertPEM,
|
||||
"issued_by": record.IssuedBy,
|
||||
"issued_at": record.IssuedAt,
|
||||
"expires_at": record.ExpiresAt,
|
||||
"issued_at": record.IssuedAt.Format(time.RFC3339),
|
||||
"expires_at": record.ExpiresAt.Format(time.RFC3339),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -857,7 +857,7 @@ func (e *CAEngine) handleListCerts(ctx context.Context, req *engine.Request) (*e
|
||||
return nil, fmt.Errorf("ca: list certs: %w", err)
|
||||
}
|
||||
|
||||
var certs []map[string]interface{}
|
||||
var certs []interface{}
|
||||
for _, p := range paths {
|
||||
if !strings.HasSuffix(p, ".json") {
|
||||
continue
|
||||
@@ -876,8 +876,8 @@ func (e *CAEngine) handleListCerts(ctx context.Context, req *engine.Request) (*e
|
||||
"cn": record.CN,
|
||||
"profile": record.Profile,
|
||||
"issued_by": record.IssuedBy,
|
||||
"issued_at": record.IssuedAt,
|
||||
"expires_at": record.ExpiresAt,
|
||||
"issued_at": record.IssuedAt.Format(time.RFC3339),
|
||||
"expires_at": record.ExpiresAt.Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1009,7 +1009,7 @@ func (e *CAEngine) handleRenew(ctx context.Context, req *engine.Request) (*engin
|
||||
"key_pem": string(newKeyPEM),
|
||||
"chain_pem": string(chainPEM),
|
||||
"cn": record.CN,
|
||||
"expires_at": newCert.NotAfter,
|
||||
"expires_at": newCert.NotAfter.Format(time.RFC3339),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -566,7 +566,7 @@ func TestGetAndListCerts(t *testing.T) {
|
||||
t.Fatalf("list-certs: %v", err)
|
||||
}
|
||||
|
||||
certs, ok := listResp.Data["certs"].([]map[string]interface{})
|
||||
certs, ok := listResp.Data["certs"].([]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("certs type: %T", listResp.Data["certs"])
|
||||
}
|
||||
@@ -575,7 +575,7 @@ func TestGetAndListCerts(t *testing.T) {
|
||||
}
|
||||
|
||||
// Get a specific cert.
|
||||
serial := certs[0]["serial"].(string) //nolint:errcheck
|
||||
serial := certs[0].(map[string]interface{})["serial"].(string) //nolint:errcheck
|
||||
getResp, err := eng.HandleRequest(ctx, &engine.Request{
|
||||
Operation: "get-cert",
|
||||
CallerInfo: userCaller(),
|
||||
|
||||
@@ -90,6 +90,12 @@ func (es *engineServer) Execute(ctx context.Context, req *pb.ExecuteRequest) (*p
|
||||
}
|
||||
}
|
||||
|
||||
username := ""
|
||||
if ti != nil {
|
||||
username = ti.Username
|
||||
}
|
||||
es.s.logger.Info("grpc: engine execute", "mount", req.Mount, "operation", req.Operation, "username", username)
|
||||
|
||||
resp, err := es.s.engines.HandleRequest(ctx, req.Mount, engReq)
|
||||
if err != nil {
|
||||
st := codes.Internal
|
||||
@@ -101,8 +107,10 @@ func (es *engineServer) Execute(ctx context.Context, req *pb.ExecuteRequest) (*p
|
||||
case strings.Contains(err.Error(), "not found"):
|
||||
st = codes.NotFound
|
||||
}
|
||||
es.s.logger.Error("grpc: engine execute failed", "mount", req.Mount, "operation", req.Operation, "username", username, "error", err)
|
||||
return nil, status.Error(st, err.Error())
|
||||
}
|
||||
es.s.logger.Info("grpc: engine execute ok", "mount", req.Mount, "operation", req.Operation, "username", username)
|
||||
|
||||
pbData, err := structpb.NewStruct(resp.Data)
|
||||
if err != nil {
|
||||
|
||||
@@ -111,7 +111,7 @@ func sealRequiredMethods() map[string]bool {
|
||||
"/metacrypt.v1.EngineService/Mount": true,
|
||||
"/metacrypt.v1.EngineService/Unmount": true,
|
||||
"/metacrypt.v1.EngineService/ListMounts": true,
|
||||
"/metacrypt.v1.EngineService/Request": true,
|
||||
"/metacrypt.v1.EngineService/Execute": true,
|
||||
"/metacrypt.v1.PKIService/GetRootCert": true,
|
||||
"/metacrypt.v1.PKIService/GetChain": true,
|
||||
"/metacrypt.v1.PKIService/GetIssuerCert": true,
|
||||
@@ -134,7 +134,7 @@ func authRequiredMethods() map[string]bool {
|
||||
"/metacrypt.v1.EngineService/Mount": true,
|
||||
"/metacrypt.v1.EngineService/Unmount": true,
|
||||
"/metacrypt.v1.EngineService/ListMounts": true,
|
||||
"/metacrypt.v1.EngineService/Request": true,
|
||||
"/metacrypt.v1.EngineService/Execute": true,
|
||||
"/metacrypt.v1.PolicyService/CreatePolicy": true,
|
||||
"/metacrypt.v1.PolicyService/ListPolicies": true,
|
||||
"/metacrypt.v1.PolicyService/GetPolicy": true,
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/credentials"
|
||||
grpcstatus "google.golang.org/grpc/status"
|
||||
|
||||
metacryptv1 "git.wntrmute.dev/kyle/metacrypt/gen/metacrypt/v1"
|
||||
"git.wntrmute.dev/kyle/metacrypt/internal/crypto"
|
||||
"git.wntrmute.dev/kyle/metacrypt/internal/seal"
|
||||
)
|
||||
|
||||
// systemServiceServer implements metacryptv1.SystemServiceServer.
|
||||
type systemServiceServer struct {
|
||||
metacryptv1.UnimplementedSystemServiceServer
|
||||
s *Server
|
||||
}
|
||||
|
||||
func (g *systemServiceServer) Status(_ context.Context, _ *metacryptv1.StatusRequest) (*metacryptv1.StatusResponse, error) {
|
||||
return &metacryptv1.StatusResponse{State: g.s.seal.State().String()}, nil
|
||||
}
|
||||
|
||||
func (g *systemServiceServer) Init(ctx context.Context, req *metacryptv1.InitRequest) (*metacryptv1.InitResponse, error) {
|
||||
params := crypto.Argon2Params{
|
||||
Time: g.s.cfg.Seal.Argon2Time,
|
||||
Memory: g.s.cfg.Seal.Argon2Memory,
|
||||
Threads: g.s.cfg.Seal.Argon2Threads,
|
||||
}
|
||||
if err := g.s.seal.Initialize(ctx, []byte(req.Password), params); err != nil {
|
||||
if errors.Is(err, seal.ErrAlreadyInitialized) {
|
||||
return nil, grpcstatus.Error(codes.AlreadyExists, "already initialized")
|
||||
}
|
||||
g.s.logger.Error("grpc init failed", "error", err)
|
||||
return nil, grpcstatus.Error(codes.Internal, "initialization failed")
|
||||
}
|
||||
return &metacryptv1.InitResponse{State: g.s.seal.State().String()}, nil
|
||||
}
|
||||
|
||||
func (g *systemServiceServer) Unseal(ctx context.Context, req *metacryptv1.UnsealRequest) (*metacryptv1.UnsealResponse, error) {
|
||||
if err := g.s.seal.Unseal([]byte(req.Password)); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, seal.ErrNotInitialized):
|
||||
return nil, grpcstatus.Error(codes.FailedPrecondition, "not initialized")
|
||||
case errors.Is(err, seal.ErrInvalidPassword):
|
||||
return nil, grpcstatus.Error(codes.Unauthenticated, "invalid password")
|
||||
case errors.Is(err, seal.ErrRateLimited):
|
||||
return nil, grpcstatus.Error(codes.ResourceExhausted, "too many attempts, try again later")
|
||||
case errors.Is(err, seal.ErrNotSealed):
|
||||
return nil, grpcstatus.Error(codes.AlreadyExists, "already unsealed")
|
||||
default:
|
||||
g.s.logger.Error("grpc unseal failed", "error", err)
|
||||
return nil, grpcstatus.Error(codes.Internal, "unseal failed")
|
||||
}
|
||||
}
|
||||
|
||||
if err := g.s.engines.UnsealAll(ctx); err != nil {
|
||||
g.s.logger.Error("grpc engine unseal failed", "error", err)
|
||||
return nil, grpcstatus.Error(codes.Internal, "engine unseal failed")
|
||||
}
|
||||
|
||||
return &metacryptv1.UnsealResponse{State: g.s.seal.State().String()}, nil
|
||||
}
|
||||
|
||||
func (g *systemServiceServer) Seal(_ context.Context, _ *metacryptv1.SealRequest) (*metacryptv1.SealResponse, error) {
|
||||
if err := g.s.engines.SealAll(); err != nil {
|
||||
g.s.logger.Error("grpc seal engines failed", "error", err)
|
||||
}
|
||||
if err := g.s.seal.Seal(); err != nil {
|
||||
g.s.logger.Error("grpc seal failed", "error", err)
|
||||
return nil, grpcstatus.Error(codes.Internal, "seal failed")
|
||||
}
|
||||
g.s.auth.ClearCache()
|
||||
return &metacryptv1.SealResponse{State: g.s.seal.State().String()}, nil
|
||||
}
|
||||
|
||||
// StartGRPC starts the gRPC server on cfg.Server.GRPCAddr using the same TLS
|
||||
// certificate as the HTTP server. It blocks until the listener closes.
|
||||
func (s *Server) StartGRPC() error {
|
||||
if s.cfg.Server.GRPCAddr == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
cert, err := tls.LoadX509KeyPair(s.cfg.Server.TLSCert, s.cfg.Server.TLSKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("grpc: load TLS key pair: %w", err)
|
||||
}
|
||||
tlsCfg := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
MinVersion: tls.VersionTLS12,
|
||||
CipherSuites: []uint16{
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
},
|
||||
}
|
||||
|
||||
grpcSrv := grpc.NewServer(grpc.Creds(credentials.NewTLS(tlsCfg)))
|
||||
metacryptv1.RegisterSystemServiceServer(grpcSrv, &systemServiceServer{s: s})
|
||||
|
||||
lis, err := net.Listen("tcp", s.cfg.Server.GRPCAddr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("grpc: listen: %w", err)
|
||||
}
|
||||
|
||||
s.grpcSrv = grpcSrv
|
||||
s.logger.Info("starting gRPC server", "addr", s.cfg.Server.GRPCAddr)
|
||||
if err := grpcSrv.Serve(lis); err != nil {
|
||||
return fmt.Errorf("grpc: serve: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ShutdownGRPC gracefully stops the gRPC server.
|
||||
func (s *Server) ShutdownGRPC() {
|
||||
if s.grpcSrv != nil {
|
||||
s.grpcSrv.GracefulStop()
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
@@ -24,9 +25,12 @@ type VaultClient struct {
|
||||
}
|
||||
|
||||
// NewVaultClient dials the vault gRPC server and returns a client.
|
||||
func NewVaultClient(addr, caCertPath string) (*VaultClient, error) {
|
||||
func NewVaultClient(addr, caCertPath string, logger *slog.Logger) (*VaultClient, error) {
|
||||
logger.Debug("connecting to vault", "addr", addr, "ca_cert", caCertPath)
|
||||
|
||||
tlsCfg := &tls.Config{MinVersion: tls.VersionTLS12}
|
||||
if caCertPath != "" {
|
||||
logger.Debug("loading vault CA certificate", "path", caCertPath)
|
||||
pemData, err := os.ReadFile(caCertPath) //nolint:gosec
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("webserver: read CA cert: %w", err)
|
||||
@@ -36,12 +40,17 @@ func NewVaultClient(addr, caCertPath string) (*VaultClient, error) {
|
||||
return nil, fmt.Errorf("webserver: parse CA cert")
|
||||
}
|
||||
tlsCfg.RootCAs = pool
|
||||
logger.Debug("vault CA certificate loaded successfully")
|
||||
} else {
|
||||
logger.Debug("no CA cert configured, using system roots")
|
||||
}
|
||||
|
||||
logger.Debug("dialing vault gRPC", "addr", addr)
|
||||
conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(credentials.NewTLS(tlsCfg)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("webserver: dial vault: %w", err)
|
||||
}
|
||||
logger.Debug("vault gRPC connection established", "addr", addr)
|
||||
|
||||
return &VaultClient{
|
||||
conn: conn,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -29,10 +29,12 @@ type WebServer struct {
|
||||
|
||||
// New creates a new WebServer. It dials the vault gRPC endpoint.
|
||||
func New(cfg *config.Config, logger *slog.Logger) (*WebServer, error) {
|
||||
vault, err := NewVaultClient(cfg.Web.VaultGRPC, cfg.Web.VaultCACert)
|
||||
logger.Info("connecting to vault", "addr", cfg.Web.VaultGRPC, "ca_cert", cfg.Web.VaultCACert)
|
||||
vault, err := NewVaultClient(cfg.Web.VaultGRPC, cfg.Web.VaultCACert, logger)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("webserver: connect to vault: %w", err)
|
||||
}
|
||||
logger.Info("vault connection ready", "addr", cfg.Web.VaultGRPC)
|
||||
|
||||
staticFS, err := fs.Sub(webui.FS, "static")
|
||||
if err != nil {
|
||||
@@ -47,9 +49,37 @@ func New(cfg *config.Config, logger *slog.Logger) (*WebServer, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// loggingMiddleware logs each incoming HTTP request.
|
||||
func (ws *WebServer) loggingMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
lw := &loggingResponseWriter{ResponseWriter: w, status: http.StatusOK}
|
||||
next.ServeHTTP(lw, r)
|
||||
ws.logger.Info("request",
|
||||
"method", r.Method,
|
||||
"path", r.URL.Path,
|
||||
"status", lw.status,
|
||||
"duration", time.Since(start),
|
||||
"remote_addr", r.RemoteAddr,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// loggingResponseWriter wraps http.ResponseWriter to capture the status code.
|
||||
type loggingResponseWriter struct {
|
||||
http.ResponseWriter
|
||||
status int
|
||||
}
|
||||
|
||||
func (lw *loggingResponseWriter) WriteHeader(code int) {
|
||||
lw.status = code
|
||||
lw.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
|
||||
// Start starts the web server. It blocks until the server is closed.
|
||||
func (ws *WebServer) Start() error {
|
||||
r := chi.NewRouter()
|
||||
r.Use(ws.loggingMiddleware)
|
||||
ws.registerRoutes(r)
|
||||
|
||||
ws.httpSrv = &http.Server{
|
||||
|
||||
Reference in New Issue
Block a user