Add web UI for SSH CA, Transit, and User engines; full security audit and remediation
Web UI: Added browser-based management for all three remaining engines (SSH CA, Transit, User E2E). Includes gRPC client wiring, handler files, 7 HTML templates, dashboard mount forms, and conditional navigation links. Fixed REST API routes to match design specs (SSH CA cert singular paths, Transit PATCH for update-key-config). Security audit: Conducted full-system audit covering crypto core, all engine implementations, API servers, policy engine, auth, deployment, and documentation. Identified 42 new findings (#39-#80) across all severity levels. Remediation of all 8 High findings: - #68: Replaced 14 JSON-injection-vulnerable error responses with safe json.Encoder via writeJSONError helper - #48: Added two-layer path traversal defense (barrier validatePath rejects ".." segments; engine ValidateName enforces safe name pattern) - #39: Extended RLock through entire crypto operations in barrier Get/Put/Delete/List to eliminate TOCTOU race with Seal - #40: Unified ReWrapKeys and seal_config UPDATE into single SQLite transaction to prevent irrecoverable data loss on crash during MEK rotation - #49: Added resolveTTL to CA engine enforcing issuer MaxTTL ceiling on handleIssue and handleSignCSR - #61: Store raw ECDH private key bytes in userState for effective zeroization on Seal - #62: Fixed user engine policy resource path from mountPath to mountName() so policy rules match correctly - #69: Added newPolicyChecker helper and passed service-level policy evaluation to all 25 typed REST handler engine.Request structs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -124,6 +124,120 @@ func (m *mockVault) CreatePolicy(ctx context.Context, token string, rule PolicyR
|
||||
|
||||
func (m *mockVault) DeletePolicy(ctx context.Context, token, id string) error { return nil }
|
||||
|
||||
// SSH CA stubs
|
||||
func (m *mockVault) GetSSHCAPublicKey(ctx context.Context, mount string) (*SSHCAPublicKey, error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
func (m *mockVault) SSHCASignHost(ctx context.Context, token, mount string, req SSHCASignRequest) (*SSHCASignResult, error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
func (m *mockVault) SSHCASignUser(ctx context.Context, token, mount string, req SSHCASignRequest) (*SSHCASignResult, error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
func (m *mockVault) ListSSHCAProfiles(ctx context.Context, token, mount string) ([]SSHCAProfileSummary, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockVault) GetSSHCAProfile(ctx context.Context, token, mount, name string) (*SSHCAProfile, error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
func (m *mockVault) CreateSSHCAProfile(ctx context.Context, token, mount string, req SSHCAProfileRequest) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockVault) UpdateSSHCAProfile(ctx context.Context, token, mount, name string, req SSHCAProfileRequest) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockVault) DeleteSSHCAProfile(ctx context.Context, token, mount, name string) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockVault) ListSSHCACerts(ctx context.Context, token, mount string) ([]SSHCACertSummary, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockVault) GetSSHCACert(ctx context.Context, token, mount, serial string) (*SSHCACertDetail, error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
func (m *mockVault) RevokeSSHCACert(ctx context.Context, token, mount, serial string) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockVault) DeleteSSHCACert(ctx context.Context, token, mount, serial string) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockVault) GetSSHCAKRL(ctx context.Context, mount string) ([]byte, error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
// Transit stubs
|
||||
func (m *mockVault) ListTransitKeys(ctx context.Context, token, mount string) ([]TransitKeySummary, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockVault) GetTransitKey(ctx context.Context, token, mount, name string) (*TransitKeyDetail, error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
func (m *mockVault) CreateTransitKey(ctx context.Context, token, mount, name, keyType string) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockVault) DeleteTransitKey(ctx context.Context, token, mount, name string) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockVault) RotateTransitKey(ctx context.Context, token, mount, name string) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockVault) UpdateTransitKeyConfig(ctx context.Context, token, mount, name string, minDecryptVersion int, allowDeletion bool) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockVault) TrimTransitKey(ctx context.Context, token, mount, name string) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
func (m *mockVault) TransitEncrypt(ctx context.Context, token, mount, key, plaintext, transitCtx string) (string, error) {
|
||||
return "", fmt.Errorf("not implemented")
|
||||
}
|
||||
func (m *mockVault) TransitDecrypt(ctx context.Context, token, mount, key, ciphertext, transitCtx string) (string, error) {
|
||||
return "", fmt.Errorf("not implemented")
|
||||
}
|
||||
func (m *mockVault) TransitRewrap(ctx context.Context, token, mount, key, ciphertext, transitCtx string) (string, error) {
|
||||
return "", fmt.Errorf("not implemented")
|
||||
}
|
||||
func (m *mockVault) TransitSign(ctx context.Context, token, mount, key, input string) (string, error) {
|
||||
return "", fmt.Errorf("not implemented")
|
||||
}
|
||||
func (m *mockVault) TransitVerify(ctx context.Context, token, mount, key, input, signature string) (bool, error) {
|
||||
return false, fmt.Errorf("not implemented")
|
||||
}
|
||||
func (m *mockVault) TransitHMAC(ctx context.Context, token, mount, key, input string) (string, error) {
|
||||
return "", fmt.Errorf("not implemented")
|
||||
}
|
||||
func (m *mockVault) GetTransitPublicKey(ctx context.Context, token, mount, name string) (string, error) {
|
||||
return "", fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
// User stubs
|
||||
func (m *mockVault) UserRegister(ctx context.Context, token, mount string) (*UserKeyInfo, error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
func (m *mockVault) UserProvision(ctx context.Context, token, mount, username string) (*UserKeyInfo, error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
func (m *mockVault) GetUserPublicKey(ctx context.Context, token, mount, username string) (*UserKeyInfo, error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
func (m *mockVault) ListUsers(ctx context.Context, token, mount string) ([]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockVault) UserEncrypt(ctx context.Context, token, mount, plaintext, metadata string, recipients []string) (string, error) {
|
||||
return "", fmt.Errorf("not implemented")
|
||||
}
|
||||
func (m *mockVault) UserDecrypt(ctx context.Context, token, mount, envelope string) (*UserDecryptResult, error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
func (m *mockVault) UserReEncrypt(ctx context.Context, token, mount, envelope string) (string, error) {
|
||||
return "", fmt.Errorf("not implemented")
|
||||
}
|
||||
func (m *mockVault) UserRotateKey(ctx context.Context, token, mount string) (*UserKeyInfo, error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
func (m *mockVault) UserDeleteUser(ctx context.Context, token, mount, username string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockVault) Close() error { return nil }
|
||||
|
||||
// ---- handleTGZDownload tests ----
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
@@ -17,13 +18,16 @@ import (
|
||||
|
||||
// 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
|
||||
ca pb.CAServiceClient
|
||||
policy pb.PolicyServiceClient
|
||||
conn *grpc.ClientConn
|
||||
system pb.SystemServiceClient
|
||||
auth pb.AuthServiceClient
|
||||
engine pb.EngineServiceClient
|
||||
pki pb.PKIServiceClient
|
||||
ca pb.CAServiceClient
|
||||
policy pb.PolicyServiceClient
|
||||
sshca pb.SSHCAServiceClient
|
||||
transit pb.TransitServiceClient
|
||||
user pb.UserServiceClient
|
||||
}
|
||||
|
||||
// NewVaultClient dials the vault gRPC server and returns a client.
|
||||
@@ -55,13 +59,16 @@ func NewVaultClient(addr, caCertPath string, logger *slog.Logger) (*VaultClient,
|
||||
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),
|
||||
ca: pb.NewCAServiceClient(conn),
|
||||
policy: pb.NewPolicyServiceClient(conn),
|
||||
conn: conn,
|
||||
system: pb.NewSystemServiceClient(conn),
|
||||
auth: pb.NewAuthServiceClient(conn),
|
||||
engine: pb.NewEngineServiceClient(conn),
|
||||
pki: pb.NewPKIServiceClient(conn),
|
||||
ca: pb.NewCAServiceClient(conn),
|
||||
policy: pb.NewPolicyServiceClient(conn),
|
||||
sshca: pb.NewSSHCAServiceClient(conn),
|
||||
transit: pb.NewTransitServiceClient(conn),
|
||||
user: pb.NewUserServiceClient(conn),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -496,3 +503,610 @@ func (c *VaultClient) ListCerts(ctx context.Context, token, mount string) ([]Cer
|
||||
}
|
||||
return certs, nil
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SSH CA
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// SSHCAPublicKey holds the CA public key details for display.
|
||||
type SSHCAPublicKey struct {
|
||||
PublicKey string // authorized_keys format
|
||||
}
|
||||
|
||||
// GetSSHCAPublicKey returns the SSH CA public key for a mount.
|
||||
func (c *VaultClient) GetSSHCAPublicKey(ctx context.Context, mount string) (*SSHCAPublicKey, error) {
|
||||
resp, err := c.sshca.GetCAPublicKey(ctx, &pb.SSHGetCAPublicKeyRequest{Mount: mount})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &SSHCAPublicKey{PublicKey: resp.PublicKey}, nil
|
||||
}
|
||||
|
||||
// SSHCASignRequest holds parameters for signing an SSH certificate.
|
||||
type SSHCASignRequest struct {
|
||||
PublicKey string
|
||||
Principals []string
|
||||
Profile string
|
||||
TTL string
|
||||
}
|
||||
|
||||
// SSHCASignResult holds the result of signing an SSH certificate.
|
||||
type SSHCASignResult struct {
|
||||
Serial string
|
||||
CertType string
|
||||
Principals []string
|
||||
CertData string
|
||||
KeyID string
|
||||
IssuedBy string
|
||||
IssuedAt string
|
||||
ExpiresAt string
|
||||
}
|
||||
|
||||
func sshSignResultFromHost(resp *pb.SSHSignHostResponse) *SSHCASignResult {
|
||||
r := &SSHCASignResult{
|
||||
Serial: resp.Serial,
|
||||
CertType: resp.CertType,
|
||||
Principals: resp.Principals,
|
||||
CertData: resp.CertData,
|
||||
KeyID: resp.KeyId,
|
||||
IssuedBy: resp.IssuedBy,
|
||||
}
|
||||
if resp.IssuedAt != nil {
|
||||
r.IssuedAt = resp.IssuedAt.AsTime().Format("2006-01-02T15:04:05Z")
|
||||
}
|
||||
if resp.ExpiresAt != nil {
|
||||
r.ExpiresAt = resp.ExpiresAt.AsTime().Format("2006-01-02T15:04:05Z")
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func sshSignResultFromUser(resp *pb.SSHSignUserResponse) *SSHCASignResult {
|
||||
r := &SSHCASignResult{
|
||||
Serial: resp.Serial,
|
||||
CertType: resp.CertType,
|
||||
Principals: resp.Principals,
|
||||
CertData: resp.CertData,
|
||||
KeyID: resp.KeyId,
|
||||
IssuedBy: resp.IssuedBy,
|
||||
}
|
||||
if resp.IssuedAt != nil {
|
||||
r.IssuedAt = resp.IssuedAt.AsTime().Format("2006-01-02T15:04:05Z")
|
||||
}
|
||||
if resp.ExpiresAt != nil {
|
||||
r.ExpiresAt = resp.ExpiresAt.AsTime().Format("2006-01-02T15:04:05Z")
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// SSHCASignHost signs an SSH host certificate.
|
||||
func (c *VaultClient) SSHCASignHost(ctx context.Context, token, mount string, req SSHCASignRequest) (*SSHCASignResult, error) {
|
||||
hostname := ""
|
||||
if len(req.Principals) > 0 {
|
||||
hostname = req.Principals[0]
|
||||
}
|
||||
resp, err := c.sshca.SignHost(withToken(ctx, token), &pb.SSHSignHostRequest{
|
||||
Mount: mount,
|
||||
PublicKey: req.PublicKey,
|
||||
Hostname: hostname,
|
||||
Ttl: req.TTL,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sshSignResultFromHost(resp), nil
|
||||
}
|
||||
|
||||
// SSHCASignUser signs an SSH user certificate.
|
||||
func (c *VaultClient) SSHCASignUser(ctx context.Context, token, mount string, req SSHCASignRequest) (*SSHCASignResult, error) {
|
||||
resp, err := c.sshca.SignUser(withToken(ctx, token), &pb.SSHSignUserRequest{
|
||||
Mount: mount,
|
||||
PublicKey: req.PublicKey,
|
||||
Principals: req.Principals,
|
||||
Profile: req.Profile,
|
||||
Ttl: req.TTL,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sshSignResultFromUser(resp), nil
|
||||
}
|
||||
|
||||
// SSHCAProfileSummary holds lightweight profile data for list views.
|
||||
type SSHCAProfileSummary struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
// ListSSHCAProfiles returns all signing profile names for a mount.
|
||||
func (c *VaultClient) ListSSHCAProfiles(ctx context.Context, token, mount string) ([]SSHCAProfileSummary, error) {
|
||||
resp, err := c.sshca.ListProfiles(withToken(ctx, token), &pb.SSHListProfilesRequest{Mount: mount})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
profiles := make([]SSHCAProfileSummary, 0, len(resp.Profiles))
|
||||
for _, name := range resp.Profiles {
|
||||
profiles = append(profiles, SSHCAProfileSummary{Name: name})
|
||||
}
|
||||
return profiles, nil
|
||||
}
|
||||
|
||||
// SSHCAProfile holds full profile data for the detail view.
|
||||
type SSHCAProfile struct {
|
||||
Name string
|
||||
CriticalOptions map[string]string
|
||||
Extensions map[string]string
|
||||
MaxTTL string
|
||||
AllowedPrincipals []string
|
||||
}
|
||||
|
||||
// GetSSHCAProfile retrieves a signing profile by name.
|
||||
func (c *VaultClient) GetSSHCAProfile(ctx context.Context, token, mount, name string) (*SSHCAProfile, error) {
|
||||
resp, err := c.sshca.GetProfile(withToken(ctx, token), &pb.SSHGetProfileRequest{Mount: mount, Name: name})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &SSHCAProfile{
|
||||
Name: resp.Name,
|
||||
CriticalOptions: resp.CriticalOptions,
|
||||
Extensions: resp.Extensions,
|
||||
MaxTTL: resp.MaxTtl,
|
||||
AllowedPrincipals: resp.AllowedPrincipals,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SSHCAProfileRequest holds parameters for creating or updating a profile.
|
||||
type SSHCAProfileRequest struct {
|
||||
Name string
|
||||
CriticalOptions map[string]string
|
||||
Extensions map[string]string
|
||||
MaxTTL string
|
||||
AllowedPrincipals []string
|
||||
}
|
||||
|
||||
// CreateSSHCAProfile creates a new signing profile.
|
||||
func (c *VaultClient) CreateSSHCAProfile(ctx context.Context, token, mount string, req SSHCAProfileRequest) error {
|
||||
_, err := c.sshca.CreateProfile(withToken(ctx, token), &pb.SSHCreateProfileRequest{
|
||||
Mount: mount,
|
||||
Name: req.Name,
|
||||
CriticalOptions: req.CriticalOptions,
|
||||
Extensions: req.Extensions,
|
||||
MaxTtl: req.MaxTTL,
|
||||
AllowedPrincipals: req.AllowedPrincipals,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateSSHCAProfile updates an existing signing profile.
|
||||
func (c *VaultClient) UpdateSSHCAProfile(ctx context.Context, token, mount, name string, req SSHCAProfileRequest) error {
|
||||
_, err := c.sshca.UpdateProfile(withToken(ctx, token), &pb.SSHUpdateProfileRequest{
|
||||
Mount: mount,
|
||||
Name: name,
|
||||
CriticalOptions: req.CriticalOptions,
|
||||
Extensions: req.Extensions,
|
||||
MaxTtl: req.MaxTTL,
|
||||
AllowedPrincipals: req.AllowedPrincipals,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteSSHCAProfile removes a signing profile.
|
||||
func (c *VaultClient) DeleteSSHCAProfile(ctx context.Context, token, mount, name string) error {
|
||||
_, err := c.sshca.DeleteProfile(withToken(ctx, token), &pb.SSHDeleteProfileRequest{Mount: mount, Name: name})
|
||||
return err
|
||||
}
|
||||
|
||||
// SSHCACertSummary holds lightweight cert data for list views.
|
||||
type SSHCACertSummary struct {
|
||||
Serial string
|
||||
CertType string
|
||||
Principals string
|
||||
KeyID string
|
||||
Profile string
|
||||
IssuedBy string
|
||||
IssuedAt string
|
||||
ExpiresAt string
|
||||
Revoked bool
|
||||
}
|
||||
|
||||
// ListSSHCACerts returns all SSH certificate summaries for a mount.
|
||||
func (c *VaultClient) ListSSHCACerts(ctx context.Context, token, mount string) ([]SSHCACertSummary, error) {
|
||||
resp, err := c.sshca.ListCerts(withToken(ctx, token), &pb.SSHListCertsRequest{Mount: mount})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
certs := make([]SSHCACertSummary, 0, len(resp.Certs))
|
||||
for _, s := range resp.Certs {
|
||||
cs := SSHCACertSummary{
|
||||
Serial: s.Serial,
|
||||
CertType: s.CertType,
|
||||
Principals: strings.Join(s.Principals, ", "),
|
||||
KeyID: s.KeyId,
|
||||
Profile: s.Profile,
|
||||
IssuedBy: s.IssuedBy,
|
||||
Revoked: s.Revoked,
|
||||
}
|
||||
if s.IssuedAt != nil {
|
||||
cs.IssuedAt = s.IssuedAt.AsTime().Format("2006-01-02T15:04:05Z")
|
||||
}
|
||||
if s.ExpiresAt != nil {
|
||||
cs.ExpiresAt = s.ExpiresAt.AsTime().Format("2006-01-02T15:04:05Z")
|
||||
}
|
||||
certs = append(certs, cs)
|
||||
}
|
||||
return certs, nil
|
||||
}
|
||||
|
||||
// SSHCACertDetail holds full SSH certificate data for the detail view.
|
||||
type SSHCACertDetail struct {
|
||||
Serial string
|
||||
CertType string
|
||||
Principals []string
|
||||
CertData string
|
||||
KeyID string
|
||||
Profile string
|
||||
IssuedBy string
|
||||
IssuedAt string
|
||||
ExpiresAt string
|
||||
Revoked bool
|
||||
RevokedAt string
|
||||
RevokedBy string
|
||||
}
|
||||
|
||||
// GetSSHCACert retrieves a full SSH certificate record by serial.
|
||||
func (c *VaultClient) GetSSHCACert(ctx context.Context, token, mount, serial string) (*SSHCACertDetail, error) {
|
||||
resp, err := c.sshca.GetCert(withToken(ctx, token), &pb.SSHGetCertRequest{Mount: mount, Serial: serial})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rec := resp.GetCert()
|
||||
if rec == nil {
|
||||
return nil, fmt.Errorf("cert not found")
|
||||
}
|
||||
cd := &SSHCACertDetail{
|
||||
Serial: rec.Serial,
|
||||
CertType: rec.CertType,
|
||||
Principals: rec.Principals,
|
||||
CertData: rec.CertData,
|
||||
KeyID: rec.KeyId,
|
||||
Profile: rec.Profile,
|
||||
IssuedBy: rec.IssuedBy,
|
||||
Revoked: rec.Revoked,
|
||||
RevokedBy: rec.RevokedBy,
|
||||
}
|
||||
if rec.IssuedAt != nil {
|
||||
cd.IssuedAt = rec.IssuedAt.AsTime().Format("2006-01-02T15:04:05Z")
|
||||
}
|
||||
if rec.ExpiresAt != nil {
|
||||
cd.ExpiresAt = rec.ExpiresAt.AsTime().Format("2006-01-02T15:04:05Z")
|
||||
}
|
||||
if rec.RevokedAt != nil {
|
||||
cd.RevokedAt = rec.RevokedAt.AsTime().Format("2006-01-02T15:04:05Z")
|
||||
}
|
||||
return cd, nil
|
||||
}
|
||||
|
||||
// RevokeSSHCACert marks an SSH certificate as revoked.
|
||||
func (c *VaultClient) RevokeSSHCACert(ctx context.Context, token, mount, serial string) error {
|
||||
_, err := c.sshca.RevokeCert(withToken(ctx, token), &pb.SSHRevokeCertRequest{Mount: mount, Serial: serial})
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteSSHCACert permanently removes an SSH certificate record.
|
||||
func (c *VaultClient) DeleteSSHCACert(ctx context.Context, token, mount, serial string) error {
|
||||
_, err := c.sshca.DeleteCert(withToken(ctx, token), &pb.SSHDeleteCertRequest{Mount: mount, Serial: serial})
|
||||
return err
|
||||
}
|
||||
|
||||
// GetSSHCAKRL returns the binary KRL data for a mount.
|
||||
func (c *VaultClient) GetSSHCAKRL(ctx context.Context, mount string) ([]byte, error) {
|
||||
resp, err := c.sshca.GetKRL(ctx, &pb.SSHGetKRLRequest{Mount: mount})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.Krl, nil
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Transit
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// TransitKeySummary holds lightweight key data for list views.
|
||||
type TransitKeySummary struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
// ListTransitKeys returns all key names for a mount.
|
||||
func (c *VaultClient) ListTransitKeys(ctx context.Context, token, mount string) ([]TransitKeySummary, error) {
|
||||
resp, err := c.transit.ListKeys(withToken(ctx, token), &pb.ListTransitKeysRequest{Mount: mount})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keys := make([]TransitKeySummary, 0, len(resp.Keys))
|
||||
for _, name := range resp.Keys {
|
||||
keys = append(keys, TransitKeySummary{Name: name})
|
||||
}
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
// TransitKeyDetail holds full key metadata for the detail view.
|
||||
type TransitKeyDetail struct {
|
||||
Name string
|
||||
Type string
|
||||
CurrentVersion int
|
||||
MinDecryptionVersion int
|
||||
AllowDeletion bool
|
||||
Versions []int
|
||||
}
|
||||
|
||||
// GetTransitKey retrieves key metadata.
|
||||
func (c *VaultClient) GetTransitKey(ctx context.Context, token, mount, name string) (*TransitKeyDetail, error) {
|
||||
resp, err := c.transit.GetKey(withToken(ctx, token), &pb.GetTransitKeyRequest{Mount: mount, Name: name})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
versions := make([]int, 0, len(resp.Versions))
|
||||
for _, v := range resp.Versions {
|
||||
versions = append(versions, int(v))
|
||||
}
|
||||
return &TransitKeyDetail{
|
||||
Name: resp.Name,
|
||||
Type: resp.Type,
|
||||
CurrentVersion: int(resp.CurrentVersion),
|
||||
MinDecryptionVersion: int(resp.MinDecryptionVersion),
|
||||
AllowDeletion: resp.AllowDeletion,
|
||||
Versions: versions,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateTransitKey creates a new named key.
|
||||
func (c *VaultClient) CreateTransitKey(ctx context.Context, token, mount, name, keyType string) error {
|
||||
_, err := c.transit.CreateKey(withToken(ctx, token), &pb.CreateTransitKeyRequest{
|
||||
Mount: mount,
|
||||
Name: name,
|
||||
Type: keyType,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteTransitKey permanently removes a named key.
|
||||
func (c *VaultClient) DeleteTransitKey(ctx context.Context, token, mount, name string) error {
|
||||
_, err := c.transit.DeleteKey(withToken(ctx, token), &pb.DeleteTransitKeyRequest{Mount: mount, Name: name})
|
||||
return err
|
||||
}
|
||||
|
||||
// RotateTransitKey creates a new version of the named key.
|
||||
func (c *VaultClient) RotateTransitKey(ctx context.Context, token, mount, name string) error {
|
||||
_, err := c.transit.RotateKey(withToken(ctx, token), &pb.RotateTransitKeyRequest{Mount: mount, Name: name})
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateTransitKeyConfig updates key config (min_decryption_version, allow_deletion).
|
||||
func (c *VaultClient) UpdateTransitKeyConfig(ctx context.Context, token, mount, name string, minDecryptVersion int, allowDeletion bool) error {
|
||||
_, err := c.transit.UpdateKeyConfig(withToken(ctx, token), &pb.UpdateTransitKeyConfigRequest{
|
||||
Mount: mount,
|
||||
Name: name,
|
||||
MinDecryptionVersion: int32(minDecryptVersion),
|
||||
AllowDeletion: allowDeletion,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// TrimTransitKey deletes old key versions below min_decryption_version.
|
||||
func (c *VaultClient) TrimTransitKey(ctx context.Context, token, mount, name string) (int, error) {
|
||||
resp, err := c.transit.TrimKey(withToken(ctx, token), &pb.TrimTransitKeyRequest{Mount: mount, Name: name})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return int(resp.Trimmed), nil
|
||||
}
|
||||
|
||||
// TransitEncrypt encrypts plaintext with a named key.
|
||||
func (c *VaultClient) TransitEncrypt(ctx context.Context, token, mount, key, plaintext, transitCtx string) (string, error) {
|
||||
resp, err := c.transit.Encrypt(withToken(ctx, token), &pb.TransitEncryptRequest{
|
||||
Mount: mount,
|
||||
Key: key,
|
||||
Plaintext: plaintext,
|
||||
Context: transitCtx,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resp.Ciphertext, nil
|
||||
}
|
||||
|
||||
// TransitDecrypt decrypts ciphertext with a named key.
|
||||
func (c *VaultClient) TransitDecrypt(ctx context.Context, token, mount, key, ciphertext, transitCtx string) (string, error) {
|
||||
resp, err := c.transit.Decrypt(withToken(ctx, token), &pb.TransitDecryptRequest{
|
||||
Mount: mount,
|
||||
Key: key,
|
||||
Ciphertext: ciphertext,
|
||||
Context: transitCtx,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resp.Plaintext, nil
|
||||
}
|
||||
|
||||
// TransitRewrap re-encrypts ciphertext with the latest key version.
|
||||
func (c *VaultClient) TransitRewrap(ctx context.Context, token, mount, key, ciphertext, transitCtx string) (string, error) {
|
||||
resp, err := c.transit.Rewrap(withToken(ctx, token), &pb.TransitRewrapRequest{
|
||||
Mount: mount,
|
||||
Key: key,
|
||||
Ciphertext: ciphertext,
|
||||
Context: transitCtx,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resp.Ciphertext, nil
|
||||
}
|
||||
|
||||
// TransitSign signs input data with an asymmetric key.
|
||||
func (c *VaultClient) TransitSign(ctx context.Context, token, mount, key, input string) (string, error) {
|
||||
resp, err := c.transit.Sign(withToken(ctx, token), &pb.TransitSignRequest{
|
||||
Mount: mount,
|
||||
Key: key,
|
||||
Input: input,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resp.Signature, nil
|
||||
}
|
||||
|
||||
// TransitVerify verifies a signature against input data.
|
||||
func (c *VaultClient) TransitVerify(ctx context.Context, token, mount, key, input, signature string) (bool, error) {
|
||||
resp, err := c.transit.Verify(withToken(ctx, token), &pb.TransitVerifyRequest{
|
||||
Mount: mount,
|
||||
Key: key,
|
||||
Input: input,
|
||||
Signature: signature,
|
||||
})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return resp.Valid, nil
|
||||
}
|
||||
|
||||
// TransitHMAC computes an HMAC.
|
||||
func (c *VaultClient) TransitHMAC(ctx context.Context, token, mount, key, input string) (string, error) {
|
||||
resp, err := c.transit.Hmac(withToken(ctx, token), &pb.TransitHmacRequest{
|
||||
Mount: mount,
|
||||
Key: key,
|
||||
Input: input,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resp.Hmac, nil
|
||||
}
|
||||
|
||||
// GetTransitPublicKey returns the public key for an asymmetric transit key.
|
||||
func (c *VaultClient) GetTransitPublicKey(ctx context.Context, token, mount, name string) (string, error) {
|
||||
resp, err := c.transit.GetPublicKey(withToken(ctx, token), &pb.GetTransitPublicKeyRequest{Mount: mount, Name: name})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resp.PublicKey, nil
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// User (E2E Encryption)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// UserKeyInfo holds a user's public key details.
|
||||
type UserKeyInfo struct {
|
||||
Username string
|
||||
PublicKey string
|
||||
Algorithm string
|
||||
}
|
||||
|
||||
// UserRegister self-registers the caller.
|
||||
func (c *VaultClient) UserRegister(ctx context.Context, token, mount string) (*UserKeyInfo, error) {
|
||||
resp, err := c.user.Register(withToken(ctx, token), &pb.UserRegisterRequest{Mount: mount})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &UserKeyInfo{
|
||||
Username: resp.Username,
|
||||
PublicKey: resp.PublicKey,
|
||||
Algorithm: resp.Algorithm,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UserProvision creates a keypair for a given username. Admin only.
|
||||
func (c *VaultClient) UserProvision(ctx context.Context, token, mount, username string) (*UserKeyInfo, error) {
|
||||
resp, err := c.user.Provision(withToken(ctx, token), &pb.UserProvisionRequest{Mount: mount, Username: username})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &UserKeyInfo{
|
||||
Username: resp.Username,
|
||||
PublicKey: resp.PublicKey,
|
||||
Algorithm: resp.Algorithm,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetUserPublicKey returns the public key for a username.
|
||||
func (c *VaultClient) GetUserPublicKey(ctx context.Context, token, mount, username string) (*UserKeyInfo, error) {
|
||||
resp, err := c.user.GetPublicKey(withToken(ctx, token), &pb.UserGetPublicKeyRequest{Mount: mount, Username: username})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &UserKeyInfo{
|
||||
Username: resp.Username,
|
||||
PublicKey: resp.PublicKey,
|
||||
Algorithm: resp.Algorithm,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListUsers returns all registered usernames.
|
||||
func (c *VaultClient) ListUsers(ctx context.Context, token, mount string) ([]string, error) {
|
||||
resp, err := c.user.ListUsers(withToken(ctx, token), &pb.UserListUsersRequest{Mount: mount})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.Users, nil
|
||||
}
|
||||
|
||||
// UserEncrypt encrypts plaintext for one or more recipients.
|
||||
func (c *VaultClient) UserEncrypt(ctx context.Context, token, mount, plaintext, userMetadata string, recipients []string) (string, error) {
|
||||
resp, err := c.user.Encrypt(withToken(ctx, token), &pb.UserEncryptRequest{
|
||||
Mount: mount,
|
||||
Plaintext: plaintext,
|
||||
Metadata: userMetadata,
|
||||
Recipients: recipients,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resp.Envelope, nil
|
||||
}
|
||||
|
||||
// UserDecryptResult holds the result of decrypting an envelope.
|
||||
type UserDecryptResult struct {
|
||||
Plaintext string
|
||||
Sender string
|
||||
Metadata string
|
||||
}
|
||||
|
||||
// UserDecrypt decrypts an envelope addressed to the caller.
|
||||
func (c *VaultClient) UserDecrypt(ctx context.Context, token, mount, envelope string) (*UserDecryptResult, error) {
|
||||
resp, err := c.user.Decrypt(withToken(ctx, token), &pb.UserDecryptRequest{Mount: mount, Envelope: envelope})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &UserDecryptResult{
|
||||
Plaintext: resp.Plaintext,
|
||||
Sender: resp.Sender,
|
||||
Metadata: resp.Metadata,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UserReEncrypt re-encrypts an envelope with current keys.
|
||||
func (c *VaultClient) UserReEncrypt(ctx context.Context, token, mount, envelope string) (string, error) {
|
||||
resp, err := c.user.ReEncrypt(withToken(ctx, token), &pb.UserReEncryptRequest{Mount: mount, Envelope: envelope})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resp.Envelope, nil
|
||||
}
|
||||
|
||||
// UserRotateKey generates a new keypair for the caller.
|
||||
func (c *VaultClient) UserRotateKey(ctx context.Context, token, mount string) (*UserKeyInfo, error) {
|
||||
resp, err := c.user.RotateKey(withToken(ctx, token), &pb.UserRotateKeyRequest{Mount: mount})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &UserKeyInfo{
|
||||
Username: resp.Username,
|
||||
PublicKey: resp.PublicKey,
|
||||
Algorithm: resp.Algorithm,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UserDeleteUser removes a user's keys. Admin only.
|
||||
func (c *VaultClient) UserDeleteUser(ctx context.Context, token, mount, username string) error {
|
||||
_, err := c.user.DeleteUser(withToken(ctx, token), &pb.UserDeleteUserRequest{Mount: mount, Username: username})
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ func (ws *WebServer) registerRoutes(r chi.Router) {
|
||||
r.HandleFunc("/login", ws.handleLogin)
|
||||
r.Get("/dashboard", ws.requireAuth(ws.handleDashboard))
|
||||
r.Post("/dashboard/mount-ca", ws.requireAuth(ws.handleDashboardMountCA))
|
||||
r.Post("/dashboard/mount-engine", ws.requireAuth(ws.handleDashboardMountEngine))
|
||||
|
||||
r.Route("/policy", func(r chi.Router) {
|
||||
r.Get("/", ws.requireAuth(ws.handlePolicy))
|
||||
@@ -45,6 +46,47 @@ func (ws *WebServer) registerRoutes(r chi.Router) {
|
||||
r.Post("/delete", ws.requireAuth(ws.handlePolicyDelete))
|
||||
})
|
||||
|
||||
r.Route("/sshca", func(r chi.Router) {
|
||||
r.Get("/", ws.requireAuth(ws.handleSSHCA))
|
||||
r.Post("/sign-user", ws.requireAuth(ws.handleSSHCASignUser))
|
||||
r.Post("/sign-host", ws.requireAuth(ws.handleSSHCASignHost))
|
||||
r.Get("/cert/{serial}", ws.requireAuth(ws.handleSSHCACertDetail))
|
||||
r.Post("/cert/{serial}/revoke", ws.requireAuth(ws.handleSSHCACertRevoke))
|
||||
r.Post("/cert/{serial}/delete", ws.requireAuth(ws.handleSSHCACertDelete))
|
||||
r.Post("/profile/create", ws.requireAuth(ws.handleSSHCACreateProfile))
|
||||
r.Get("/profile/{name}", ws.requireAuth(ws.handleSSHCAProfileDetail))
|
||||
r.Post("/profile/{name}/update", ws.requireAuth(ws.handleSSHCAUpdateProfile))
|
||||
r.Post("/profile/{name}/delete", ws.requireAuth(ws.handleSSHCADeleteProfile))
|
||||
})
|
||||
|
||||
r.Route("/transit", func(r chi.Router) {
|
||||
r.Get("/", ws.requireAuth(ws.handleTransit))
|
||||
r.Get("/key/{name}", ws.requireAuth(ws.handleTransitKeyDetail))
|
||||
r.Post("/key/create", ws.requireAuth(ws.handleTransitCreateKey))
|
||||
r.Post("/key/{name}/rotate", ws.requireAuth(ws.handleTransitRotateKey))
|
||||
r.Post("/key/{name}/config", ws.requireAuth(ws.handleTransitUpdateConfig))
|
||||
r.Post("/key/{name}/trim", ws.requireAuth(ws.handleTransitTrimKey))
|
||||
r.Post("/key/{name}/delete", ws.requireAuth(ws.handleTransitDeleteKey))
|
||||
r.Post("/encrypt", ws.requireAuth(ws.handleTransitEncrypt))
|
||||
r.Post("/decrypt", ws.requireAuth(ws.handleTransitDecrypt))
|
||||
r.Post("/rewrap", ws.requireAuth(ws.handleTransitRewrap))
|
||||
r.Post("/sign", ws.requireAuth(ws.handleTransitSign))
|
||||
r.Post("/verify", ws.requireAuth(ws.handleTransitVerify))
|
||||
r.Post("/hmac", ws.requireAuth(ws.handleTransitHMAC))
|
||||
})
|
||||
|
||||
r.Route("/user", func(r chi.Router) {
|
||||
r.Get("/", ws.requireAuth(ws.handleUser))
|
||||
r.Post("/register", ws.requireAuth(ws.handleUserRegister))
|
||||
r.Post("/provision", ws.requireAuth(ws.handleUserProvision))
|
||||
r.Get("/key/{username}", ws.requireAuth(ws.handleUserKeyDetail))
|
||||
r.Post("/encrypt", ws.requireAuth(ws.handleUserEncrypt))
|
||||
r.Post("/decrypt", ws.requireAuth(ws.handleUserDecrypt))
|
||||
r.Post("/re-encrypt", ws.requireAuth(ws.handleUserReEncrypt))
|
||||
r.Post("/rotate", ws.requireAuth(ws.handleUserRotateKey))
|
||||
r.Post("/delete/{username}", ws.requireAuth(ws.handleUserDeleteUser))
|
||||
})
|
||||
|
||||
r.Route("/pki", func(r chi.Router) {
|
||||
r.Get("/", ws.requireAuth(ws.handlePKI))
|
||||
r.Post("/import-root", ws.requireAuth(ws.handleImportRoot))
|
||||
@@ -201,13 +243,11 @@ func (ws *WebServer) handleDashboard(w http.ResponseWriter, r *http.Request) {
|
||||
token := extractCookie(r)
|
||||
mounts, _ := ws.vault.ListMounts(r.Context(), token)
|
||||
state, _ := ws.vault.Status(r.Context())
|
||||
ws.renderTemplate(w, "dashboard.html", map[string]interface{}{
|
||||
"Username": info.Username,
|
||||
"IsAdmin": info.IsAdmin,
|
||||
"Roles": info.Roles,
|
||||
"Mounts": mounts,
|
||||
"State": state,
|
||||
})
|
||||
data := ws.baseData(r, info)
|
||||
data["Roles"] = info.Roles
|
||||
data["Mounts"] = mounts
|
||||
data["State"] = state
|
||||
ws.renderTemplate(w, "dashboard.html", data)
|
||||
}
|
||||
|
||||
func (ws *WebServer) handleDashboardMountCA(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -258,18 +298,57 @@ func (ws *WebServer) handleDashboardMountCA(w http.ResponseWriter, r *http.Reque
|
||||
http.Redirect(w, r, "/pki", http.StatusFound)
|
||||
}
|
||||
|
||||
func (ws *WebServer) handleDashboardMountEngine(w http.ResponseWriter, r *http.Request) {
|
||||
info := tokenInfoFromContext(r.Context())
|
||||
if !info.IsAdmin {
|
||||
http.Error(w, "forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
|
||||
_ = r.ParseForm()
|
||||
|
||||
mountName := r.FormValue("name")
|
||||
engineType := r.FormValue("type")
|
||||
if mountName == "" || engineType == "" {
|
||||
ws.renderDashboardWithError(w, r, info, "Mount name and engine type are required")
|
||||
return
|
||||
}
|
||||
|
||||
cfg := map[string]interface{}{}
|
||||
if v := r.FormValue("key_algorithm"); v != "" {
|
||||
cfg["key_algorithm"] = v
|
||||
}
|
||||
|
||||
token := extractCookie(r)
|
||||
if err := ws.vault.Mount(r.Context(), token, mountName, engineType, cfg); err != nil {
|
||||
ws.renderDashboardWithError(w, r, info, grpcMessage(err))
|
||||
return
|
||||
}
|
||||
|
||||
// Redirect to the appropriate engine page.
|
||||
switch engineType {
|
||||
case "sshca":
|
||||
http.Redirect(w, r, "/sshca", http.StatusFound)
|
||||
case "transit":
|
||||
http.Redirect(w, r, "/transit", http.StatusFound)
|
||||
case "user":
|
||||
http.Redirect(w, r, "/user", http.StatusFound)
|
||||
default:
|
||||
http.Redirect(w, r, "/dashboard", http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
func (ws *WebServer) renderDashboardWithError(w http.ResponseWriter, r *http.Request, info *TokenInfo, errMsg string) {
|
||||
token := extractCookie(r)
|
||||
mounts, _ := ws.vault.ListMounts(r.Context(), token)
|
||||
state, _ := ws.vault.Status(r.Context())
|
||||
ws.renderTemplate(w, "dashboard.html", map[string]interface{}{
|
||||
"Username": info.Username,
|
||||
"IsAdmin": info.IsAdmin,
|
||||
"Roles": info.Roles,
|
||||
"Mounts": mounts,
|
||||
"State": state,
|
||||
"MountError": errMsg,
|
||||
})
|
||||
data := ws.baseData(r, info)
|
||||
data["Roles"] = info.Roles
|
||||
data["Mounts"] = mounts
|
||||
data["State"] = state
|
||||
data["MountError"] = errMsg
|
||||
ws.renderTemplate(w, "dashboard.html", data)
|
||||
}
|
||||
|
||||
func (ws *WebServer) handlePKI(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -282,11 +361,8 @@ func (ws *WebServer) handlePKI(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"Username": info.Username,
|
||||
"IsAdmin": info.IsAdmin,
|
||||
"MountName": mountName,
|
||||
}
|
||||
data := ws.baseData(r, info)
|
||||
data["MountName"] = mountName
|
||||
|
||||
if rootPEM, err := ws.vault.GetRootCert(r.Context(), mountName); err == nil && len(rootPEM) > 0 {
|
||||
if cert, err := parsePEMCert(rootPEM); err == nil {
|
||||
@@ -482,15 +558,12 @@ func (ws *WebServer) handleIssuerDetail(w http.ResponseWriter, r *http.Request)
|
||||
certs[i].IssuedBy = ws.resolveUser(certs[i].IssuedBy)
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"Username": info.Username,
|
||||
"IsAdmin": info.IsAdmin,
|
||||
"MountName": mountName,
|
||||
"IssuerName": issuerName,
|
||||
"Certs": certs,
|
||||
"NameFilter": r.URL.Query().Get("name"),
|
||||
"SortBy": sortBy,
|
||||
}
|
||||
data := ws.baseData(r, info)
|
||||
data["MountName"] = mountName
|
||||
data["IssuerName"] = issuerName
|
||||
data["Certs"] = certs
|
||||
data["NameFilter"] = r.URL.Query().Get("name")
|
||||
data["SortBy"] = sortBy
|
||||
|
||||
ws.renderTemplate(w, "issuer_detail.html", data)
|
||||
}
|
||||
@@ -640,12 +713,10 @@ func (ws *WebServer) handleCertDetail(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
cert.IssuedBy = ws.resolveUser(cert.IssuedBy)
|
||||
cert.RevokedBy = ws.resolveUser(cert.RevokedBy)
|
||||
ws.renderTemplate(w, "cert_detail.html", map[string]interface{}{
|
||||
"Username": info.Username,
|
||||
"IsAdmin": info.IsAdmin,
|
||||
"MountName": mountName,
|
||||
"Cert": cert,
|
||||
})
|
||||
data := ws.baseData(r, info)
|
||||
data["MountName"] = mountName
|
||||
data["Cert"] = cert
|
||||
ws.renderTemplate(w, "cert_detail.html", data)
|
||||
}
|
||||
|
||||
func (ws *WebServer) handleCertDownload(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -776,12 +847,9 @@ func (ws *WebServer) handleSignCSR(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"Username": info.Username,
|
||||
"IsAdmin": info.IsAdmin,
|
||||
"MountName": mountName,
|
||||
"SignedCert": signed,
|
||||
}
|
||||
data := ws.baseData(r, info)
|
||||
data["MountName"] = mountName
|
||||
data["SignedCert"] = signed
|
||||
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
|
||||
@@ -800,12 +868,9 @@ func (ws *WebServer) handleSignCSR(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
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,
|
||||
"MountName": mountName,
|
||||
"Error": errMsg,
|
||||
}
|
||||
data := ws.baseData(r, info)
|
||||
data["MountName"] = mountName
|
||||
data["Error"] = errMsg
|
||||
|
||||
if rootPEM, err := ws.vault.GetRootCert(r.Context(), mountName); err == nil && len(rootPEM) > 0 {
|
||||
if cert, err := parsePEMCert(rootPEM); err == nil {
|
||||
@@ -825,16 +890,45 @@ func (ws *WebServer) renderPKIWithError(w http.ResponseWriter, r *http.Request,
|
||||
}
|
||||
|
||||
func (ws *WebServer) findCAMount(r *http.Request, token string) (string, error) {
|
||||
return ws.findMount(r, token, "ca")
|
||||
}
|
||||
|
||||
func (ws *WebServer) findSSHCAMount(r *http.Request, token string) (string, error) {
|
||||
return ws.findMount(r, token, "sshca")
|
||||
}
|
||||
|
||||
func (ws *WebServer) findTransitMount(r *http.Request, token string) (string, error) {
|
||||
return ws.findMount(r, token, "transit")
|
||||
}
|
||||
|
||||
func (ws *WebServer) findUserMount(r *http.Request, token string) (string, error) {
|
||||
return ws.findMount(r, token, "user")
|
||||
}
|
||||
|
||||
func (ws *WebServer) findMount(r *http.Request, token, engineType string) (string, error) {
|
||||
mounts, err := ws.vault.ListMounts(r.Context(), token)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, m := range mounts {
|
||||
if m.Type == "ca" {
|
||||
if m.Type == engineType {
|
||||
return m.Name, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("no CA engine mounted")
|
||||
return "", fmt.Errorf("no %s engine mounted", engineType)
|
||||
}
|
||||
|
||||
// mountTypes returns a set of engine types that are currently mounted.
|
||||
func (ws *WebServer) mountTypes(r *http.Request, token string) map[string]bool {
|
||||
mounts, err := ws.vault.ListMounts(r.Context(), token)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
types := make(map[string]bool, len(mounts))
|
||||
for _, m := range mounts {
|
||||
types[m.Type] = true
|
||||
}
|
||||
return types
|
||||
}
|
||||
|
||||
func (ws *WebServer) handlePolicy(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -848,11 +942,9 @@ func (ws *WebServer) handlePolicy(w http.ResponseWriter, r *http.Request) {
|
||||
if err != nil {
|
||||
rules = []PolicyRule{}
|
||||
}
|
||||
ws.renderTemplate(w, "policy.html", map[string]interface{}{
|
||||
"Username": info.Username,
|
||||
"IsAdmin": info.IsAdmin,
|
||||
"Rules": rules,
|
||||
})
|
||||
data := ws.baseData(r, info)
|
||||
data["Rules"] = rules
|
||||
ws.renderTemplate(w, "policy.html", data)
|
||||
}
|
||||
|
||||
func (ws *WebServer) handlePolicyCreate(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -927,12 +1019,23 @@ func (ws *WebServer) handlePolicyDelete(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
func (ws *WebServer) renderPolicyWithError(w http.ResponseWriter, r *http.Request, info *TokenInfo, token, errMsg string) {
|
||||
rules, _ := ws.vault.ListPolicies(r.Context(), token)
|
||||
ws.renderTemplate(w, "policy.html", map[string]interface{}{
|
||||
"Username": info.Username,
|
||||
"IsAdmin": info.IsAdmin,
|
||||
"Rules": rules,
|
||||
"Error": errMsg,
|
||||
})
|
||||
data := ws.baseData(r, info)
|
||||
data["Rules"] = rules
|
||||
data["Error"] = errMsg
|
||||
ws.renderTemplate(w, "policy.html", data)
|
||||
}
|
||||
|
||||
// baseData returns a template data map pre-populated with user info and nav flags.
|
||||
func (ws *WebServer) baseData(r *http.Request, info *TokenInfo) map[string]interface{} {
|
||||
token := extractCookie(r)
|
||||
types := ws.mountTypes(r, token)
|
||||
return map[string]interface{}{
|
||||
"Username": info.Username,
|
||||
"IsAdmin": info.IsAdmin,
|
||||
"HasSSHCA": types["sshca"],
|
||||
"HasTransit": types["transit"],
|
||||
"HasUser": types["user"],
|
||||
}
|
||||
}
|
||||
|
||||
// grpcMessage extracts a human-readable message from a gRPC error.
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
// vaultBackend is the interface used by WebServer to communicate with the vault.
|
||||
// It is satisfied by *VaultClient and can be replaced with a mock in tests.
|
||||
type vaultBackend interface {
|
||||
// System
|
||||
Status(ctx context.Context) (string, error)
|
||||
Init(ctx context.Context, password string) error
|
||||
Unseal(ctx context.Context, password string) error
|
||||
@@ -30,6 +31,9 @@ type vaultBackend interface {
|
||||
ValidateToken(ctx context.Context, token string) (*TokenInfo, error)
|
||||
ListMounts(ctx context.Context, token string) ([]MountInfo, error)
|
||||
Mount(ctx context.Context, token, name, engineType string, config map[string]interface{}) error
|
||||
Close() error
|
||||
|
||||
// PKI / CA
|
||||
GetRootCert(ctx context.Context, mount string) ([]byte, error)
|
||||
GetIssuerCert(ctx context.Context, mount, issuer string) ([]byte, error)
|
||||
ImportRoot(ctx context.Context, token, mount, certPEM, keyPEM string) error
|
||||
@@ -41,11 +45,54 @@ type vaultBackend interface {
|
||||
ListCerts(ctx context.Context, token, mount string) ([]CertSummary, error)
|
||||
RevokeCert(ctx context.Context, token, mount, serial string) error
|
||||
DeleteCert(ctx context.Context, token, mount, serial string) error
|
||||
|
||||
// Policy
|
||||
ListPolicies(ctx context.Context, token string) ([]PolicyRule, error)
|
||||
GetPolicy(ctx context.Context, token, id string) (*PolicyRule, error)
|
||||
CreatePolicy(ctx context.Context, token string, rule PolicyRule) (*PolicyRule, error)
|
||||
DeletePolicy(ctx context.Context, token, id string) error
|
||||
Close() error
|
||||
|
||||
// SSH CA
|
||||
GetSSHCAPublicKey(ctx context.Context, mount string) (*SSHCAPublicKey, error)
|
||||
SSHCASignHost(ctx context.Context, token, mount string, req SSHCASignRequest) (*SSHCASignResult, error)
|
||||
SSHCASignUser(ctx context.Context, token, mount string, req SSHCASignRequest) (*SSHCASignResult, error)
|
||||
ListSSHCAProfiles(ctx context.Context, token, mount string) ([]SSHCAProfileSummary, error)
|
||||
GetSSHCAProfile(ctx context.Context, token, mount, name string) (*SSHCAProfile, error)
|
||||
CreateSSHCAProfile(ctx context.Context, token, mount string, req SSHCAProfileRequest) error
|
||||
UpdateSSHCAProfile(ctx context.Context, token, mount, name string, req SSHCAProfileRequest) error
|
||||
DeleteSSHCAProfile(ctx context.Context, token, mount, name string) error
|
||||
ListSSHCACerts(ctx context.Context, token, mount string) ([]SSHCACertSummary, error)
|
||||
GetSSHCACert(ctx context.Context, token, mount, serial string) (*SSHCACertDetail, error)
|
||||
RevokeSSHCACert(ctx context.Context, token, mount, serial string) error
|
||||
DeleteSSHCACert(ctx context.Context, token, mount, serial string) error
|
||||
GetSSHCAKRL(ctx context.Context, mount string) ([]byte, error)
|
||||
|
||||
// Transit
|
||||
ListTransitKeys(ctx context.Context, token, mount string) ([]TransitKeySummary, error)
|
||||
GetTransitKey(ctx context.Context, token, mount, name string) (*TransitKeyDetail, error)
|
||||
CreateTransitKey(ctx context.Context, token, mount, name, keyType string) error
|
||||
DeleteTransitKey(ctx context.Context, token, mount, name string) error
|
||||
RotateTransitKey(ctx context.Context, token, mount, name string) error
|
||||
UpdateTransitKeyConfig(ctx context.Context, token, mount, name string, minDecryptVersion int, allowDeletion bool) error
|
||||
TrimTransitKey(ctx context.Context, token, mount, name string) (int, error)
|
||||
TransitEncrypt(ctx context.Context, token, mount, key, plaintext, transitCtx string) (string, error)
|
||||
TransitDecrypt(ctx context.Context, token, mount, key, ciphertext, transitCtx string) (string, error)
|
||||
TransitRewrap(ctx context.Context, token, mount, key, ciphertext, transitCtx string) (string, error)
|
||||
TransitSign(ctx context.Context, token, mount, key, input string) (string, error)
|
||||
TransitVerify(ctx context.Context, token, mount, key, input, signature string) (bool, error)
|
||||
TransitHMAC(ctx context.Context, token, mount, key, input string) (string, error)
|
||||
GetTransitPublicKey(ctx context.Context, token, mount, name string) (string, error)
|
||||
|
||||
// User (E2E encryption)
|
||||
UserRegister(ctx context.Context, token, mount string) (*UserKeyInfo, error)
|
||||
UserProvision(ctx context.Context, token, mount, username string) (*UserKeyInfo, error)
|
||||
GetUserPublicKey(ctx context.Context, token, mount, username string) (*UserKeyInfo, error)
|
||||
ListUsers(ctx context.Context, token, mount string) ([]string, error)
|
||||
UserEncrypt(ctx context.Context, token, mount, plaintext, metadata string, recipients []string) (string, error)
|
||||
UserDecrypt(ctx context.Context, token, mount, envelope string) (*UserDecryptResult, error)
|
||||
UserReEncrypt(ctx context.Context, token, mount, envelope string) (string, error)
|
||||
UserRotateKey(ctx context.Context, token, mount string) (*UserKeyInfo, error)
|
||||
UserDeleteUser(ctx context.Context, token, mount, username string) error
|
||||
}
|
||||
|
||||
const userCacheTTL = 5 * time.Minute
|
||||
|
||||
400
internal/webserver/sshca.go
Normal file
400
internal/webserver/sshca.go
Normal file
@@ -0,0 +1,400 @@
|
||||
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)
|
||||
}
|
||||
417
internal/webserver/transit.go
Normal file
417
internal/webserver/transit.go
Normal file
@@ -0,0 +1,417 @@
|
||||
package webserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
func (ws *WebServer) handleTransit(w http.ResponseWriter, r *http.Request) {
|
||||
info := tokenInfoFromContext(r.Context())
|
||||
token := extractCookie(r)
|
||||
|
||||
mountName, err := ws.findTransitMount(r, token)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/dashboard", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
data := ws.baseData(r, info)
|
||||
data["MountName"] = mountName
|
||||
|
||||
if keys, err := ws.vault.ListTransitKeys(r.Context(), token, mountName); err == nil {
|
||||
data["Keys"] = keys
|
||||
}
|
||||
|
||||
ws.renderTemplate(w, "transit.html", data)
|
||||
}
|
||||
|
||||
func (ws *WebServer) handleTransitKeyDetail(w http.ResponseWriter, r *http.Request) {
|
||||
info := tokenInfoFromContext(r.Context())
|
||||
token := extractCookie(r)
|
||||
|
||||
mountName, err := ws.findTransitMount(r, token)
|
||||
if err != nil {
|
||||
http.Error(w, "no transit engine mounted", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
name := chi.URLParam(r, "name")
|
||||
key, err := ws.vault.GetTransitKey(r.Context(), token, mountName, name)
|
||||
if err != nil {
|
||||
st, _ := status.FromError(err)
|
||||
if st.Code() == codes.NotFound {
|
||||
http.Error(w, "key not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
http.Error(w, grpcMessage(err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
data := ws.baseData(r, info)
|
||||
data["MountName"] = mountName
|
||||
data["Key"] = key
|
||||
|
||||
// Fetch public key for asymmetric signing keys.
|
||||
switch key.Type {
|
||||
case "ed25519", "ecdsa-p256", "ecdsa-p384":
|
||||
if pubkey, err := ws.vault.GetTransitPublicKey(r.Context(), token, mountName, name); err == nil {
|
||||
data["PublicKey"] = pubkey
|
||||
}
|
||||
}
|
||||
|
||||
ws.renderTemplate(w, "transit_key_detail.html", data)
|
||||
}
|
||||
|
||||
func (ws *WebServer) handleTransitCreateKey(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.findTransitMount(r, token)
|
||||
if err != nil {
|
||||
http.Error(w, "no transit 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.renderTransitWithError(w, r, mountName, info, "Key name is required")
|
||||
return
|
||||
}
|
||||
|
||||
keyType := r.FormValue("type")
|
||||
if keyType == "" {
|
||||
keyType = "aes256-gcm"
|
||||
}
|
||||
|
||||
if err := ws.vault.CreateTransitKey(r.Context(), token, mountName, name, keyType); err != nil {
|
||||
ws.renderTransitWithError(w, r, mountName, info, grpcMessage(err))
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/transit/key/"+name, http.StatusFound)
|
||||
}
|
||||
|
||||
func (ws *WebServer) handleTransitRotateKey(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.findTransitMount(r, token)
|
||||
if err != nil {
|
||||
http.Error(w, "no transit engine mounted", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
name := chi.URLParam(r, "name")
|
||||
if err := ws.vault.RotateTransitKey(r.Context(), token, mountName, name); err != nil {
|
||||
http.Error(w, grpcMessage(err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/transit/key/"+name, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (ws *WebServer) handleTransitUpdateConfig(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.findTransitMount(r, token)
|
||||
if err != nil {
|
||||
http.Error(w, "no transit engine mounted", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
name := chi.URLParam(r, "name")
|
||||
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
|
||||
_ = r.ParseForm()
|
||||
|
||||
minDecrypt := 0
|
||||
if v := r.FormValue("min_decryption_version"); v != "" {
|
||||
if n, err := strconv.Atoi(v); err == nil {
|
||||
minDecrypt = n
|
||||
}
|
||||
}
|
||||
allowDeletion := r.FormValue("allow_deletion") == "on"
|
||||
|
||||
if err := ws.vault.UpdateTransitKeyConfig(r.Context(), token, mountName, name, minDecrypt, allowDeletion); err != nil {
|
||||
http.Error(w, grpcMessage(err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/transit/key/"+name, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (ws *WebServer) handleTransitTrimKey(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.findTransitMount(r, token)
|
||||
if err != nil {
|
||||
http.Error(w, "no transit engine mounted", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
name := chi.URLParam(r, "name")
|
||||
if _, err := ws.vault.TrimTransitKey(r.Context(), token, mountName, name); err != nil {
|
||||
http.Error(w, grpcMessage(err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/transit/key/"+name, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (ws *WebServer) handleTransitDeleteKey(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.findTransitMount(r, token)
|
||||
if err != nil {
|
||||
http.Error(w, "no transit engine mounted", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
name := chi.URLParam(r, "name")
|
||||
if err := ws.vault.DeleteTransitKey(r.Context(), token, mountName, name); err != nil {
|
||||
http.Error(w, grpcMessage(err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/transit", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (ws *WebServer) handleTransitEncrypt(w http.ResponseWriter, r *http.Request) {
|
||||
info := tokenInfoFromContext(r.Context())
|
||||
token := extractCookie(r)
|
||||
|
||||
mountName, err := ws.findTransitMount(r, token)
|
||||
if err != nil {
|
||||
http.Error(w, "no transit engine mounted", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
|
||||
_ = r.ParseForm()
|
||||
|
||||
key := r.FormValue("key")
|
||||
plaintext := r.FormValue("plaintext")
|
||||
transitCtx := r.FormValue("context")
|
||||
|
||||
if key == "" || plaintext == "" {
|
||||
ws.renderTransitWithError(w, r, mountName, info, "Key and plaintext are required")
|
||||
return
|
||||
}
|
||||
|
||||
ciphertext, err := ws.vault.TransitEncrypt(r.Context(), token, mountName, key, plaintext, transitCtx)
|
||||
if err != nil {
|
||||
ws.renderTransitWithError(w, r, mountName, info, grpcMessage(err))
|
||||
return
|
||||
}
|
||||
|
||||
ws.renderTransitWithResult(w, r, mountName, info, "EncryptResult", ciphertext)
|
||||
}
|
||||
|
||||
func (ws *WebServer) handleTransitDecrypt(w http.ResponseWriter, r *http.Request) {
|
||||
info := tokenInfoFromContext(r.Context())
|
||||
token := extractCookie(r)
|
||||
|
||||
mountName, err := ws.findTransitMount(r, token)
|
||||
if err != nil {
|
||||
http.Error(w, "no transit engine mounted", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
|
||||
_ = r.ParseForm()
|
||||
|
||||
key := r.FormValue("key")
|
||||
ciphertext := r.FormValue("ciphertext")
|
||||
transitCtx := r.FormValue("context")
|
||||
|
||||
if key == "" || ciphertext == "" {
|
||||
ws.renderTransitWithError(w, r, mountName, info, "Key and ciphertext are required")
|
||||
return
|
||||
}
|
||||
|
||||
plaintext, err := ws.vault.TransitDecrypt(r.Context(), token, mountName, key, ciphertext, transitCtx)
|
||||
if err != nil {
|
||||
ws.renderTransitWithError(w, r, mountName, info, grpcMessage(err))
|
||||
return
|
||||
}
|
||||
|
||||
ws.renderTransitWithResult(w, r, mountName, info, "DecryptResult", plaintext)
|
||||
}
|
||||
|
||||
func (ws *WebServer) handleTransitRewrap(w http.ResponseWriter, r *http.Request) {
|
||||
info := tokenInfoFromContext(r.Context())
|
||||
token := extractCookie(r)
|
||||
|
||||
mountName, err := ws.findTransitMount(r, token)
|
||||
if err != nil {
|
||||
http.Error(w, "no transit engine mounted", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
|
||||
_ = r.ParseForm()
|
||||
|
||||
key := r.FormValue("key")
|
||||
ciphertext := r.FormValue("ciphertext")
|
||||
transitCtx := r.FormValue("context")
|
||||
|
||||
if key == "" || ciphertext == "" {
|
||||
ws.renderTransitWithError(w, r, mountName, info, "Key and ciphertext are required")
|
||||
return
|
||||
}
|
||||
|
||||
newCiphertext, err := ws.vault.TransitRewrap(r.Context(), token, mountName, key, ciphertext, transitCtx)
|
||||
if err != nil {
|
||||
ws.renderTransitWithError(w, r, mountName, info, grpcMessage(err))
|
||||
return
|
||||
}
|
||||
|
||||
ws.renderTransitWithResult(w, r, mountName, info, "RewrapResult", newCiphertext)
|
||||
}
|
||||
|
||||
func (ws *WebServer) handleTransitSign(w http.ResponseWriter, r *http.Request) {
|
||||
info := tokenInfoFromContext(r.Context())
|
||||
token := extractCookie(r)
|
||||
|
||||
mountName, err := ws.findTransitMount(r, token)
|
||||
if err != nil {
|
||||
http.Error(w, "no transit engine mounted", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
|
||||
_ = r.ParseForm()
|
||||
|
||||
key := r.FormValue("key")
|
||||
input := r.FormValue("input")
|
||||
|
||||
if key == "" || input == "" {
|
||||
ws.renderTransitWithError(w, r, mountName, info, "Key and input are required")
|
||||
return
|
||||
}
|
||||
|
||||
signature, err := ws.vault.TransitSign(r.Context(), token, mountName, key, input)
|
||||
if err != nil {
|
||||
ws.renderTransitWithError(w, r, mountName, info, grpcMessage(err))
|
||||
return
|
||||
}
|
||||
|
||||
ws.renderTransitWithResult(w, r, mountName, info, "SignResult", signature)
|
||||
}
|
||||
|
||||
func (ws *WebServer) handleTransitVerify(w http.ResponseWriter, r *http.Request) {
|
||||
info := tokenInfoFromContext(r.Context())
|
||||
token := extractCookie(r)
|
||||
|
||||
mountName, err := ws.findTransitMount(r, token)
|
||||
if err != nil {
|
||||
http.Error(w, "no transit engine mounted", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
|
||||
_ = r.ParseForm()
|
||||
|
||||
key := r.FormValue("key")
|
||||
input := r.FormValue("input")
|
||||
signature := r.FormValue("signature")
|
||||
|
||||
if key == "" || input == "" || signature == "" {
|
||||
ws.renderTransitWithError(w, r, mountName, info, "Key, input, and signature are required")
|
||||
return
|
||||
}
|
||||
|
||||
valid, err := ws.vault.TransitVerify(r.Context(), token, mountName, key, input, signature)
|
||||
if err != nil {
|
||||
ws.renderTransitWithError(w, r, mountName, info, grpcMessage(err))
|
||||
return
|
||||
}
|
||||
|
||||
ws.renderTransitWithResult(w, r, mountName, info, "VerifyResult", valid)
|
||||
}
|
||||
|
||||
func (ws *WebServer) handleTransitHMAC(w http.ResponseWriter, r *http.Request) {
|
||||
info := tokenInfoFromContext(r.Context())
|
||||
token := extractCookie(r)
|
||||
|
||||
mountName, err := ws.findTransitMount(r, token)
|
||||
if err != nil {
|
||||
http.Error(w, "no transit engine mounted", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
|
||||
_ = r.ParseForm()
|
||||
|
||||
key := r.FormValue("key")
|
||||
input := r.FormValue("input")
|
||||
|
||||
if key == "" || input == "" {
|
||||
ws.renderTransitWithError(w, r, mountName, info, "Key and input are required")
|
||||
return
|
||||
}
|
||||
|
||||
hmac, err := ws.vault.TransitHMAC(r.Context(), token, mountName, key, input)
|
||||
if err != nil {
|
||||
ws.renderTransitWithError(w, r, mountName, info, grpcMessage(err))
|
||||
return
|
||||
}
|
||||
|
||||
ws.renderTransitWithResult(w, r, mountName, info, "HMACResult", hmac)
|
||||
}
|
||||
|
||||
func (ws *WebServer) renderTransitWithError(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 keys, err := ws.vault.ListTransitKeys(r.Context(), token, mountName); err == nil {
|
||||
data["Keys"] = keys
|
||||
}
|
||||
|
||||
ws.renderTemplate(w, "transit.html", data)
|
||||
}
|
||||
|
||||
func (ws *WebServer) renderTransitWithResult(w http.ResponseWriter, r *http.Request, mountName string, info *TokenInfo, resultKey string, result interface{}) {
|
||||
token := extractCookie(r)
|
||||
data := ws.baseData(r, info)
|
||||
data["MountName"] = mountName
|
||||
data[resultKey] = result
|
||||
|
||||
if keys, err := ws.vault.ListTransitKeys(r.Context(), token, mountName); err == nil {
|
||||
data["Keys"] = keys
|
||||
}
|
||||
|
||||
ws.renderTemplate(w, "transit.html", data)
|
||||
}
|
||||
277
internal/webserver/user.go
Normal file
277
internal/webserver/user.go
Normal file
@@ -0,0 +1,277 @@
|
||||
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) handleUser(w http.ResponseWriter, r *http.Request) {
|
||||
info := tokenInfoFromContext(r.Context())
|
||||
token := extractCookie(r)
|
||||
|
||||
mountName, err := ws.findUserMount(r, token)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/dashboard", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
data := ws.baseData(r, info)
|
||||
data["MountName"] = mountName
|
||||
|
||||
// Try to fetch the caller's own key.
|
||||
if keyInfo, err := ws.vault.GetUserPublicKey(r.Context(), token, mountName, info.Username); err == nil {
|
||||
data["OwnKey"] = keyInfo
|
||||
}
|
||||
|
||||
if users, err := ws.vault.ListUsers(r.Context(), token, mountName); err == nil {
|
||||
data["Users"] = users
|
||||
}
|
||||
|
||||
ws.renderTemplate(w, "user.html", data)
|
||||
}
|
||||
|
||||
func (ws *WebServer) handleUserRegister(w http.ResponseWriter, r *http.Request) {
|
||||
info := tokenInfoFromContext(r.Context())
|
||||
token := extractCookie(r)
|
||||
|
||||
mountName, err := ws.findUserMount(r, token)
|
||||
if err != nil {
|
||||
http.Error(w, "no user engine mounted", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := ws.vault.UserRegister(r.Context(), token, mountName); err != nil {
|
||||
ws.renderUserWithError(w, r, mountName, info, grpcMessage(err))
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/user", http.StatusFound)
|
||||
}
|
||||
|
||||
func (ws *WebServer) handleUserProvision(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.findUserMount(r, token)
|
||||
if err != nil {
|
||||
http.Error(w, "no user engine mounted", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
|
||||
_ = r.ParseForm()
|
||||
|
||||
username := strings.TrimSpace(r.FormValue("username"))
|
||||
if username == "" {
|
||||
ws.renderUserWithError(w, r, mountName, info, "Username is required")
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := ws.vault.UserProvision(r.Context(), token, mountName, username); err != nil {
|
||||
ws.renderUserWithError(w, r, mountName, info, grpcMessage(err))
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/user", http.StatusFound)
|
||||
}
|
||||
|
||||
func (ws *WebServer) handleUserKeyDetail(w http.ResponseWriter, r *http.Request) {
|
||||
info := tokenInfoFromContext(r.Context())
|
||||
token := extractCookie(r)
|
||||
|
||||
mountName, err := ws.findUserMount(r, token)
|
||||
if err != nil {
|
||||
http.Error(w, "no user engine mounted", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
username := chi.URLParam(r, "username")
|
||||
keyInfo, err := ws.vault.GetUserPublicKey(r.Context(), token, mountName, username)
|
||||
if err != nil {
|
||||
st, _ := status.FromError(err)
|
||||
if st.Code() == codes.NotFound {
|
||||
http.Error(w, "user not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
http.Error(w, grpcMessage(err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
data := ws.baseData(r, info)
|
||||
data["MountName"] = mountName
|
||||
data["KeyInfo"] = keyInfo
|
||||
ws.renderTemplate(w, "user_key_detail.html", data)
|
||||
}
|
||||
|
||||
func (ws *WebServer) handleUserEncrypt(w http.ResponseWriter, r *http.Request) {
|
||||
info := tokenInfoFromContext(r.Context())
|
||||
token := extractCookie(r)
|
||||
|
||||
mountName, err := ws.findUserMount(r, token)
|
||||
if err != nil {
|
||||
http.Error(w, "no user engine mounted", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
|
||||
_ = r.ParseForm()
|
||||
|
||||
var recipients []string
|
||||
for _, line := range strings.Split(r.FormValue("recipients"), "\n") {
|
||||
if v := strings.TrimSpace(line); v != "" {
|
||||
recipients = append(recipients, v)
|
||||
}
|
||||
}
|
||||
plaintext := r.FormValue("plaintext")
|
||||
metadata := r.FormValue("metadata")
|
||||
|
||||
if len(recipients) == 0 || plaintext == "" {
|
||||
ws.renderUserWithError(w, r, mountName, info, "Recipients and plaintext are required")
|
||||
return
|
||||
}
|
||||
|
||||
envelope, err := ws.vault.UserEncrypt(r.Context(), token, mountName, plaintext, metadata, recipients)
|
||||
if err != nil {
|
||||
ws.renderUserWithError(w, r, mountName, info, grpcMessage(err))
|
||||
return
|
||||
}
|
||||
|
||||
ws.renderUserWithResult(w, r, mountName, info, "EncryptResult", envelope)
|
||||
}
|
||||
|
||||
func (ws *WebServer) handleUserDecrypt(w http.ResponseWriter, r *http.Request) {
|
||||
info := tokenInfoFromContext(r.Context())
|
||||
token := extractCookie(r)
|
||||
|
||||
mountName, err := ws.findUserMount(r, token)
|
||||
if err != nil {
|
||||
http.Error(w, "no user engine mounted", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
|
||||
_ = r.ParseForm()
|
||||
|
||||
envelope := r.FormValue("envelope")
|
||||
if envelope == "" {
|
||||
ws.renderUserWithError(w, r, mountName, info, "Envelope is required")
|
||||
return
|
||||
}
|
||||
|
||||
result, err := ws.vault.UserDecrypt(r.Context(), token, mountName, envelope)
|
||||
if err != nil {
|
||||
ws.renderUserWithError(w, r, mountName, info, grpcMessage(err))
|
||||
return
|
||||
}
|
||||
|
||||
ws.renderUserWithResult(w, r, mountName, info, "DecryptResult", result)
|
||||
}
|
||||
|
||||
func (ws *WebServer) handleUserReEncrypt(w http.ResponseWriter, r *http.Request) {
|
||||
info := tokenInfoFromContext(r.Context())
|
||||
token := extractCookie(r)
|
||||
|
||||
mountName, err := ws.findUserMount(r, token)
|
||||
if err != nil {
|
||||
http.Error(w, "no user engine mounted", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
|
||||
_ = r.ParseForm()
|
||||
|
||||
envelope := r.FormValue("envelope")
|
||||
if envelope == "" {
|
||||
ws.renderUserWithError(w, r, mountName, info, "Envelope is required")
|
||||
return
|
||||
}
|
||||
|
||||
newEnvelope, err := ws.vault.UserReEncrypt(r.Context(), token, mountName, envelope)
|
||||
if err != nil {
|
||||
ws.renderUserWithError(w, r, mountName, info, grpcMessage(err))
|
||||
return
|
||||
}
|
||||
|
||||
ws.renderUserWithResult(w, r, mountName, info, "ReEncryptResult", newEnvelope)
|
||||
}
|
||||
|
||||
func (ws *WebServer) handleUserRotateKey(w http.ResponseWriter, r *http.Request) {
|
||||
info := tokenInfoFromContext(r.Context())
|
||||
token := extractCookie(r)
|
||||
|
||||
mountName, err := ws.findUserMount(r, token)
|
||||
if err != nil {
|
||||
http.Error(w, "no user engine mounted", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := ws.vault.UserRotateKey(r.Context(), token, mountName); err != nil {
|
||||
ws.renderUserWithError(w, r, mountName, info, grpcMessage(err))
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/user", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (ws *WebServer) handleUserDeleteUser(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.findUserMount(r, token)
|
||||
if err != nil {
|
||||
http.Error(w, "no user engine mounted", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
username := chi.URLParam(r, "username")
|
||||
if err := ws.vault.UserDeleteUser(r.Context(), token, mountName, username); err != nil {
|
||||
http.Error(w, grpcMessage(err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/user", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (ws *WebServer) renderUserWithError(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 keyInfo, err := ws.vault.GetUserPublicKey(r.Context(), token, mountName, info.Username); err == nil {
|
||||
data["OwnKey"] = keyInfo
|
||||
}
|
||||
if users, err := ws.vault.ListUsers(r.Context(), token, mountName); err == nil {
|
||||
data["Users"] = users
|
||||
}
|
||||
|
||||
ws.renderTemplate(w, "user.html", data)
|
||||
}
|
||||
|
||||
func (ws *WebServer) renderUserWithResult(w http.ResponseWriter, r *http.Request, mountName string, info *TokenInfo, resultKey string, result interface{}) {
|
||||
token := extractCookie(r)
|
||||
data := ws.baseData(r, info)
|
||||
data["MountName"] = mountName
|
||||
data[resultKey] = result
|
||||
|
||||
if keyInfo, err := ws.vault.GetUserPublicKey(r.Context(), token, mountName, info.Username); err == nil {
|
||||
data["OwnKey"] = keyInfo
|
||||
}
|
||||
if users, err := ws.vault.ListUsers(r.Context(), token, mountName); err == nil {
|
||||
data["Users"] = users
|
||||
}
|
||||
|
||||
ws.renderTemplate(w, "user.html", data)
|
||||
}
|
||||
Reference in New Issue
Block a user