initial core-zero import implementation

This commit is contained in:
Joe Kralicky 2024-09-05 20:27:24 -04:00
parent c011957389
commit b598d139e5
No known key found for this signature in database
GPG key ID: 75C4875F34A9FB79
34 changed files with 3825 additions and 688 deletions

View file

@ -0,0 +1,105 @@
package cmd
import (
"bytes"
"context"
"fmt"
"os"
"strings"
"github.com/pomerium/pomerium/config"
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/pkg/envoy/files"
"github.com/rs/zerolog"
"github.com/spf13/cobra"
)
func BuildImportCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "import",
Short: "Import an existing configuration to a Zero cluster",
RunE: func(cmd *cobra.Command, _ []string) error {
configFlag := cmd.InheritedFlags().Lookup("config")
var configFile string
if configFlag != nil {
configFile = configFlag.Value.String()
}
if configFile == "" {
// try looking up what pid 1 is using, we are likely in a container anyway
info, err := os.ReadFile("/proc/1/cmdline")
if err == nil {
args := bytes.Split(info, []byte{0})
if len(args) > 0 && strings.Contains(string(args[0]), "pomerium") {
for i, arg := range args {
if strings.Contains(string(arg), "-config") {
if strings.Contains(string(arg), "-config=") {
configFile = strings.Split(string(arg), "=")[1]
cmd.PrintErrf("detected config file: %s\n", configFile)
} else if len(args) > i+1 {
configFile = string(args[i+1])
cmd.PrintErrf("detected config file: %s\n", configFile)
}
}
}
}
}
// try some common locations
if configFile == "" {
if _, err := os.Stat("/pomerium/config.yaml"); err == nil {
configFile = "/pomerium/config.yaml"
} else if _, err := os.Stat("/etc/pomerium/config.yaml"); err == nil {
configFile = "/etc/pomerium/config.yaml"
} else if _, err := os.Stat("config.yaml"); err == nil {
configFile = "config.yaml"
}
if configFile != "" {
cmd.PrintErrf("detected config file: %s\n", configFile)
}
}
}
if configFile == "" {
return fmt.Errorf("no config file provided")
}
log.SetLevel(zerolog.ErrorLevel)
src, err := config.NewFileOrEnvironmentSource(configFile, files.FullVersion())
if err != nil {
return err
}
cfgC := make(chan *config.Config, 1)
src.OnConfigChange(cmd.Context(), func(_ context.Context, cfg *config.Config) {
cfgC <- cfg
})
if cfg := src.GetConfig(); cfg != nil {
cfgC <- cfg
}
var cfg *config.Config
select {
case <-cmd.Context().Done():
return cmd.Context().Err()
case cfg = <-cfgC:
}
client := zeroClientFromContext(cmd.Context())
quotas, err := client.GetQuotas(cmd.Context())
if err != nil {
return fmt.Errorf("error getting quotas: %w", err)
}
converted := cfg.Options.ToProto()
ui := NewImportUI(converted, quotas)
if err := ui.Run(cmd.Context()); err != nil {
return err
}
ui.ApplySelections(converted)
_, err = client.ImportConfig(cmd.Context(), converted)
if err != nil {
return fmt.Errorf("error importing config: %w", err)
}
cmd.PrintErrln("config imported successfully")
return nil
},
}
return cmd
}

View file

@ -0,0 +1,71 @@
package cmd
import (
"context"
"errors"
zero "github.com/pomerium/pomerium/internal/zero/api"
"github.com/spf13/cobra"
)
type zeroClientContextKeyType struct{}
var zeroClientContextKey zeroClientContextKeyType
func zeroClientFromContext(ctx context.Context) *zero.API {
return ctx.Value(zeroClientContextKey).(*zero.API)
}
func BuildRootCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "zero",
Short: "Interact with the Pomerium Zero cloud service",
PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
configFlag := cmd.InheritedFlags().Lookup("config")
var configFile string
if configFlag != nil {
configFile = configFlag.Value.String()
}
if err := setupLogger(); err != nil {
return err
}
var token string
if tokenFlag := cmd.InheritedFlags().Lookup("token"); tokenFlag != nil && tokenFlag.Changed {
token = tokenFlag.Value.String()
} else {
token = getToken(configFile)
}
if token == "" {
return errors.New("no token provided")
}
var clusterAPIEndpoint string
if endpointFlag := cmd.InheritedFlags().Lookup("cluster-api-endpoint"); endpointFlag != nil && endpointFlag.Changed {
clusterAPIEndpoint = endpointFlag.Value.String()
} else {
clusterAPIEndpoint = getClusterAPIEndpoint()
}
client, err := zero.NewAPI(cmd.Context(),
zero.WithAPIToken(token),
zero.WithClusterAPIEndpoint(clusterAPIEndpoint),
zero.WithConnectAPIEndpoint(getConnectAPIEndpoint()),
zero.WithOTELEndpoint(getOTELAPIEndpoint()),
)
if err != nil {
return err
}
cmd.SetContext(context.WithValue(cmd.Context(), zeroClientContextKey, client))
return nil
},
}
cmd.AddCommand(BuildImportCmd())
cmd.PersistentFlags().String("config", "", "Specify configuration file location")
cmd.PersistentFlags().String("token", "", "Pomerium Zero Token (default: $POMERIUM_ZERO_TOKEN)")
cmd.PersistentFlags().String("cluster-api-endpoint", "", "Pomerium Zero Cluster API Endpoint (default: $CLUSTER_API_ENDPOINT)")
cmd.PersistentFlags().Lookup("cluster-api-endpoint").Hidden = true
return cmd
}

View file

@ -0,0 +1,7 @@
package cmd
import "github.com/charmbracelet/huh"
func (ui *ImportUI) XForm() *huh.Form {
return ui.form
}

View file

