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

@ -3,11 +3,12 @@ package main
import (
"context"
"errors"
"flag"
"fmt"
"os"
"strings"
"github.com/rs/zerolog"
"github.com/spf13/cobra"
"github.com/pomerium/pomerium/config"
"github.com/pomerium/pomerium/internal/log"
@ -19,43 +20,65 @@ import (
"github.com/pomerium/pomerium/pkg/envoy/files"
)
var (
versionFlag = flag.Bool("version", false, "prints the version")
configFile = flag.String("config", "", "Specify configuration file location")
)
func main() {
flag.Parse()
if *versionFlag {
fmt.Println("pomerium:", version.FullVersion())
fmt.Println("envoy:", files.FullVersion())
return
convertOldStyleFlags()
var configFile string
root := &cobra.Command{
Use: "pomerium",
Version: fmt.Sprintf("pomerium: %s\nenvoy: %s\n", version.FullVersion(), files.FullVersion()),
SilenceUsage: true,
}
root.AddCommand(zero_cmd.BuildRootCmd())
root.PersistentFlags().StringVar(&configFile, "config", "", "Specify configuration file location")
ctx := context.Background()
log.SetLevel(zerolog.InfoLevel)
runFn := run
if zero_cmd.IsManagedMode(*configFile) {
runFn = func(ctx context.Context) error { return zero_cmd.Run(ctx, *configFile) }
if zero_cmd.IsManagedMode(configFile) {
runFn = zero_cmd.Run
}
root.RunE = func(_ *cobra.Command, _ []string) error {
defer log.Info(ctx).Msg("cmd/pomerium: exiting")
return runFn(ctx, configFile)
}
if err := runFn(ctx); err != nil && !errors.Is(err, context.Canceled) {
if err := root.ExecuteContext(ctx); err != nil {
log.Fatal().Err(err).Msg("cmd/pomerium")
}
log.Info(ctx).Msg("cmd/pomerium: exiting")
}
func run(ctx context.Context) error {
func run(ctx context.Context, configFile string) error {
ctx = log.WithContext(ctx, func(c zerolog.Context) zerolog.Context {
return c.Str("config_file_source", *configFile).Bool("bootstrap", true)
return c.Str("config_file_source", configFile).Bool("bootstrap", true)
})
var src config.Source
src, err := config.NewFileOrEnvironmentSource(*configFile, files.FullVersion())
src, err := config.NewFileOrEnvironmentSource(configFile, files.FullVersion())
if err != nil {
return err
}
return pomerium.Run(ctx, src)
}
// Converts the "-config" and "-version" single-dash style flags to the
// equivalent "--config" and "--version" flags compatible with cobra. These
// are the only two flags that existed previously, so we don't need to check
// for any others.
func convertOldStyleFlags() {
for i, arg := range os.Args {
var found bool
if arg == "-config" || strings.HasPrefix(arg, "-config=") {
found = true
fmt.Fprintln(os.Stderr, "Warning: syntax '-config' is deprecated, use '--config' instead")
} else if arg == "-version" {
found = true
// don't log a warning here, since it could interfere with tools that
// parse the -version output
}
if found {
os.Args[i] = "-" + arg
}
}
}

View file

@ -4,6 +4,7 @@ import (
"encoding/base64"
"github.com/pomerium/pomerium/pkg/cryptutil"
"github.com/pomerium/pomerium/pkg/grpc/crypt"
)
// A PublicKeyEncryptionKeyOptions represents options for a public key encryption key.
@ -24,3 +25,13 @@ func (o *Options) GetAuditKey() (*cryptutil.PublicKeyEncryptionKey, error) {
}
return cryptutil.NewPublicKeyEncryptionKeyWithID(o.AuditKey.ID, raw)
}
func (o *PublicKeyEncryptionKeyOptions) ToProto() *crypt.PublicKeyEncryptionKey {
if o == nil {
return nil
}
return &crypt.PublicKeyEncryptionKey{
Id: o.ID,
Data: []byte(o.Data),
}
}

View file

@ -192,6 +192,30 @@ func (s *DownstreamMTLSSettings) applySettingsProto(
s.Enforcement = mtlsEnforcementFromProtoEnum(ctx, p.Enforcement)
}
func (s *DownstreamMTLSSettings) toSettingsProto() *config.DownstreamMtlsSettings {
var settings config.DownstreamMtlsSettings
if s.CA != "" || s.CAFile != "" {
settings.Ca = valueOrFromFileBase64(s.CA, s.CAFile)
}
if s.CRL != "" || s.CRLFile != "" {
settings.Crl = valueOrFromFileBase64(s.CRL, s.CRLFile)
}
if s.Enforcement != "" {
switch s.Enforcement {
case MTLSEnforcementPolicy:
settings.Enforcement = config.MtlsEnforcementMode_POLICY.Enum()
case MTLSEnforcementPolicyWithDefaultDeny:
settings.Enforcement = config.MtlsEnforcementMode_POLICY_WITH_DEFAULT_DENY.Enum()
case MTLSEnforcementRejectConnection:
settings.Enforcement = config.MtlsEnforcementMode_REJECT_CONNECTION.Enum()
}
}
if settings.Ca == nil && settings.Crl == nil && settings.Enforcement == nil {
return nil
}
return &settings
}
func mtlsEnforcementFromProtoEnum(
ctx context.Context, mode *config.MtlsEnforcementMode,
) MTLSEnforcement {

View file

@ -39,6 +39,7 @@ import (
"github.com/pomerium/pomerium/pkg/hpke"
"github.com/pomerium/pomerium/pkg/identity/oauth"
"github.com/pomerium/pomerium/pkg/identity/oauth/apple"
"github.com/pomerium/pomerium/pkg/policy/parser"
)
// DisableHeaderKey is the key used to check whether to disable setting header
@ -1556,6 +1557,233 @@ func (o *Options) ApplySettings(ctx context.Context, certsIndex *cryptutil.Certi
})
}
func (o *Options) ToProto() *config.Config {
var settings config.Settings
copySrcToOptionalDest(&settings.InstallationId, &o.InstallationID)
copySrcToOptionalDest(&settings.LogLevel, (*string)(&o.LogLevel))
settings.AccessLogFields = toStringList(o.AccessLogFields)
settings.AuthorizeLogFields = toStringList(o.AuthorizeLogFields)
copySrcToOptionalDest(&settings.ProxyLogLevel, (*string)(&o.ProxyLogLevel))
copySrcToOptionalDest(&settings.SharedSecret, valueOrFromFileBase64(o.SharedKey, o.SharedSecretFile))
copySrcToOptionalDest(&settings.Services, &o.Services)
copySrcToOptionalDest(&settings.Address, &o.Addr)
copySrcToOptionalDest(&settings.InsecureServer, &o.InsecureServer)
copySrcToOptionalDest(&settings.DnsLookupFamily, &o.DNSLookupFamily)
settings.Certificates = getCertificates(o)
copySrcToOptionalDest(&settings.HttpRedirectAddr, &o.HTTPRedirectAddr)
settings.TimeoutRead = durationpb.New(o.ReadTimeout)
settings.TimeoutWrite = durationpb.New(o.WriteTimeout)
settings.TimeoutIdle = durationpb.New(o.IdleTimeout)
copySrcToOptionalDest(&settings.AuthenticateServiceUrl, &o.AuthenticateURLString)
copySrcToOptionalDest(&settings.AuthenticateInternalServiceUrl, &o.AuthenticateInternalURLString)
copySrcToOptionalDest(&settings.SignoutRedirectUrl, &o.SignOutRedirectURLString)
copySrcToOptionalDest(&settings.AuthenticateCallbackPath, &o.AuthenticateCallbackPath)
copySrcToOptionalDest(&settings.CookieName, &o.CookieName)
copySrcToOptionalDest(&settings.CookieSecret, valueOrFromFileBase64(o.CookieSecret, o.CookieSecretFile))
copySrcToOptionalDest(&settings.CookieDomain, &o.CookieDomain)
copySrcToOptionalDest(&settings.CookieHttpOnly, &o.CookieHTTPOnly)
settings.CookieExpire = durationpb.New(o.CookieExpire)
copySrcToOptionalDest(&settings.CookieSameSite, &o.CookieSameSite)
copySrcToOptionalDest(&settings.IdpClientId, &o.ClientID)
copySrcToOptionalDest(&settings.IdpClientSecret, valueOrFromFileBase64(o.ClientSecret, o.ClientSecretFile))
copySrcToOptionalDest(&settings.IdpProvider, &o.Provider)
copySrcToOptionalDest(&settings.IdpProviderUrl, &o.ProviderURL)
settings.Scopes = o.Scopes
settings.RequestParams = o.RequestParams
settings.AuthorizeServiceUrls = o.AuthorizeURLStrings
copySrcToOptionalDest(&settings.AuthorizeInternalServiceUrl, &o.AuthorizeInternalURLString)
copySrcToOptionalDest(&settings.OverrideCertificateName, &o.OverrideCertificateName)
copySrcToOptionalDest(&settings.CertificateAuthority, valueOrFromFileBase64(o.CA, o.CAFile))
settings.DeriveTls = o.DeriveInternalDomainCert
copySrcToOptionalDest(&settings.SigningKey, valueOrFromFileBase64(o.SigningKey, o.SigningKeyFile))
settings.SetResponseHeaders = o.SetResponseHeaders
settings.JwtClaimsHeaders = o.JWTClaimsHeaders
settings.DefaultUpstreamTimeout = durationpb.New(o.DefaultUpstreamTimeout)
copySrcToOptionalDest(&settings.MetricsAddress, &o.MetricsAddr)
copySrcToOptionalDest(&settings.MetricsBasicAuth, &o.MetricsBasicAuth)
settings.MetricsCertificate = toCertificateOrFromFile(o.MetricsCertificate, o.MetricsCertificateKey, o.MetricsCertificateFile, o.MetricsCertificateKeyFile)
copySrcToOptionalDest(&settings.MetricsClientCa, valueOrFromFileBase64(o.MetricsClientCA, o.MetricsClientCAFile))
copySrcToOptionalDest(&settings.TracingProvider, &o.TracingProvider)
copySrcToOptionalDest(&settings.TracingSampleRate, &o.TracingSampleRate)
copySrcToOptionalDest(&settings.TracingDatadogAddress, &o.TracingDatadogAddress)
copySrcToOptionalDest(&settings.TracingJaegerCollectorEndpoint, &o.TracingJaegerCollectorEndpoint)
copySrcToOptionalDest(&settings.TracingJaegerAgentEndpoint, &o.TracingJaegerAgentEndpoint)
copySrcToOptionalDest(&settings.TracingZipkinEndpoint, &o.ZipkinEndpoint)
copySrcToOptionalDest(&settings.GrpcAddress, &o.GRPCAddr)
settings.GrpcInsecure = o.GRPCInsecure
settings.GrpcClientTimeout = durationpb.New(o.GRPCClientTimeout)
copySrcToOptionalDest(&settings.GrpcClientDnsRoundrobin, &o.GRPCClientDNSRoundRobin)
settings.DatabrokerServiceUrls = o.DataBrokerURLStrings
copySrcToOptionalDest(&settings.DatabrokerInternalServiceUrl, &o.DataBrokerInternalURLString)
copySrcToOptionalDest(&settings.DatabrokerStorageType, &o.DataBrokerStorageType)
copySrcToOptionalDest(&settings.DatabrokerStorageConnectionString, valueOrFromFileRaw(o.DataBrokerStorageConnectionString, o.DataBrokerStorageConnectionStringFile))
settings.DownstreamMtls = o.DownstreamMTLS.toSettingsProto()
copySrcToOptionalDest(&settings.GoogleCloudServerlessAuthenticationServiceAccount, &o.GoogleCloudServerlessAuthenticationServiceAccount)
copySrcToOptionalDest(&settings.UseProxyProtocol, &o.UseProxyProtocol)
copySrcToOptionalDest(&settings.Autocert, &o.AutocertOptions.Enable)
copySrcToOptionalDest(&settings.AutocertCa, &o.AutocertOptions.CA)
copySrcToOptionalDest(&settings.AutocertEmail, &o.AutocertOptions.Email)
copySrcToOptionalDest(&settings.AutocertEabKeyId, &o.AutocertOptions.EABKeyID)
copySrcToOptionalDest(&settings.AutocertEabMacKey, &o.AutocertOptions.EABMACKey)
copySrcToOptionalDest(&settings.AutocertUseStaging, &o.AutocertOptions.UseStaging)
copySrcToOptionalDest(&settings.AutocertMustStaple, &o.AutocertOptions.MustStaple)
copySrcToOptionalDest(&settings.AutocertDir, &o.AutocertOptions.Folder)
copySrcToOptionalDest(&settings.AutocertTrustedCa, &o.AutocertOptions.TrustedCA)
copySrcToOptionalDest(&settings.SkipXffAppend, &o.SkipXffAppend)
copySrcToOptionalDest(&settings.XffNumTrustedHops, &o.XffNumTrustedHops)
settings.ProgrammaticRedirectDomainWhitelist = o.ProgrammaticRedirectDomainWhitelist
settings.AuditKey = o.AuditKey.ToProto()
if o.CodecType != "" {
codecType := o.CodecType.ToEnvoy()
settings.CodecType = &codecType
}
if o.BrandingOptions != nil {
primaryColor := o.BrandingOptions.GetPrimaryColor()
secondaryColor := o.BrandingOptions.GetSecondaryColor()
darkmodePrimaryColor := o.BrandingOptions.GetDarkmodePrimaryColor()
darkmodeSecondaryColor := o.BrandingOptions.GetDarkmodeSecondaryColor()
logoURL := o.BrandingOptions.GetLogoUrl()
faviconURL := o.BrandingOptions.GetFaviconUrl()
errorMessageFirstParagraph := o.BrandingOptions.GetErrorMessageFirstParagraph()
copySrcToOptionalDest(&settings.PrimaryColor, &primaryColor)
copySrcToOptionalDest(&settings.SecondaryColor, &secondaryColor)
copySrcToOptionalDest(&settings.DarkmodePrimaryColor, &darkmodePrimaryColor)
copySrcToOptionalDest(&settings.DarkmodeSecondaryColor, &darkmodeSecondaryColor)
copySrcToOptionalDest(&settings.LogoUrl, &logoURL)
copySrcToOptionalDest(&settings.FaviconUrl, &faviconURL)
copySrcToOptionalDest(&settings.ErrorMessageFirstParagraph, &errorMessageFirstParagraph)
}
copyMap(&settings.RuntimeFlags, o.RuntimeFlags, func(k RuntimeFlag, v bool) (string, bool) {
return string(k), v
})
routes := make([]*config.Route, 0, o.NumPolicies())
for p := range o.GetAllPolicies() {
routepb, err := p.ToProto()
if err != nil {
continue
}
ppl := p.ToPPL()
pplIsEmpty := true
for _, rule := range ppl.Rules {
if rule.Action == parser.ActionAllow &&
len(rule.And) > 0 ||
len(rule.Nor) > 0 ||
len(rule.Not) > 0 ||
len(rule.Or) > 0 {
pplIsEmpty = false
break
}
}
if !pplIsEmpty {
raw, err := ppl.MarshalJSON()
if err != nil {
continue
}
routepb.PplPolicies = append(routepb.PplPolicies, &config.PPLPolicy{
Raw: raw,
})
}
routes = append(routes, routepb)
}
return &config.Config{
Settings: &settings,
Routes: routes,
}
}
func copySrcToOptionalDest[T comparable](dst **T, src *T) {
var zero T
if *src == zero {
*dst = nil
} else {
if *dst == nil {
*dst = src
} else {
**dst = *src
}
}
}
func toStringList[T ~string](s []T) *config.Settings_StringList {
if len(s) == 0 {
return nil
}
strings := make([]string, len(s))
for i, v := range s {
strings[i] = string(v)
}
return &config.Settings_StringList{Values: strings}
}
func toCertificateOrFromFile(
cert string, key string,
certFile string, keyFile string,
) *config.Settings_Certificate {
var crt *tls.Certificate
if cert == "" && key == "" {
if certFile != "" && keyFile != "" {
crt, _ = cryptutil.CertificateFromFile(certFile, keyFile)
}
} else {
crt, _ = cryptutil.CertificateFromBase64(cert, key)
}
if crt == nil {
return nil
}
certBytes, keyBytes, err := cryptutil.EncodeCertificate(crt)
if err != nil {
return nil
}
return &config.Settings_Certificate{
CertBytes: certBytes,
KeyBytes: keyBytes,
}
}
func getCertificates(o *Options) []*config.Settings_Certificate {
certs, err := o.GetCertificates()
if err != nil {
return nil
}
out := make([]*config.Settings_Certificate, len(certs))
for i, crt := range certs {
certBytes, keyBytes, err := cryptutil.EncodeCertificate(&crt)
if err != nil {
return nil
}
out[i] = &config.Settings_Certificate{
CertBytes: certBytes,
KeyBytes: keyBytes,
}
}
return out
}
func valueOrFromFileRaw(value string, valueFile string) *string {
if value != "" {
return &value
}
if valueFile == "" {
return &valueFile
}
data, _ := os.ReadFile(valueFile)
dataStr := string(data)
return &dataStr
}
func valueOrFromFileBase64(value string, valueFile string) *string {
if value != "" {
return &value
}
if valueFile == "" {
return &valueFile
}
data, _ := os.ReadFile(valueFile)
encoded := base64.StdEncoding.EncodeToString(data)
return &encoded
}
func dataDir() string {
homeDir, _ := os.UserHomeDir()
if homeDir == "" {

View file

@ -7,6 +7,7 @@ import (
"encoding/base64"
"encoding/pem"
"fmt"
"math/rand/v2"
"net/http"
"net/url"
"os"
@ -17,16 +18,20 @@ import (
"testing"
"time"
envoy_config_cluster_v3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/pomerium/csrf"
"github.com/pomerium/protoutil/protorand"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/fieldmaskpb"
"github.com/pomerium/csrf"
"github.com/pomerium/pomerium/internal/testutil"
"github.com/pomerium/pomerium/pkg/cryptutil"
"github.com/pomerium/pomerium/pkg/grpc/config"
configpb "github.com/pomerium/pomerium/pkg/grpc/config"
"github.com/pomerium/pomerium/pkg/identity/oauth/apple"
)
@ -932,8 +937,8 @@ func TestOptions_ApplySettings(t *testing.T) {
xc1, _ := x509.ParseCertificate(cert1.Certificate[0])
certsIndex.Add(xc1)
settings := &config.Settings{
Certificates: []*config.Settings_Certificate{
settings := &configpb.Settings{
Certificates: []*configpb.Settings_Certificate{
{CertBytes: encodeCert(cert2)},
{CertBytes: encodeCert(cert3)},
},
@ -944,7 +949,7 @@ func TestOptions_ApplySettings(t *testing.T) {
t.Run("pass_identity_headers", func(t *testing.T) {
options := NewDefaultOptions()
options.ApplySettings(ctx, nil, &config.Settings{
options.ApplySettings(ctx, nil, &configpb.Settings{
PassIdentityHeaders: proto.Bool(true),
})
assert.Equal(t, proto.Bool(true), options.PassIdentityHeaders)
@ -1364,8 +1369,111 @@ func encodeCert(cert *tls.Certificate) []byte {
return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Certificate[0]})
}
func mustParseWeightedURLs(t *testing.T, urls ...string) []WeightedURL {
wu, err := ParseWeightedUrls(urls...)
require.NoError(t, err)
return wu
func TestShallowCopyToProto(t *testing.T) {
routeGen := protorand.New[*configpb.Route]()
routeGen.MaxCollectionElements = 2
routeGen.UseGoDurationLimits = true
routeGen.ExcludeMask(&fieldmaskpb.FieldMask{
Paths: []string{"from", "name", "to", "load_balancing_weights", "redirect", "response", "envoy_opts"},
})
redirectGen := protorand.New[*configpb.RouteRedirect]()
responseGen := protorand.New[*configpb.RouteDirectResponse]()
randomDomain := func() string {
numSegments := rand.IntN(5) + 1
segments := make([]string, numSegments)
for i := range segments {
b := make([]rune, rand.IntN(10)+10)
for j := range b {
b[j] = rune(rand.IntN(26) + 'a')
}
segments[i] = string(b)
}
return strings.Join(segments, ".")
}
newCompleteRoute := func() *configpb.Route {
pb, err := routeGen.Gen()
require.NoError(t, err)
pb.From = "https://" + randomDomain()
// EnvoyOpts is set to an empty non-nil message during conversion, if nil
pb.EnvoyOpts = &envoy_config_cluster_v3.Cluster{}
switch rand.IntN(3) {
case 0:
pb.To = make([]string, rand.IntN(3)+1)
for i := range pb.To {
pb.To[i] = "https://" + randomDomain()
}
pb.LoadBalancingWeights = make([]uint32, len(pb.To))
for i := range pb.LoadBalancingWeights {
pb.LoadBalancingWeights[i] = rand.Uint32N(10000) + 1
}
case 1:
pb.Redirect, err = redirectGen.Gen()
require.NoError(t, err)
case 2:
pb.Response, err = responseGen.Gen()
require.NoError(t, err)
}
return pb
}
t.Run("Round Trip", func(t *testing.T) {
for range 100 {
route := newCompleteRoute()
policy, err := NewPolicyFromProto(route)
require.NoError(t, err)
route2, err := policy.ToProto()
require.NoError(t, err)
route2.Name = ""
testutil.AssertProtoEqual(t, route, route2)
}
})
t.Run("Multiple routes", func(t *testing.T) {
for range 100 {
route1 := newCompleteRoute()
route2 := newCompleteRoute()
{
// create a new policy every time, since reusing the target will mutate
// the underlying route
policy1, err := NewPolicyFromProto(route1)
require.NoError(t, err)
target, err := policy1.ToProto()
require.NoError(t, err)
target.Name = ""
testutil.AssertProtoEqual(t, route1, target)
}
{
policy2, err := NewPolicyFromProto(route2)
require.NoError(t, err)
target, err := policy2.ToProto()
require.NoError(t, err)
target.Name = ""
testutil.AssertProtoEqual(t, route2, target)
}
{
policy1, err := NewPolicyFromProto(route1)
require.NoError(t, err)
target, err := policy1.ToProto()
require.NoError(t, err)
target.Name = ""
testutil.AssertProtoEqual(t, route1, target)
}
{
policy2, err := NewPolicyFromProto(route2)
require.NoError(t, err)
target, err := policy2.ToProto()
require.NoError(t, err)
target.Name = ""
testutil.AssertProtoEqual(t, route2, target)
}
}
})
}

View file

@ -302,6 +302,7 @@ func NewPolicyFromProto(pb *configpb.Route) (*Policy, error) {
}
p.To = to
p.LbWeights = pb.LoadBalancingWeights
}
p.EnvoyOpts = pb.EnvoyOpts
@ -333,7 +334,7 @@ func NewPolicyFromProto(pb *configpb.Route) (*Policy, error) {
Remediation: sp.GetRemediation(),
})
}
return p, p.Validate()
return p, nil
}
// ToProto converts the policy to a protobuf type.
@ -356,6 +357,8 @@ func (p *Policy) ToProto() (*configpb.Route, error) {
AllowedUsers: sp.AllowedUsers,
AllowedDomains: sp.AllowedDomains,
AllowedIdpClaims: sp.AllowedIDPClaims.ToPB(),
Explanation: sp.Explanation,
Remediation: sp.Remediation,
Rego: sp.Rego,
})
}
@ -372,6 +375,7 @@ func (p *Policy) ToProto() (*configpb.Route, error) {
PrefixRewrite: p.PrefixRewrite,
RegexRewritePattern: p.RegexRewritePattern,
RegexRewriteSubstitution: p.RegexRewriteSubstitution,
RegexPriorityOrder: p.RegexPriorityOrder,
CorsAllowPreflight: p.CORSAllowPreflight,
AllowPublicUnauthenticatedAccess: p.AllowPublicUnauthenticatedAccess,
AllowAnyAuthenticatedUser: p.AllowAnyAuthenticatedUser,
@ -396,8 +400,22 @@ func (p *Policy) ToProto() (*configpb.Route, error) {
PreserveHostHeader: p.PreserveHostHeader,
PassIdentityHeaders: p.PassIdentityHeaders,
KubernetesServiceAccountToken: p.KubernetesServiceAccountToken,
Policies: sps,
SetResponseHeaders: p.SetResponseHeaders,
EnableGoogleCloudServerlessAuthentication: p.EnableGoogleCloudServerlessAuthentication,
Policies: sps,
EnvoyOpts: p.EnvoyOpts,
SetResponseHeaders: p.SetResponseHeaders,
}
if p.HostPathRegexRewritePattern != "" {
pb.HostPathRegexRewritePattern = proto.String(p.HostPathRegexRewritePattern)
}
if p.HostPathRegexRewriteSubstitution != "" {
pb.HostPathRegexRewriteSubstitution = proto.String(p.HostPathRegexRewriteSubstitution)
}
if p.HostRewrite != "" {
pb.HostRewrite = proto.String(p.HostRewrite)
}
if p.HostRewriteHeader != "" {
pb.HostRewriteHeader = proto.String(p.HostRewriteHeader)
}
if p.IDPClientID != "" {
pb.IdpClientId = proto.String(p.IDPClientID)
@ -512,10 +530,11 @@ func (p *Policy) Validate() error {
return fmt.Errorf("config: couldn't decode custom ca: %w", err)
}
} else if p.TLSCustomCAFile != "" {
_, err := os.Stat(p.TLSCustomCAFile)
ca, err := os.ReadFile(p.TLSCustomCAFile)
if err != nil {
return fmt.Errorf("config: couldn't load client ca file: %w", err)
}
p.TLSCustomCA = base64.StdEncoding.EncodeToString(ca)
}
const clientCADeprecationMsg = "config: %s is deprecated, see https://www.pomerium.com/docs/" +

View file

@ -369,3 +369,9 @@ func TestPolicy_IsTCPUpstream(t *testing.T) {
}
assert.False(t, p3.IsTCPUpstream())
}
func mustParseWeightedURLs(t testing.TB, urls ...string) []WeightedURL {
wu, err := ParseWeightedUrls(urls...)
require.NoError(t, err)
return wu
}

34
go.mod
View file

@ -17,6 +17,11 @@ require (
github.com/caddyserver/certmagic v0.21.3
github.com/cenkalti/backoff/v4 v4.3.0
github.com/cespare/xxhash/v2 v2.3.0
github.com/charmbracelet/bubbletea v1.1.1
github.com/charmbracelet/huh v0.6.0
github.com/charmbracelet/lipgloss v0.13.0
github.com/charmbracelet/x/ansi v0.2.3
github.com/charmbracelet/x/exp/teatest v0.0.0-20240913162256-9ef7ff40e654
github.com/cloudflare/circl v1.4.0
github.com/coreos/go-oidc/v3 v3.11.0
github.com/docker/docker v27.2.0+incompatible
@ -43,6 +48,7 @@ require (
github.com/minio/minio-go/v7 v7.0.76
github.com/mitchellh/hashstructure/v2 v2.0.2
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a
github.com/natefinch/atomic v1.0.1
github.com/oapi-codegen/runtime v1.1.1
github.com/open-policy-agent/opa v0.68.0
@ -51,6 +57,7 @@ require (
github.com/peterbourgon/ff/v3 v3.4.0
github.com/pomerium/csrf v1.7.0
github.com/pomerium/datasource v0.18.2-0.20221108160055-c6134b5ed524
github.com/pomerium/protoutil v0.0.0-20240813175624-47b7ac43ff46
github.com/pomerium/webauthn v0.0.0-20240603205124-0428df511172
github.com/prometheus/client_golang v1.20.2
github.com/prometheus/client_model v0.6.1
@ -59,6 +66,7 @@ require (
github.com/rs/cors v1.11.1
github.com/rs/zerolog v1.33.0
github.com/shirou/gopsutil/v3 v3.24.5
github.com/spf13/cobra v1.8.1
github.com/spf13/viper v1.19.0
github.com/stretchr/testify v1.9.0
github.com/tniswong/go.rfcx v0.0.0-20181019234604-07783c52761f
@ -82,7 +90,7 @@ require (
golang.org/x/net v0.28.0
golang.org/x/oauth2 v0.22.0
golang.org/x/sync v0.8.0
golang.org/x/sys v0.24.0
golang.org/x/sys v0.25.0
golang.org/x/time v0.6.0
google.golang.org/api v0.196.0
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1
@ -109,6 +117,7 @@ require (
github.com/agnivade/levenshtein v1.1.1 // indirect
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.31 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 // indirect
@ -124,9 +133,16 @@ require (
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.6 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.30.6 // indirect
github.com/aws/smithy-go v1.20.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymanbagabas/go-udiff v0.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/caddyserver/zerossl v0.1.3 // indirect
github.com/catppuccin/go v0.2.0 // indirect
github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
github.com/charmbracelet/bubbles v0.20.0 // indirect
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b // indirect
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
github.com/charmbracelet/x/term v0.2.0 // indirect
github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b // indirect
github.com/containerd/continuity v0.4.3 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
@ -135,6 +151,7 @@ require (
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/fxamacker/cbor/v2 v2.6.0 // indirect
@ -160,23 +177,29 @@ require (
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
github.com/kralicky/go-adaptive-radix-tree v0.0.0-20240624235931-330eb762e74c // indirect
github.com/libdns/libdns v0.2.2 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/miekg/dns v1.1.59 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect
github.com/onsi/gomega v1.30.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/opencontainers/runc v1.1.14 // indirect
@ -188,6 +211,7 @@ require (
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/prometheus/statsd_exporter v0.22.7 // indirect
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
@ -197,6 +221,7 @@ require (
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/sryoya/protorand v0.0.0-20240429201223-e7440656b2a4 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tchap/go-patricia/v2 v2.3.1 // indirect
@ -212,13 +237,14 @@ require (
github.com/yusufpapurcu/wmi v1.2.4 // indirect
github.com/zeebo/assert v1.3.1 // indirect
github.com/zeebo/blake3 v0.2.3 // indirect
github.com/zeebo/xxh3 v1.0.2 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 // indirect
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/mod v0.20.0 // indirect
golang.org/x/text v0.17.0 // indirect
golang.org/x/text v0.18.0 // indirect
golang.org/x/tools v0.24.0 // indirect
google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed // indirect
@ -226,3 +252,5 @@ require (
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
replace github.com/charmbracelet/huh => github.com/kralicky/huh v0.0.0-20240910153959-781f516d4413

79
go.sum
View file

@ -67,6 +67,8 @@ github.com/DataDog/datadog-go v3.5.0+incompatible h1:AShr9cqkF+taHjyQgcBcQUt/ZNK
github.com/DataDog/datadog-go v3.5.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/DataDog/opencensus-go-exporter-datadog v0.0.0-20200406135749-5c268882acf0 h1:Y6HFfo8UuntPOpfmUmLb0o3MNYKfUuH2aNmvypsDbY4=
github.com/DataDog/opencensus-go-exporter-datadog v0.0.0-20200406135749-5c268882acf0/go.mod h1:/VV3EFO/hTNQZHAqaj+CPGy2+ioFrP4EX3iRwozubhQ=
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
@ -94,6 +96,8 @@ github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7D
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aws/aws-sdk-go-v2 v1.30.5 h1:mWSRTwQAb0aLE17dSzztCVJWI9+cRMgqebndjwDyK0g=
github.com/aws/aws-sdk-go-v2 v1.30.5/go.mod h1:CT+ZPWXbYrci8chcARI3OmI/qgd+f6WtuLOoaIA8PR0=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4 h1:70PVAiL15/aBMh5LThwgXdSQorVr91L127ttckI9QQU=
@ -130,6 +134,10 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.30.6 h1:TrQadF7GcqvQ63kgwEcjlrVc2Fa0
github.com/aws/aws-sdk-go-v2/service/sts v1.30.6/go.mod h1:NXi1dIAGteSaRLqYgarlhP/Ij0cFT+qmCwiJqWh/U5o=
github.com/aws/smithy-go v1.20.4 h1:2HK1zBdPgRbjFOHlfeQZfpC4r72MOb9bZkiFwggKO+4=
github.com/aws/smithy-go v1.20.4/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@ -143,6 +151,8 @@ github.com/caddyserver/certmagic v0.21.3 h1:pqRRry3yuB4CWBVq9+cUqu+Y6E2z8TswbhNx
github.com/caddyserver/certmagic v0.21.3/go.mod h1:Zq6pklO9nVRl3DIFUw9gVUfXKdpc/0qwTUAQMBlfgtI=
github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA=
github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA=
github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@ -155,6 +165,22 @@ github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE=
github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU=
github.com/charmbracelet/bubbletea v1.1.1 h1:KJ2/DnmpfqFtDNVTvYZ6zpPFL9iRCRr0qqKOCvppbPY=
github.com/charmbracelet/bubbletea v1.1.1/go.mod h1:9Ogk0HrdbHolIKHdjfFpyXJmiCzGwy+FesYkZr7hYU4=
github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw=
github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY=
github.com/charmbracelet/x/ansi v0.2.3 h1:VfFN0NUpcjBRd4DnKfRaIRo53KRgey/nhOoEqosGDEY=
github.com/charmbracelet/x/ansi v0.2.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q=
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
github.com/charmbracelet/x/exp/teatest v0.0.0-20240913162256-9ef7ff40e654 h1:d+B9FUkxeEex8Q5p4pafFxZbUMzE/TJ64Y5bFDPKcd4=
github.com/charmbracelet/x/exp/teatest v0.0.0-20240913162256-9ef7ff40e654/go.mod h1:NDRRSMP6bZbCs4jyc4i1/4UG4M+0PEiQdpivQgD0Mio=
github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0=
github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@ -171,6 +197,7 @@ github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3
github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI=
github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -207,6 +234,8 @@ github.com/envoyproxy/go-control-plane v0.13.0/go.mod h1:GRaKG3dwvFoTg4nj7aXdZnv
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM=
github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
@ -257,6 +286,9 @@ github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpv
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
@ -342,6 +374,8 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg=
github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=
github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA=
@ -385,6 +419,8 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
@ -425,12 +461,18 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kralicky/go-adaptive-radix-tree v0.0.0-20240624235931-330eb762e74c h1:TRkEV8M5PhQU55WI49FKTszEIpFlwZ1wfxcACCRT7SE=
github.com/kralicky/go-adaptive-radix-tree v0.0.0-20240624235931-330eb762e74c/go.mod h1:oJwexVSshEat0E3evyKOH6QzN8GFWrhLvEoh8GiJzss=
github.com/kralicky/huh v0.0.0-20240910153959-781f516d4413 h1:PX6KYaLRxKuc67ONo77pUGBBNwFICIrxi8wzDTxNm1U=
github.com/kralicky/huh v0.0.0-20240910153959-781f516d4413/go.mod h1:GGNKeWCeNzKpEOh/OJD8WBwTQjV3prFAtQPpLv+AVwU=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s=
github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae h1:dIZY4ULFcto4tAFlj1FYZl8ztUZ13bdq+PLY+NOfbyI=
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
@ -444,6 +486,10 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mholt/acmez/v2 v2.0.2 h1:OmK6xckte2JfKGPz4OAA8aNHTiLvGp8tLzmrd/wfSyw=
github.com/mholt/acmez/v2 v2.0.2/go.mod h1:fX4c9r5jYwMyMsC+7tkYRxHibkOTgta5DIFGoe67e1U=
@ -468,6 +514,12 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg=
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
@ -484,11 +536,13 @@ github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/ginkgo/v2 v2.19.1 h1:QXgq3Z8Crl5EL1WBAC98A5sEBHARrAJNzAmMxzLcRF0=
github.com/onsi/ginkgo/v2 v2.19.1/go.mod h1:O3DtEWQkPa/F7fBMgmZQKKsluAy8pd3rEQdrjkPb9zA=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8=
github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/open-policy-agent/opa v0.68.0 h1:Jl3U2vXRjwk7JrHmS19U3HZO5qxQRinQbJ2eCJYSqJQ=
github.com/open-policy-agent/opa v0.68.0/go.mod h1:5E5SvaPwTpwt2WM177I9Z3eT7qUpmOGjk1ZdHs+TZ4w=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
@ -528,6 +582,8 @@ github.com/pomerium/csrf v1.7.0 h1:Qp4t6oyEod3svQtKfJZs589mdUTWKVf7q0PgCKYCshY=
github.com/pomerium/csrf v1.7.0/go.mod h1:hAPZV47mEj2T9xFs+ysbum4l7SF1IdrryYaY6PdoIqw=
github.com/pomerium/datasource v0.18.2-0.20221108160055-c6134b5ed524 h1:3YQY1sb54tEEbr0L73rjHkpLB0IB6qh3zl1+XQbMLis=
github.com/pomerium/datasource v0.18.2-0.20221108160055-c6134b5ed524/go.mod h1:7fGbUYJnU8RcxZJvUvhukOIBv1G7LWDAHMfDxAf5+Y0=
github.com/pomerium/protoutil v0.0.0-20240813175624-47b7ac43ff46 h1:NRTg8JOXCxcIA1lAgD74iYud0rbshbWOB3Ou4+Huil8=
github.com/pomerium/protoutil v0.0.0-20240813175624-47b7ac43ff46/go.mod h1:QqZmx6ZgPxz18va7kqoT4t/0yJtP7YFIDiT/W2n2fZ4=
github.com/pomerium/webauthn v0.0.0-20240603205124-0428df511172 h1:TqoPqRgXSHpn+tEJq6H72iCS5pv66j3rPprThUEZg0E=
github.com/pomerium/webauthn v0.0.0-20240603205124-0428df511172/go.mod h1:kBQ45E9LluzW7FP1Scn3esaiS2WVbvNRLMOTHareZNQ=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
@ -570,6 +626,9 @@ github.com/prometheus/statsd_exporter v0.22.7/go.mod h1:N/TevpjkIh9ccs6nuzY3jQn9
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM=
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
@ -580,6 +639,7 @@ github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
@ -603,11 +663,15 @@ github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
github.com/sryoya/protorand v0.0.0-20240429201223-e7440656b2a4 h1:/jKH9ivHOUkahZs3zPfJfOmkXDFB6OdsHZ4W8gyDb/c=
github.com/sryoya/protorand v0.0.0-20240429201223-e7440656b2a4/go.mod h1:9a23nlv6vzBeVlQq6JQCjljZ6sfzsB6aha1m5Ly1W2Y=
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@ -675,6 +739,8 @@ github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg=
github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvvKCaQ=
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
@ -877,6 +943,7 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -893,8 +960,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
@ -912,8 +979,8 @@ golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=

View file

@ -2,18 +2,23 @@
package zero
import (
"bytes"
"compress/gzip"
"context"
"fmt"
"io"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/keepalive"
"google.golang.org/protobuf/proto"
"github.com/pomerium/pomerium/internal/zero/apierror"
connect_mux "github.com/pomerium/pomerium/internal/zero/connect-mux"
"github.com/pomerium/pomerium/internal/zero/grpcconn"
token_api "github.com/pomerium/pomerium/internal/zero/token"
"github.com/pomerium/pomerium/pkg/fanout"
configpb "github.com/pomerium/pomerium/pkg/grpc/config"
cluster_api "github.com/pomerium/pomerium/pkg/zero/cluster"
connect_api "github.com/pomerium/pomerium/pkg/zero/connect"
)
@ -116,6 +121,30 @@ func (api *API) GetClusterResourceBundles(ctx context.Context) (*cluster_api.Get
)
}
func (api *API) ImportConfig(ctx context.Context, cfg *configpb.Config) (*cluster_api.EmptyResponse, error) {
data, err := proto.Marshal(cfg)
if err != nil {
return nil, err
}
var compressedData bytes.Buffer
w := gzip.NewWriter(&compressedData)
_, err = io.Copy(w, bytes.NewReader(data))
if err != nil {
return nil, err
}
if err := w.Close(); err != nil {
return nil, err
}
return apierror.CheckResponse(api.cluster.ImportConfigurationWithBodyWithResponse(ctx,
"application/octet-stream",
&compressedData,
))
}
func (api *API) GetQuotas(ctx context.Context) (*cluster_api.ConfigQuotas, error) {
return apierror.CheckResponse(api.cluster.GetQuotasWithResponse(ctx))
}
func (api *API) GetTelemetryConn() *grpc.ClientConn {
return api.telemetryConn
}

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-----

File diff suppressed because it is too large Load diff

View file

@ -108,6 +108,7 @@ message Route {
envoy.config.cluster.v3.Cluster envoy_opts = 36;
repeated Policy policies = 27;
repeated PPLPolicy ppl_policies = 63;
string id = 28;
optional string host_rewrite = 50;
@ -120,6 +121,10 @@ message Route {
bool show_error_details = 59;
}
message PPLPolicy {
bytes raw = 1;
}
message Policy {
string id = 1;
string name = 2;

View file

@ -103,6 +103,12 @@ type ClientInterface interface {
ReportClusterResourceBundleStatus(ctx context.Context, bundleId BundleId, body ReportClusterResourceBundleStatusJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error)
// ImportConfigurationWithBody request with any body
ImportConfigurationWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error)
// GetQuotas request
GetQuotas(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error)
// ExchangeClusterIdentityTokenWithBody request with any body
ExchangeClusterIdentityTokenWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error)
@ -174,6 +180,30 @@ func (c *Client) ReportClusterResourceBundleStatus(ctx context.Context, bundleId
return c.Client.Do(req)
}
func (c *Client) ImportConfigurationWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) {
req, err := NewImportConfigurationRequestWithBody(c.Server, contentType, body)
if err != nil {
return nil, err
}
req = req.WithContext(ctx)
if err := c.applyEditors(ctx, req, reqEditors); err != nil {
return nil, err
}
return c.Client.Do(req)
}
func (c *Client) GetQuotas(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) {
req, err := NewGetQuotasRequest(c.Server)
if err != nil {
return nil, err
}
req = req.WithContext(ctx)
if err := c.applyEditors(ctx, req, reqEditors); err != nil {
return nil, err
}
return c.Client.Do(req)
}
func (c *Client) ExchangeClusterIdentityTokenWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) {
req, err := NewExchangeClusterIdentityTokenRequestWithBody(c.Server, contentType, body)
if err != nil {
@ -357,6 +387,62 @@ func NewReportClusterResourceBundleStatusRequestWithBody(server string, bundleId
return req, nil
}
// NewImportConfigurationRequestWithBody generates requests for ImportConfiguration with any type of body
func NewImportConfigurationRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) {
var err error
serverURL, err := url.Parse(server)
if err != nil {
return nil, err
}
operationPath := fmt.Sprintf("/config/import")
if operationPath[0] == '/' {
operationPath = "." + operationPath
}
queryURL, err := serverURL.Parse(operationPath)
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", queryURL.String(), body)
if err != nil {
return nil, err
}
req.Header.Add("Content-Type", contentType)
return req, nil
}
// NewGetQuotasRequest generates requests for GetQuotas
func NewGetQuotasRequest(server string) (*http.Request, error) {
var err error
serverURL, err := url.Parse(server)
if err != nil {
return nil, err
}
operationPath := fmt.Sprintf("/config/quotas")
if operationPath[0] == '/' {
operationPath = "." + operationPath
}
queryURL, err := serverURL.Parse(operationPath)
if err != nil {
return nil, err
}
req, err := http.NewRequest("GET", queryURL.String(), nil)
if err != nil {
return nil, err
}
return req, nil
}
// NewExchangeClusterIdentityTokenRequest calls the generic ExchangeClusterIdentityToken builder with application/json body
func NewExchangeClusterIdentityTokenRequest(server string, body ExchangeClusterIdentityTokenJSONRequestBody) (*http.Request, error) {
var bodyReader io.Reader
@ -494,6 +580,12 @@ type ClientWithResponsesInterface interface {
ReportClusterResourceBundleStatusWithResponse(ctx context.Context, bundleId BundleId, body ReportClusterResourceBundleStatusJSONRequestBody, reqEditors ...RequestEditorFn) (*ReportClusterResourceBundleStatusResp, error)
// ImportConfigurationWithBodyWithResponse request with any body
ImportConfigurationWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ImportConfigurationResp, error)
// GetQuotasWithResponse request
GetQuotasWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetQuotasResp, error)
// ExchangeClusterIdentityTokenWithBodyWithResponse request with any body
ExchangeClusterIdentityTokenWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ExchangeClusterIdentityTokenResp, error)
@ -601,6 +693,53 @@ func (r ReportClusterResourceBundleStatusResp) StatusCode() int {
return 0
}
type ImportConfigurationResp struct {
Body []byte
HTTPResponse *http.Response
JSON400 *ErrorResponse
JSON500 *ErrorResponse
}
// Status returns HTTPResponse.Status
func (r ImportConfigurationResp) Status() string {
if r.HTTPResponse != nil {
return r.HTTPResponse.Status
}
return http.StatusText(0)
}
// StatusCode returns HTTPResponse.StatusCode
func (r ImportConfigurationResp) StatusCode() int {
if r.HTTPResponse != nil {
return r.HTTPResponse.StatusCode
}
return 0
}
type GetQuotasResp struct {
Body []byte
HTTPResponse *http.Response
JSON200 *ConfigQuotas
JSON400 *ErrorResponse
JSON500 *ErrorResponse
}
// Status returns HTTPResponse.Status
func (r GetQuotasResp) Status() string {
if r.HTTPResponse != nil {
return r.HTTPResponse.Status
}
return http.StatusText(0)
}
// StatusCode returns HTTPResponse.StatusCode
func (r GetQuotasResp) StatusCode() int {
if r.HTTPResponse != nil {
return r.HTTPResponse.StatusCode
}
return 0
}
type ExchangeClusterIdentityTokenResp struct {
Body []byte
HTTPResponse *http.Response
@ -692,6 +831,24 @@ func (c *ClientWithResponses) ReportClusterResourceBundleStatusWithResponse(ctx
return ParseReportClusterResourceBundleStatusResp(rsp)
}
// ImportConfigurationWithBodyWithResponse request with arbitrary body returning *ImportConfigurationResp
func (c *ClientWithResponses) ImportConfigurationWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ImportConfigurationResp, error) {
rsp, err := c.ImportConfigurationWithBody(ctx, contentType, body, reqEditors...)
if err != nil {
return nil, err
}
return ParseImportConfigurationResp(rsp)
}
// GetQuotasWithResponse request returning *GetQuotasResp
func (c *ClientWithResponses) GetQuotasWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetQuotasResp, error) {
rsp, err := c.GetQuotas(ctx, reqEditors...)
if err != nil {
return nil, err
}
return ParseGetQuotasResp(rsp)
}
// ExchangeClusterIdentityTokenWithBodyWithResponse request with arbitrary body returning *ExchangeClusterIdentityTokenResp
func (c *ClientWithResponses) ExchangeClusterIdentityTokenWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ExchangeClusterIdentityTokenResp, error) {
rsp, err := c.ExchangeClusterIdentityTokenWithBody(ctx, contentType, body, reqEditors...)
@ -886,6 +1043,79 @@ func ParseReportClusterResourceBundleStatusResp(rsp *http.Response) (*ReportClus
return response, nil
}
// ParseImportConfigurationResp parses an HTTP response from a ImportConfigurationWithResponse call
func ParseImportConfigurationResp(rsp *http.Response) (*ImportConfigurationResp, error) {
bodyBytes, err := io.ReadAll(rsp.Body)
defer func() { _ = rsp.Body.Close() }()
if err != nil {
return nil, err
}
response := &ImportConfigurationResp{
Body: bodyBytes,
HTTPResponse: rsp,
}
switch {
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400:
var dest ErrorResponse
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
return nil, err
}
response.JSON400 = &dest
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500:
var dest ErrorResponse
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
return nil, err
}
response.JSON500 = &dest
}
return response, nil
}
// ParseGetQuotasResp parses an HTTP response from a GetQuotasWithResponse call
func ParseGetQuotasResp(rsp *http.Response) (*GetQuotasResp, error) {
bodyBytes, err := io.ReadAll(rsp.Body)
defer func() { _ = rsp.Body.Close() }()
if err != nil {
return nil, err
}
response := &GetQuotasResp{
Body: bodyBytes,
HTTPResponse: rsp,
}
switch {
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
var dest ConfigQuotas
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
return nil, err
}
response.JSON200 = &dest
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400:
var dest ErrorResponse
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
return nil, err
}
response.JSON400 = &dest
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500:
var dest ErrorResponse
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
return nil, err
}
response.JSON500 = &dest
}
return response, nil
}
// ParseExchangeClusterIdentityTokenResp parses an HTTP response from a ExchangeClusterIdentityTokenWithResponse call
func ParseExchangeClusterIdentityTokenResp(rsp *http.Response) (*ExchangeClusterIdentityTokenResp, error) {
bodyBytes, err := io.ReadAll(rsp.Body)
@ -1073,6 +1303,61 @@ func (r *ReportClusterResourceBundleStatusResp) GetValue() *EmptyResponse {
return &EmptyResponse{}
}
// GetHTTPResponse implements apierror.APIResponse
func (r *ImportConfigurationResp) GetHTTPResponse() *http.Response {
return r.HTTPResponse
}
// GetBadRequestError implements apierror.APIResponse
func (r *ImportConfigurationResp) GetBadRequestError() (string, bool) {
if r.JSON400 == nil {
return "", false
}
return r.JSON400.Error, true
}
// GetInternalServerError implements apierror.APIResponse
func (r *ImportConfigurationResp) GetInternalServerError() (string, bool) {
if r.JSON500 == nil {
return "", false
}
return r.JSON500.Error, true
}
// GetValue implements apierror.APIResponse
func (r *ImportConfigurationResp) GetValue() *EmptyResponse {
if r.StatusCode()/100 != 2 {
return nil
}
return &EmptyResponse{}
}
// GetHTTPResponse implements apierror.APIResponse
func (r *GetQuotasResp) GetHTTPResponse() *http.Response {
return r.HTTPResponse
}
// GetValue implements apierror.APIResponse
func (r *GetQuotasResp) GetValue() *ConfigQuotas {
return r.JSON200
}
// GetBadRequestError implements apierror.APIResponse
func (r *GetQuotasResp) GetBadRequestError() (string, bool) {
if r.JSON400 == nil {
return "", false
}
return r.JSON400.Error, true
}
// GetInternalServerError implements apierror.APIResponse
func (r *GetQuotasResp) GetInternalServerError() (string, bool) {
if r.JSON500 == nil {
return "", false
}
return r.JSON500.Error, true
}
// GetHTTPResponse implements apierror.APIResponse
func (r *ExchangeClusterIdentityTokenResp) GetHTTPResponse() *http.Response {
return r.HTTPResponse

View file

@ -13,4 +13,6 @@ var (
_ apierror.APIResponse[GetBundlesResponse] = (*GetClusterResourceBundlesResp)(nil)
_ apierror.APIResponse[DownloadBundleResponse] = (*DownloadClusterResourceBundleResp)(nil)
_ apierror.APIResponse[EmptyResponse] = (*ReportClusterResourceBundleStatusResp)(nil)
_ apierror.APIResponse[EmptyResponse] = (*ImportConfigurationResp)(nil)
_ apierror.APIResponse[ConfigQuotas] = (*GetQuotasResp)(nil)
)

View file

@ -62,6 +62,13 @@ type BundleStatusSuccess struct {
Metadata map[string]string `json:"metadata"`
}
// ConfigQuotas defines model for ConfigQuotas.
type ConfigQuotas struct {
Certificates int `json:"certificates"`
Policies int `json:"policies"`
Routes int `json:"routes"`
}
// DownloadBundleResponse defines model for DownloadBundleResponse.
type DownloadBundleResponse struct {
// CaptureMetadataHeaders bundle metadata that need be picked up by the client from the download URL

View file

@ -148,7 +148,6 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/reportUsage:
post:
description: Report usage for the cluster
@ -176,6 +175,62 @@ paths:
schema:
$ref: "#/components/schemas/ErrorResponse"
/config/import:
post:
description: |
Apply the raw configuration directly to the cluster.
This operation only has an effect before any other modifications to the
cluster's configuration have been made.
operationId: importConfiguration
tags: [cluster]
requestBody:
required: true
content:
# TODO: add max payload size?
application/octet-stream:
schema:
type: string
contentMediaType: application/octet-stream
contentEncoding: gzip
description: type.googleapis.com/pomerium.config.Config
responses:
"200":
description: OK
"400":
description: Bad Request
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"500":
description: Internal Server Error
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/config/quotas:
get:
description: Get the cluster's current configuration quotas
operationId: getQuotas
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/ConfigQuotas"
"400":
description: Bad Request
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"500":
description: Internal Server Error
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
components:
parameters:
bundleId:
@ -333,3 +388,16 @@ components:
- lastSignedInAt
- pseudonymousEmail
- pseudonymousId
ConfigQuotas:
type: object
properties:
certificates:
type: integer
policies:
type: integer
routes:
type: integer
required:
- certificates
- policies
- routes

View file

@ -7,6 +7,7 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/go-chi/chi/v5"
@ -29,6 +30,12 @@ type ServerInterface interface {
// (POST /bundles/{bundleId}/status)
ReportClusterResourceBundleStatus(w http.ResponseWriter, r *http.Request, bundleId BundleId)
// (POST /config/import)
ImportConfiguration(w http.ResponseWriter, r *http.Request)
// (GET /config/quotas)
GetQuotas(w http.ResponseWriter, r *http.Request)
// (POST /exchangeToken)
ExchangeClusterIdentityToken(w http.ResponseWriter, r *http.Request)
@ -60,6 +67,16 @@ func (_ Unimplemented) ReportClusterResourceBundleStatus(w http.ResponseWriter,
w.WriteHeader(http.StatusNotImplemented)
}
// (POST /config/import)
func (_ Unimplemented) ImportConfiguration(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotImplemented)
}
// (GET /config/quotas)
func (_ Unimplemented) GetQuotas(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotImplemented)
}
// (POST /exchangeToken)
func (_ Unimplemented) ExchangeClusterIdentityToken(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotImplemented)
@ -169,6 +186,40 @@ func (siw *ServerInterfaceWrapper) ReportClusterResourceBundleStatus(w http.Resp
handler.ServeHTTP(w, r.WithContext(ctx))
}
// ImportConfiguration operation middleware
func (siw *ServerInterfaceWrapper) ImportConfiguration(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
ctx = context.WithValue(ctx, BearerAuthScopes, []string{})
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
siw.Handler.ImportConfiguration(w, r)
}))
for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- {
handler = siw.HandlerMiddlewares[i](handler)
}
handler.ServeHTTP(w, r.WithContext(ctx))
}
// GetQuotas operation middleware
func (siw *ServerInterfaceWrapper) GetQuotas(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
ctx = context.WithValue(ctx, BearerAuthScopes, []string{})
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
siw.Handler.GetQuotas(w, r)
}))
for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- {
handler = siw.HandlerMiddlewares[i](handler)
}
handler.ServeHTTP(w, r.WithContext(ctx))
}
// ExchangeClusterIdentityToken operation middleware
func (siw *ServerInterfaceWrapper) ExchangeClusterIdentityToken(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
@ -326,6 +377,12 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl
r.Group(func(r chi.Router) {
r.Post(options.BaseURL+"/bundles/{bundleId}/status", wrapper.ReportClusterResourceBundleStatus)
})
r.Group(func(r chi.Router) {
r.Post(options.BaseURL+"/config/import", wrapper.ImportConfiguration)
})
r.Group(func(r chi.Router) {
r.Get(options.BaseURL+"/config/quotas", wrapper.GetQuotas)
})
r.Group(func(r chi.Router) {
r.Post(options.BaseURL+"/exchangeToken", wrapper.ExchangeClusterIdentityToken)
})
@ -483,6 +540,74 @@ func (response ReportClusterResourceBundleStatus500JSONResponse) VisitReportClus
return json.NewEncoder(w).Encode(response)
}
type ImportConfigurationRequestObject struct {
Body io.Reader
}
type ImportConfigurationResponseObject interface {
VisitImportConfigurationResponse(w http.ResponseWriter) error
}
type ImportConfiguration200Response struct {
}
func (response ImportConfiguration200Response) VisitImportConfigurationResponse(w http.ResponseWriter) error {
w.WriteHeader(200)
return nil
}
type ImportConfiguration400JSONResponse ErrorResponse
func (response ImportConfiguration400JSONResponse) VisitImportConfigurationResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(400)
return json.NewEncoder(w).Encode(response)
}
type ImportConfiguration500JSONResponse ErrorResponse
func (response ImportConfiguration500JSONResponse) VisitImportConfigurationResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(500)
return json.NewEncoder(w).Encode(response)
}
type GetQuotasRequestObject struct {
}
type GetQuotasResponseObject interface {
VisitGetQuotasResponse(w http.ResponseWriter) error
}
type GetQuotas200JSONResponse ConfigQuotas
func (response GetQuotas200JSONResponse) VisitGetQuotasResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
return json.NewEncoder(w).Encode(response)
}
type GetQuotas400JSONResponse ErrorResponse
func (response GetQuotas400JSONResponse) VisitGetQuotasResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(400)
return json.NewEncoder(w).Encode(response)
}
type GetQuotas500JSONResponse ErrorResponse
func (response GetQuotas500JSONResponse) VisitGetQuotasResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(500)
return json.NewEncoder(w).Encode(response)
}
type ExchangeClusterIdentityTokenRequestObject struct {
Body *ExchangeClusterIdentityTokenJSONRequestBody
}
@ -567,6 +692,12 @@ type StrictServerInterface interface {
// (POST /bundles/{bundleId}/status)
ReportClusterResourceBundleStatus(ctx context.Context, request ReportClusterResourceBundleStatusRequestObject) (ReportClusterResourceBundleStatusResponseObject, error)
// (POST /config/import)
ImportConfiguration(ctx context.Context, request ImportConfigurationRequestObject) (ImportConfigurationResponseObject, error)
// (GET /config/quotas)
GetQuotas(ctx context.Context, request GetQuotasRequestObject) (GetQuotasResponseObject, error)
// (POST /exchangeToken)
ExchangeClusterIdentityToken(ctx context.Context, request ExchangeClusterIdentityTokenRequestObject) (ExchangeClusterIdentityTokenResponseObject, error)
@ -710,6 +841,56 @@ func (sh *strictHandler) ReportClusterResourceBundleStatus(w http.ResponseWriter
}
}
// ImportConfiguration operation middleware
func (sh *strictHandler) ImportConfiguration(w http.ResponseWriter, r *http.Request) {
var request ImportConfigurationRequestObject
request.Body = r.Body
handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) {
return sh.ssi.ImportConfiguration(ctx, request.(ImportConfigurationRequestObject))
}
for _, middleware := range sh.middlewares {
handler = middleware(handler, "ImportConfiguration")
}
response, err := handler(r.Context(), w, r, request)
if err != nil {
sh.options.ResponseErrorHandlerFunc(w, r, err)
} else if validResponse, ok := response.(ImportConfigurationResponseObject); ok {
if err := validResponse.VisitImportConfigurationResponse(w); err != nil {
sh.options.ResponseErrorHandlerFunc(w, r, err)
}
} else if response != nil {
sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response))
}
}
// GetQuotas operation middleware
func (sh *strictHandler) GetQuotas(w http.ResponseWriter, r *http.Request) {
var request GetQuotasRequestObject
handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) {
return sh.ssi.GetQuotas(ctx, request.(GetQuotasRequestObject))
}
for _, middleware := range sh.middlewares {
handler = middleware(handler, "GetQuotas")
}
response, err := handler(r.Context(), w, r, request)
if err != nil {
sh.options.ResponseErrorHandlerFunc(w, r, err)
} else if validResponse, ok := response.(GetQuotasResponseObject); ok {
if err := validResponse.VisitGetQuotasResponse(w); err != nil {
sh.options.ResponseErrorHandlerFunc(w, r, err)
}
} else if response != nil {
sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response))
}
}
// ExchangeClusterIdentityToken operation middleware
func (sh *strictHandler) ExchangeClusterIdentityToken(w http.ResponseWriter, r *http.Request) {
var request ExchangeClusterIdentityTokenRequestObject

View file

@ -0,0 +1,366 @@
package importutil
import (
"crypto/x509"
"fmt"
"iter"
"net/url"
"regexp"
"slices"
"strconv"
"strings"
"github.com/cespare/xxhash/v2"
configpb "github.com/pomerium/pomerium/pkg/grpc/config"
)
func GenerateCertName(cert *x509.Certificate) *string {
var out string
if cert.IsCA {
if cert.Subject.CommonName != "" {
out = cert.Subject.CommonName
} else {
out = cert.Subject.String()
}
} else {
if cert.Subject.CommonName != "" {
out = cert.Subject.CommonName
} else if len(cert.DNSNames) > 0 {
out = pickDNSName(cert.DNSNames)
} else {
out = "leaf"
}
}
if strings.Contains(out, "-") {
out = strings.ReplaceAll(out, " ", "_")
} else {
out = strings.ReplaceAll(out, " ", "-")
}
suffix := fmt.Sprintf("@%d", cert.NotBefore.Unix())
if !strings.Contains(out, suffix) {
out += suffix
}
return &out
}
func pickDNSName(names []string) string {
if len(names) == 1 {
return names[0]
}
// prefer wildcard names
for _, name := range names {
if strings.HasPrefix(name, "*.") {
return name
}
}
return names[0]
}
func GenerateRouteNames(routes []*configpb.Route) []string {
out := make([]string, len(routes))
prefixes := make([][]string, len(routes))
indexes := map[*configpb.Route]int{}
trie := newDomainTrie()
for i, route := range routes {
trie.Insert(route)
indexes[route] = i
}
trie.Compact()
trie.Walk(func(parents []string, node *domainTreeNode) {
for subdomain, child := range node.children {
for route, name := range differentiateRoutes(subdomain, child.routes) {
idx := indexes[route]
out[idx] = name
prefixes[idx] = parents
}
}
})
seen := map[string]int{}
for idx, name := range out {
prevIdx, ok := seen[name]
if !ok {
out[idx] = name
seen[name] = idx
continue
}
delete(seen, name)
var b strings.Builder
b.WriteString(name)
var prevNameB strings.Builder
prevNameB.WriteString(out[prevIdx])
var nameB strings.Builder
nameB.WriteString(name)
minLen := min(len(prefixes[prevIdx]), len(prefixes[idx]))
maxLen := max(len(prefixes[prevIdx]), len(prefixes[idx]))
for j := range maxLen {
if j >= minLen {
if j < len(prefixes[prevIdx]) {
prevNameB.WriteRune('-')
prevNameB.WriteString(strings.ReplaceAll(prefixes[prevIdx][j], ".", "-"))
} else {
nameB.WriteRune('-')
nameB.WriteString(strings.ReplaceAll(prefixes[idx][j], ".", "-"))
}
continue
}
prevPrefix, prefix := trimCommonSubdomains(prefixes[prevIdx][j], prefixes[idx][j])
if prevPrefix != prefix {
prevNameB.WriteRune('-')
prevNameB.WriteString(prevPrefix)
nameB.WriteRune('-')
nameB.WriteString(prefix)
}
}
out[prevIdx] = prevNameB.String()
out[idx] = nameB.String()
seen[out[prevIdx]] = prevIdx
seen[out[idx]] = idx
}
for i, name := range out {
if name == "" {
out[i] = fmt.Sprintf("route-%d", i)
}
}
return out
}
func trimCommonSubdomains(a, b string) (string, string) {
aParts := strings.Split(a, ".")
bParts := strings.Split(b, ".")
for len(aParts) > 1 && len(bParts) > 1 && aParts[0] == bParts[0] {
aParts = aParts[1:]
bParts = bParts[1:]
}
for len(aParts) > 1 && len(bParts) > 1 && aParts[len(aParts)-1] == bParts[len(bParts)-1] {
aParts = aParts[:len(aParts)-1]
bParts = bParts[:len(bParts)-1]
}
return strings.Join(aParts, "-"), strings.Join(bParts, "-")
}
func differentiateRoutes(subdomain string, routes []*configpb.Route) iter.Seq2[*configpb.Route, string] {
return func(yield func(*configpb.Route, string) bool) {
if len(routes) == 1 {
yield(routes[0], subdomain)
return
}
names := map[string][]*configpb.Route{}
replacer := strings.NewReplacer(
" ", "_",
"/", "-",
"*", "",
)
simplePathName := func(pathOrPrefix string) string {
if p, err := url.PathUnescape(pathOrPrefix); err == nil {
pathOrPrefix = strings.ToLower(p)
}
return replacer.Replace(strings.Trim(pathOrPrefix, "/ "))
}
genericRegexCounter := 0
regexName := func(regex string) string {
if path, pattern, ok := commonRegexPattern(regex); ok {
name := simplePathName(path)
if name == "" && pattern != "" {
return "re-any"
}
return fmt.Sprintf("re-%s-prefix", name)
}
genericRegexCounter++
return fmt.Sprintf("re-%d", genericRegexCounter)
}
var prefixCount, pathCount int
for _, route := range routes {
// each route will have the same domain, but a unique prefix/path/regex.
var name string
switch {
case route.Prefix != "":
name = simplePathName(route.Prefix)
prefixCount++
case route.Path != "":
name = simplePathName(route.Path)
pathCount++
case route.Regex != "":
name = regexName(route.Regex)
}
names[name] = append(names[name], route)
}
nameCounts := map[uint64]int{}
for name, routes := range names {
if len(routes) == 1 {
var b strings.Builder
b.WriteString(subdomain)
if name != "" {
b.WriteRune('-')
b.WriteString(name)
}
if !yield(routes[0], b.String()) {
return
}
} else {
// assign a "-prefix" or "-path" suffix to routes with the same name
// but different configurations
prefixSuffix := "-prefix"
pathSuffix := "-path"
switch {
case prefixCount == 1 && pathCount == 1:
pathSuffix = ""
case prefixCount > 1 && pathCount == 1:
prefixSuffix = ""
case prefixCount == 1 && pathCount > 1:
pathSuffix = ""
case prefixCount == 0:
pathSuffix = ""
case pathCount == 0:
prefixSuffix = ""
}
var b strings.Builder
for _, route := range routes {
b.Reset()
b.WriteString(subdomain)
b.WriteRune('-')
b.WriteString(name)
if route.Prefix != "" {
b.WriteString(prefixSuffix)
} else if route.Path != "" {
b.WriteString(pathSuffix)
}
sum := xxhash.Sum64String(b.String())
nameCounts[sum]++
if c := nameCounts[sum]; c > 1 {
b.WriteString(" (")
b.WriteString(strconv.Itoa(c))
b.WriteString(")")
}
if !yield(route, b.String()) {
return
}
}
}
}
}
}
type domainTreeNode struct {
parent *domainTreeNode
children map[string]*domainTreeNode
routes []*configpb.Route
}
func (n *domainTreeNode) insert(key string, route *configpb.Route) *domainTreeNode {
if existing, ok := n.children[key]; ok {
if route != nil {
existing.routes = append(existing.routes, route)
}
return existing
}
node := &domainTreeNode{
parent: n,
children: map[string]*domainTreeNode{},
}
if route != nil {
node.routes = append(node.routes, route)
}
n.children[key] = node
return node
}
type domainTrie struct {
root *domainTreeNode
}
func newDomainTrie() *domainTrie {
t := &domainTrie{
root: &domainTreeNode{
children: map[string]*domainTreeNode{},
},
}
return t
}
type walkFn = func(parents []string, node *domainTreeNode)
func (t *domainTrie) Walk(fn walkFn) {
t.root.walk(nil, fn)
}
func (n *domainTreeNode) walk(prefix []string, fn walkFn) {
for key, child := range n.children {
fn(append(prefix, key), child)
child.walk(append(prefix, key), fn)
}
}
func (t *domainTrie) Insert(route *configpb.Route) {
u, _ := url.Parse(route.From)
if u == nil {
// ignore invalid urls, they will be assigned generic fallback names
return
}
parts := strings.Split(u.Hostname(), ".")
slices.Reverse(parts)
cur := t.root
for _, part := range parts[:len(parts)-1] {
cur = cur.insert(part, nil)
}
cur.insert(parts[len(parts)-1], route)
}
func (t *domainTrie) Compact() {
t.root.compact()
}
func (n *domainTreeNode) compact() {
for _, child := range n.children {
child.compact()
}
if n.parent == nil {
return
}
var firstKey string
var firstChild *domainTreeNode
for key, child := range n.children {
firstKey, firstChild = key, child
break
}
// compact intermediate nodes, not leaves
if len(n.children) == 1 && len(firstChild.routes) == 0 {
firstChild.parent = n.parent
for key, child := range n.parent.children {
if child == n {
delete(n.parent.children, key)
n.parent.children[fmt.Sprintf("%s.%s", key, firstKey)] = firstChild
*n = domainTreeNode{}
break
}
}
}
}
// Matches an optional leading slash, then zero or more path segments separated
// by '/' characters, where the final path segment contains one of the following
// commonly used regex patterns used to match path segments:
// - '.*' or '.+'
// - '[^/]*', '[^/]+', '[^\/]*', or '[^\/]+'
// - '\w*' or '\w+'
// - any of the above patterns, enclosed by parentheses
// The first capture group contains the path leading up to the wildcard segment
// and can be empty or have leading/trailing slashes. The second capture group
// contains the wildcard segment with no leading or trailing slashes.
var pathPrefixMatchRegex = regexp.MustCompile(`^(\/?(?:\w+\/)*)(\(?(?:\.\+|\.\*|\[\^\\?\/\][\+\*]|\\w[\+\*])\)?)$`)
func commonRegexPattern(re string) (path string, pattern string, found bool) {
re = strings.TrimSuffix(strings.TrimPrefix(re, "^"), "$")
if match := pathPrefixMatchRegex.FindStringSubmatch(re); match != nil {
return match[1], match[2], true
}
return "", "", false
}

View file

@ -0,0 +1,377 @@
package importutil_test
import (
"crypto/x509"
"crypto/x509/pkix"
"fmt"
"slices"
"testing"
"time"
configpb "github.com/pomerium/pomerium/pkg/grpc/config"
"github.com/pomerium/pomerium/pkg/zero/importutil"
"github.com/stretchr/testify/assert"
)
func TestGenerateCertName(t *testing.T) {
cases := []struct {
name string
input x509.Certificate
expected string
}{
{
name: "cert with common name",
input: x509.Certificate{
IsCA: true,
Subject: pkix.Name{CommonName: "sample"},
},
expected: "sample",
},
{
name: "cert with common name and other subject fields",
input: x509.Certificate{
IsCA: true,
Subject: pkix.Name{
CommonName: "sample",
Organization: []string{"foo"},
OrganizationalUnit: []string{"bar"},
},
},
expected: "sample",
},
{
name: "common name with spaces",
input: x509.Certificate{
IsCA: true,
Subject: pkix.Name{CommonName: "sample name"},
},
expected: "sample-name",
},
{
name: "common name with special characters",
input: x509.Certificate{
IsCA: true,
Subject: pkix.Name{CommonName: "sample common-name"},
},
expected: "sample_common-name",
},
{
name: "cert with other subject fields but no common name",
input: x509.Certificate{
IsCA: true,
Subject: pkix.Name{
Organization: []string{"foo"},
OrganizationalUnit: []string{"bar"},
},
},
expected: "OU=bar,O=foo",
},
{
name: "leaf cert with common name",
input: x509.Certificate{
IsCA: false,
Subject: pkix.Name{CommonName: "sample"},
},
expected: "sample",
},
{
name: "leaf cert with dns name",
input: x509.Certificate{
IsCA: false,
DNSNames: []string{"example.com"},
},
expected: "example.com",
},
{
name: "leaf cert with dns names",
input: x509.Certificate{
IsCA: false,
DNSNames: []string{"example.com", "*.example.com"},
},
expected: "*.example.com",
},
{
name: "leaf cert with neither common name nor dns names",
input: x509.Certificate{
IsCA: false,
},
expected: "leaf",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
nbf := time.Now()
tc.input.NotBefore = nbf
tc.expected += fmt.Sprintf("@%d", nbf.Unix())
out := importutil.GenerateCertName(&tc.input)
assert.Equal(t, tc.expected, *out)
})
}
}
func TestGenerateRouteNames(t *testing.T) {
const testExample = "https://test.example.com"
cases := []struct {
name string
input []*configpb.Route
expected []string
}{
{
name: "single domain name",
input: []*configpb.Route{
{From: "https://foo.example.com"},
{From: "https://bar.example.com"},
{From: "https://baz.example.com"},
},
expected: []string{"foo", "bar", "baz"},
},
{
name: "multiple domain names, unique subdomains",
input: []*configpb.Route{
{From: "https://a.domain1.example.com"},
{From: "https://b.domain1.example.com"},
{From: "https://c.domain1.example.com"},
{From: "https://d.domain2.example.com"},
{From: "https://e.domain2.example.com"},
{From: "https://f.domain2.example.com"},
},
expected: []string{"a", "b", "c", "d", "e", "f"},
},
{
name: "multiple domain names, conflicting subdomains",
input: []*configpb.Route{
{From: "https://a.domain1.example.com"},
{From: "https://b.domain1.example.com"},
{From: "https://c.domain1.example.com"},
{From: "https://a.domain2.example.com"},
{From: "https://b.domain2.example.com"},
{From: "https://c.domain2.example.com"},
},
expected: []string{
"a-domain1",
"b-domain1",
"c-domain1",
"a-domain2",
"b-domain2",
"c-domain2",
},
},
{
name: "multiple nested domain names, conflicting subdomains",
input: []*configpb.Route{
{From: "https://a.domain1.domain2.domain3.example.com"},
{From: "https://b.domain1.domain2.domain3.example.com"},
{From: "https://c.domain1.domain2.domain3.example.com"},
{From: "https://a.domain1.domain2.domain4.example.com"},
{From: "https://b.domain1.domain2.domain4.example.com"},
{From: "https://c.domain1.domain2.domain4.example.com"},
{From: "https://a.domain1.domain2.domain5.example.com"},
{From: "https://b.domain2.domain2.domain5.example.com"},
{From: "https://c.domain3.domain2.domain5.example.com"},
{From: "https://a.domain1.domain2.domain6.example.com"},
{From: "https://b.domain2.domain2.domain6.example.com"},
{From: "https://c.domain3.domain2.domain6.example.com"},
},
expected: []string{
"a-domain3",
"b-domain3",
"c-domain3",
"a-domain4",
"b-domain4",
"c-domain4",
"a-domain5",
"b-domain5",
"c-domain5",
"a-domain6",
"b-domain6",
"c-domain6",
},
},
{
name: "conflicting subdomain names nested at different levels",
input: []*configpb.Route{
{From: "https://a.domain1.domain2.example.com"},
{From: "https://a.domain1.example.com"},
{From: "https://a.example.com"},
{From: "https://a.domain3.domain2.example.com"},
{From: "https://a.domain3.example.com"},
},
expected: []string{
"a-domain2-domain1",
"a-domain1",
"a",
"a-domain2-domain3",
"a-domain3",
},
},
{
name: "conflicting subdomain names nested at different levels, unique paths",
input: []*configpb.Route{
{From: "https://a.domain1.domain2.example.com"},
{From: "https://a.domain1.example.com"},
{From: "https://a.example.com"},
},
expected: []string{
"a-domain2-domain1",
"a-domain1",
"a",
},
},
{
name: "same domain, separate prefix options",
input: []*configpb.Route{
{From: testExample, Prefix: "/a"},
{From: testExample, Prefix: "/b"},
{From: testExample, Prefix: "/c"},
},
expected: []string{"test-a", "test-b", "test-c"},
},
{
name: "same domain, mixed prefix/path options",
input: []*configpb.Route{
{From: testExample, Prefix: "/a"},
{From: testExample, Path: "/b"},
{From: testExample, Prefix: "/c"},
{From: testExample, Path: "/d"},
},
expected: []string{"test-a", "test-b", "test-c", "test-d"},
},
{
name: "same domain, name-conflicting prefix/path options (1 prefix/1 path)",
input: []*configpb.Route{
{From: testExample, Prefix: "/a/"},
{From: testExample, Path: "/a"},
},
expected: []string{"test-a-prefix", "test-a"},
},
{
name: "same domain, name-conflicting prefix/path options (more prefixes than paths)",
input: []*configpb.Route{
{From: testExample, Prefix: "/a/"},
{From: testExample, Prefix: "/b/"},
{From: testExample, Prefix: "/c/"},
{From: testExample, Path: "/a"},
},
expected: []string{"test-a", "test-b", "test-c", "test-a-path"},
},
{
name: "same domain, name-conflicting prefix/path options (more paths than prefixes)",
input: []*configpb.Route{
{From: testExample, Path: "/a"},
{From: testExample, Path: "/b"},
{From: testExample, Path: "/c"},
{From: testExample, Prefix: "/a/"},
},
expected: []string{"test-a", "test-b", "test-c", "test-a-prefix"},
},
{
name: "same domain, name-conflicting path options, duplicate names",
input: []*configpb.Route{
{From: testExample, Path: "/a"},
{From: testExample, Path: "/a/"},
},
expected: []string{"test-a", "test-a (2)"},
},
{
name: "same domain, name-conflicting prefix options, duplicate names",
input: []*configpb.Route{
{From: testExample, Prefix: "/a"},
{From: testExample, Prefix: "/a/"},
},
expected: []string{"test-a", "test-a (2)"},
},
{
name: "missing domain name",
input: []*configpb.Route{{From: "https://:1234"}},
expected: []string{"route-0"},
},
{
name: "invalid URL",
input: []*configpb.Route{{From: "https://\x7f"}},
expected: []string{"route-0"},
},
{
name: "regex paths",
input: []*configpb.Route{
{From: testExample, Regex: `/a/(.*)/b`},
{From: testExample, Regex: `/a/(foo|bar)/b`},
{From: testExample, Regex: `/(authorize.*|login|logout)`},
{From: testExample, Regex: `/foo.+=-())(*+=,;:@~!'''-+_/.*`},
{From: testExample, Regex: `/*`},
{From: testExample, Regex: `/other/(.*)`},
{From: testExample, Regex: `/other/.*`},
{From: testExample, Regex: `/other/([^/]+)`},
{From: testExample, Regex: `/other/([^/]*)`},
{From: testExample, Regex: `/other/([^\/]+)`},
{From: testExample, Regex: `/other/([^\/]*)`},
{From: testExample, Regex: `/other/[^/]+`},
{From: testExample, Regex: `/other/[^/]*`},
{From: testExample, Regex: `/other/[^\/]+`},
{From: testExample, Regex: `/other/[^\/]*`},
{From: testExample, Regex: `/foo/bar/baz/.*`},
{From: testExample, Regex: `/.*`},
{From: testExample, Regex: `/.*`},
{From: testExample, Regex: `/(.*)`},
{From: testExample, Regex: `/.+`},
{From: testExample, Regex: `/(.+)`},
{From: testExample, Regex: `/([^/]+)`},
{From: testExample, Regex: `/([^/]*)`},
{From: testExample, Regex: `/([^\/]+)`},
{From: testExample, Regex: `/([^\/]*)`},
{From: testExample, Regex: `/[^/]+`},
{From: testExample, Regex: `/[^/]*`},
{From: testExample, Regex: `/[^\/]+`},
{From: testExample, Regex: `/[^\/]*`},
{From: testExample, Regex: `.+`},
{From: testExample, Regex: `(.+)`},
{From: testExample, Regex: `([^/]+)`},
{From: testExample, Regex: `([^/]*)`},
{From: testExample, Regex: `([^\/]+)`},
{From: testExample, Regex: `([^\/]*)`},
{From: testExample, Regex: `[^/]+`},
{From: testExample, Regex: `[^/]*`},
{From: testExample, Regex: `[^\/]+`},
{From: testExample, Regex: `[^\/]*`},
{From: testExample, Regex: `\w+`},
{From: testExample, Regex: `\w*`},
{From: testExample, Regex: `/\w+`},
{From: testExample, Regex: `/\w*`},
{From: testExample, Regex: `/(\w+)`},
{From: testExample, Regex: `/(\w*)`},
{From: testExample, Regex: `foo/.*`},
{From: testExample, Regex: `/foo/.*`},
{From: testExample, Regex: `/foo/\w+`},
{From: testExample, Regex: `/foo/\w*`},
},
expected: slices.Collect(func(yield func(string) bool) {
yield("test-re-1")
yield("test-re-2")
yield("test-re-3")
yield("test-re-4")
yield("test-re-5")
yield("test-re-other-prefix")
for i := 2; i <= 10; i++ {
yield(fmt.Sprintf("test-re-other-prefix (%d)", i))
}
yield("test-re-foo-bar-baz-prefix")
yield("test-re-any")
for i := 2; i <= 29; i++ {
yield(fmt.Sprintf("test-re-any (%d)", i))
}
yield("test-re-foo-prefix")
yield("test-re-foo-prefix (2)")
yield("test-re-foo-prefix (3)")
yield("test-re-foo-prefix (4)")
}),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, importutil.GenerateRouteNames(tc.input))
})
}
}