Files
metacrypt/internal/webserver/client.go

209 lines
5.9 KiB
Go

package webserver
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"log/slog"
"os"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/metadata"
pb "git.wntrmute.dev/kyle/metacrypt/gen/metacrypt/v1"
)
// VaultClient wraps the gRPC stubs for communicating with the vault.
type VaultClient struct {
conn *grpc.ClientConn
system pb.SystemServiceClient
auth pb.AuthServiceClient
engine pb.EngineServiceClient
pki pb.PKIServiceClient
}
// NewVaultClient dials the vault gRPC server and returns a client.
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)
}
pool := x509.NewCertPool()
if !pool.AppendCertsFromPEM(pemData) {
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,
system: pb.NewSystemServiceClient(conn),
auth: pb.NewAuthServiceClient(conn),
engine: pb.NewEngineServiceClient(conn),
pki: pb.NewPKIServiceClient(conn),
}, nil
}
// Close closes the underlying connection.
func (c *VaultClient) Close() error {
return c.conn.Close()
}
// withToken returns a context with the Bearer token in outgoing metadata.
func withToken(ctx context.Context, token string) context.Context {
return metadata.AppendToOutgoingContext(ctx, "authorization", "Bearer "+token)
}
// Status returns the current vault state string (e.g. "unsealed").
func (c *VaultClient) Status(ctx context.Context) (string, error) {
resp, err := c.system.Status(ctx, &pb.StatusRequest{})
if err != nil {
return "", err
}
return resp.State, nil
}
// Init initializes the vault with the given password.
func (c *VaultClient) Init(ctx context.Context, password string) error {
_, err := c.system.Init(ctx, &pb.InitRequest{Password: password})
return err
}
// Unseal unseals the vault with the given password.
func (c *VaultClient) Unseal(ctx context.Context, password string) error {
_, err := c.system.Unseal(ctx, &pb.UnsealRequest{Password: password})
return err
}
// TokenInfo holds validated token details returned by the vault.
type TokenInfo struct {
Username string
Roles []string
IsAdmin bool
}
// Login authenticates against the vault and returns the session token.
func (c *VaultClient) Login(ctx context.Context, username, password, totpCode string) (string, error) {
resp, err := c.auth.Login(ctx, &pb.LoginRequest{
Username: username,
Password: password,
TotpCode: totpCode,
})
if err != nil {
return "", err
}
return resp.Token, nil
}
// ValidateToken validates a token against the vault and returns the token info.
func (c *VaultClient) ValidateToken(ctx context.Context, token string) (*TokenInfo, error) {
resp, err := c.auth.TokenInfo(withToken(ctx, token), &pb.TokenInfoRequest{})
if err != nil {
return nil, err
}
return &TokenInfo{
Username: resp.Username,
Roles: resp.Roles,
IsAdmin: resp.IsAdmin,
}, nil
}
// MountInfo holds metadata about an engine mount.
type MountInfo struct {
Name string
Type string
MountPath string
}
// ListMounts returns all engine mounts from the vault.
func (c *VaultClient) ListMounts(ctx context.Context, token string) ([]MountInfo, error) {
resp, err := c.engine.ListMounts(withToken(ctx, token), &pb.ListMountsRequest{})
if err != nil {
return nil, err
}
mounts := make([]MountInfo, 0, len(resp.Mounts))
for _, m := range resp.Mounts {
mounts = append(mounts, MountInfo{
Name: m.Name,
Type: m.Type,
MountPath: m.MountPath,
})
}
return mounts, nil
}
// Mount creates a new engine mount on the vault.
func (c *VaultClient) Mount(ctx context.Context, token, name, engineType string, config map[string]interface{}) error {
req := &pb.MountRequest{
Name: name,
Type: engineType,
}
if len(config) > 0 {
s, err := structFromMap(config)
if err != nil {
return fmt.Errorf("webserver: encode mount config: %w", err)
}
req.Config = s
}
_, err := c.engine.Mount(withToken(ctx, token), req)
return err
}
// EngineRequest sends a generic engine operation to the vault.
func (c *VaultClient) EngineRequest(ctx context.Context, token, mount, operation string, data map[string]interface{}) (map[string]interface{}, error) {
req := &pb.ExecuteRequest{
Mount: mount,
Operation: operation,
}
if len(data) > 0 {
s, err := structFromMap(data)
if err != nil {
return nil, fmt.Errorf("webserver: encode engine request: %w", err)
}
req.Data = s
}
resp, err := c.engine.Execute(withToken(ctx, token), req)
if err != nil {
return nil, err
}
if resp.Data == nil {
return nil, nil
}
return resp.Data.AsMap(), nil
}
// GetRootCert returns the root CA certificate PEM for the given mount.
func (c *VaultClient) GetRootCert(ctx context.Context, mount string) ([]byte, error) {
resp, err := c.pki.GetRootCert(ctx, &pb.GetRootCertRequest{Mount: mount})
if err != nil {
return nil, err
}
return resp.CertPem, nil
}
// GetIssuerCert returns a named issuer certificate PEM for the given mount.
func (c *VaultClient) GetIssuerCert(ctx context.Context, mount, issuer string) ([]byte, error) {
resp, err := c.pki.GetIssuerCert(ctx, &pb.GetIssuerCertRequest{Mount: mount, Issuer: issuer})
if err != nil {
return nil, err
}
return resp.CertPem, nil
}