@ -0,0 +1,538 @@
package cmd
import (
"bytes"
"context"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
"os"
"slices"
"strconv"
"strings"
"github.com/charmbracelet/huh"
"github.com/charmbracelet/lipgloss"
http_connection_managerv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
"github.com/muesli/termenv"
"github.com/pomerium/pomerium/config"
"github.com/pomerium/pomerium/internal/log"
configpb "github.com/pomerium/pomerium/pkg/grpc/config"
cluster_api "github.com/pomerium/pomerium/pkg/zero/cluster"
"github.com/pomerium/pomerium/pkg/zero/importutil"
"github.com/pomerium/protoutil/fieldmasks"
"github.com/pomerium/protoutil/paths"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/reflect/protopath"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/known/fieldmaskpb"
)
type onCursorUpdate struct {
Field interface{ Cursor() int }
}
func (u onCursorUpdate) Hash() (uint64, error) {
return uint64(u.Field.Cursor()), nil
}
var (
yellowText = lipgloss.NewStyle().Foreground(lipgloss.ANSIColor(3))
faintText = lipgloss.NewStyle().Faint(true).UnsetForeground()
redText = lipgloss.NewStyle().Foreground(lipgloss.ANSIColor(1))
)
func errText(err error) string {
return redText.Render(fmt.Sprintf("(error: %v)", err))
}
func certInfoFromSettingsCertificate(v protoreflect.Value) string {
switch v := v.Interface().(type) {
case protoreflect.List:
buf := bytes.Buffer{}
for i, l := 0, v.Len(); i < l; i++ {
crtBytes := string(v.Get(i).Message().Interface().(*configpb.Settings_Certificate).GetCertBytes())
buf.WriteString(crtBytes)
if i < l-1 {
buf.WriteRune('\n')
}
}
return certInfoFromBytes(buf.Bytes())
case protoreflect.Message:
crtBytes := string(v.Interface().(*configpb.Settings_Certificate).GetCertBytes())
return certInfoFromBytes([]byte(crtBytes))
default:
panic(fmt.Sprintf("bug: unexpected value type %T", v))
}
}
func certInfoFromBase64(v protoreflect.Value) string {
crtBytes, err := base64.StdEncoding.DecodeString(v.String())
if err != nil {
return errText(err)
}
return certInfoFromBytes(crtBytes)
}
func certInfoFromBytes(b []byte) string {
if len(b) == 0 {
return faintText.Render("(empty)")
}
block, rest := pem.Decode(b)
if block == nil {
return errText(errors.New("no PEM data found"))
}
extraBlocks := []*pem.Block{}
for len(rest) > 0 {
block, rest = pem.Decode(rest)
if block != nil {
extraBlocks = append(extraBlocks, block)
}
}
blockType := block.Type
var info string
switch block.Type {
case "X509 CRL":
crl, err := x509.ParseRevocationList(block.Bytes)
if err != nil {
return errText(err)
}
info = fmt.Sprintf("%d entries", len(crl.RevokedCertificateEntries))
default:
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return errText(err)
}
info = *importutil.GenerateCertName(cert)
}
out := yellowText.Render(fmt.Sprintf("(%s: %s)", blockType, info))
if len(extraBlocks) > 0 {
s := ""
if len(extraBlocks) != 1 {
s = "s"
}
out += faintText.Render(fmt.Sprintf(" ...+%d block%s", len(extraBlocks), s))
}
return out
}
func secret(s protoreflect.Value) string {
length := len(s.String())
return yellowText.Render(fmt.Sprintf("(secret: %d bytes)", length))
}
var customSettingsInfoByPath = map[string]func(v protoreflect.Value) string{
"(pomerium.config.Settings).metrics_certificate": certInfoFromSettingsCertificate,
"(pomerium.config.Settings).metrics_client_ca": certInfoFromBase64,
"(pomerium.config.Settings).certificates": certInfoFromSettingsCertificate,
"(pomerium.config.Settings).certificate_authority": certInfoFromBase64,
"(pomerium.config.Settings).downstream_mtls.ca": certInfoFromBase64,
"(pomerium.config.Settings).downstream_mtls.crl": certInfoFromBase64,
"(pomerium.config.Settings).shared_secret": secret,
"(pomerium.config.Settings).cookie_secret": secret,
"(pomerium.config.Settings).google_cloud_serverless_authentication_service_account": secret,
"(pomerium.config.Settings).idp_client_secret": secret,
"(pomerium.config.Settings).databroker_storage_connection_string": secret,
}
type ImportHints struct {
// Indicates that the field is ignored during Zero import
Ignored bool
// Indicates that the field is entirely unsupported by Zero, and will likely
// break an existing configuration if imported. If any of these fields are
// selected, an error will be displayed.
Unsupported bool
// An optional note explaining why a field is ignored or unsupported, if
// additional context would be helpful. This message will be user facing.
Note string
// Indicates that the field is treated as a secret, and will be encrypted.
Secret bool
}
const (
noteSplitService = "split-service mode"
noteEnterpriseOnly = "enterprise only"
noteFeatureNotYetAvailable = "feature not yet available"
)
func noteCertificate(n int) string {
suffix := ""
if n != 1 {
suffix = "s"
}
return fmt.Sprintf("+%d certificate%s", n, suffix)
}
func notePolicy(n int) string {
suffix := "y"
if n != 1 {
suffix = "ies"
}
return fmt.Sprintf("+%d polic%s", n, suffix)
}
func computeSettingsImportHints(cfg *configpb.Config) map[string]ImportHints {
m := map[string]ImportHints{
"authenticate_callback_path": {Ignored: true},
"shared_secret": {Ignored: true},
"cookie_secret": {Ignored: true},
"signing_key": {Ignored: true},
"authenticate_internal_service_url": {Unsupported: true, Note: noteSplitService},
"authorize_internal_service_url": {Unsupported: true, Note: noteSplitService},
"databroker_internal_service_url": {Unsupported: true, Note: noteSplitService},
"derive_tls": {Unsupported: true, Note: noteSplitService},
"audit_key": {Unsupported: true, Note: noteEnterpriseOnly},
"primary_color": {Unsupported: true, Note: noteEnterpriseOnly},
"secondary_color": {Unsupported: true, Note: noteEnterpriseOnly},
"darkmode_primary_color": {Unsupported: true, Note: noteEnterpriseOnly},
"darkmode_secondary_color": {Unsupported: true, Note: noteEnterpriseOnly},
"logo_url": {Unsupported: true, Note: noteEnterpriseOnly},
"favicon_url": {Unsupported: true, Note: noteEnterpriseOnly},
"error_message_first_paragraph": {Unsupported: true, Note: noteEnterpriseOnly},
"use_proxy_protocol": {Unsupported: true, Note: noteFeatureNotYetAvailable},
"programmatic_redirect_domain_whitelist": {Unsupported: true, Note: noteFeatureNotYetAvailable},
"grpc_client_timeout": {Unsupported: true, Note: noteFeatureNotYetAvailable},
"grpc_client_dns_roundrobin": {Unsupported: true, Note: noteFeatureNotYetAvailable},
"envoy_bind_config_freebind": {Unsupported: true, Note: noteFeatureNotYetAvailable},
"envoy_bind_config_source_address": {Unsupported: true, Note: noteFeatureNotYetAvailable},
"google_cloud_serverless_authentication_service_account": {Secret: true},
"idp_client_secret": {Secret: true},
"databroker_storage_connection_string": {Secret: true},
"metrics_certificate": {Unsupported: true, Note: noteFeatureNotYetAvailable},
"metrics_client_ca": {Unsupported: true, Note: noteFeatureNotYetAvailable},
// "metrics_certificate": {Note: noteCertificate(1)},
// "metrics_client_ca": {Note: noteCertificate(1)},
"certificate_authority": {Note: noteCertificate(1)},
"certificates": {Note: noteCertificate(len(cfg.GetSettings().GetCertificates()))},
"downstream_mtls.crl": {Unsupported: true, Note: noteFeatureNotYetAvailable},
"downstream_mtls.ca": {Note: noteCertificate(1)},
}
if dm := cfg.GetSettings().GetDownstreamMtls(); dm != nil {
if dm.Enforcement != nil {
switch *dm.Enforcement {
case configpb.MtlsEnforcementMode_POLICY:
case configpb.MtlsEnforcementMode_POLICY_WITH_DEFAULT_DENY:
case configpb.MtlsEnforcementMode_REJECT_CONNECTION:
// this is a special case - zero does not support this mode, but we cannot continue
// with a partial import because it fundamentally changes the behavior of all routes
// and policies in the system
log.Fatal().Msg("downstream mtls enforcement mode 'reject_connection' is not supported")
}
}
}
if cfg.GetSettings().GetServices() != "all" {
m["services"] = ImportHints{Ignored: true, Note: `only "all" is supported`}
}
if cfg.GetSettings().GetCodecType() != http_connection_managerv3.HttpConnectionManager_AUTO {
m["codec_type"] = ImportHints{Ignored: true, Note: `only "auto" is supported`}
}
return m
}
type ImportUI struct {
form *huh.Form
selectedSettings []string
selectedRoutes []string
}
func NewImportUI(cfg *configpb.Config, quotas *cluster_api.ConfigQuotas) *ImportUI {
settingsImportHints := computeSettingsImportHints(cfg)
presentSettings := fieldmasks.Leaves(
fieldmasks.Diff(
config.NewDefaultOptions().ToProto().GetSettings().ProtoReflect(),
cfg.GetSettings().ProtoReflect(),
),
cfg.Settings.ProtoReflect().Descriptor(),
)
slices.Sort(presentSettings.Paths)
settingsOptions := huh.NewOptions(presentSettings.Paths...)
ui := &ImportUI{
selectedSettings: slices.Clone(presentSettings.Paths),
}
for i, value := range presentSettings.Paths {
if hints, ok := settingsImportHints[value]; ok {
switch {
case hints.Ignored:
note := ""
if hints.Note != "" {
note = fmt.Sprintf(": %s", hints.Note)
}
settingsOptions[i].Key = fmt.Sprintf("\x1b[9m%s\x1b[29m \x1b[2m(ignored%s)\x1b[22m", settingsOptions[i].Key, note)
ui.selectedSettings[i] = ""
case hints.Unsupported:
note := ""
if hints.Note != "" {
note = fmt.Sprintf(": %s", hints.Note)
}
settingsOptions[i].Key = fmt.Sprintf("\x1b[9m%s\x1b[29m \x1b[2m(unsupported%s)\x1b[22m", settingsOptions[i].Key, note)
ui.selectedSettings[i] = ""
case hints.Secret:
settingsOptions[i].Key += " \x1b[2m(secret)\x1b[22m"
default:
if hints.Note != "" {
settingsOptions[i].Key += fmt.Sprintf(" \x1b[2m(%s)\x1b[22m", hints.Note)
}
}
}
}
ui.selectedSettings = slices.DeleteFunc(ui.selectedSettings, func(s string) bool {
return s == ""
})
settingsSelect := huh.NewMultiSelect[string]().
Filterable(false).
Title("Import Settings").
Description("Choose settings to import from your existing configuration").
Options(settingsOptions...).
Validate(func(selected []string) error {
var unsupportedCount int
for _, s := range selected {
if hints, ok := settingsImportHints[s]; ok && hints.Unsupported {
unsupportedCount++
}
}
if unsupportedCount == 1 {
return fmt.Errorf("1 selected setting is unsupported")
} else if unsupportedCount > 1 {
return fmt.Errorf("%d selected settings are unsupported", unsupportedCount)
}
return nil
}).
Value(&ui.selectedSettings)
settingsSelect.Focus()
escapeNoteText := strings.NewReplacer(
"*", "\\*",
"_", "\\_",
"`", "\\`",
)
settingsNoteDescription := func(idx int) string {
if idx < 0 || idx > len(presentSettings.Paths) {
return ""
}
path, err := paths.ParseFrom(cfg.Settings.ProtoReflect().Descriptor(), "."+presentSettings.Paths[idx])
if err != nil {
return errText(err)
}
val, err := paths.Evaluate(cfg.Settings, path)
if err != nil {
return errText(err)
}
if infoFunc, ok := customSettingsInfoByPath[path.String()]; ok {
return escapeNoteText.Replace(infoFunc(val))
}
return escapeNoteText.Replace(formatValue(path, val))
}
settingsNote := huh.NewNote().
Title(fmt.Sprintf("Value: %s", presentSettings.Paths[0])).
TitleFunc(func() string {
return fmt.Sprintf("Value: %s", presentSettings.Paths[settingsSelect.Cursor()])
}, onCursorUpdate{settingsSelect}).
Description(settingsNoteDescription(0)).
DescriptionFunc(func() string {
return settingsNoteDescription(settingsSelect.Cursor())
}, onCursorUpdate{settingsSelect}).
Height(3)
settingsNote.Focus()
routeNames := make([]string, len(cfg.Routes))
for i, name := range importutil.GenerateRouteNames(cfg.Routes) {
routeNames[i] = name
cfg.Routes[i].Name = name
}
routeOptions := huh.NewOptions(routeNames...)
for i, name := range routeNames {
if i < quotas.Routes {
ui.selectedRoutes = append(ui.selectedRoutes, name)
}
if n := includedCertificatesInRoute(cfg.Routes[i]); n > 0 {
routeOptions[i].Key += fmt.Sprintf(" \x1b[2m(%s)\x1b[22m", noteCertificate(n))
}
if n := includedPoliciesInRoute(cfg.Routes[i]); n > 0 {
routeOptions[i].Key += fmt.Sprintf(" \x1b[2m(%s)\x1b[22m", notePolicy(n))
}
}
routesSelectDescription := func() string {
return fmt.Sprintf(`
Choose routes to import from your existing configuration. Policies and
certificates associated with selected routes will also be imported.
Pomerium Zero routes require unique names. We've generated default names
from the contents of each route, but these can always be changed later on.
Selected: %d/%d`[1:], len(ui.selectedRoutes), quotas.Routes)
}
topMarginLines := 1 + len(strings.Split(routesSelectDescription(), "\n"))
routesSelect := huh.NewMultiSelect[string]().
Filterable(true).
Title("Import Routes").
Description(routesSelectDescription()).
DescriptionFunc(routesSelectDescription, &ui.selectedRoutes).
Height(min(30, len(cfg.Routes)) + topMarginLines).
Options(routeOptions...).
Validate(func(_ []string) error {
if len(ui.selectedRoutes) > quotas.Routes {
return fmt.Errorf("A maximum of %d routes can be imported", quotas.Routes) //nolint:stylecheck
}
return nil
}).
Value(&ui.selectedRoutes)
var (
labelFrom = yellowText.Render(" from: ")
labelPath = yellowText.Render(" path: ")
labelPrefix = yellowText.Render(" prefix: ")
labelRegex = yellowText.Render(" regex: ")
labelTo = yellowText.Render(" to: ")
labelRedirect = yellowText.Render("redirect: ")
labelResponse = yellowText.Render("response: ")
)
routesNoteDescription := func(idx int) string {
selected := cfg.Routes[idx]
var b strings.Builder
b.WriteString(labelFrom)
b.WriteString(selected.From)
switch {
case selected.Path != "":
b.WriteRune('\n')
b.WriteString(labelPath)
b.WriteString(selected.Path)
case selected.Prefix != "":
b.WriteRune('\n')
b.WriteString(labelPrefix)
b.WriteString(selected.Prefix)
case selected.Regex != "":
b.WriteRune('\n')
b.WriteString(labelRegex)
b.WriteString(selected.Regex)
}
switch {
case len(selected.To) > 0:
b.WriteRune('\n')
b.WriteString(labelTo)
b.WriteString(selected.To[0])
for _, t := range selected.To[1:] {
b.WriteString(", ")
b.WriteString(t)
}
case selected.Redirect != nil:
b.WriteRune('\n')
b.WriteString(labelRedirect)
b.WriteString(selected.Redirect.String())
case selected.Response != nil:
b.WriteRune('\n')
b.WriteString(labelResponse)
b.WriteString(fmt.Sprint(selected.Response.Status))
b.WriteRune(' ')
b.WriteString(strconv.Quote(selected.Response.Body))
}
return b.String()
}
routesNote := huh.NewNote().
Title("Route Info").
Description(routesNoteDescription(0)).
DescriptionFunc(func() string {
return routesNoteDescription(routesSelect.Cursor())
}, onCursorUpdate{routesSelect}).Height(3)
routesNote.Focus()
ui.form = huh.NewForm(
huh.NewGroup(settingsSelect, settingsNote),
huh.NewGroup(routesSelect, routesNote),
).WithTheme(huh.ThemeBase16())
return ui
}
func (ui *ImportUI) Run(ctx context.Context) error {
if lipgloss.ColorProfile() == termenv.Ascii &&
!termenv.EnvNoColor() && os.Getenv("TERM") != "dumb" {
lipgloss.SetColorProfile(termenv.ANSI)
}
return ui.form.RunWithContext(ctx)
}
func (ui *ImportUI) ApplySelections(cfg *configpb.Config) {
fieldmasks.ExclusiveKeep(cfg.Settings, &fieldmaskpb.FieldMask{
Paths: ui.selectedSettings,
})
cfg.Routes = slices.DeleteFunc(cfg.Routes, func(route *configpb.Route) bool {
return !slices.Contains(ui.selectedRoutes, route.Name)
})
}
func includedCertificatesInRoute(route *configpb.Route) int {
n := 0
if route.TlsClientCert != "" && route.TlsClientKey != "" {
n++
}
if route.TlsCustomCa != "" {
n++
}
if route.TlsDownstreamClientCa != "" {
n++
}
return n
}
func includedPoliciesInRoute(route *configpb.Route) int {
n := 0
for _, policy := range route.PplPolicies {
// skip over common generated policies
switch string(policy.Raw) {
case `[{"allow":{"or":[{"accept":true}]}}]`:
case `[{"allow":{"or":[{"authenticated_user":true}]}}]`:
case `[{"allow":{"or":[{"cors_preflight":true}]}}]`:
default:
n++
}
}
return n
}
func formatValue(path protopath.Path, val protoreflect.Value) string {
switch vi := val.Interface().(type) {
case protoreflect.Message:
jsonData, err := protojson.Marshal(vi.Interface())
if err != nil {
return err.Error()
}
return string(jsonData)
case protoreflect.List:
values := []string{}
for i := 0; i < vi.Len(); i++ {
values = append(values, formatValue(path, vi.Get(i)))
}
return renderStringSlice(values)
case protoreflect.Map:
values := []string{}
vi.Range(func(mk protoreflect.MapKey, v protoreflect.Value) bool {
values = append(values, mk.String()+yellowText.Render("=")+formatValue(path, v))
return true
})
slices.Sort(values)
return renderStringSlice(values)
case protoreflect.EnumNumber:
var field protoreflect.FieldDescriptor
switch step := path.Index(-1); step.Kind() {
case protopath.FieldAccessStep:
field = step.FieldDescriptor()
case protopath.ListIndexStep, protopath.MapIndexStep:
field = path.Index(-2).FieldDescriptor()
}
if field != nil {
return strings.ToLower(string(field.Enum().Values().ByNumber(vi).Name()))
}
return fmt.Sprint(vi)
default:
return val.String()
}
}
func renderStringSlice(values []string) string {
return yellowText.Render("[") + strings.Join(values, yellowText.Render(", ")) + yellowText.Render("]")
}

View file

@ -0,0 +1,112 @@
package cmd_test
import (
"context"
"embed"
"fmt"
"os"
"path/filepath"
"slices"
"strings"
"testing"
"time"
tea "github.com/charmbracelet/bubbletea"
"github.com/pomerium/pomerium/config"
"github.com/pomerium/pomerium/pkg/envoy/files"
"github.com/pomerium/pomerium/pkg/zero/cluster"
"github.com/pomerium/pomerium/pkg/zero/importutil"
"github.com/pomerium/protoutil/fieldmasks"
"github.com/charmbracelet/x/ansi"
"github.com/charmbracelet/x/exp/teatest"
"github.com/pomerium/pomerium/internal/zero/cmd"
"github.com/stretchr/testify/require"
)
//go:embed testdata
var testdata embed.FS
func TestImportUI(t *testing.T) {
tmp := t.TempDir()
require.NoError(t, os.CopyFS(tmp, testdata))
dir, err := os.Getwd()
require.NoError(t, err)
defer os.Chdir(dir)
os.Chdir(filepath.Join(tmp, "testdata"))
src, err := config.NewFileOrEnvironmentSource("config.yaml", files.FullVersion())
require.NoError(t, err)
cfgC := make(chan *config.Config, 1)
src.OnConfigChange(context.Background(), func(_ context.Context, cfg *config.Config) {
cfgC <- cfg
})
if cfg := src.GetConfig(); cfg != nil {
cfgC <- cfg
}
cfg := (<-cfgC).Options.ToProto()
ui := cmd.NewImportUI(cfg, &cluster.ConfigQuotas{
Certificates: 10,
Policies: 10,
Routes: 10,
})
form := ui.XForm()
form.SubmitCmd = tea.Quit
form.CancelCmd = tea.Quit
tm := teatest.NewTestModel(t, form, teatest.WithInitialTermSize(80, 80))
presentSettings := fieldmasks.Leaves(
fieldmasks.Diff(
config.NewDefaultOptions().ToProto().GetSettings().ProtoReflect(),
cfg.GetSettings().ProtoReflect(),
),
cfg.Settings.ProtoReflect().Descriptor(),
)
slices.Sort(presentSettings.Paths)
for i, setting := range presentSettings.Paths {
if i > 0 {
tm.Send(tea.KeyMsg{Type: tea.KeyDown})
}
var foundSelect bool
teatest.WaitFor(t, tm.Output(), func(bts []byte) bool {
str := ansi.Strip(string(bts))
if !foundSelect {
if strings.Contains(str, fmt.Sprintf("> [•] %s", setting)) ||
strings.Contains(str, fmt.Sprintf("> [ ] %s", setting)) {
foundSelect = true
}
return false
}
return strings.Contains(str, fmt.Sprintf("Value: %s", setting))
}, teatest.WithDuration(1*time.Second), teatest.WithCheckInterval(1*time.Millisecond))
}
tm.Send(tea.KeyMsg{Type: tea.KeyTab})
names := importutil.GenerateRouteNames(cfg.Routes)
for i, route := range cfg.Routes {
if i > 0 {
tm.Send(tea.KeyMsg{Type: tea.KeyDown})
}
var foundSelect bool
teatest.WaitFor(t, tm.Output(), func(bts []byte) bool {
str := ansi.Strip(string(bts))
if !foundSelect {
if strings.Contains(str, fmt.Sprintf("> [•] %s", names[i])) ||
strings.Contains(str, fmt.Sprintf("> [ ] %s", names[i])) {
foundSelect = true
}
return false
}
if i == 0 || cfg.Routes[i-1].From != route.From {
return strings.Contains(str, fmt.Sprintf("from: %s", route.From))
}
return true
}, teatest.WithDuration(1*time.Second), teatest.WithCheckInterval(1*time.Millisecond))
}
tm.Send(tea.KeyMsg{Type: tea.KeyEnter})
tm.WaitFinished(t)
}

28
internal/zero/cmd/testdata/ca.crt vendored Normal file
View file

@ -0,0 +1,28 @@
-----BEGIN CERTIFICATE-----
MIIE1zCCAz+gAwIBAgIQZ139cd/paPdkS2JyAu7kEDANBgkqhkiG9w0BAQsFADCB
gzEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMSwwKgYDVQQLDCNjYWxl
YkBjYWxlYi1wYy1saW51eCAoQ2FsZWIgRG94c2V5KTEzMDEGA1UEAwwqbWtjZXJ0
IGNhbGViQGNhbGViLXBjLWxpbnV4IChDYWxlYiBEb3hzZXkpMB4XDTIxMDgxMDE3
MzIwOVoXDTMxMDgxMDE3MzIwOVowgYMxHjAcBgNVBAoTFW1rY2VydCBkZXZlbG9w
bWVudCBDQTEsMCoGA1UECwwjY2FsZWJAY2FsZWItcGMtbGludXggKENhbGViIERv
eHNleSkxMzAxBgNVBAMMKm1rY2VydCBjYWxlYkBjYWxlYi1wYy1saW51eCAoQ2Fs
ZWIgRG94c2V5KTCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBANbKyMz5
MVW6YKdjh1oIN1Mn7PE2pH5SbJSpWxdAGhdBkBkpAa7OxarjH5KVkCTSa7oncla7
qNuJZS6mBmoxF+R+cR3jyGdUAYlozl1jlfqLIfC/+g7V7VmOJn98tjB42fatxLl6
WPAw1JDNsWtQfhKhbcHut7RsF0rMOOHcwywTR7LOyCmIel1pcmpV4hbVcT6eVwoP
HXyJSa9cqaMQ5Xrdogai4IqZZIGLHeLsTVutOgJFXEevlX/QT3sWomEctzh38Js4
9DiAPD6d4Y7/CPLYEfk29JQ9NZhpgDsi9hu5FHHZcXwf1IHlw/CBVgn6j+jmvKKz
90Ma1oquv3W6dttid/xCcLGu2S+96Tzrykmoy5VacLtVEP41YmoVls91rlo7olpe
QWFbnmco739TI/4h+HodolperQERQl7uCnpKVPZ3WokKuRh5pkqkQp/arQjtwcRt
G43CrDpbl+uSjMCAxha958eTYvtojTMnvLtsGID1hGXnqlw+5KjKrgRHrQIDAQAB
o0UwQzAOBgNVHQ8BAf8EBAMCAgQwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4E
FgQUhYZYWIBHyk6ZVTnp3lRt/tyBP00wDQYJKoZIhvcNAQELBQADggGBAA1F/apr
l6pNT3Mp/MxhUUgo6usEJCryGQcLRfexyQXGN3huCmIrP55VFa8ETPAtjsr6PMe7
7vvEj8eFu2JtKovlQwNewYU9cjAMCVaFiNbrQa20hzhWc2js6dyildE6/DPzbeds
KDAxhFNp35SlwtRtKk1SzxJxsqSwjfxI8fp+R/0wO8g0fWTdM2gCpRwYMNwJELEg
+dSlvJCwuu+rzxLalzaPF1PMTW72OELal/j5sD+2VytQ4k+HUDbyt2DnQT7YQ3zo
q02x2u2sm1WW/o/uh8pjPxkGQqL2mryZs6VH9VCU3QkKNDssNd71lr3wPoE4YRHe
UvzD1eDeelzBUFNIpDCjdCsL55yIPqUsr6lmjpBPL0vea33QTMbcsSxu0umGXDbU
66juU4Z1jOE0wClIvaO699J+E2gBe1jUN6At6b8BSoZqCqXYoDHGei9RBUdvgqto
kVsoJfDI/TFMekYgpL5UVYmLdfgqLPPRP9pQBLDx3mszeAqnvfTICAzfXg==
-----END CERTIFICATE-----

179
internal/zero/cmd/testdata/config.yaml vendored Normal file
View file

@ -0,0 +1,179 @@
authenticate_service_url: https://authenticate.localhost.pomerium.io
certificate_file: tls.crt
certificate_authority_file: ca.crt
certificate_key_file: tls.key
cookie_secret: UYgnt8bxxK5G2sFaNzyqi5Z+OgF8m2akNc0xdQx718w=
databroker_storage_connection_string: postgres://pomerium:password@postgres:5432/test
databroker_storage_type: postgres
downstream_mtls:
crl_file: crl.pem
envoy_admin_address: 0.0.0.0:9091
google_cloud_serverless_authentication_service_account: ewoiYXV0aF9wcm92aWRlcl94NTA5X2NlcnRfdXJsIjogImh0dHA6Ly9tb2NrLWlkcDo4MDI0IiwKImF1dGhfdXJpIjogImh0dHA6Ly9tb2NrLWlkcDo4MDI0IiwKImNsaWVudF9lbWFpbCI6ICJyZWRhY3RlZEBwb21lcml1bS1yZWRhY3RlZC5pYW0uZ3NlcnZpY2VhY2NvdW50LmNvbSIsCiJjbGllbnRfaWQiOiAiMTAxMjE1OTkwNDU4MDAwMzM0Mzg3IiwKImNsaWVudF94NTA5X2NlcnRfdXJsIjogImh0dHA6Ly9tb2NrLWlkcDo4MDI0IiwKInByaXZhdGVfa2V5IjogIi0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuTUlJRXZRSUJBREFOQmdrcWhraUc5dzBCQVFFRkFBU0NCS2N3Z2dTakFnRUFBb0lCQVFDOEhMQkFJelhrUGVlZ1xubGRVZlJLSzJqUXhTVlpENWcrcXNqQXpwbXJxL0F0bXdlSzFjR2NPdFo2ZU9MK3A4YnJQRHlWaERUMFFsSS9PL1xuRUtnQ09GRnhVRHFvUjgyaVkwNlNhY0FqSG5pNitQTzl0VlJiRlYwdzE0QkRBSlNwQitWdld5bCtGb1BEVi92c1xuWjMxRnRZdytFd3FrYkR4L2thVDl1emYrTEpkbGtmMTRuUVFqOEVreS84ZDNtV0piYi85dGpPYnNhUWdKNUxMeFxuQ1lkSW1rcjc3WDJMTXVEdy8xdHBINjQyR0UyNU5yZ202UUhseUtTZllYbzM4djgzZWJFcWJaVURHK1ppb0FyUFxubXFta2F3VVd3M2VraGo4MFNKZy9USzlQUmFOL1Z2Y0kxUGdBZDdMWnp0VVJlU21UeTVoZDlyNnJPQnhweHduVFxuRHZIa0JuNnZBZ01CQUFFQ2dnRUFCMjhpMEFZVU5TYjFKbldGYkt6cnVVY3R1M3RDTlhvdkpnNkszQmlQVk1rcVxuRFQxWHJKSWdGNVJISE9scjNPc0xFNnU3WHoyY3RkTUw2UHNoaUtUdEl3dEdwaXZnUnBDaUpFc2xtcjJ6aThBV1xuOGVKZXFSTFpFZnNTU0pPWFRHN1JkR3NuNHFIRkowMHMyWlRsY0lIU1B3bkZtK1hqSmk5OVU4RzRYc1VvWG8wclxuR3krMFZDdVU3TThnSUNFSEhzclFPOVhERDNuVDJqaXU1VGpyS3dqdXQzRW1vSnNzSTVicXgzMytPQnU1QnBDUFxuQ1Q0NzNENDNQOXAzcWkvWG5mdnFHU0cyT2o0T2FqVjRmcjBvOUIzS3ZJeGtNZW03V2xJM2p5eTFrQXB5WHFWVFxuYkxrTEZ5V0JOVFdVWjJSLzJ3eG11b0M2bUxadzg3OU1MQ0tNdmsxZG9RS0JnUURobXdHYWZKTnltVGlFUVpSSVxuU3NReDRzZXFmT0tmZ0ZDN29ocUg5Y1JPT3U4SUoxbzdxMnBNMlc0WGlWK1Mzd1RkUEdtY2E2SU9qWDIzaXNWQlxuMnVxTmk5UzRNbkkyL2QyMkdkL0JSOXJ2QncxZUdKb0ticld4MjJmRThRQ0VXVDFBbk8rRHVEMGpDODV5UmxzN1xuYXh6bGFNcnhFdTNMSTlVRTdOdHJkUWlCeVFLQmdRRFZkSTZjZUlWQlQ2Umd2Vkd0OHprTGpQSUZqaFFFSEFJcFxudWhpcmdxcFM2Q1g5Qmx5ZjIrbzQwem1majNoZTVyQ2NFb0I1TXNlTStEZ0ZiY1ZoMmUvTVZuWWlOTnc2SkNEQlxuQlFrRjQwOHBacFNlS1h2TC9veVYva0ltTVRKL3RVRFkwRVh4TXdTUEpCMFdsdGJXcmVWSUhvcGlnWFJDYmFleVxudUJIVkJ2LzR0d0tCZ0h3SHVlUHk1U1UxczJxU216RDdXYzJMUGZZdTNuQ09ITlJyRkdiMjZNdVJmdVJlcmk3clxuMkc4VGdvRVNGeWNwMFFUSU44KzFKTTBYWUt4TmNKRDZCOFYxd0tiYnBRc3ltbmVJMWdqdXRpQi9JZ3cvUGtES1xuQ0w0VlA0RjRkYTVOV1cxeVdnTnlnTG9KdlovNXFpS0tpc0pjMEdXazRIS3o2bUxnek9qUTJMSnhBb0dCQUxIWlxuZk4yWWVZYnlZY2FNMTFwMVZpbHVsVlRWalkzaS9GWmlEUjRTTC9JR0pXak4vU3pnNGlYWXNLRm11K2R1bE9abFxuY0JBTHBFS3JxcG16WFl0ck42YnN2MTgrNWVPM3FHYksyRHJFcTNlV1ZldjJLb1RNb2J4ejdnKytYQklXSm1MQVxuSGhhYTZJaVBrWUQ1eXlWeUhLRGJlWGdiM285ZXFDUjd3N2ZZTGp5L0FvR0FJNEQrTUZraXZ3VUY3aHFmNWVkU1xuS3JsdHdtb2RIaXFYTmJWa3diVzFBRlBKYmlZYWk0WUZmSzRJQWJpZi9ZbXhmOUc3OGFPa3I5WnBDSXpPa0RQWlxuWXBFd1FHV3NBaEVsQ0Z2YzhFLzVkSEVTU3ArdFd0UCtObHVpbXBGcWlEZzMvU1VuTXdPMnhIMG5oTGEwemVqaFxuZ21MaDR3L0NjUHliOVp5WGNlV1UvblU9XG4tLS0tLUVORCBQUklWQVRFIEtFWS0tLS0tXG4iLAoicHJpdmF0ZV9rZXlfaWQiOiAiZTA3ZjdjOTM4NzBjN2UwM2Y4ODM1NjBlY2Q4ZmQwZjRkMjdiMDA4MSIsCiJwcm9qZWN0X2lkIjogInBvbWVyaXVtLXJlZGFjdGVkIiwKInRva2VuX3VyaSI6ICJodHRwOi8vbW9jay1pZHA6ODAyNC90b2tlbiIsCiJ0eXBlIjogInNlcnZpY2VfYWNjb3VudCIKfQ==
idp_client_id: CLIENT_ID
idp_client_secret: CLIENT_SECRET
idp_provider: oidc
idp_provider_url: https://mock-idp.localhost.pomerium.io/
jwt_claims_headers: email,groups,user
log_level: debug
routes:
- allow_public_unauthenticated_access: true
from: https://mock-idp.localhost.pomerium.io
preserve_host_header: true
to: http://mock-idp:8024
- allow_public_unauthenticated_access: true
from: https://envoy.localhost.pomerium.io
to: http://localhost:9901
- allow_any_authenticated_user: true
from: https://verify.localhost.pomerium.io
pass_identity_headers: true
to: http://verify:80
- allow_public_unauthenticated_access: true
allow_websockets: true
from: https://websocket-echo.localhost.pomerium.io
to: http://websocket-echo:80
- allow_any_authenticated_user: true
from: https://fortio-ui.localhost.pomerium.io
to: https://fortio:8080
- allow_public_unauthenticated_access: true
from: https://fortio-ping.localhost.pomerium.io
tls_custom_ca_file: route_ca_1.crt
tls_server_name: fortio-ping.localhost.pomerium.io
to: https://fortio:8079
- allow_public_unauthenticated_access: true
from: https://httpdetails-ip-address.localhost.pomerium.io
to: https://172.21.0.50:8443
- allow_public_unauthenticated_access: true
from: https://httpdetails.localhost.pomerium.io
path: /tls-skip-verify-enabled
tls_skip_verify: true
to: https://trusted-httpdetails:8443
- allow_public_unauthenticated_access: true
from: https://httpdetails.localhost.pomerium.io
path: /tls-skip-verify-disabled
tls_skip_verify: false
to: https://trusted-httpdetails:8443
- allow_public_unauthenticated_access: true
from: https://httpdetails.localhost.pomerium.io
path: /tls-server-name-enabled
tls_server_name: httpdetails.localhost.notpomerium.io
to: https://wrongly-named-httpdetails:8443
- allow_public_unauthenticated_access: true
from: https://httpdetails.localhost.pomerium.io
path: /tls-server-name-disabled
to: https://wrongly-named-httpdetails:8443
- allow_public_unauthenticated_access: true
from: https://httpdetails.localhost.pomerium.io
path: /tls-custom-ca-enabled
tls_custom_ca_file: route_ca_2.crt
tls_server_name: httpdetails.localhost.pomerium.io
to: https://untrusted-httpdetails:8443
- allow_public_unauthenticated_access: true
from: https://httpdetails.localhost.pomerium.io
path: /tls-custom-ca-disabled
to: https://untrusted-httpdetails:8443
- allow_any_authenticated_user: true
from: https://client-cert-required.localhost.pomerium.io
tls_downstream_client_ca_file: route_downstream_ca_1.crt
to: http://trusted-httpdetails:8080
- allow_any_authenticated_user: true
from: https://client-cert-overlap.localhost.pomerium.io
path: /ca1
tls_downstream_client_ca_file: route_downstream_ca_1.crt
to: http://trusted-httpdetails:8080
- allow_any_authenticated_user: true
from: https://client-cert-overlap.localhost.pomerium.io
path: /ca2
tls_downstream_client_ca_file: route_downstream_ca_2.crt
to: http://trusted-httpdetails:8080
- cors_allow_preflight: true
from: https://httpdetails.localhost.pomerium.io
prefix: /cors-enabled
to: http://trusted-httpdetails:8080
- cors_allow_preflight: false
from: https://httpdetails.localhost.pomerium.io
prefix: /cors-disabled
to: http://trusted-httpdetails:8080
- allow_public_unauthenticated_access: true
from: https://httpdetails.localhost.pomerium.io
prefix: /preserve-host-header-enabled
preserve_host_header: true
to: http://trusted-httpdetails:8080
- allow_public_unauthenticated_access: true
from: https://httpdetails.localhost.pomerium.io
prefix: /preserve-host-header-disabled
preserve_host_header: false
to: http://trusted-httpdetails:8080
- allow_any_authenticated_user: true
from: https://restricted-httpdetails.localhost.pomerium.io
pass_identity_headers: true
to: http://trusted-httpdetails:8080
- from: https://ppl-restricted-httpdetails.localhost.pomerium.io
pass_identity_headers: true
to: http://trusted-httpdetails:8080
policy:
- allow:
or:
- email:
is: foo@example.com
- email:
is: bar@example.com
- allowed_domains:
- dogs.test
from: https://httpdetails.localhost.pomerium.io
pass_identity_headers: true
prefix: /by-domain
to: http://trusted-httpdetails:8080
- allowed_users:
- user1@dogs.test
from: https://httpdetails.localhost.pomerium.io
pass_identity_headers: true
prefix: /by-user
to: http://trusted-httpdetails:8080
- allow_any_authenticated_user: true
from: https://httpdetails.localhost.pomerium.io
prefix: /round-robin
to:
- http://trusted-1-httpdetails:8080
- http://trusted-2-httpdetails:8080
- http://trusted-3-httpdetails:8080
- allow_any_authenticated_user: true
from: https://httpdetails.localhost.pomerium.io
prefix: /ring-hash
to:
- http://trusted-1-httpdetails:8080
- http://trusted-2-httpdetails:8080
- http://trusted-3-httpdetails:8080
- allow_any_authenticated_user: true
from: https://httpdetails.localhost.pomerium.io
prefix: /maglev
to:
- http://trusted-1-httpdetails:8080
- http://trusted-2-httpdetails:8080
- http://trusted-3-httpdetails:8080
- allow_public_unauthenticated_access: true
from: https://httpdetails.localhost.pomerium.io
pass_identity_headers: true
set_request_headers:
X-Custom-Request-Header: custom-request-header-value
to: http://trusted-httpdetails:8080
- allow_public_unauthenticated_access: true
allow_websockets: true
from: https://enabled-ws-echo.localhost.pomerium.io
to: http://websocket-echo:80
- allow_public_unauthenticated_access: true
from: https://disabled-ws-echo.localhost.pomerium.io
to: http://websocket-echo:80
- allow_public_unauthenticated_access: true
enable_google_cloud_serverless_authentication: true
from: https://cloudrun.localhost.pomerium.io
pass_identity_headers: true
set_request_headers:
x-idp: oidc
to: http://trusted-httpdetails:8080
- from: https://200.localhost.pomerium.io
response:
status: 200
body: OK
shared_secret: UYgnt8bxxK5G2sFaNzyqi5Z+OgF8m2akNc0xdQx718w=
tls_derive: example.com # unsupported

29
internal/zero/cmd/testdata/crl.pem vendored Normal file
View file

@ -0,0 +1,29 @@
-----BEGIN X509 CRL-----
MIICWjCBwwIBATANBgkqhkiG9w0BAQsFADA6MR4wHAYDVQQKExVta2NlcnQgZGV2
ZWxvcG1lbnQgQ0ExGDAWBgNVBAMTD2Rvd25zdHJlYW0gQ0EgMRcNMjMwNzE5MjEx
ODQ1WhcNMzMwNzE2MjExODQ1WjAjMCECEEY9jnU8Vkt2MYueskRd7bwXDTIzMDcx
OTIxMTc0N1qgMDAuMB8GA1UdIwQYMBaAFNH1NAz8Uj24PhCGdBkGi0CMQGMLMAsG
A1UdFAQEAgIQADANBgkqhkiG9w0BAQsFAAOCAYEA4w3ow4j1DaufiBBXhdC0ECyY
zDxOuACdR4zyoYbjN1g2kc0buchJ7+V0eTY/RnSNc+uqNY+LYprXQquZKlr9dFUr
vJ/pXJ+uyLR/MzehiTr3HoTLCPliKZDDayPmoZvaqHD8IoGEnQX6kCEhopb7gtqJ
U7TfHaexi0p43FH00gnZfaDMkcAd8zClsEXUrAFCQRD1M5PuCOTO7CeQcI53uBvd
8aGvyHlKA/2O17gniMngcoCO72NAUltJzMbugqeXOoiGHYoSsKTbY7MdLhY3MEBa
3ZkCFgt3HLHTz5S0PeBVrT7/y7Sz5cj0QA0JKL3J3psngVbpS4oHu6cyvg//7NdG
KNBqdas+KPAsmV+3y64Cr2hnv+WsWjiuxDgIEFzpQOcyNOZzmISACw7YXjwFuIne
OiiMuYs/2NvwQ1OPfq3jg3If8kBUcSVh+Te4FI3+07tWUvN6nVYC4VmXAcG1HuxQ
Gnne9f5hgEJPVfLT+uJ31VV16+vBnZD85DZJTrDM
-----END X509 CRL-----
-----BEGIN X509 CRL-----
MIICNTCBngIBATANBgkqhkiG9w0BAQsFADA6MR4wHAYDVQQKExVta2NlcnQgZGV2
ZWxvcG1lbnQgQ0ExGDAWBgNVBAMTD2Rvd25zdHJlYW0gQ0EgMhcNMjMwNzE5MjE1
MDE1WhcNMzMwNzE2MjE1MDE1WqAwMC4wHwYDVR0jBBgwFoAUCxQ2cBa5YzqVzamp
iNCx8KwFFyQwCwYDVR0UBAQCAhAAMA0GCSqGSIb3DQEBCwUAA4IBgQCYamx8pM+R
Clyskcu7ouhu/R1Jy1nWGyWtKphYq0XFbOLlnk2Z7eDfAX8Eej2FavqxzapR2x2O
4iJNDCmiwYYYUS2X2LJ3rRRJXyXvWhtfHrxURd6BitC2IXpykBtVlf3zAnZ8GZFQ
S1jdfyLMuEAiDwIai3Yt8HsDp/qG089oXcoStyQg/uRpmWy05A9uCVOfNHSLSZu8
lr4qatleu0wWbV1amL8tO9x4CRkO0o1YaQq4DoOruPr+3NkTmPvGidh3F71V6IEA
h+KzdbRXxFmCCWLWmpJDcrgR7KUqZOhUUt+DUqaqhV44qI0nrpR+QZLohoDor9Lw
K+ufj3n29eSRX+3Px+oVWPT8YZP2uKPdizi96me2jWTr51x9AjEoJDsTnYRl9+uY
ShiUxWnTdQsooknIfcS/0zfgZ87GvUVzinCQzJpwVxd4Alt8AlR+fXAqNIoOguyv
p/CtRVnjVE7l7HW/hQRq1J0ijCCKwmyf/KTd6EK4TdrvbX/U9msVM8Y=
-----END X509 CRL-----

View file

@ -0,0 +1,28 @@
-----BEGIN CERTIFICATE-----
MIIE1zCCAz+gAwIBAgIQZ139cd/paPdkS2JyAu7kEDANBgkqhkiG9w0BAQsFADCB
gzEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMSwwKgYDVQQLDCNjYWxl
YkBjYWxlYi1wYy1saW51eCAoQ2FsZWIgRG94c2V5KTEzMDEGA1UEAwwqbWtjZXJ0
IGNhbGViQGNhbGViLXBjLWxpbnV4IChDYWxlYiBEb3hzZXkpMB4XDTIxMDgxMDE3
MzIwOVoXDTMxMDgxMDE3MzIwOVowgYMxHjAcBgNVBAoTFW1rY2VydCBkZXZlbG9w
bWVudCBDQTEsMCoGA1UECwwjY2FsZWJAY2FsZWItcGMtbGludXggKENhbGViIERv
eHNleSkxMzAxBgNVBAMMKm1rY2VydCBjYWxlYkBjYWxlYi1wYy1saW51eCAoQ2Fs
ZWIgRG94c2V5KTCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBANbKyMz5
MVW6YKdjh1oIN1Mn7PE2pH5SbJSpWxdAGhdBkBkpAa7OxarjH5KVkCTSa7oncla7
qNuJZS6mBmoxF+R+cR3jyGdUAYlozl1jlfqLIfC/+g7V7VmOJn98tjB42fatxLl6
WPAw1JDNsWtQfhKhbcHut7RsF0rMOOHcwywTR7LOyCmIel1pcmpV4hbVcT6eVwoP
HXyJSa9cqaMQ5Xrdogai4IqZZIGLHeLsTVutOgJFXEevlX/QT3sWomEctzh38Js4
9DiAPD6d4Y7/CPLYEfk29JQ9NZhpgDsi9hu5FHHZcXwf1IHlw/CBVgn6j+jmvKKz
90Ma1oquv3W6dttid/xCcLGu2S+96Tzrykmoy5VacLtVEP41YmoVls91rlo7olpe
QWFbnmco739TI/4h+HodolperQERQl7uCnpKVPZ3WokKuRh5pkqkQp/arQjtwcRt
G43CrDpbl+uSjMCAxha958eTYvtojTMnvLtsGID1hGXnqlw+5KjKrgRHrQIDAQAB
o0UwQzAOBgNVHQ8BAf8EBAMCAgQwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4E
FgQUhYZYWIBHyk6ZVTnp3lRt/tyBP00wDQYJKoZIhvcNAQELBQADggGBAA1F/apr
l6pNT3Mp/MxhUUgo6usEJCryGQcLRfexyQXGN3huCmIrP55VFa8ETPAtjsr6PMe7
7vvEj8eFu2JtKovlQwNewYU9cjAMCVaFiNbrQa20hzhWc2js6dyildE6/DPzbeds
KDAxhFNp35SlwtRtKk1SzxJxsqSwjfxI8fp+R/0wO8g0fWTdM2gCpRwYMNwJELEg
+dSlvJCwuu+rzxLalzaPF1PMTW72OELal/j5sD+2VytQ4k+HUDbyt2DnQT7YQ3zo
q02x2u2sm1WW/o/uh8pjPxkGQqL2mryZs6VH9VCU3QkKNDssNd71lr3wPoE4YRHe
UvzD1eDeelzBUFNIpDCjdCsL55yIPqUsr6lmjpBPL0vea33QTMbcsSxu0umGXDbU
66juU4Z1jOE0wClIvaO699J+E2gBe1jUN6At6b8BSoZqCqXYoDHGei9RBUdvgqto
kVsoJfDI/TFMekYgpL5UVYmLdfgqLPPRP9pQBLDx3mszeAqnvfTICAzfXg==
-----END CERTIFICATE-----

View file

@ -0,0 +1,28 @@
-----BEGIN CERTIFICATE-----
MIIE2DCCA0CgAwIBAgIRALd9GaJR92qi7qL1eHGM6K0wDQYJKoZIhvcNAQELBQAw
gYMxHjAcBgNVBAoTFW1rY2VydCBkZXZlbG9wbWVudCBDQTEsMCoGA1UECwwjY2Fs
ZWJAY2FsZWItcGMtbGludXggKENhbGViIERveHNleSkxMzAxBgNVBAMMKm1rY2Vy
dCBjYWxlYkBjYWxlYi1wYy1saW51eCAoQ2FsZWIgRG94c2V5KTAeFw0yMTA4MTEy
MTU2MTBaFw0zMTA4MTEyMTU2MTBaMIGDMR4wHAYDVQQKExVta2NlcnQgZGV2ZWxv
cG1lbnQgQ0ExLDAqBgNVBAsMI2NhbGViQGNhbGViLXBjLWxpbnV4IChDYWxlYiBE
b3hzZXkpMTMwMQYDVQQDDCpta2NlcnQgY2FsZWJAY2FsZWItcGMtbGludXggKENh
bGViIERveHNleSkwggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQDWYpVe
BSnee2cABYofSoWxGMyFaMQ0nJkY0UWM9ckyUh7VfgN+/aFSW2ZSmXuv5drcpi20
z3elhPTe98bANbj+/bi0015QWnMenK05ZK6qDtFwo/HVC/Ycaruu96+1J2toeWuE
tykW3MCpC1pHYS5g9iVDkpdrznvXKlYuSikjrj7K5toiTvum97LxKkuj6DXjapPD
5vteSN1dQgO9CS3sqlcwYA6RjUHwY2VEh2adP37BZrZwO+yJq9qF5y5Glgi8lN4c
KlIlFUs/xSpQsxNbNQXtN9mk4imYlZGzYYbbm+foBVPPboa5jVwKDpZ65mOs7JGP
6yj+7V7UBMFpW+gKmJtgh/kkAx185h93qwLFPc8/T7n++P1bu+fakXPGPE21rDeL
PnUmucIZpJo5NpYVQv4WvTKq/zMR9Sspz2PFJnERTfTvq+F1q3ZNafEziPsB9oeS
njxwmaZOSV0vXq/qeoqx4v6MBzVAY0/8R2LcpJ4ug0OZ3w0b2t6yo86P5Q8CAwEA
AaNFMEMwDgYDVR0PAQH/BAQDAgIEMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0O
BBYEFLcY8EoNofMcrrxzyxIn3W6ZOMVXMA0GCSqGSIb3DQEBCwUAA4IBgQCZzDCv
KIHX3GvjNSY5w5bOn4E3w7QHP09ABjT/wuT4LDkZHJMmlrLo3s8bcsQ0sMD1Y///
s07cp4xYlqD7BA0AcpvYVYq58xKxsoCwVXmG5cEeOoZmWf3qY2mS8eW96vOFrdIb
L4OF4xYUOMRqAOGAAr6VlO7gXa406HzrsA1hYZwreXhOTCZZPZOUnAu05SHFdgaM
TJNB/o01tpwQlrTxNmfropoOzyuvH0zU2RrMs0+EbOuC4A2cQ83DIFxvq67lyU0A
s1Q6tRM0+UDmJOLz3SdgN+D00hcuuj92GV4bH8BfyUv8NCY0vDij0TSjj4c4Qtc7
IPLTZ2g545oczhNgAmT7d+B5InyfiSIKemXqes2jpiAfzPNl9BVxsakcs/YzoYs1
+qTjAWuaDsKohEnO4BJuzv0xrce40enRgXyGGFvXu2s4FY2vJqTSo6ysDWnhI3LW
dcg6O2F4APCGGe7zsuqiqkpcknBabgzEs9foHq2mfo7XiEzedMN8BNqfSbA=
-----END CERTIFICATE-----

View file

@ -0,0 +1,25 @@
-----BEGIN CERTIFICATE-----
MIIEPDCCAqSgAwIBAgIJAKmtj1u+hOdzMA0GCSqGSIb3DQEBCwUAMDoxHjAcBgNV
BAoTFW1rY2VydCBkZXZlbG9wbWVudCBDQTEYMBYGA1UEAxMPZG93bnN0cmVhbSBD
QSAxMB4XDTIzMDYwODE4NTgyMloXDTMzMDYwODE4NTgyMlowOjEeMBwGA1UEChMV
bWtjZXJ0IGRldmVsb3BtZW50IENBMRgwFgYDVQQDEw9kb3duc3RyZWFtIENBIDEw
ggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQDue3fuI704DazewdWmKJQs
YGYR2ZapQQeQynXaqOhqMOLTc7M18uVOnfhvFVtUB5OCtxL2TMmy8/ytIQlU8CUc
bUo1AFcXu1MGORJNu5zbJymsrOE8fKqopb3muGNRM6tulIHhpRCcF3m8pKFBZBWs
CR7A2MhgKHJvd1yVMc6/GpO/RqIHiFAiCV9XguadKTwapPJ54vJwBDZoDM4/qA34
xFR1uCAzob0D4yFW/C7u57SMZDjSy2jxxZkcFQAvmRPPgzutaAHuRUUnPhw3f9PF
+DLNDeo6kXdS6aQOb/weCPl/VjlskXyvgNuzGE2xixZYBQwpXAE8AuBcXNvlxT0T
1oyoU8aggymnTFWnLmN/ipQ7+9CHS2+apFDG7nrf9q5UgLtRiVLOytoVxWDOhoY5
pqbS05aDjWXbXyPf2e318Ntjc6Hl7nSffHlCGsb/zqiJnJX6ti/k0VR1WHJZyu7e
CYeu+mtqNATrS7h+nBUMNZ9Bb1EIHQOJ/yyToULy/nECAwEAAaNFMEMwDgYDVR0P
AQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFNH1NAz8Uj24
PhCGdBkGi0CMQGMLMA0GCSqGSIb3DQEBCwUAA4IBgQBltym8hRgXSAaGTZAciCBc
sRtyEkQ584oHUiOmaKvITjnHys/EiETnNaxRw7t/69DKe5g4UaqgdlMwecjJk/Hl
jSvXI4mAUERkcIJIEJspMapsEp5QcTAlvskoXjNPFrOW+x0iOLdAM41x5kBDQRkc
+N2ie0ITJ5ZX530Ai4ukt76NZNIOio5xoHs1q170kn6xwfS12x1g7CksHlN5Mbw1
wtFFeLfQCZVXPNspH7LHJUkrULSTyhleZFJ3ZZqqT9oybpDUhdZB0nZJ6ZC1JiQo
2HMwIFV+OsEEG7fNzHhbVKaJmaiOiW2t/CpltebVLSTinz2LmZhzVFRT+y/cdhn3
5IsQHzGwEKKtL5XfqJjqWhry+mw/vb+Rze6yy9Li7FkBnetQq8Tb0a2u/UHyzqTA
NVhu1wgbRD93vnZqGOkb0gzMRPJC/KibNvFRfaeDXDOiW69Npm/xxXBO/My0CWF1
p7cQCkgpkStnWEmm/48WiwGcFWTC2W+mims7JcIpSpc=
-----END CERTIFICATE-----

View file

@ -0,0 +1,25 @@
-----BEGIN CERTIFICATE-----
MIIEPDCCAqSgAwIBAgIJAPjvgLbEIVj/MA0GCSqGSIb3DQEBCwUAMDoxHjAcBgNV
BAoTFW1rY2VydCBkZXZlbG9wbWVudCBDQTEYMBYGA1UEAxMPZG93bnN0cmVhbSBD
QSAyMB4XDTIzMDYwOTAwNDQzOFoXDTMzMDYwOTAwNDQzOFowOjEeMBwGA1UEChMV
bWtjZXJ0IGRldmVsb3BtZW50IENBMRgwFgYDVQQDEw9kb3duc3RyZWFtIENBIDIw
ggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQC/8Kog2Zz8e68EGpfiXN7u
Xgau38h63ydspucrjhtnSTWXtHO1hYLmUYWAewi79iGYzOuYgWCD3cFxd+tMKLrB
yoriJ3KioTtY0pmyLDJ1TXMSaFGgnZqjXHmjMvio0x/jQNkCbYkFBGQSZZvkA8sQ
m5AsRDeIUPkPlhFMnb2x4iRcLBP6zDNFfX+y1qSolKbh3K9/E3PT4Unja8gObzCJ
nrOcF5SBqTOjRHif/S/wZ9TSFWzLmqGLhq73RahyTiaYP46UvJhrNb5Mo9Hbb94/
4zS5B2Zuo4pshSZDWpqwvBecQN0VaLVvIymuSyg5TzuH4ktM0ptzv6rXinDla7rz
Mu/FrFVQPksOhTDt5UCSqODwPZiO7g5ST0s+jMpbp1XN8KP2prtElUWdabvHlb0M
D2E0hHVi444YkQxZaCoed2obrTB2Df2CwHATgFKvLF1SGS2Q9v0pbUc6Z+0o912b
nRfGzi2p7iBsWULuINI3nbNAzlmWPmGiwV1SY1Y0dU8CAwEAAaNFMEMwDgYDVR0P
AQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFAsUNnAWuWM6
lc2pqYjQsfCsBRckMA0GCSqGSIb3DQEBCwUAA4IBgQBU6YiRXQ4jkrqugtuLj2a5
AQ+URPlfkFFN0BDpWCIzV50w+Y1ZtH2HvGX44zDjbQTwv+AU4T+F75C8Pnc5yvYo
v6FIMOOZIrvilokyVf3dKRC3Y2cQac4u64aQk+XR/qjiYoFK0B9yw8UA3O7wA46b
ceoZUFZLc5oSsnB9tW72i8lEkBFt2X62rqSQNGYtzCV64bM+ezCsBYPaCIKW0ARB
0CbNFGoaPJzAuuGukvOcBDytJ3RJBXJ7l3626KNGxCLsRMcDcTxvXBf7gFWtetW9
kuofvlJMiPi3BDMl/FAE5ikj0UR47rjYUxM2SF6F+z8pEcPcePSYzClMECL9a/02
I12sEnU3Rf+RpwSTHSCjyXGtWl4dGSJlOElwrYMBAyX62dfFY9GEGgHCnyO1tj39
JIhgiIBEZsBL9LOOK8vTYzZ5kBkZ1NXh2Bj3nS/B/M5zotp4/S6P30Li44/Jbpvc
70fXruF69zwPMc5b3x7yX7hPLYHk0hm3BOWaodPI4t0=
-----END CERTIFICATE-----

26
internal/zero/cmd/testdata/tls.crt vendored Normal file
View file

@ -0,0 +1,26 @@
-----BEGIN CERTIFICATE-----
MIIEUjCCArqgAwIBAgIRAKNaEqCmmZfhmcYgZy01WCswDQYJKoZIhvcNAQELBQAw
gYMxHjAcBgNVBAoTFW1rY2VydCBkZXZlbG9wbWVudCBDQTEsMCoGA1UECwwjY2Fs
ZWJAY2FsZWItcGMtbGludXggKENhbGViIERveHNleSkxMzAxBgNVBAMMKm1rY2Vy
dCBjYWxlYkBjYWxlYi1wYy1saW51eCAoQ2FsZWIgRG94c2V5KTAeFw0yMzExMTAy
MDA4NDRaFw0zMzExMDcyMDA4NDRaMFcxJzAlBgNVBAoTHm1rY2VydCBkZXZlbG9w
bWVudCBjZXJ0aWZpY2F0ZTEsMCoGA1UECwwjY2FsZWJAY2FsZWItcGMtbGludXgg
KENhbGViIERveHNleSkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC8
HLBAIzXkPeegldUfRKK2jQxSVZD5g+qsjAzpmrq/AtmweK1cGcOtZ6eOL+p8brPD
yVhDT0QlI/O/EKgCOFFxUDqoR82iY06SacAjHni6+PO9tVRbFV0w14BDAJSpB+Vv
Wyl+FoPDV/vsZ31FtYw+EwqkbDx/kaT9uzf+LJdlkf14nQQj8Eky/8d3mWJbb/9t
jObsaQgJ5LLxCYdImkr77X2LMuDw/1tpH642GE25Nrgm6QHlyKSfYXo38v83ebEq
bZUDG+ZioArPmqmkawUWw3ekhj80SJg/TK9PRaN/VvcI1PgAd7LZztUReSmTy5hd
9r6rOBxpxwnTDvHkBn6vAgMBAAGjbDBqMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUE
DDAKBggrBgEFBQcDATAfBgNVHSMEGDAWgBSFhlhYgEfKTplVOeneVG3+3IE/TTAi
BgNVHREEGzAZghcqLmxvY2FsaG9zdC5wb21lcml1bS5pbzANBgkqhkiG9w0BAQsF
AAOCAYEApqVzJ3Qf9VqkujFbc0MBDqWD/8gjfd7mW29fRtMIP3zdJliyevRj73AL
ifX5ZZunT7n/j52ZziFib4j8uc4R6VwAE7lLpDesfsL4AgvG6ujJaJLh+q6fPFVm
8UwIr3/HjZAGPvbwceAO00mtfqn8aK1KeKxfEk9UhTUWhsquby88EcJVhxkTsAHo
kKQkEaf9NLazhZ0P0u9J/14VGhMN8QUHvILVjckCDhIj38IUK7UtZHkM72GmKrj2
SC40IDdNt4zb1ATLVeyOLdwKjwEFgKWzkvI/7Uj9pA26/eYGPQ7oxRF+IExVIhDr
EJvHrWQ0s0EKNPdpU/Ihqtk0rYkj81peqM8TmI6vqrZqAEPza1tYk6WQszDonpPW
uKlfr9GYYf5Mu9a2y26AgluDniAcnfWjRXmr1rvRHBpzsLSD3STnPE5t6HJieP7r
v6k/flXQ9SEw0U3lI/nZKKwiLfWC2O5BpKwMz19cZ8/kLSJWHg4lkDb2Uo1JKniW
+kMEI9nN
-----END CERTIFICATE-----

28
internal/zero/cmd/testdata/tls.key vendored Normal file
View file

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC8HLBAIzXkPeeg
ldUfRKK2jQxSVZD5g+qsjAzpmrq/AtmweK1cGcOtZ6eOL+p8brPDyVhDT0QlI/O/
EKgCOFFxUDqoR82iY06SacAjHni6+PO9tVRbFV0w14BDAJSpB+VvWyl+FoPDV/vs
Z31FtYw+EwqkbDx/kaT9uzf+LJdlkf14nQQj8Eky/8d3mWJbb/9tjObsaQgJ5LLx
CYdImkr77X2LMuDw/1tpH642GE25Nrgm6QHlyKSfYXo38v83ebEqbZUDG+ZioArP
mqmkawUWw3ekhj80SJg/TK9PRaN/VvcI1PgAd7LZztUReSmTy5hd9r6rOBxpxwnT
DvHkBn6vAgMBAAECggEAB28i0AYUNSb1JnWFbKzruUctu3tCNXovJg6K3BiPVMkq
DT1XrJIgF5RHHOlr3OsLE6u7Xz2ctdML6PshiKTtIwtGpivgRpCiJEslmr2zi8AW
8eJeqRLZEfsSSJOXTG7RdGsn4qHFJ00s2ZTlcIHSPwnFm+XjJi99U8G4XsUoXo0r
Gy+0VCuU7M8gICEHHsrQO9XDD3nT2jiu5TjrKwjut3EmoJssI5bqx33+OBu5BpCP
CT473D43P9p3qi/XnfvqGSG2Oj4OajV4fr0o9B3KvIxkMem7WlI3jyy1kApyXqVT
bLkLFyWBNTWUZ2R/2wxmuoC6mLZw879MLCKMvk1doQKBgQDhmwGafJNymTiEQZRI
SsQx4seqfOKfgFC7ohqH9cROOu8IJ1o7q2pM2W4XiV+S3wTdPGmca6IOjX23isVB
2uqNi9S4MnI2/d22Gd/BR9rvBw1eGJoKbrWx22fE8QCEWT1AnO+DuD0jC85yRls7
axzlaMrxEu3LI9UE7NtrdQiByQKBgQDVdI6ceIVBT6RgvVGt8zkLjPIFjhQEHAIp
uhirgqpS6CX9Blyf2+o40zmfj3he5rCcEoB5MseM+DgFbcVh2e/MVnYiNNw6JCDB
BQkF408pZpSeKXvL/oyV/kImMTJ/tUDY0EXxMwSPJB0WltbWreVIHopigXRCbaey
uBHVBv/4twKBgHwHuePy5SU1s2qSmzD7Wc2LPfYu3nCOHNRrFGb26MuRfuReri7r
2G8TgoESFycp0QTIN8+1JM0XYKxNcJD6B8V1wKbbpQsymneI1gjutiB/Igw/PkDK
CL4VP4F4da5NWW1yWgNygLoJvZ/5qiKKisJc0GWk4HKz6mLgzOjQ2LJxAoGBALHZ
fN2YeYbyYcaM11p1VilulVTVjY3i/FZiDR4SL/IGJWjN/Szg4iXYsKFmu+dulOZl
cBALpEKrqpmzXYtrN6bsv18+5eO3qGbK2DrEq3eWVev2KoTMobxz7g++XBIWJmLA
Hhaa6IiPkYD5yyVyHKDbeXgb3o9eqCR7w7fYLjy/AoGAI4D+MFkivwUF7hqf5edS
KrltwmodHiqXNbVkwbW1AFPJbiYai4YFfK4IAbif/Ymxf9G78aOkr9ZpCIzOkDPZ
YpEwQGWsAhElCFvc8E/5dHESSp+tWtP+NluimpFqiDg3/SUnMwO2xH0nhLa0zejh
gmLh4w/CcPyb9ZyXceWU/nU=
-----END PRIVATE KEY-----