From e4832cb4eda2a1ee1c38575daa1955b7f33f1b84 Mon Sep 17 00:00:00 2001 From: Caleb Doxsey Date: Thu, 21 May 2020 16:01:07 -0600 Subject: [PATCH] authorize: add client mTLS support (#751) * authorize: add client mtls support * authorize: better error messages for envoy * switch from function to input * add TrustedCa to envoy config so that users are prompted for the correct client certificate * update documentation * fix invalid ClientCAFile * regenerate cache protobuf * avoid recursion, add test * move comment line * use http.StatusOK * various fixes --- authorize/authorize.go | 26 +- authorize/errors.go | 106 ++++ authorize/evaluator/evaluator.go | 7 +- authorize/evaluator/opa/functions.go | 58 ++ authorize/evaluator/opa/functions_test.go | 122 ++++ authorize/evaluator/opa/opa.go | 85 ++- authorize/evaluator/opa/opa_test.go | 25 +- authorize/evaluator/opa/policy/authz.rego | 4 + authorize/evaluator/opa/policy/statik.go | 7 +- authorize/grpc.go | 82 ++- cache/grpc.go | 2 +- cache/grpc_test.go | 3 +- config/options.go | 17 + docs/configuration/readme.md | 9 + go.mod | 1 + internal/controlplane/luascripts/statik.go | 2 +- internal/controlplane/xds_listeners.go | 19 + internal/frontend/statik/statik.go | 3 +- internal/grpc/authorize/authorize.pb.go | 655 ++++++++++++++------- internal/grpc/authorize/authorize.proto | 6 + internal/grpc/cache/cache.pb.go | 4 +- internal/httputil/httputil.go | 6 + scripts/protoc | 22 + scripts/protoc-gen-go | 3 + 24 files changed, 995 insertions(+), 279 deletions(-) create mode 100644 authorize/errors.go create mode 100644 authorize/evaluator/opa/functions.go create mode 100644 authorize/evaluator/opa/functions_test.go create mode 100644 internal/httputil/httputil.go create mode 100755 scripts/protoc create mode 100755 scripts/protoc-gen-go diff --git a/authorize/authorize.go b/authorize/authorize.go index c9357228b..cd6615ca2 100644 --- a/authorize/authorize.go +++ b/authorize/authorize.go @@ -2,10 +2,14 @@ // if a given request should be authorized (AuthZ). package authorize +//go:generate ../scripts/protoc -I ../internal/grpc/authorize/ --go_out=plugins=grpc:../internal/grpc/authorize/ ../internal/grpc/authorize/authorize.proto + import ( "context" "encoding/base64" "fmt" + "html/template" + "io/ioutil" "sync/atomic" "github.com/pomerium/pomerium/authorize/evaluator" @@ -14,6 +18,7 @@ import ( "github.com/pomerium/pomerium/internal/cryptutil" "github.com/pomerium/pomerium/internal/encoding" "github.com/pomerium/pomerium/internal/encoding/jws" + "github.com/pomerium/pomerium/internal/frontend" "github.com/pomerium/pomerium/internal/log" "github.com/pomerium/pomerium/internal/telemetry/metrics" "github.com/pomerium/pomerium/internal/telemetry/trace" @@ -51,6 +56,7 @@ type Authorize struct { currentOptions atomicOptions currentEncoder atomicMarshalUnmarshaler + templates *template.Template } // New validates and creates a new Authorize service from a set of config options. @@ -58,7 +64,9 @@ func New(opts config.Options) (*Authorize, error) { if err := validateOptions(opts); err != nil { return nil, fmt.Errorf("authorize: bad options: %w", err) } - var a Authorize + a := Authorize{ + templates: template.Must(frontend.NewTemplates()), + } var host string if opts.AuthenticateURL != nil { @@ -117,12 +125,28 @@ func newPolicyEvaluator(opts *config.Options) (evaluator.Evaluator, error) { jwk.Key = keyBytes } + var clientCA string + if opts.ClientCA != "" { + bs, err := base64.StdEncoding.DecodeString(opts.ClientCA) + if err != nil { + return nil, fmt.Errorf("authorize: invalid client ca: %w", err) + } + clientCA = string(bs) + } else if opts.ClientCAFile != "" { + bs, err := ioutil.ReadFile(opts.ClientCAFile) + if err != nil { + return nil, fmt.Errorf("authorize: invalid client ca file: %w", err) + } + clientCA = string(bs) + } + data := map[string]interface{}{ "shared_key": opts.SharedKey, "route_policies": opts.Policies, "admins": opts.Administrators, "signing_key": jwk, "authenticate_url": opts.AuthenticateURLString, + "client_ca": clientCA, } return opa.New(ctx, &opa.Options{Data: data}) diff --git a/authorize/errors.go b/authorize/errors.go new file mode 100644 index 000000000..61a55d0fa --- /dev/null +++ b/authorize/errors.go @@ -0,0 +1,106 @@ +package authorize + +import ( + "bytes" + "net/http" + "strings" + + envoy_api_v2_core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core" + envoy_service_auth_v2 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v2" + envoy_type "github.com/envoyproxy/go-control-plane/envoy/type" + "google.golang.org/genproto/googleapis/rpc/status" + "google.golang.org/grpc/codes" + + "github.com/pomerium/pomerium/internal/httputil" + "github.com/pomerium/pomerium/internal/log" +) + +func (a *Authorize) deniedResponse(in *envoy_service_auth_v2.CheckRequest, + code int32, reason string, headers map[string]string) *envoy_service_auth_v2.CheckResponse { + + returnHTMLError := true + inHeaders := in.GetAttributes().GetRequest().GetHttp().GetHeaders() + if inHeaders != nil { + returnHTMLError = strings.Contains(inHeaders["accept"], "text/html") + } + + if returnHTMLError { + return a.htmlDeniedResponse(code, reason, headers) + } + return a.plainTextDeniedResponse(code, reason, headers) +} + +func (a *Authorize) htmlDeniedResponse(code int32, reason string, headers map[string]string) *envoy_service_auth_v2.CheckResponse { + var details string + switch code { + case httputil.StatusInvalidClientCertificate: + details = "a valid client certificate is required to access this page" + case http.StatusForbidden: + details = "access to this page is forbidden" + default: + details = reason + } + + var buf bytes.Buffer + err := a.templates.ExecuteTemplate(&buf, "error.html", map[string]interface{}{ + "Status": code, + "StatusText": reason, + "CanDebug": code/100 == 4, + "Error": details, + }) + if err != nil { + buf.WriteString(reason) + log.Error().Err(err).Msg("error executing error template") + } + + envoyHeaders := []*envoy_api_v2_core.HeaderValueOption{ + mkHeader("Content-Type", "text/html"), + } + for k, v := range headers { + envoyHeaders = append(envoyHeaders, mkHeader(k, v)) + } + + return &envoy_service_auth_v2.CheckResponse{ + Status: &status.Status{Code: int32(codes.PermissionDenied), Message: "Access Denied"}, + HttpResponse: &envoy_service_auth_v2.CheckResponse_DeniedResponse{ + DeniedResponse: &envoy_service_auth_v2.DeniedHttpResponse{ + Status: &envoy_type.HttpStatus{ + Code: envoy_type.StatusCode(code), + }, + Headers: envoyHeaders, + Body: buf.String(), + }, + }, + } +} + +func (a *Authorize) plainTextDeniedResponse(code int32, reason string, headers map[string]string) *envoy_service_auth_v2.CheckResponse { + envoyHeaders := []*envoy_api_v2_core.HeaderValueOption{ + mkHeader("Content-Type", "text/plain"), + } + for k, v := range headers { + envoyHeaders = append(envoyHeaders, mkHeader(k, v)) + } + + return &envoy_service_auth_v2.CheckResponse{ + Status: &status.Status{Code: int32(codes.PermissionDenied), Message: "Access Denied"}, + HttpResponse: &envoy_service_auth_v2.CheckResponse_DeniedResponse{ + DeniedResponse: &envoy_service_auth_v2.DeniedHttpResponse{ + Status: &envoy_type.HttpStatus{ + Code: envoy_type.StatusCode(code), + }, + Headers: envoyHeaders, + Body: reason, + }, + }, + } +} + +func mkHeader(k, v string) *envoy_api_v2_core.HeaderValueOption { + return &envoy_api_v2_core.HeaderValueOption{ + Header: &envoy_api_v2_core.HeaderValue{ + Key: k, + Value: v, + }, + } +} diff --git a/authorize/evaluator/evaluator.go b/authorize/evaluator/evaluator.go index 6a58f9d9f..acfb5c153 100644 --- a/authorize/evaluator/evaluator.go +++ b/authorize/evaluator/evaluator.go @@ -10,7 +10,7 @@ import ( // Evaluator specifies the interface for a policy engine. type Evaluator interface { - IsAuthorized(ctx context.Context, input interface{}) (*pb.IsAuthorizedReply, error) + IsAuthorized(ctx context.Context, req *Request) (*pb.IsAuthorizedReply, error) PutData(ctx context.Context, data map[string]interface{}) error } @@ -44,6 +44,11 @@ type Request struct { // It is an error to set this field in an HTTP client request. RequestURI string `json:"request_uri,omitempty"` + // Connection context + // + // ClientCertificate is the PEM-encoded public certificate used for the user's TLS connection. + ClientCertificate string `json:"client_certificate"` + // Device context // // todo(bdd): Use the peer TLS certificate to bind device state with a request diff --git a/authorize/evaluator/opa/functions.go b/authorize/evaluator/opa/functions.go new file mode 100644 index 000000000..231d84277 --- /dev/null +++ b/authorize/evaluator/opa/functions.go @@ -0,0 +1,58 @@ +package opa + +import ( + "crypto/x509" + "encoding/pem" + "fmt" + + lru "github.com/hashicorp/golang-lru" +) + +var isValidClientCertificateCache, _ = lru.New2Q(100) + +func isValidClientCertificate(ca, cert string) (bool, error) { + // when ca is the empty string, client certificates are always accepted + if ca == "" { + return true, nil + } + + // when cert is the empty string, no client certificate was supplied + if cert == "" { + return false, nil + } + + cacheKey := [2]string{ca, cert} + + value, ok := isValidClientCertificateCache.Get(cacheKey) + if ok { + return value.(bool), nil + } + + roots := x509.NewCertPool() + roots.AppendCertsFromPEM([]byte(ca)) + + xcert, err := parseCertificate(cert) + if err != nil { + return false, err + } + + _, verifyErr := xcert.Verify(x509.VerifyOptions{ + Roots: roots, + }) + valid := verifyErr == nil + + isValidClientCertificateCache.Add(cacheKey, valid) + + return valid, nil +} + +func parseCertificate(pemStr string) (*x509.Certificate, error) { + block, _ := pem.Decode([]byte(pemStr)) + if block == nil { + return nil, fmt.Errorf("invalid certificate") + } + if block.Type != "CERTIFICATE" { + return nil, fmt.Errorf("unknown PEM type: %s", block.Type) + } + return x509.ParseCertificate(block.Bytes) +} diff --git a/authorize/evaluator/opa/functions_test.go b/authorize/evaluator/opa/functions_test.go new file mode 100644 index 000000000..c9df75931 --- /dev/null +++ b/authorize/evaluator/opa/functions_test.go @@ -0,0 +1,122 @@ +package opa + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +const ( + testCA = ` +-----BEGIN CERTIFICATE----- +MIIEtjCCAx6gAwIBAgIRAJFkXxMjoQzoojykk6CiiGkwDQYJKoZIhvcNAQELBQAw +czEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMSQwIgYDVQQLDBtjYWxl +YkBwb3Atb3MgKENhbGViIERveHNleSkxKzApBgNVBAMMIm1rY2VydCBjYWxlYkBw +b3Atb3MgKENhbGViIERveHNleSkwHhcNMjAwNDI0MTY1MzEwWhcNMzAwNDI0MTY1 +MzEwWjBzMR4wHAYDVQQKExVta2NlcnQgZGV2ZWxvcG1lbnQgQ0ExJDAiBgNVBAsM +G2NhbGViQHBvcC1vcyAoQ2FsZWIgRG94c2V5KTErMCkGA1UEAwwibWtjZXJ0IGNh +bGViQHBvcC1vcyAoQ2FsZWIgRG94c2V5KTCCAaIwDQYJKoZIhvcNAQEBBQADggGP +ADCCAYoCggGBAL2QSyQGjaGD97K7HSExJfMcuyEoh+ewAkPZ/HZR4n12zwAn1sLK +RqusKSfMe8qG6KgsojXrJ9AXEkD7x3bmK5j/4M/lwlNGulg+k5MSu3leoLpOZwfX +JQTu+HDzWubu5cjy7taHyeZc35VbOBWEaDJgVxmJvE9TJIOr8POZ7DD/rlkbgQas +s6G/8cg2mRX0Rh3O20/1bvi9Uen/kraBgGMOyG5MfuiiTl3KsrGST848Q+jiSbu3 +5F5MAzdO4tlR6kqEZk/Igog6OPkTb82vMli/R+mR37JYncQcj0WNYS4PkfjofVpb +FwrHtfdkVYJ9T2yNvQnJVu6MF9fhj9FqWQbsdbYKlUDow5KwI+BxmCAmGwgzmCOy +ONkglj76fPKFkoF4s+DSFocbAwhdazaViAcCB+x6yohOUjgG7H9NJo0MasPHuqUO +8d56Bf0BTXfNX6nOgYYisrOoEATCbs729vHMaQ/7pG2zf9dnEuw95gZTSr9Rv3dx +2NjmM6+tNOMCzwIDAQABo0UwQzAOBgNVHQ8BAf8EBAMCAgQwEgYDVR0TAQH/BAgw +BgEB/wIBADAdBgNVHQ4EFgQUShofXNkcXh2q4wnnWZ2bco24XEQwDQYJKoZIhvcN +AQELBQADggGBAJQzfmr84xtvoUgnq8T4ND0Q166dlnbDnASRFhbmyZcxzvDJsPs4 +N45HbIg0xOsXOaBaE+jTSV4GmZ/vtyP8WbQrhabL2zpnjnLF1d9B0qx/RlIzoDEa +e/0zc0R6RAd64/aE/jHNDhfTNXD/NmnI25RqgnsZXXXRVMTl+PzQ1A8XQghZVWHN +vbyFFd3GE5Qs+vxMzwKCqp6f3MI8KyI2aM4hZZ+zULdEuSw0hWzMOkeZY6LC0flW +/rpkT+GLA3uZ357iehSISLqnkIozw92ldov5oZDthoy3i1I6gIDkngk7BGKr42pD +L2sWi1MEEIhymy4K1DnRkGre3mqzus2y/nE4ruuJlctq6QXcCSnko717vukVtoE8 +o5SkW4usivU8yZeBLt56sySRyCpe/T1XAFTQZ5Q4S5ssGmNpOLS9Aa5iOUz9/62S +uvjFyvOEE3yqd/d3py8qm6olcjaMooVA8j5G+QF/UiH951azGIez6/Ui1lg1m0T6 ++YLkPqNIt0o9dQ== +-----END CERTIFICATE----- +` + testValidCert = ` +-----BEGIN CERTIFICATE----- +MIIESDCCArCgAwIBAgIQG/h9GflpINqLLv4Tde9+STANBgkqhkiG9w0BAQsFADBz +MR4wHAYDVQQKExVta2NlcnQgZGV2ZWxvcG1lbnQgQ0ExJDAiBgNVBAsMG2NhbGVi +QHBvcC1vcyAoQ2FsZWIgRG94c2V5KTErMCkGA1UEAwwibWtjZXJ0IGNhbGViQHBv +cC1vcyAoQ2FsZWIgRG94c2V5KTAeFw0xOTA2MDEwMDAwMDBaFw0zMDA1MjAyMDM4 +NDRaME8xJzAlBgNVBAoTHm1rY2VydCBkZXZlbG9wbWVudCBjZXJ0aWZpY2F0ZTEk +MCIGA1UECwwbY2FsZWJAcG9wLW9zIChDYWxlYiBEb3hzZXkpMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5ouz2dlXHALdxiLcLwAvxg02CN/Jdcrmyyzm +bzKHqIpknotZSlbPgE/mp5wMwIoyMqFIEm3IzXFEf3cjFYYG4b6wp4zlFrx7jCOa +vhEHpH3yM71xt1I/BME6VrmX7sRKO90dwpTxCOadx9aGEn1AlHuPfhMMm/WTLynD +d5hbsHKp7eZMYHvQnferTelq5NnBySBP/HaAtF76qTSQzHev5K/cgioDZAaM0dnP +bicl0Zay+f5INrDr9XtQo/FHwGI/YLMW5TWXYmHjYmdD8s4Tg/KUoRMgJp4mlkkF +9t1pwArbNFU/4wQWPbpWBLh1gcnQxojSZ3a6aI+V+REDzV/PVQIDAQABo3wwejAO +BgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMAwG +A1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUShofXNkcXh2q4wnnWZ2bco24XEQwGgYD +VR0RBBMwEYIPZXhhbXBsZS1zdWJqZWN0MA0GCSqGSIb3DQEBCwUAA4IBgQC78S2n +6jcKfWbm24g/U5tkWiBVnBk1jphH7Ct69Lw2JNstGLtNs4AiE9lKmXDQ82MiAFYg +gaeiHRhTebkOTF9Kx3Jwq7bwhhzONqPp5a0SkY4EWjZ7c5k/fZc8DkrRE71hOgMf +rFbRBZCywBVtGbXIA1uMsCijTe4sQF1ZA918NmfjhpIhRHljQJM16RJ753s+0CZ8 +WomOW4JrtjJefRuV97PRADvRNQbtZYelnoTfbp1afGhbQpKjyylCDGlpJS4mGrSA +lPaRVhEB+wI8gA3lzpa6adXsc1yueZ19++dxQNYxAawCMQNjjxy3aLWzy8aPWxxq +Qo/Q9rqjre3SpJfARLOV9ezQNbqsXvJW+5DcoG5dx8s6jAhMusNjUHpf6oVgnv65 +3Bvl124bZyf9q4lW9g8pvZkrgQ3Fx2IahqhXhyF5zrqf2r9+1l0fXocIUP2GQ+Fr +b9j9bWWhov5aidEjPwpFeTmzcGqCWQBEA4H+yo/4YaIN0sOfE2yaAmc3gcU= +-----END CERTIFICATE----- +` + testUnsignedCert = ` +-----BEGIN CERTIFICATE----- +MIIESTCCArGgAwIBAgIRAIE9860UHBIVofXB5cu/aWAwDQYJKoZIhvcNAQELBQAw +czEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMSQwIgYDVQQLDBtjYWxl +YkBwb3Atb3MgKENhbGViIERveHNleSkxKzApBgNVBAMMIm1rY2VydCBjYWxlYkBw +b3Atb3MgKENhbGViIERveHNleSkwHhcNMTkwNjAxMDAwMDAwWhcNMzAwNTIwMjIw +NDAxWjBPMScwJQYDVQQKEx5ta2NlcnQgZGV2ZWxvcG1lbnQgY2VydGlmaWNhdGUx +JDAiBgNVBAsMG2NhbGViQHBvcC1vcyAoQ2FsZWIgRG94c2V5KTCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAKPgWHAJ58p7ZZ6MHA6QHA9rQQWKSvYbN9zz +fCCURqHFbQHCCJs2D39XPioo9EMZcD6J7ldwEOJsdSNw3+dzBCvIl7wP6fqtbo/3 +SNgRaLAB+Mb4S8oek6P6zHkjuOXzodhCZjLO7oxY9pjGREy6hC/SjylJFgw9mKEG +SYmsyCqeP5BfW9DghRgd5uJe0HtwlBZLPS91Mk5whn7YOxnWslS/REwZdd12s3DI +WQdmvGhMakIAiMKmx+LX9qS3Ua2gUArHnSFXcOAg9iK+MM68T1KsQTCYnRZVK4v5 +Na4qEjiPhmkzzEExZa787ClL6UXfoXB+jXy2sXu0CDD4tv2D7R8CAwEAAaN8MHow +DgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATAM +BgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFH8wenPOF2tE2EIksItmlkWfgEMkMBoG +A1UdEQQTMBGCD2ludmFsaWQtc3ViamVjdDANBgkqhkiG9w0BAQsFAAOCAYEAJCdl +c6J/x/UY6vEDzplwR8iZ5s7dyKKF7bwNdjEvBREgkTY6GmwDC9HOmWWPs7vENqEX +jUwHEK+v7A7AUIS4WeJrJgogzEDPI7ZlVtzQNviqMavzk/I1Us00WYtMQQFb1Sgz +xIRskug5wH6vPcR4XbCftx6NP9UFG8pJLPTJ67ZUaTP23ccsToMM/Dd17LFrtleE +9xAvdqA54vcBiJ99uts+xWlQznjIgdauNC6sOmL3JAflyj6aBy+Dcos9R35ERIXz +3rRl25yXjtidPDo8YxmtHs+Ijw4R3iJ44NCcc/+LfACYUcua0cBF2Ixk2JrFYx8n +wwRJukrHXI+RFBmSOlUripyyJH92H5vXvj8lO5wM8wVVVe8anr5TOvxFOAjNC5a3 +vJByvJQTUEkx8rT7zZi8eSQJHP3Eoqr9g4ajqIU22yrCxiiQXpZLJ4JFQQEgyD9A +Y+E5W+FKfIBv9yvdNBYZsL6IZ0Yh1ctKwB5gnajO8+swx5BeaCIbBrCtOBSB +-----END CERTIFICATE----- +` +) + +func Test_isValidClientCertificate(t *testing.T) { + t.Run("no ca", func(t *testing.T) { + valid, err := isValidClientCertificate("", "WHATEVER!") + assert.NoError(t, err, "should not return an error") + assert.True(t, valid, "should return true") + }) + t.Run("no cert", func(t *testing.T) { + valid, err := isValidClientCertificate(testCA, "") + assert.NoError(t, err, "should not return an error") + assert.False(t, valid, "should return false") + }) + t.Run("valid cert", func(t *testing.T) { + valid, err := isValidClientCertificate(testCA, testValidCert) + assert.NoError(t, err, "should not return an error") + assert.True(t, valid, "should return true") + }) + t.Run("unsigned cert", func(t *testing.T) { + valid, err := isValidClientCertificate(testCA, testUnsignedCert) + assert.NoError(t, err, "should not return an error") + assert.False(t, valid, "should return false") + }) + t.Run("not a cert", func(t *testing.T) { + valid, err := isValidClientCertificate(testCA, "WHATEVER!") + assert.Error(t, err, "should return an error") + assert.False(t, valid, "should return false") + }) +} diff --git a/authorize/evaluator/opa/opa.go b/authorize/evaluator/opa/opa.go index afc27c737..086bb959e 100644 --- a/authorize/evaluator/opa/opa.go +++ b/authorize/evaluator/opa/opa.go @@ -1,4 +1,5 @@ -//go:generate statik -src=./policy -include=*.rego -ns rego -p policy +//go:generate go run github.com/rakyll/statik -src=./policy -include=*.rego -ns rego -p policy +//go:generate go fmt ./policy/statik.go // Package opa implements the policy evaluator interface to make authorization // decisions. @@ -6,9 +7,11 @@ package opa import ( "context" + "encoding/json" "errors" "fmt" "io/ioutil" + "strconv" "sync" "github.com/open-policy-agent/opa/rego" @@ -37,6 +40,7 @@ type PolicyEvaluator struct { mu sync.RWMutex store storage.Store isAuthorized rego.PreparedEvalQuery + clientCA string } // Options represent OPA's evaluator configurations. @@ -96,10 +100,10 @@ func (pe *PolicyEvaluator) UpdatePolicy(ctx context.Context, authz string) error } // IsAuthorized determines if a given request input is authorized. -func (pe *PolicyEvaluator) IsAuthorized(ctx context.Context, input interface{}) (*pb.IsAuthorizedReply, error) { +func (pe *PolicyEvaluator) IsAuthorized(ctx context.Context, req *evaluator.Request) (*pb.IsAuthorizedReply, error) { ctx, span := trace.StartSpan(ctx, "authorize.evaluator.opa.IsAuthorized") defer span.End() - return pe.runBoolQuery(ctx, input, pe.isAuthorized) + return pe.runBoolQuery(ctx, req, pe.isAuthorized) } // PutData adds (or replaces if the mapping key is the same) contextual data @@ -110,6 +114,11 @@ func (pe *PolicyEvaluator) PutData(ctx context.Context, data map[string]interfac pe.mu.Lock() defer pe.mu.Unlock() + + if ca, ok := data["client_ca"].(string); ok { + pe.clientCA = ca + } + txn, err := pe.store.NewTransaction(ctx, storage.WriteParams) if err != nil { return fmt.Errorf("opa: bad transaction: %w", err) @@ -171,12 +180,48 @@ func decisionFromInterface(i interface{}) (*pb.IsAuthorizedReply, error) { if v, ok := m["signed_jwt"].(string); ok { d.SignedJwt = v } + + // http_status = [200, "OK", { "HEADER": "VALUE" }] + if v, ok := m["http_status"].([]interface{}); ok { + d.HttpStatus = new(pb.HTTPStatus) + if len(v) > 0 { + d.HttpStatus.Code = int32(anyToInt(v[0])) + } + if len(v) > 1 { + if msg, ok := v[1].(string); ok { + d.HttpStatus.Message = msg + } + } + if len(v) > 2 { + if headers, ok := v[2].(map[string]interface{}); ok { + d.HttpStatus.Headers = make(map[string]string) + for hk, hv := range headers { + d.HttpStatus.Headers[hk] = fmt.Sprint(hv) + } + } + } + } + return &d, nil } -func (pe *PolicyEvaluator) runBoolQuery(ctx context.Context, input interface{}, q rego.PreparedEvalQuery) (*pb.IsAuthorizedReply, error) { +func (pe *PolicyEvaluator) runBoolQuery(ctx context.Context, req *evaluator.Request, q rego.PreparedEvalQuery) (*pb.IsAuthorizedReply, error) { pe.mu.RLock() defer pe.mu.RUnlock() + + // `opa test` doesn't support custom function, so we'll pre-compute is_valid_client_certificate + isValid, err := isValidClientCertificate(pe.clientCA, req.ClientCertificate) + if err != nil { + return nil, fmt.Errorf("certificate error: %w", err) + } + input := struct { + *evaluator.Request + IsValidClientCertificate bool `json:"is_valid_client_certificate"` + }{ + Request: req, + IsValidClientCertificate: isValid, + } + rs, err := q.Eval(ctx, rego.EvalInput(input)) if err != nil { return nil, fmt.Errorf("eval query: %w", err) @@ -199,3 +244,35 @@ func readPolicy(fn string) ([]byte, error) { defer r.Close() return ioutil.ReadAll(r) } + +func anyToInt(obj interface{}) int { + switch v := obj.(type) { + case int: + return v + case int64: + return int(v) + case int32: + return int(v) + case int16: + return int(v) + case int8: + return int(v) + case uint64: + return int(v) + case uint32: + return int(v) + case uint16: + return int(v) + case uint8: + return int(v) + case json.Number: + i, _ := v.Int64() + return int(i) + case string: + i, _ := strconv.Atoi(v) + return i + default: + i, _ := strconv.Atoi(fmt.Sprint(v)) + return i + } +} diff --git a/authorize/evaluator/opa/opa_test.go b/authorize/evaluator/opa/opa_test.go index 5667dcb23..169eec5e2 100644 --- a/authorize/evaluator/opa/opa_test.go +++ b/authorize/evaluator/opa/opa_test.go @@ -5,9 +5,12 @@ import ( "testing" "time" - "github.com/pomerium/pomerium/config" + "github.com/stretchr/testify/assert" "gopkg.in/square/go-jose.v2" "gopkg.in/square/go-jose.v2/jwt" + + "github.com/pomerium/pomerium/authorize/evaluator" + "github.com/pomerium/pomerium/config" ) func Test_Eval(t *testing.T) { @@ -89,11 +92,7 @@ func Test_Eval(t *testing.T) { if err != nil { t.Fatal(err) } - req := struct { - Host string `json:"host,omitempty"` - URL string `json:"url,omitempty"` - User string `json:"user,omitempty"` - }{ + req := &evaluator.Request{ Host: tt.route, URL: "https://" + tt.route, User: rawJWT, @@ -108,3 +107,17 @@ func Test_Eval(t *testing.T) { }) } } + +func Test_anyToInt(t *testing.T) { + assert.Equal(t, 5, anyToInt("5")) + assert.Equal(t, 7, anyToInt(7)) + assert.Equal(t, 9, anyToInt(int8(9))) + assert.Equal(t, 9, anyToInt(int16(9))) + assert.Equal(t, 9, anyToInt(int32(9))) + assert.Equal(t, 9, anyToInt(int64(9))) + assert.Equal(t, 11, anyToInt(uint8(11))) + assert.Equal(t, 11, anyToInt(uint16(11))) + assert.Equal(t, 11, anyToInt(uint32(11))) + assert.Equal(t, 11, anyToInt(uint64(11))) + assert.Equal(t, 13, anyToInt(13.0)) +} diff --git a/authorize/evaluator/opa/policy/authz.rego b/authorize/evaluator/opa/policy/authz.rego index 4661096cb..87b5255a1 100644 --- a/authorize/evaluator/opa/policy/authz.rego +++ b/authorize/evaluator/opa/policy/authz.rego @@ -5,6 +5,10 @@ import data.shared_key default allow = false +http_status = [495, "invalid client certificate"]{ + not input.is_valid_client_certificate +} + # allow public allow { route := first_allowed_route(input.url) diff --git a/authorize/evaluator/opa/policy/statik.go b/authorize/evaluator/opa/policy/statik.go index cee0f2638..b38fb89b9 100644 --- a/authorize/evaluator/opa/policy/statik.go +++ b/authorize/evaluator/opa/policy/statik.go @@ -6,10 +6,9 @@ import ( "github.com/rakyll/statik/fs" ) - const Rego = "rego" // static asset namespace func init() { - data := "PK\x03\x04\x14\x00\x08\x00\x08\x00\x05\xa2\xa8P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\n\x00 \x00authz.regoUT\x05\x00\x01\x0b\xbe\xb5^\xbcX_o\xdb6\x10\x7f\x16?\xc5\x95E\x01iU\xe4\xb6\xc0\x06\xcc\x9b\x97\x15\xc5\x1e\xf6\xb0\xa5h\xb7'AUi\x89\xb6\x98H\xa4FR\x8d]\xcf\xdf}8R\xb2\x1d\xc7v\xd3$\xebK\x14\x1d\xef~\xfc\xdd\x1f\xdd\x91nYq\xc5\xe6\x1cZ\xd5p-\xba&a\x9d\xad>\x13\"\x9aVi\x0b%\xb3,\xd1\xaa\xb3\xfe\x02{oiR\xff\xcc\xa2\xafqb7%\xdf\xc2\xa1[\x85\xf6\xd8\xcem\x86B?v\xa0\xd3\xb5\xd9\xfaT(i\x11o\xcb?\x06:J\x06\xed\x11\x8dH \x95\x85\x03z\xbbj\xacl\x84\xa4\x91\x0f\xa6\xe6\xb6\xd3\xd2\x80\xad\xb8\x0f\x154\xcc\x16\x95\x90s\xef\x1b9\x1a\xbf\x1c\xe37|j7B\xee\xc3\x00\xff\x82\x8b\xad\x7f\xf9 \x8e\xa4\xe0H\x0c\xa3,}\x91!\xc5#;\xc7\xe0\x0c\x96\xd1\xaa\x9f&(\xcc\xd5\xf4\x12 \xb4L\x1b\x8e\x82\x1d\xa6$\xb8\x81\x94\x1b\xd5\xe9b\x07\x10m7\xa0\xfb\xca8\x1d\xc5\xe2\xae\xca\xccVwT\xd5|\xce\x8f\xc2\xee;\x7f\x9a2f`g\xd4yi\x0c\xd4\x1b\xd1\x18(\x8d\xdc\xa8\xa5d\xfd\xe8\xb8O\x1cn\xe0e\x873\xe1\x0d\x13\xaf\x12\xed%-\xa9\x94q\xc7\x83\x9b\x08N|;\x0e'\xb3q\x8c\xaf7:\x19\x87\x87\xe3\x0eq\xb0L[s-\xf6\xeb \xc1\xd2\x18\x10\x13oy \xcf'\n\xe8(\x0bf\xab\xd3\xbe=\x08\xb3\xf7k \xcel\x85\xdb\xdc\xf6\xed\xb6/\xa7*\xfc\xd8\xc6\xce\xe6\xa47\x0fE\xed\xfd\xd1\xe5\x13X\xd1\x1e\xc2\xb5\x8b>g\xd4\x05\x83\x8e\xc1=}\x03q\xff\xc6\xb0\x97\xdf\xdb\xc9\xcd?q-fK\xec\x19\xdb4\xc7\x08\x11\x04\xd4\xf0Bs\xecS\xdb\x9f*b\xb7\xc0:\xdcn\xa7\xb2H\x10\xacI\xe0\xa8\"\xc0xr\xd3_\x94\xf9\xef{\x7f\xc5_\xee\xfc\xedd\x7f\xcdK\x89\x11s\xc9\xcb\xfc\xf2\xda\x8e'=w.\x1dw\\ W\x94\xd5s:\x06\xfa\xdb\xfbW\xdf\xff@\xd7{\xb1\x8e\xfb\x1f[\xc4\\b\x9b\xbc\xe2\xcb\x88\x10\xb2\x9f+\xfc\x13\xbb\x0cb\xa3\x01\xc0\xf74\xc7n\x8b2\xb2&\xff\x05\x00\x00\xff\xffPK\x07\x08\xc9\xac\x1e\xe7\x01\x05\x00\x00\xec\x11\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00\x07\xa2\xa8P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\x00 \x00authz_test.regoUT\x05\x00\x01\x0f\xbe\xb5^\xd4W[s\xda:\x17}\xb6\x7f\x85\xc7O\xcd7\xdc>\x87K\x9b\x99\xce)Mh\xca\xad\x10\x0c\x01\x9a\xc9x\x84\xad\xda\x02\xdb2\x92\x1c.\x1d\xfe\xfb\x19\xc9\\\x9c\xc4ILN\xe6\xe4\xf4\x89\xb1\xb5\xb5\xf7Z{\xad-\x8b\x00\x983`C%\xc0\x1e$(\xf4r d\xceZ\x96\xa7\x0bf8\x10X\x90(g\x9f\x95\xdf\xb2\xa4\xb2U\xa0\x9e)jc\xd8W3\xb2\xa4\x02\xd7\xe6\x8f\xdfu\xadTV\xe5\x8dL\x91\xed#\xdf6fp\xb5\xdb1c+\x1e\x82M&v\xcc\xf8Cg\xf6\xcd\x9b\xb7\x9b\x9f\x06\x05\xcb\xeb:\xed\xf3a\xe1z\xbc*_\x18\x044\x9a\x8bZ\x83\xb6\xad\xe5\xdc\xf2\xc3Y\xdfY\xcf\xf0\xe9\x851\"\x149\x8bq\xad\x10,\xc9@\x0f\xbcB\xa3O\x86\xdaUP_\x17I\xff\xffwV\xed\xee\xe7\xa2\\\x19v\x8bK2\x9f\xa2\xc5\xca\xaat\xed\xa0\xdb\xbf(-\xef\xae\xbe\xb6+\xfdz\x13\xe9\xc3\xc2H\xeb\x15\x82_s\xa3Sgt\xdd\xbd\xea\xb1I\xe5\x1a\x11\xa2O.\x1b\xa8\xf5C\xcf\xfeh\xb4\xdbd|\xdd\x1c\x0e\xd9`r\xad\xf7G\xb5i\xabrm~\x9b\xb7[\xa5.\xd2aet\xe1\xad\xce\x7fN\x03\xbb\x16\xfc\xaa\x95\xae>j\xeb:\x1c\xb55\xda\"\xeb\xf2\xf7\xa1V\xfdT_\\\xce*\xdeP/\x98\xa5J\xcf\xd0\x1a\x97\xabo\x1d\x8d\x9dW\x8b\xebZ}\xec\x0c\xefZ\xb5\xb2\xd6\xa1\x1a\xfbY\x1e\x13\xb2\xb0\xbe~\xf4OKS\xb7\x1b\xd8\x83Z9\xc0\xb5\xbb\xfa@+\xb8\xdd\x16\xc0&^\x8f\xc6\xedyu\x16f\x9b\x0d\xdf\xc5\x0d\xb7\xban\xda\xda\x08\x18\x05\xa4#\xdd\xd6\xab\xa1\xb7,\x16\xbf\x9e\xfa\x95\x8b\xab\xa9}:\xed:=[\x08\xe0\x00\x02\xad]\xff'\x80\xc2r1$n\xce\x82&\xb6\xe0\x87\x98>\xb9\xd9\x89,3H\x99\x01=\x80\\\x03\xb8.^@\x8bk\x16\xd2Hp\x84s\xd3\x05\xcbA\x9f\xef5\xf8\xde\x0f\x07Gdx\xa4\xa4\x82\xd0R\xcf\x94\x1b\x15.\x81\x17\xb80gbO\xbd\xcd\xf0\x15\x91\x96\xab=\xc5\xf0K|Y\x966\x19%\x86\xe4D\x96%Q]Y \xe6(\x16` Gp\xc8\xa0\x11`\x17\x99\x08R\x05P\xe5F\x94\xa38$&\xe4Y\xe3\x19E\xbd-\x01\x83\xa3\xa7\x02\xd3\xc3\xc2\xb7\xb2\xb4\xb9\x8d\x15\x89\xbb\x15\xd08\xa4x\xd0\xa1\xa3<\xe6\xf0$B\x90\x1f\x84\x8c/\x08t!\x11\x84\x1d\xc6\x82\xb3|\xfe\x11B\x07S\x96\x08\x9dCV\xcf\x14\xfe#K\x1by\xb3\x13&\x8a{\x17I$\x1f3%\x85*\xb2$\xf1\xa2qe\x9e\xa0/\xa9\x01`\x0e\x0f\xc8\x83\xed\x8b\x9dd\x16\xf6\x00\xf2\xe9c\xd4\xb2$m2\xaf*1yP\xe2\xe0\n\x1fc\x1f~\xd9\x1fu\xf1:\xefc\x8e-\xd6\xe3\xed!\xe6\xd6\x82>z\xb7\xb1Mi\x92\xe3Gw\x82'\x7f\xf4\xe8\x06\xe1\xc4Ef\xfcP}\x836Uy\x8a\xae\xc8<\xf0\xf9'\x1a\xfa\x0c\x99\x80A\xabj\x9a\x90\xf2\xce1\x12\xc2C\xab\xfe1\xc5\x88R\x9c\xd1\xc1n)\xb5O\x98\xde\x84\xb1%\xf0\x17ZF\x83\xbb\xca\x8a\x9e>5\xbe \xceH>#\x1eWI\xdd?i\xb3?\x0bR\xb6\xf0\x1e\xecgZ\xb9\xed\xe5\xf6\xf0yc\x7f\xa4j\xd6\x91\xde\xc8\xe7v`\xf3/q\xbbO\xedh\xa3\xbc3;`y\xc8\x7f\x89b\xc4\xd1\xc4\x84\x1a\xdc\xb2.\xb2\x1d\xf6\xef\x8b(\xe2\xce;==2\xf4\x0e\xc8k\xc7\xff\x19a\xc5\x82\x07\x99\x83\xf9\xe7B\xedt\xfb\xf5\xce\x0f}\x1b/\xbe'\x1cb4y\x1d\x82l\xe4\x0b\xc4\x14{\x10G\x8f\xb7\xdb\xd1\x13\x03\x96=\xc7>#\xd8\xcd\xf6\xe0<\x84\x94e\xdb\xbb\xd47\xeae\xad\x1f\xcdr\xccL\x0f\x1a\xfdgX\xea\xbf\xd8\xcd\xedl\x02B\xa1\x11\x12W\xdc\x16\x88\xcb/\x0b\xfbw\x1f\x92\xb8\xf0\xd2y~\xa7\xfakN\xd5\x13\xb1)GM\x07zP\xf9\xfc9b\xafFo9_\xf1\xee\xfe\xd0\xf0%\xbe_,\x1d\xd2\xa9{L; \"\xf5\"\xad\xf6\x93\xb4{\x9f\x84M\xcd(\xbf\x9f\xd0vsr\xfc\xfe\x84\x80\xd7\xa6\xa1\xaf\xc9\x93\x7f\xabD/\xe7\xc9\xa7K\x94\x02Q\x94i?\x8cOf\xc3\xc4~^\xaed7D\xb7\x82\xf4n\x88\xdd\"RR\x14\x87\xbe\xb0\xa5p\xe5\x83$\xd1'!\x1d\xc5\x04\x0c\xfb\xedO\xb0\xe3c\x91\x9e\xdb\xee\x8fMJf \x9bR\x91Hl\xc9\xfe\x7f\xdbk\x1a\xf2hsr;\x08\xb4\xe1\x11Z\x8bp\x9e7\xf7\xbf\x94\x1dI \xb6O\xb2]\xdc\xe6JOn\x9f\xe0f\xb9Z\xdfF\xe4\xfe\x0e\x00\x00\xff\xffPK\x07\x08\x92\xac+\xdev\x04\x00\x00M\x13\x00\x00PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\x05\xa2\xa8P\xc9\xac\x1e\xe7\x01\x05\x00\x00\xec\x11\x00\x00\n\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xb4\x81\x00\x00\x00\x00authz.regoUT\x05\x00\x01\x0b\xbe\xb5^PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\x07\xa2\xa8P\x92\xac+\xdev\x04\x00\x00M\x13\x00\x00\x0f\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xb4\x81B\x05\x00\x00authz_test.regoUT\x05\x00\x01\x0f\xbe\xb5^PK\x05\x06\x00\x00\x00\x00\x02\x00\x02\x00\x87\x00\x00\x00\xfe \x00\x00\x00\x00" - fs.RegisterWithNamespace("rego", data) - } + data := "PK\x03\x04\x14\x00\x08\x00\x08\x00\xd1\x9a\xb5P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\n\x00 \x00authz.regoUT\x05\x00\x01\xfa\xd4\xc6^\xbcXQs\xdb\xb6\x0f\x7f\x16?\x05\xca^\xef\xa4\x7f\x15\xa5\xed\xfd\xbb\xbbz\xf3\xb2^o\x0f{\xd8\xd2k\xb7'\x9d\xaa\xd2\x12m1\x91H\x8d\xa4\x1a\xbb\x99\xbf\xfb\x0e\xa4d+\x8e\xed\xa6i\xd6\x97(\x02\x81\x1f\x7f\x00(\x80p\xcb\x8aK\xb6\xe0\xd0\xaa\x86k\xd15 \xebl\xf5\x99\x10\xd1\xb4J[(\x99e\x89V\x9d\xe5y\xabjQ\x08nn,\x99\x8ai^\xe6\x97|EH\xc9\xe7\xac\xab-\xb0\xbaVW0\x859\xab\x0d'\xa4\xb2\xb6\xcd\x8de\xb630\x85\xf4\xff\xaf^\xc6@\x85\xfc\xc4jQBQ\x0b.-\x14\\[1\x17\x05\xb3\x9cf\xd7$\x90\xca\x82\x90mg\x13ar\xa7\x99{\xcd|\xa4I\xd6\x84<\xeewk\xbbY-\n\xe2_\xaeI\xe0(\xc3d\ns\xa1\x8d\xcd\x9d\x9c\x97\xb9\x13\x87\x1e\xb9\xd3u\xd4+n|K\xddk\x96\xbcF\xfd\xb7\x0e\xf3/\x89\x11\xe1\xd2\xba=\xcb\xd7E\xc1\x8d\x81\xe9\x14\xac\xeenP(\x946\xd0j>\xaf\xc5\xa2\xb2\x0fF\xe5\xcd\xf9\xbb\xf7\x9e\xce\x00\xbd\xd9<\xf0\xd6\x0d\xb7\x95*QJ\xcf\xdf\xfe\xf9\xdb\xf9\x1f\xef) \n\xd5I\x1b\xaa\xd9\x05/l\xb2\xe0\xb6\xdf\xa9\xe2\xac\xe4\xda\xc4@\xbd#'o\x94\xb4Z\xd5'\xef\xf8\xdf\x1d7\xf6\xe4w\x07FcH\xb3(\x82\x9f\xe1\xd9\x1d\xa0\xce\xb5X\x089\xb6Y\x93m\\f+\xe0\x0d\x13\xf5=\"b\xd5%\x97I\xcbV\xb5be\xe2P`\n\xfb\xe34\xa0t\x86k\x93\xe6\xd9`\xedN\xcf\xe0D\xc9\xe5*\x9aN\x9f\x8d\xf3\xb6\xd0\xaak\xefA\xce\xa8\x86\xf7\xc6;D\x9d\xd0\xa4\xee\x91a^\x8e3\xee\xd5\xbf\x82\xf2l\x05\xa2i\xb96J2\xcb\x1f(\xbc#\xc4\xfc?\n\xf5\x0e\xef\x87\x8f\xfc\xd8\x87\xef\x91\x85R5L\xc8\xfb\xba\xd0[\x07.\xda\xb9\x90\xb9\x17\x84{\x8e}\xfc\x05\xf6\xde\xd2\xa4\xfe\x99E_\xe3\xc48%\xdf\xc3\xa1[\x07\xed\xa1\x9d\xdb4\x85\xbe\xa7A\xa7k\xb3\xf5\xa9P\xd2\"\xde\x96\x7f\x0c\xf44\x19\xb4Oi\xe4\x1b\xd0\x1e\xbd\xb1\x1a+\x1b!i\xe4\x83\xa9\xb9\xed\xb44`+\xeeC\x05\x0d\xb3E%\xe4\xc2\xfbF\x0e\xc6/\xc7\xf8\x0d\x9f\xda\x8d\x90\xfb0\xc0?\xe0b\xeb_~\x84\x03)8\x10\xc3(K\x9feH\xf1\xc0\xce18\x83Ut\xddw\x13\x14\xe6jv\x81\x04Z\xa6\x0dG\xc1\x88) n \xe5Fu\xba\x18\x01\xa2\xed\x06tW\x19\xbb\xa3X\xdeU\x99\xd9\xea\x8e\xaa\x9a/\xf8A\xd8]\xe7\x8fS\xc6\x0c\x8cZ\x9d\x97\xc6@\xbd\x11\x8d\x81\xd2\xc8\xb5ZJ\xd6\x0f\x8e\xfb\xc8\xe1\x06^\xb6?\x13\xde0\xf1*\xd1N\xd2\x92J\x19w=\xb8\x89\xe0\xc4\xb7\xe3p4\x1b\x87\xf8z\xa3\xa3q\xf8v\xdc!\x0e\x96ik\xae\xc4\xee9H\xf0h\x0c\x88\x89\xb7\xdc\x93\xe7#\x07\xe8 \x0bf\xab\xe3\xbe}\x13f\xef\xd7@\x9c\xd9\n\xb7\xb9\xed\xdbm_\x8e\x9d\xf0C\x1b;\x9b\xa3\xde|+j\xef\x8f\xe6\xb9\xabv\xc3\xe1t*\xf1\x1e\xbf\\\x92\xb6g\xd9X\x8d\x95\xef\x1a\xa8)*\xdep:\x01\xffO\x0c\x14\x8f,\x9d\x00>\x86\x18N\xc0El\x8d\xcc\xd2<\xde\xe8z\x1d\xcd\xaep9\xc3R\x8a\xfb's!K,\xb8\xb9\xb1Z\xc8En\xba\x99c\x99\xcb\x90\x04\xc1\xc7\xf0l\x12\xe2h\x92\x9a\xec,\x9a\x9c\x9eFga\xfa\xe14{\x1a\x85\xe9\x87\xb3\xc7\xd9\xff\xa2\x8f1 \x02cu\x0c\xcf#,\xa2\x81\xcf\x17H\xa5\x1bV\x8b\xcf\xfe\xf3r\x07\xa2\xdf\xdb\xb9\xb7g\xb9\xf7\x93\x9eR\xa4n\xac\xde\xa4\xe3\xb02j\xf5\xca\x8fze\xb2\xdbV\xfb\xe6\xe9\xdf\\\xc2\x96X,L[\x0b;,\xd2_\xb0\x9d\xf9\x16\xb9t\xe7\xe0\x05 \x96\xe9sw#\xea\xfb\xf5z;\xbb\xf1e+4/\xb7\xd3\xdb pC\xd9Unx\xa1di&S+\x1a\x9e\xa0D\x9a0:}\xce_\x91 \xf5\x13A\x0c}\xa3\x8f!\xcf\x90\x8fP\xc9\xc5\x95MJ^\xa8r\xd3\xb1\x0c\xd7\x11\x86\xb4\xbf\xe3,[\xf8 F\x1bxNr\x95R\xd7\xeaA\x98\x0d\xb5\x90/\xdb\xc8M\x89\xbdd\xa3kZ-\xa4\x9d\x87\xbdM\xc5\x0c\xccX \xac+\x05\x97\x05\x87\x90ue4\x81'\x06\xfc| O\x9e~\xa2q\xda\x8f3x\x8a\x06>\xac+\xb3\x08\xb7\xb8\x87O\x88\xcdk\xde\xe0\xb4*d^\x0bc\xc3\x11n\xbc\xdd.\x1a_\xc4\xd0\x1a\xddt\xd7\x8a\xedee\x17\xc9M\xddN\xc7\xc4\xb0\xe7\xa2\xf8\x85\xcb\xd1\xbe\xcb\x0f\xdd{\xa7!\x8f\x01\xad@*y\xe2\xc4\x8e\xa1\x81\xb9V\x0d0\x1c\x1e\xf1r\xe3W\\51}\xc2\x06G0\x0e\x1eo\x98\xe8\xef\xe1\xcb\x9d\xe9\xfa\x94O\xe1\x9a\xf6\x10\xae\\\xf49\xa3.\x18t\x02\xee\xe9\x0b\x88\xfb7\x86\x9d\xfc\xdeNn\xfe\x89k1_a\xcd\xd8\xa69F\x88 \xa0\x86\x17\x9ac\x9d\xda\xfe\x0e\x12\xbb\x05\xd6\xe1v\xa3\x93E\x82`M\x02G\x15\x01&\xd3\x9b\xfe\xa2\xcc\x7f\xdf\xbb+~\xb8\xf3\xd3\xc9\xee\x9a\x97\x12#\x16\x92\x97\xf9\xc5\x95\x9dL{\xee\\:\xee\xb8\x12^SV/\xe8\x04\xe8\xaf\xef_\xbc\xfc\x81\xaewb\x1d\xf7\xbf\xe4\x88\x85\xc42y\xc9W\x11!d7W\xf8'v\x19\xc4B\x03\x80\xefi\x8e\xd5\x16edM\xfe\x0d\x00\x00\xff\xffPK\x07\x08\x1f\xad<2-\x05\x00\x00I\x12\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00T\x99\xb5P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\x00 \x00authz_test.regoUT\x05\x00\x011\xd2\xc6^\xd4W[s\xda:\x17}\xb6\x7f\x85\xc7O\xcd7\xdc>\x87K\x9b\x99\xce)Mh\xca\xad\x10\x0c\x01\x9a\xc9x\x84\xad\xda\x02\xdb2\x92\x1c.\x1d\xfe\xfb\x19\xc9\\\x9c\xc4ILN\xe6\xe4\xf4\x89\xb1\xb5\xb5\xf7Z{\xad-\x8b\x00\x983`C%\xc0\x1e$(\xf4r d\xceZ\x96\xa7\x0bf8\x10X\x90(g\x9f\x95\xdf\xb2\xa4\xb2U\xa0\x9e)jc\xd8W3\xb2\xa4\x02\xd7\xe6\x8f\xdfu\xadTV\xe5\x8dL\x91\xed#\xdf6fp\xb5\xdb1c+\x1e\x82M&v\xcc\xf8Cg\xf6\xcd\x9b\xb7\x9b\x9f\x06\x05\xcb\xeb:\xed\xf3a\xe1z\xbc*_\x18\x044\x9a\x8bZ\x83\xb6\xad\xe5\xdc\xf2\xc3Y\xdfY\xcf\xf0\xe9\x851\"\x149\x8bq\xad\x10,\xc9@\x0f\xbcB\xa3O\x86\xdaUP_\x17I\xff\xffwV\xed\xee\xe7\xa2\\\x19v\x8bK2\x9f\xa2\xc5\xca\xaat\xed\xa0\xdb\xbf(-\xef\xae\xbe\xb6+\xfdz\x13\xe9\xc3\xc2H\xeb\x15\x82_s\xa3Sgt\xdd\xbd\xea\xb1I\xe5\x1a\x11\xa2O.\x1b\xa8\xf5C\xcf\xfeh\xb4\xdbd|\xdd\x1c\x0e\xd9`r\xad\xf7G\xb5i\xabrm~\x9b\xb7[\xa5.\xd2aet\xe1\xad\xce\x7fN\x03\xbb\x16\xfc\xaa\x95\xae>j\xeb:\x1c\xb55\xda\"\xeb\xf2\xf7\xa1V\xfdT_\\\xce*\xdeP/\x98\xa5J\xcf\xd0\x1a\x97\xabo\x1d\x8d\x9dW\x8b\xebZ}\xec\x0c\xefZ\xb5\xb2\xd6\xa1\x1a\xfbY\x1e\x13\xb2\xb0\xbe~\xf4OKS\xb7\x1b\xd8\x83Z9\xc0\xb5\xbb\xfa@+\xb8\xdd\x16\xc0&^\x8f\xc6\xedyu\x16f\x9b\x0d\xdf\xc5\x0d\xb7\xban\xda\xda\x08\x18\x05\xa4#\xdd\xd6\xab\xa1\xb7,\x16\xbf\x9e\xfa\x95\x8b\xab\xa9}:\xed:=[\x08\xe0\x00\x02\xad]\xff'\x80\xc2r1$n\xce\x82&\xb6\xe0\x87\x98>\xb9\xd9\x89,3H\x99\x01=\x80\\\x03\xb8.^@\x8bk\x16\xd2Hp\x84s\xd3\x05\xcbA\x9f\xef5\xf8\xde\x0f\x07Gdx\xa4\xa4\x82\xd0R\xcf\x94\x1b\x15.\x81\x17\xb80gbO\xbd\xcd\xf0\x15\x91\x96\xab=\xc5\xf0K|Y\x966\x19%\x86\xe4D\x96%Q]Y \xe6(\x16` Gp\xc8\xa0\x11`\x17\x99\x08R\x05P\xe5F\x94\xa38$&\xe4Y\xe3\x19E\xbd-\x01\x83\xa3\xa7\x02\xd3\xc3\xc2\xb7\xb2\xb4\xb9\x8d\x15\x89\xbb\x15\xd08\xa4x\xd0\xa1\xa3<\xe6\xf0$B\x90\x1f\x84\x8c/\x08t!\x11\x84\x1d\xc6\x82\xb3|\xfe\x11B\x07S\x96\x08\x9dCV\xcf\x14\xfe#K\x1by\xb3\x13&\x8a{\x17I$\x1f3%\x85*\xb2$\xf1\xa2qe\x9e\xa0/\xa9\x01`\x0e\x0f\xc8\x83\xed\x8b\x9dd\x16\xf6\x00\xf2\xe9c\xd4\xb2$m2\xaf*1yP\xe2\xe0\n\x1fc\x1f~\xd9\x1fu\xf1:\xefc\x8e-\xd6\xe3\xed!\xe6\xd6\x82>z\xb7\xb1Mi\x92\xe3Gw\x82'\x7f\xf4\xe8\x06\xe1\xc4Ef\xfcP}\x836Uy\x8a\xae\xc8<\xf0\xf9'\x1a\xfa\x0c\x99\x80A\xabj\x9a\x90\xf2\xce1\x12\xc2C\xab\xfe1\xc5\x88R\x9c\xd1\xc1n)\xb5O\x98\xde\x84\xb1%\xf0\x17ZF\x83\xbb\xca\x8a\x9e>5\xbe \xceH>#\x1eWI\xdd?i\xb3?\x0bR\xb6\xf0\x1e\xecgZ\xb9\xed\xe5\xf6\xf0yc\x7f\xa4j\xd6\x91\xde\xc8\xe7v`\xf3/q\xbbO\xedh\xa3\xbc3;`y\xc8\x7f\x89b\xc4\xd1\xc4\x84\x1a\xdc\xb2.\xb2\x1d\xf6\xef\x8b(\xe2\xce;==2\xf4\x0e\xc8k\xc7\xff\x19a\xc5\x82\x07\x99\x83\xf9\xe7B\xedt\xfb\xf5\xce\x0f}\x1b/\xbe'\x1cb4y\x1d\x82l\xe4\x0b\xc4\x14{\x10G\x8f\xb7\xdb\xd1\x13\x03\x96=\xc7>#\xd8\xcd\xf6\xe0<\x84\x94e\xdb\xbb\xd47\xeae\xad\x1f\xcdr\xccL\x0f\x1a\xfdgX\xea\xbf\xd8\xcd\xedl\x02B\xa1\x11\x12W\xdc\x16\x88\xcb/\x0b\xfbw\x1f\x92\xb8\xf0\xd2y~\xa7\xfakN\xd5\x13\xb1)GM\x07zP\xf9\xfc9b\xafFo9_\xf1\xee\xfe\xd0\xf0%\xbe_,\x1d\xd2\xa9{L; \"\xf5\"\xad\xf6\x93\xb4{\x9f\x84M\xcd(\xbf\x9f\xd0vsr\xfc\xfe\x84\x80\xd7\xa6\xa1\xaf\xc9\x93\x7f\xabD/\xe7\xc9\xa7K\x94\x02Q\x94i?\x8cOf\xc3\xc4~^\xaed7D\xb7\x82\xf4n\x88\xdd\"RR\x14\x87\xbe\xb0\xa5p\xe5\x83$\xd1'!\x1d\xc5\x04\x0c\xfb\xedO\xb0\xe3c\x91\x9e\xdb\xee\x8fMJf \x9bR\x91Hl\xc9\xfe\x7f\xdbk\x1a\xf2hsr;\x08\xb4\xe1\x11Z\x8bp\x9e7\xf7\xbf\x94\x1dI \xb6O\xb2]\xdc\xe6JOn\x9f\xe0f\xb9Z\xdfF\xe4\xfe\x0e\x00\x00\xff\xffPK\x07\x08\x92\xac+\xdev\x04\x00\x00M\x13\x00\x00PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\xd1\x9a\xb5P\x1f\xad<2-\x05\x00\x00I\x12\x00\x00\n\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xb4\x81\x00\x00\x00\x00authz.regoUT\x05\x00\x01\xfa\xd4\xc6^PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00T\x99\xb5P\x92\xac+\xdev\x04\x00\x00M\x13\x00\x00\x0f\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xb4\x81n\x05\x00\x00authz_test.regoUT\x05\x00\x011\xd2\xc6^PK\x05\x06\x00\x00\x00\x00\x02\x00\x02\x00\x87\x00\x00\x00*\n\x00\x00\x00\x00" + fs.RegisterWithNamespace("rego", data) +} diff --git a/authorize/grpc.go b/authorize/grpc.go index 8426ecc3e..28defd522 100644 --- a/authorize/grpc.go +++ b/authorize/grpc.go @@ -20,7 +20,6 @@ import ( envoy_api_v2_core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core" envoy_service_auth_v2 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v2" - envoy_type "github.com/envoyproxy/go-control-plane/envoy/type" "google.golang.org/genproto/googleapis/rpc/status" "google.golang.org/grpc/codes" ) @@ -60,13 +59,14 @@ func (a *Authorize) Check(ctx context.Context, in *envoy_service_auth_v2.CheckRe requestURL := getCheckRequestURL(in) req := &evaluator.Request{ - User: string(sess), - Header: hdrs, - Host: hattrs.GetHost(), - Method: hattrs.GetMethod(), - RequestURI: requestURL.String(), - RemoteAddr: in.GetAttributes().GetSource().GetAddress().String(), - URL: requestURL.String(), + User: string(sess), + Header: hdrs, + Host: hattrs.GetHost(), + Method: hattrs.GetMethod(), + RequestURI: requestURL.String(), + RemoteAddr: in.GetAttributes().GetSource().GetAddress().String(), + URL: requestURL.String(), + ClientCertificate: getPeerCertificate(in), } reply, err := a.pe.IsAuthorized(ctx, req) if err != nil { @@ -88,7 +88,12 @@ func (a *Authorize) Check(ctx context.Context, in *envoy_service_auth_v2.CheckRe evt = evt.Strs("deny-reasons", reply.GetDenyReasons()) evt = evt.Str("email", reply.GetEmail()) evt = evt.Strs("groups", reply.GetGroups()) - evt = evt.Str("session", string(sess)) + if sess != nil { + evt = evt.Str("session", string(sess)) + } + if reply.GetHttpStatus() != nil { + evt = evt.Interface("http_status", reply.GetHttpStatus()) + } evt.Msg("authorize check") requestHeaders = append(requestHeaders, @@ -99,6 +104,14 @@ func (a *Authorize) Check(ctx context.Context, in *envoy_service_auth_v2.CheckRe }, }) + if reply.GetHttpStatus().GetCode() > 0 && reply.GetHttpStatus().GetCode() != http.StatusOK { + return a.deniedResponse(in, + reply.GetHttpStatus().GetCode(), + reply.GetHttpStatus().GetMessage(), + reply.GetHttpStatus().GetHeaders(), + ), nil + } + if reply.Allow { return &envoy_service_auth_v2.CheckResponse{ Status: &status.Status{Code: int32(codes.OK), Message: "OK"}, @@ -123,30 +136,12 @@ func (a *Authorize) Check(ctx context.Context, in *envoy_service_auth_v2.CheckRe msg = sesserr.Error() } // all other errors - return &envoy_service_auth_v2.CheckResponse{ - Status: &status.Status{Code: int32(codes.PermissionDenied), Message: msg}, - HttpResponse: &envoy_service_auth_v2.CheckResponse_DeniedResponse{ - DeniedResponse: &envoy_service_auth_v2.DeniedHttpResponse{ - Status: &envoy_type.HttpStatus{ - Code: envoy_type.StatusCode_Forbidden, - }, - }, - }, - }, nil + return a.deniedResponse(in, http.StatusForbidden, msg, nil), nil } // no redirect for forward auth, that's handled by a separate config setting if isForwardAuth { - return &envoy_service_auth_v2.CheckResponse{ - Status: &status.Status{Code: int32(codes.Unauthenticated)}, - HttpResponse: &envoy_service_auth_v2.CheckResponse_DeniedResponse{ - DeniedResponse: &envoy_service_auth_v2.DeniedHttpResponse{ - Status: &envoy_type.HttpStatus{ - Code: envoy_type.StatusCode_Unauthorized, - }, - }, - }, - }, nil + return a.deniedResponse(in, http.StatusUnauthorized, "Unauthenticated", nil), nil } signinURL := opts.AuthenticateURL.ResolveReference(&url.URL{Path: "/.pomerium/sign_in"}) @@ -155,25 +150,9 @@ func (a *Authorize) Check(ctx context.Context, in *envoy_service_auth_v2.CheckRe signinURL.RawQuery = q.Encode() redirectTo := urlutil.NewSignedURL(opts.SharedKey, signinURL).String() - return &envoy_service_auth_v2.CheckResponse{ - Status: &status.Status{ - Code: int32(codes.Unauthenticated), - Message: "unauthenticated", - }, - HttpResponse: &envoy_service_auth_v2.CheckResponse_DeniedResponse{ - DeniedResponse: &envoy_service_auth_v2.DeniedHttpResponse{ - Status: &envoy_type.HttpStatus{ - Code: envoy_type.StatusCode_Found, - }, - Headers: []*envoy_api_v2_core.HeaderValueOption{{ - Header: &envoy_api_v2_core.HeaderValue{ - Key: "Location", - Value: redirectTo, - }, - }}, - }, - }, - }, nil + return a.deniedResponse(in, http.StatusFound, "Login", map[string]string{ + "Location": redirectTo, + }), nil } func (a *Authorize) getEnvoyRequestHeaders(rawjwt []byte, isNewSession bool) ([]*envoy_api_v2_core.HeaderValueOption, error) { @@ -329,3 +308,10 @@ func handleForwardAuth(opts config.Options, req *envoy_service_auth_v2.CheckRequ return false } + +// getPeerCertificate gets the PEM-encoded peer certificate from the check request +func getPeerCertificate(in *envoy_service_auth_v2.CheckRequest) string { + // ignore the error as we will just return the empty string in that case + cert, _ := url.QueryUnescape(in.GetAttributes().GetSource().GetCertificate()) + return cert +} diff --git a/cache/grpc.go b/cache/grpc.go index 829ccd6cc..e504e6c77 100644 --- a/cache/grpc.go +++ b/cache/grpc.go @@ -1,4 +1,4 @@ -//go:generate protoc -I ../internal/grpc/cache/ --go_out=plugins=grpc:../internal/grpc/cache/ ../internal/grpc/cache/cache.proto +//go:generate ../scripts/protoc -I ../internal/grpc/cache/ --go_out=plugins=grpc:../internal/grpc/cache/ ../internal/grpc/cache/cache.proto package cache diff --git a/cache/grpc_test.go b/cache/grpc_test.go index c7165db4e..766187022 100644 --- a/cache/grpc_test.go +++ b/cache/grpc_test.go @@ -1,5 +1,3 @@ -//go:generate protoc -I ../internal/grpc/cache/ --go_out=plugins=grpc:../internal/grpc/cache/ ../internal/grpc/cache/cache.proto - package cache import ( @@ -12,6 +10,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/pomerium/pomerium/config" "github.com/pomerium/pomerium/internal/cryptutil" "github.com/pomerium/pomerium/internal/grpc/cache" diff --git a/config/options.go b/config/options.go index ae95cbfb6..935cda2ea 100644 --- a/config/options.go +++ b/config/options.go @@ -238,6 +238,11 @@ type Options struct { // CacheStorePath is the path to use for a given cache store. e.g. /etc/bolt.db CacheStorePath string `mapstructure:"cache_store_path" yaml:"cache_store_path,omitempty"` + // ClientCA is the base64-encoded certificate authority to validate client mTLS certificates against. + ClientCA string `mapstructure:"client_ca" yaml:"client_ca,omitempty"` + // ClientCAFile points to a file that contains the certificate authority to validate client mTLS certificates against. + ClientCAFile string `mapstructure:"client_ca_file" yaml:"client_ca_file,omitempty"` + viper *viper.Viper } @@ -562,6 +567,18 @@ func (o *Options) Validate() error { o.Certificates = append(o.Certificates, *cert) } + if o.ClientCA != "" { + if _, err := base64.StdEncoding.DecodeString(o.ClientCA); err != nil { + return fmt.Errorf("config: bad client ca base64: %w", err) + } + } + + if o.ClientCAFile != "" { + if _, err := os.Stat(o.ClientCAFile); err != nil { + return fmt.Errorf("config: bad client ca file: %w", err) + } + } + RedirectAndAutocertServer.update(o) err = AutocertManager.update(o) diff --git a/docs/configuration/readme.md b/docs/configuration/readme.md index 99bafed98..6b82fec92 100644 --- a/docs/configuration/readme.md +++ b/docs/configuration/readme.md @@ -209,6 +209,15 @@ certificates: key: "$HOME/.acme.sh/prometheus.example.com_ecc/prometheus.example.com.key" ``` +### Client Certificate Authority + +- Environment Variable: `CLIENT_CA` / `CLIENT_CA_FILE` +- Config File Key: `client_ca` / `client_ca_file` +- Type: [base64 encoded] `string` or relative file location +- Optional + +The Client Certificate Authority is the x509 _public-key_ used to validate [mTLS](https://en.wikipedia.org/wiki/Mutual_authentication) client certificates. If not set, no client certificate will be required. + ### Global Timeouts - Environmental Variables: `TIMEOUT_READ` `TIMEOUT_WRITE` `TIMEOUT_IDLE` diff --git a/go.mod b/go.mod index 117daa766..5d47540a0 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( github.com/gorilla/handlers v1.4.2 github.com/gorilla/mux v1.7.4 github.com/gorilla/websocket v1.4.2 + github.com/hashicorp/golang-lru v0.5.4 github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect github.com/lithammer/shortuuid/v3 v3.0.4 github.com/mitchellh/hashstructure v1.0.0 diff --git a/internal/controlplane/luascripts/statik.go b/internal/controlplane/luascripts/statik.go index 7a7febe28..704a4455a 100644 --- a/internal/controlplane/luascripts/statik.go +++ b/internal/controlplane/luascripts/statik.go @@ -9,6 +9,6 @@ import ( const Luascripts = "luascripts" // static asset namespace func init() { - data := "PK\x03\x04\x14\x00\x08\x00\x08\x00el\xb3P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x12\x00 \x00clean-upstream.luaUT\x05\x00\x01\x8f\xe0\xc3^\x94S\xc1n\x9c0\x10\xbd\xf3\x15O\xf4PV%\x91z\xdd\xc8\xff\xd0{\xd5\"\x17f\x17\xab`\xbb\xf68\x9b\xe4\xd0o\xaf\xbc\x18\x16\x07VU|\xc0cy\xde\x9b\xc7\x9b\xf1)\xe8\x96\x95\xd1p4\x9agj\xac\x19\xc9\xa906\xad1\xbf\x15U\xd3\xd6h9R\x8d\xe9p(\x00\xe0\xe1\x01C\x90\xe8\x0cy\xfd\x99\xe1\x83\xb5\xc61\x8c\x8dlr@+-\x07G8;\x13\xac\x9f!\xde\xe0Bpd\x07\xd9\x12\xf8\xa2\xe2\xd7\xa0\x97\xba\x1b\x08sq\xf1\xf2\xfa\x06\xc9\xe0\x9e@\xba\x839]C\xcfN\xe9\xf3\x95jR\x02\x91\x82\xe3\xd9\x87_k\xadx|D)\xbe\xff|\xfa\xf1\xe5 e\x8d\xb2<|\x14\xb7B9\xe2\xe0t\xc2\x14\xa4\xbb\xa2X|\xeb\xa5o\xac\xa3\x93z\xa9<\xbb\x1aS\x9c\xe1<;\xfc\x15\xd0j\x80\xd4]<\x1ec\xd9\xaf5>\xa5l\x08\x91\x80\xef\xd8I?\x9b\xd7\xc6\xe8\xc6\xd1\x9f@\x9e\xab\xb47\x93cS\x99\xc1\xb4r@O\xb2#\xe7!\x90\xe7\x1c\xd3E\xb5N\x1e\x89e'Yn\xb3\xe7\x9b\xeaP\xac\xf2\xd3t\xac\x9d\x12\x0b\xc9\xf1L\\\x95\xfb\x03\x94\x1cT\xa7=\n\xeeI_\xafo\x85\x96\x06%\xd5\x13w\xc6\x95\xf8Rf26\xa3\x8aK\xd3e\xe1\xba3\xdb[E\xf9\x88\xcfk\x96\x92\xc6v\x91S\xdf\x8a\xdc\x00\xb1\x7f\xf3\xbe5P\x06\xee\x8dSo\xf2\xda\xdd\xffY\x98eo\x9c\xcc\xb9v\xbc|_,\xb3t\x8f\xfb\x0e4\xcd7\x04\xcaoI\x1a\xcau+Vo \x03\xd6\xbb<\x87m\xb3n\x0e\xc7?\xbb/nm\xee\xdd\x87\xe2\xad\xd1>vw\n\x96\xa7\x12\x11\xff\x02\x00\x00\xff\xffPK\x07\x08\x08Z\x88f\xa3\x01\x00\x00\xef\x04\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00\x10\x9d\xb2P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x18\x00 \x00ext-authz-set-cookie.luaUT\x05\x00\x01\xb0\xe4\xc2^\x8c\x91An\x830\x10E\xf7\x9cb\xc4\xcaHI\x0e\x80\x94\x03t\xd1\x13T\x955\xc5C\xb0j\x8fS{\x88\x9aM\xcf^AM\x04\x0dM\x19 \x01\xe2\xff?\xf8\xbf\xb6\xe7Fl` \xbe\x84\xab\x0e\xac#}\xf4\x94D\xe5\xbb\xee\x90\x8d\xa3\xaa\x00\x00p\xa1A\x07\x1d\xa1\xa1\x98\xe0\x08KM\x9d?\xa8\xb9\xd8\\\x19\xbdm\xb4'\xc1{G\x92H\xe8\x9f\xb8\x0d\xaa\xaa\xb3\xf4\x99\x04\x0d\n\xe6\x18\xdbN\x0b\xeb\x13\x89*?\xf7\xe7\xe0)\xda\xde\xef\x13\xc9\xbe \xe1\xddRY\xc1\xd7\x11\xd8:\x90\x8ex\xf4\x0d3_^\xa7\xc1=\x1e\xf3\xd0Z'\x14\xd3\xa1\x139\x1f\\\x8f\xe5\x0e\xca)U'\x12\x9dSw\xb7\xa4\xbb\xd9\xf2OU\xf1[\x1d\xc9\x87\x0b\xfdi\x18\xf5\xc4\xa6\x18\xaeb\x8dM:\x07N\xa4\xa6\x87\x7f\xe8,D\xdb\xf0,-\x1b\xf8\xfc\xe4\xc8\x9b\x83\xe3\xb2\xef\xd3\x83\xbeoh\x07_&\x87l\x86\xd7\x97U\x12\xaf\xab|\xa7Z\xd1\x18U\xce\x8a\xdc=\x08Z\x96\xfc\x1d\x00\x00\xff\xffPK\x07\x08\x93\xe7\xad\x94\x07\x01\x00\x00\x00\x03\x00\x00PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00el\xb3P\x08Z\x88f\xa3\x01\x00\x00\xef\x04\x00\x00\x12\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xb4\x81\x00\x00\x00\x00clean-upstream.luaUT\x05\x00\x01\x8f\xe0\xc3^PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\x10\x9d\xb2P\x93\xe7\xad\x94\x07\x01\x00\x00\x00\x03\x00\x00\x18\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xb4\x81\xec\x01\x00\x00ext-authz-set-cookie.luaUT\x05\x00\x01\xb0\xe4\xc2^PK\x05\x06\x00\x00\x00\x00\x02\x00\x02\x00\x98\x00\x00\x00B\x03\x00\x00\x00\x00" + data := "PK\x03\x04\x14\x00\x08\x00\x08\x00@\x89\xb3P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x12\x00 \x00clean-upstream.luaUT\x05\x00\x01\xe8\x12\xc4^\x94S\xc1n\x9c0\x10\xbd\xf3\x15O\xf4PV%\x91z\xdd\xc8\xff\xd0{\xd5\"\x17f\x17\xab`\xbb\xf68\x9b\xe4\xd0o\xaf\x00\xc3\xe2\x00\xaa\xe2\x03\x1e\xcb\xf3\xde<\xde\x8c/A\xd7\xac\x8c\x86\xa3\xde\xfcZk\xc5\xe3#r\xf1\xfd\xe7\xd3\x8f/O\xc8K\xe4\xf9\xe9\xa3\xb8\x15\xca\x11\x07\xa7#&#\xddd\xd9\xe2[+}e\x1d]\xd4K\xe1\xd9\x95\x98\xe2\x04\xe7\xd9\xe1\xaf\x80V\x1d\xa4n\x86\xe3y(\xfb\xb5\xc4\xa7\x98\x0d!\"\xf0\x1d;\xe9g\xf3Z\x19]9\xfa\x13\xc8s\x11\xf7jrl*\xd3\x99ZvhI6\xe4<\x04\xd2\x9cs\xbc(\xd6\xc9=\xb1l$\xcbm\xf6|S\x9c\xb2U~\x9c\x8e\xb5Sb!9_\x89\x8b|\x7f\x80\xa2\x83\xea\xb2G\xc1-\xe9\xf1\xfa^hiPT=q'\\\x91/fFc\x13\xaaai\xba-\\\x07\xb3\xbdU\x94\x8e\xf8\xbcf)ql\x179\xe5\xbd\xc8\x1d0\xf4o\xde\xb7\x06\xca\xc0\xadq\xeaM\x8e\xdd\xfd\x9f\x85I\xf6\xc6\xc9\x94k\xc7\xcb\xf7\xc5\x12K\xf7\xb8\x0f\xa0q\xbe!\x90\x7f\x8b\xd2\x90\xaf[\xb1z\x03 \xb0\xdc\xe59m\x9buwx\xf8\xb3cqks\x0f\x1f\x8a\xb7F\xfb\xa1\xbbS\xb0<\x95\x11\xf1/\x00\x00\xff\xffPK\x07\x08\xfb\x06j<\xa2\x01\x00\x00\xf0\x04\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00bz\xb3P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x18\x00 \x00ext-authz-set-cookie.luaUT\x05\x00\x01\xe8\xf8\xc3^\x8c\x91An\x830\x10E\xf7\x9cb\xc4\xcaHI\x0e\x80\x94\x03t\xd1\x13T\x955\xc5C\xb0j\x8fS{\x88\x9aM\xcf^AM\x04\x0dM\x19 \x01\xe2\xff?\xf8\xbf\xb6\xe7Fl` \xbe\x84\xab\x0e\xac#}\xf4\x94D\xe5\xbb\xee\x90\x8d\xa3\xaa\x00\x00p\xa1A\x07\x1d\xa1\xa1\x98\xe0\x08KM\x9d?\xa8\xb9\xd8\\\x19\xbdm\xb4'\xc1{G\x92H\xe8\x9f\xb8\x0d\xaa\xaa\xb3\xf4\x99\x04\x0d\n\xe6\x18\xdbN\x0b\xeb\x13\x89*?\xf7\xe7\xe0)\xda\xde\xef\x13\xc9\xbe \xe1\xddRY\xc1\xd7\x11\xd8:\x90\x8ex\xf4\x0d3_^\xa7\xc1=\x1e\xf3\xd0Z'\x14\xd3\xa1\x139\x1f\\\x8f\xe5\x0e\xca)U'\x12\x9dSw\xb7\xa4\xbb\xd9\xf2OU\xf1[\x1d\xc9\x87\x0b\xfdi\x18\xf5\xc4\xa6\x18\xaeb\x8dM:\x07N\xa4\xa6\x87\x7f\xe8,D\xdb\xf0,-\x1b\xf8\xfc\xe4\xc8\x9b\x83\xe3\xb2\xef\xd3\x83\xbeoh\x07_&\x87l\x86\xd7\x97U\x12\xaf\xab|\xa7Z\xd1\x18U\xce\x8a\xdc=\x08Z\x96\xfc\x1d\x00\x00\xff\xffPK\x07\x08\x93\xe7\xad\x94\x07\x01\x00\x00\x00\x03\x00\x00PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00@\x89\xb3P\xfb\x06j<\xa2\x01\x00\x00\xf0\x04\x00\x00\x12\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xb4\x81\x00\x00\x00\x00clean-upstream.luaUT\x05\x00\x01\xe8\x12\xc4^PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00bz\xb3P\x93\xe7\xad\x94\x07\x01\x00\x00\x00\x03\x00\x00\x18\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xb4\x81\xeb\x01\x00\x00ext-authz-set-cookie.luaUT\x05\x00\x01\xe8\xf8\xc3^PK\x05\x06\x00\x00\x00\x00\x02\x00\x02\x00\x98\x00\x00\x00A\x03\x00\x00\x00\x00" fs.RegisterWithNamespace("luascripts", data) } diff --git a/internal/controlplane/xds_listeners.go b/internal/controlplane/xds_listeners.go index 24f7e5c35..97a6808c6 100644 --- a/internal/controlplane/xds_listeners.go +++ b/internal/controlplane/xds_listeners.go @@ -1,6 +1,7 @@ package controlplane import ( + "encoding/base64" "sort" "time" @@ -164,6 +165,7 @@ func (srv *Server) buildMainHTTPConnectionManagerFilter(options *config.Options, }, }, }, + IncludePeerCertificate: true, }) extAuthzSetCookieLua, _ := ptypes.MarshalAny(&envoy_extensions_filters_http_lua_v3.Lua{ @@ -326,11 +328,28 @@ func (srv *Server) buildDownstreamTLSContext(options *config.Options, domain str return nil } + var trustedCA *envoy_config_core_v3.DataSource + if options.ClientCA != "" { + bs, err := base64.StdEncoding.DecodeString(options.ClientCA) + if err != nil { + log.Warn().Msg("client_ca does not appear to be a base64 encoded string") + } + trustedCA = inlineBytesAsFilename("client-ca", bs) + } else if options.ClientCAFile != "" { + trustedCA = inlineFilename(options.ClientCAFile) + } + envoyCert := envoyTLSCertificateFromGoTLSCertificate(cert) return &envoy_extensions_transport_sockets_tls_v3.DownstreamTlsContext{ CommonTlsContext: &envoy_extensions_transport_sockets_tls_v3.CommonTlsContext{ TlsCertificates: []*envoy_extensions_transport_sockets_tls_v3.TlsCertificate{envoyCert}, AlpnProtocols: []string{"h2", "http/1.1"}, + ValidationContextType: &envoy_extensions_transport_sockets_tls_v3.CommonTlsContext_ValidationContext{ + ValidationContext: &envoy_extensions_transport_sockets_tls_v3.CertificateValidationContext{ + TrustedCa: trustedCA, + TrustChainVerification: envoy_extensions_transport_sockets_tls_v3.CertificateValidationContext_ACCEPT_UNTRUSTED, + }, + }, }, } } diff --git a/internal/frontend/statik/statik.go b/internal/frontend/statik/statik.go index d55f98542..9ce435452 100644 --- a/internal/frontend/statik/statik.go +++ b/internal/frontend/statik/statik.go @@ -10,7 +10,6 @@ import ( const Web = "web" // static asset namespace func init() { - data := "PK\x03\x04\x14\x00\x08\x00\x08\x00v {P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x16\x00 \x00html/dashboard.go.htmlUT\x05\x00\x01!{}^\xecYKo\xe36\x10\xbe\xe7WL\x85\x1ck1H{(\x16\xb2\xd1`\x1f\xc5^\x9a\x00\xc9\x1e\xf6\x14\xd0\xd2X\x9e\x82\x0f\x95\xa4\x1c\x07F\xfe{\xa1\x87\x1d\xbd\x1c\xcb\x8a\x925\x8a\xf5%\x129\x1f9\xf3}$\x87\x19m6\x11.H!x\x11\xb7\xcb\xb9\xe6&\xf2\x97N\n\xef\xe9\xe9,\xf8\xe5\xd3\xf5\xc7\xbb\xef7\x9f!k\x99\x9d\x05\xd9\x1f\x10\\\xc5S\x0f\x95\x07\xe1\x92\x1b\x8bn\xea\xa5n1\xf9\xc3\x9b\x9d\x01\x04K\xe4Q\xf6\x00\x108r\x02g7Z\xa2\xa1T\x06\xacx\xcf\xfb6\x1b\x872\x11\xdc!x\x19\x02\xcdnR\x80\x80\x15\x83d\x8fs\x1d=\x96\xc3E\xb4\x02\x8a\xa6\x9e\xe4\xa4\xbc\xa2\xad\xd2Jj\xa1's\xbd\xde\xf5\x94}\xa1\xe0\xd6N\xbd\x90\x9b\xa8\xd2\xd5\xee\x9c\x14n\xd4l\xb2p.g\x1fScP9H-\x9a\x80-/\xeb\x16\x9b\x0d-\xc0\xbfEkI+\xff\x86B\x97\x1a\x84<\x8e\xca0$\xe3\xedd\x14j\xe5\x815\xe1\xd4\xdbl\x9a\xc0\xa7'\x0f\xb8\xc8\x18\xb5h\x80$\x8f\xd1\x03\xd6\x9c\x11\x85\xc5\x8e\x19j\x0dP\x9b\xaf\xd1\x95\xcf\xce\xfc\xa4\x94\x86qk\xd1YF2f<\x0cu\xaa\xdc}H&\x148\xb9\xfc=Y\xfbv\x157GXK\xa1\xec\xd4[:\x97|`\xec\xe1\xe1\xc1\x7f\xf8\xcd\xd7&f\x97\x17\x17\x17\xac\x05h\x87\xa0\xa2Z\x04\x01\x8bh\x95K\xbekYh#A\xa2[\xeah\xea\xdd\\\xdf\xdey\xc0CGZ\xd5\\\xb7\x14\xab{\x9d\xba\xa6p\x16s\xdbY\xc3\xef \xd9\xf2\"\xd1\xda\x8c\xde\xd9w\x9d\x1a\x08K\x91m\xa1\x07D\xe88 \xeb\x07,i\x0d\xb1 \x14\x91E\xd7\xech.\x86\xbf\xb9l\xca\x94\xe3\x05\x9f\xa3h\x833\xa7\x13\xaef\x19,`\xf9c\x97\x0d\xa9$u\x1d\x1d\x00\xee1\xc1\xa9\xe7p\xed\x9aj\x15\xbf2\xf0\xdc\xfdn\x8b\x15\x17)\xd6\xd6e\x11C\xb7u\xbe\x9f{[Gd\xf9\\`\xd4\xd1\xc9\xda\x81\x06l\x0fK\xdb\xd5\xdf\xe0\xfa/Z\xa1\x1aHx\x8e\x85\x93\xa2\xbd\x12N_\xee\x0fB\xc6\x13 \xdb\xbb\x0d\xfe\xbfpI\xe2q\xa0\x00\x05\xf8\xb4\x14\xa8\x06\xd4W\x82\xc3\x98\xd15\xe8\xd2\xe26\x9d\xff\x83\xa1\x1b \xc47\x8b\xe6\xeb\xa7\x93\xd1`\x17H_\x01\x0e\x00Fg\xbf\xca\xfag\xc9I\x0c\xe0<\xc7\xbd\x82r\xcc\xf0\xe3q^\x86\xd1\x97\xf1\x17\xcd\xdf\x94\xefl\xad\x0e\xa0\xfb[~\x8b\x1b\xcc\xf6\xb8\x0b\xbc\x88\xa1/\xd7/Y\x8fN\xb5\xe1*F8\xa7_\xcf\xef?L+y\xd6\xe84\xb1G\xf1\x9e\xcb\x86\xff\xc29\xc1E\x07p\x97\x85\xb3\x91\xf7+\xd3y\xeb\xad\xe2_\x846n\x9b;\xe4\xfb\xc8}X\xe2w\x93\xb5vb\xad\x132\x8fGiY\x1eY9\xf0dvQ\xe1\x8e\x7fG\xc7\xe4\xea\x1e\xa07%\xff\xab\xb5)FWC\xb2t\x01=\x19\xfa\xb7\x91\x1c)@/\xd8\xdbK0$\x87\x14\xc0\x1f-@;\x90\x03\xd4\xf7\x05\x8cN\xfa.\x97@=\x99\\\xa5\x11\xa1\n\x8f\xfb\x97\xa1g:\xd9\x8e\xfd\x7f\xca(\x15%a\x9fx\x95\xe5\xb1\xd7ft\x81k\xbbJ&h\xacV\xdc\xe1\xd0+\xf1\xf3\x10\xa4bx\xed\x05\xf95\x8cw\x9cv\xad\xf0\xde\x89\xe3=\x17\xb2\x8a?ow7\xab\x0b\xf2\xf3\xa6\xd6\xf1\x1bW\xef\xb3\xa6yw\xcd1`\x9de\xcejm{!p\xed5\xe7\xc9N\x87\xd0\x9a\xc5\x97l\xd4f\xb1\x1a \x98\xa7\xcei\xb5\x1d\xa2|[\xa4Bx%\xdb6\x9dKr\xde\xec\x96b\x05\xd7\xa9\x0bXa\xd4t//\xeaV\x1b\x16\xda\xc8\xe7\x96\x9aA\xbd\x04\xfc\x0e\x85\xfd\xcc\xf9 \xa9 \xb7\xed\xb2\xfeh\xf5t\x9b&hVd1\xbaO-\x9a\xb7\xac\xab\x0f\xae\xa1\xf3H\x92b\xf4|\x92\x0c/\xa6\xb7\x16\xf4U66Yg\xb8\xd3\xc6B\xc8\x158\x94\x896\xdc\x90x\x84\xca\xa4\xc0\x95vK4\xf9\x87\x16\xbf\xb5\x03\x8e\xa9\xc3\x1fJ0\xc3S\x8a\xe2\xb2\xcc\xae\xcdD\xb07\xdb\x8eW\xa6\xe9\xeeL\x04\x0fq\xa9E\x84\xa6\xf8n\xf4'\xae\xb9L\x04\xfa\xa1\x96]\x90c\xce\xa3CL\x1e\xc8\x05\xc72Y\xa4\xb0\x03T\x8ep\xd4\xf7`\x12UL\n\xd1\x90jmR8\x8a\xc4\x1fsx\xb7\xbc\xeb\xe4\xfb*w\xa0\x93\xef\x92)\x8b\x1d\\w$\x86\x96M-Q4z\xdb\xdcU|j\xd17Zj\xa9=\x06\xac\xf8\xb6\x1c\xb0\xe2\xe3\xf66\xf3\xfe\x17\x00\x00\xff\xffPK\x07\x08\x06\xa8c9W\x04\x00\x00\x11\x1f\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00\x99\x06vP\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x12\x00 \x00html/error.go.htmlUT\x05\x00\x01\xe3\xb6v^\x9cUMo\xdb8\x10\xbd\xe7W\xcc\xf2\x1c\x8b\x81w\xb1h\x0b\xda\x97$\x87\x00\x05\x1a\xa4i\x81\x9e\x02\x9a\x1cI\x04D\xd2%G\xfe\x80\xa0\xff^P\xb6S\x8br\x02\xb4'K\xf3\xc5\xf7F\xef\xd1]\xa7\xb14\x0e\x81a\x08>\x145\xd9\x86\xf5\xfd\x95\xf8\xe7\xee\xcb\xed\xf3\x8f\xc7{H\x91\xe5\x95H?\xd0HW-\x18:\x06\xaa\x96!\"-XK\xe5\xec\x03[^\x01\x88\x1a\xa5N\x0f\x00\x82\x0c5\xb8\xec\xba\xe2+Ijc\xdf\xc3\x0c^\xdf\x9eqG}/\xf8\xa1hh\xe8:B\xbbn$!\xb04\x06\x7f#\x01\x10\xfc4Y\xac\xbc\xde\x1f\x8f\xd0f\x03F/\x98\x95\xc6\xb1C\xec,j\\\xe9g+\xbf{\xcd\x1cs\xaa\x911.\x98\x92A\x9f\xa5\xa6\xc9\xd9\x01\xc5\xa8\x06@\x18[\x8d\x02p\xea1\xca;\x96\xa5bP\x0b\xc6\x8b\xb5\xb7\x18Lk\xb9\x8c\x11)rc+>,{6\xffo\xbd+\xe2\xa6\xca\x1bw\xb6qq\xc1j\xa2\xf5'\xce\xb7\xdbm\xb1\xfd\xb7\xf0\xa1\xe2\xf3\x9b\x9b\x1b>i\xe0\x19\xc8z\xbe\x9c\xec\xba\x9e\xbfS4-\x10\\\x9b\xcd(\x10Q\x91\xf1.\x9br\xb65\x8b1\xca\n\xb3\x8d\x8dk\x08w4\xb3\xde\xf9\xb8\x96\nYBp\x9f6\x91\x00d\x07^\xc0\x90dbJ(n\xa5\xbb\xc3U[\x0d\xe2\xb8|\xd0[`\x1eJ\xd8\xfb\x16b\xed\xdbFC-7\x08R)\x8c\xf1\x1a\x94w$\x15\xa5|\x00\xa9\xadq&R\x90\xe4\x03H\xa7a\x1d\xfc\xc6h\xcc\xe6Q\x8d\x16\xb6\x86\xea\xa1-g.\xa1\x0eX\x8e4\xc0\x96\x01\x7f\xb6\x18 4\x924M\x14\\.\x8bK\xbc3\xe2\xe8t\xdf\x0f\x0b(\x9e\x90\xc2\xfe\xdb\xd3\xe7\xbf\xe7\xbf\xc2\xc6\xe0\x06\x13|\x18\xa4\x08&Br\xa0\x0f2\xec\xaf\x87\x1a%\xdd[|\xba\xee\x0cCbDa\x9fx\x0c\xf3\x8e\xfc.r:\x0f\x1d)\x8dDwAd\x13c\x96\xde\xd3\xd4\x98'h\xc93\xf1h\x9a\xd3\xd2\x0b\xe3\xa7\xaa\x9c:\xf9]\xc3\x9eb/\xca\x04\xd5\xe0\xcb\xc7\xff/\x19\xf7\x8f\xad\x0b\xef\xdf \xb9\xb3\xb9\xcct11W0UMp\xf0YK\xa8!Z\xd94\x13\xfa\xc3\x17\x1c\xbe\xd3\xc3]\xdf\x83X\x85\xfc,\x80\xc7#\xe5T\xfc\x1dC4\xde\xe5\x8a\x9b\xdc\x12\xe3\xc0\xe8\xf5\xec\xe5\xf5Q\xf0\xc3\x85.\xf8\xe1_\xe6\xa4\x89_\x01\x00\x00\xff\xffPK\x07\x08\xe4\x92\xc0\x7fU\x02\x00\x00\x96\x06\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00\x99\x06vP\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x13\x00 \x00html/header.go.htmlUT\x05\x00\x01\xe3\xb6v^D\x8dKn\xc30\x0cD\xf7:\x85\xa0ub\xa1\xfb(wa\xed DT\xa4\x0d\x91\xce\x07\x86\xef^T\x9b.\xdf\xc3\x0c\xdeq,x\xb0\"\xa6\nZ\xd0\xa7\xea\xd2\xd2y\x86\x9b\xc0)\xc4\xa8$(\xe9\xc9xmk\xf7\x14b\x9cWu\xa8\x97\xf4\xe2\xc5kY\xf0\xe4\x19\xd7\x01\x97\xc8\xca\xce\xd4\xae6SC\xf9\xbaD\xa17\xcb.\xffb7\xf4A\xf4\xddPtM!\xdf\xc3\xad\xb1\xfe\x84\x18;ZI\xe6\x9f\x06\xab\xc0\xc8\xf9gCI\x8e\xb7\xe7\xd9\xec\xcf\xd4\x8eGIy\xdaVA\xe7]2\x99\xc1-\x8f_\x16b\x9d\xc62\xdf\xc3q@\x97\xf3\x0c\xbf\x01\x00\x00\xff\xffPK\x07\x08\x9c\xd5a\xdc\xaa\x00\x00\x00\xe7\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00\x17\x029P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1b\x00 \x00img/account_circle-24px.svgUT\x05\x00\x01\xee\x88+^<\x90Ao\x830\x0c\x85\xff\xcaSv\x8eq\x9c\x90\xc2Tz\xd8.\xbb\xec\xb4_0\xb5\x8c \xb5\xa5\x1a\x88T\xfc\xfa\xc9\x80&\x81\xfc\xf9\xe5=K\xf6q\x9c;\xcd\xcb\xeb\xa5\xd1\xf5/\x00\x00\xff\xffPK\x07\x08\xfc\xc6x\x8f\xb7\x00\x00\x00\xf9\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00\x17\x029P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00 \x00img/pomerium.svgUT\x05\x00\x01\xee\x88+^\xc4UMo#7\x0c\xfd+\x84{i\x0fz\x16I}\x16\xeb\x1c\xfaO\x06\x89\xe3Y\xc0N\x16q:Y\xf8\xd7\x17\xa4\xc6N\x8a\xdd\xf4\xd4\xa2\x08\xc2\x91\x1f)\x0d\xf9\x9e\xc8\xf9r^\x0e\xb4|\xdd\xbf\xfd\xf1\xfc}\xb7\x89\x14\x89\xb5\x93\xc6\x0d}?\x1d\x9f\xce\xbb\xcd\xfc\xfa\xfa\xed\xf7\xed\xf6\xed\xed\x0do\x8a\xe7\x97\xc3Vb\x8c\xdb\xf3r\xd8\xdc}9\xd0\xeb\xcb\xf4t~|~9\xed6\xbe\xe9\xbe\x8d\x9f\xe1\xe5\xcf\xe3~\xb7\xd9/\xfb\xa7\xe7\x87\x87\xcd\xd6\xaaZ\xc3\x1e\x1f\x1f?\x96P3\x12IA\x9190R92X\x83\xa0\xf6 \x90l\x89\xf4\x99\x91\xd3\x91-\x01E+\xc4H\x1a\x14\xad\xba\xe3r\xb2 \xb6\x04\xd3\xb4f\x99\x84\x9cv\n\x8c\x1c\x90\xf3\x02\xad\xf6\x82\xccKhHi\xf6\xa5Z|#7#~\x0d\xbfgd\x83\x04\xc5^'m\xac\x8c\xa2s`\xc4f\xcb\x16\xae`\xbb\x9c\x02TBBk\x13\xa34r\x13\xfd/0\xb8!\xeb\"\xc8b\xdeLn\x86w8\xd3=Z\xf6_)\x05T\x1d\x0bF;\x07\xe4\x1e\xd8\xe8X\xa1\xda/\xa7\xd0\xd0L<\xcb_-7m\x1f\xea\xb5\xfc\xedY)z\x86\x81!u\xac\xbc\x00F\xecC\xed\x1bf<$rs\xe3\xc1X\x0bjD\xc9\xd2\x90\xb2\xd3'\x97S4\xea\xad\x0e\xcb\xb2\xe8\x87*k@\xd6\xfb`\x12EK7\xa3\xaa?\xc9\xd2>#wb\x187\x8e\xf8\x19v\xc4;\x13v\x80\xe9\xd9\x9d?\xf3\xfb\xaa\xdf\"|s'E\xce\xc3\\\x1d\xb5\x04\xe4\xb20\xa4L\xc9\x08t\xb3r\"\x84TH\xe9\x06\xd8\xeda%A\xab\xc3\x0c\\\xd0\x9a\xd1\xa1Cm7WO\xee\xa4\xd6j\xab\x19p$\xe4r91\xd4\xf8\xd54\xb1)\xc1W9\x9c\x15\xad\x06\x94Y\x90\xeb\xe4W\xc9\xcd;k\xa6\xac\x16\xbf\xc5\x9a\xd0uQt\xe7z \x19}\\T\xe4> \xc9M\x9c\x94\xc3\xca\xcd\x87\xcb\xc4\x84\xd4\x11\x8d\x05\xcd\x13\xa3\x99@\xed&\x10\xb2\x06D\xcb\xa6\xad\xe6\x96C2\x02/\xa7P`\x9e\xc4?a\xbe\x96\xff\x8d\xfa\xf2\x1f1/ze\xbe\xa0TJh\x06H\xb0.\x13J\xa6\x81\x1e}\x16\x0e\x15l4uJ\xe4\x9a\xd9\x04\xd1\xfa\x03\x94\xf9\x18l\xf6Z\x93v\x88\x82m&u\xeb\xad\x9e>r\x10\xfb\xca\n\x13S1]\"\xfd\x18h\xb8\xf1e]\x97\xd0\xc5\x9a\xab\x8d\xe6\xea\xa8\xd9\x9f.\xf2\x19\x85i\x95|\x85\xac\x875\xb8\xa2Y\xc7\x08A\xb1-^\xa6CMG\xf1\xe2\xf7\xc1\x0b.\x8b\xa0\xb3\xcf\xe2n\xa32\xd6\xd9d\x9a\xc4^\xe0f\xa4l\x9f\x19\xb1\xb1'\x12\x9c*N\x8b\xa0\xf1l\xab\xbf\xa9a\xb16\x88j\xbe|\xf6qx\xff\xa0\xa9\xa02qB\x9f\x14]\xc9\xcd\xaa\x98\xad\x837\x87&\xf0'\x01\x06\xc8\xc2\x05\x9cf;k \x99\xa7l\xac\xb8\x19\xe93S\\\xcc3\xcb\xbf\x11\x81T/\xa7\xa0\x1d\x9a(\xe1\xe7\xa1\x97\x13\x0bz\xa6\xf8\x0f\xde\xf4\x99\xd7X\xda\x1e\xd6\xff\xf3r\xb8\xfb+\x00\x00\xff\xffPK\x07\x08K\xfe\x8b#M\x03\x00\x00d\x08\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00\x99\x06vP\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1a\x00 \x00img/pomerium_circle_96.svgUT\x05\x00\x01\xe3\xb6v^l\x95\xc7\x0e\xe4HvE\xf7\xf3\x15\x85\xdaR\x9a\xa47\xad\xa9\x06\x82Lz\x9f\xf4\xb9\x19\xd0{\xef\xf9\xf5B\xb5\xd0\x82\x16\n \x16\x07\xe7\xe2\xdd\xb7{\xffZ\x8f\xf2\xc7\xd5w\xc3\xfa\xebg\xb5m\xd3\x1f\xaf\xd7y\x9e\xff<\xb1\x7f\x8eK\xf9Ba\x18~\xadG\xf9\xf3\x7f\"\x7f\\]=\xb4\xff_\x10a\x18\xe6\xf5\x97\xfd\xf9\xe3\xac\xb3\xad\xfa\xf5\x93!\xff c?\x7fTy]V\xdb\xff\xe2Q\xe7';^\xbf~\xc2?\xe0\x1f\x0c\xfc\xd7\xff\xf9\xe7?~\xfc\xf8W\xdd\xc7e\xfe\xa3\xce~\xfd\xf4\xf3t\x1b\x97\x7f;}\xbcl\xff6\x93&O\xb7\x9f?\xb2x\x8b\xffs\x88\xfb\xfco\xff\xe3/\xff\xe3o\xffw+\x0c\xff\x9f\xce\xdf\xf0\xd7V\x7fTK^\xfc\xfa\xf9{\xc8\x1fu_\xbe\xa6\xa1\xfc\xaf$^s\x12\xff\x8f\xdag\xcd\xcf \xabb9\x02\x00\x80\xe1x\x15\xef\x95\x00\x00\xf17\xb2%\x07\"\x00\xc0\xbb\xee\xa5\x14\xff\xedI\xbe\xe3m\xff\x83\x0f&\x9aq!\xeb\xf9\x9fX<0\x9bs8n\xb5GPQ\xb2\x8d\x8f\\S\x86#I5{\xa9\x93K\x95\xd8j\xdb\xe8\x8f/\xec\xd1\x93\x98\x1c\xd9!{]v\xf2x\xd6S\xa2\xee:?\xce$\x18\xe5\xc6\xe4\xd6\xb1/9 \n\xab\xde\xf6v+s\x9c-\xdeL\xe6N\xf9\xb3\x11d\xe8b\x01u\xb8N1\x1fd;\xf9\x13\x031\x04A=\xd4\x82\x0d/3\xa7!D\x13\xdfV\xe0\x8eb)\x0d@\x83\xd9dd\x8b\x08\xac2k\x8c\x00\xb5\xc1\x93\x84\xa3\xe0\x81+eK\x1c\x98\xa7\xb8\x95\xc0\xe9\xc1a5X\xd2\x1e\xcf\x81*p\x95\xbe_g\xb8;\xe0\xddq\xc4\xc8\x1a\xa7^\x00\x86\x06\x11\xa4}\xf3\xe4\xd4S\x16;\x87\x84\xd2\x9e\x91q\x07\x18?\xdfRi\xa0\xc0M\x85\x916#\x14\x05:\xce\x1f%;\x80\xd9\x95\xf6\x1b\xdeZ\x80Xp\xa3\x95xWa\xc2]\xbe\xa7S>\x80\xb3c\xbe!\xb8r#\xd4m\xf3\xe5\xbf\xf5#\xf1g\xc9\xbd\x10\xdf\xc2\x87\xaa\x86C\x1f\xc2\xdd\xe85}\xe7\x17\xc50#xN\xb1\x00\x1b\xcc$M\xfa\x81\xd98b\x97g\x9a\x9c+\x17c.\x0b\xf7\xb5\xa0\xd5\x9a\xf9\xe8\xc9v\ns\xd9]\xe9\x9d\xf7<\xa8\xfd\xe9\x11_U\x9c\\\xf9\xbb\xb4\x91\xbc1\xfa\x0d8\x94M\xed\xc1\x81\x81\xc5\x01\x8a\xb8\x7f_(\x92\xb4\x1f,\xc2\xc1\xb2\xd9\xef\xeb\xbdG\xce;\xb1\xd2\x0b\\f\x17\xe6DS\xdd\xae\xc7\x02,\x0b\x1d\xfam\x81$\xe2\x05\x87\x0e\xc3S\x188\x06\x8b\x8eU\xac\x81\x14\x98\x8a~\xd41K\x86\xf1$4\xab\x18\xab(\xc1pN\x8dc\x0ch\x902\x8a8\xc9\xd6\xe3\x8e\xf6^5\x9c\xa2\x1aq!\xd1\x19\xd6\x99\xde.@t\xbe\x10D\x11\x1e\xfb\xe5Q\xca\";\x1c\xa5\x9a\x0f7d\xed\xb7|+b\nm\xd2\xabM\xd8\x8c\nr4\xe3{v(?\x1e\xc2)9\xaf\xb7=B=d\xcb^\xa6\x97\xf6\x88C\xc7\xf8c\x9b\x05w\xd6b\x0c^\xb7\x0b\xa1\xec\xa8Om\xe4\x18\x8a\xcc.\xfdU\xaa\xe9\x87\xff\\E\xa9AB\xdb\x88\x0ep<$\xf0\x13\xe9\xbdK`\xab\x1e\x0bP\xb8\x94\xbfC\xc7\xae\xa3[O\xa5m\xdeuu\xdd\xdc\xa5\xc2A\x03\xcdk\xbc]\x9f\x0fFX+4\xaa\xc7U3\xef\x18\x16\x05\xb4\xfbB\xcex\xe09\x06\xf2\xae\xbaE\xa4\x0f\xd6\x8a\xbf2h\xe6jI\xd8\xbb3jR\xc4\x9dV,ds\xe6\\O\xad\xa8\x84\xb9\xd4\x8emS\xe2\x98\xad}\xa1\xd5\xe8\xb0\xeaN\xb9\xa4!\xe9\x11H\xbbk\xcb\xee\xb8\xc8z.\x17\x94\xc2\xb5\xf4\x9b\xb03Hl\x86\xcf&\xc4k\xe3\xab,v\x83m\xc0n\xc7\x08\\\x87\xb9\xda%_z\x0e\xc1(\xe1xVf\xa2u\xe2\xf5\x99cOs\xe5\x99\x1b\xa5s\x83[\xd9\xbb\xba ;\x08\x84e\x16z\xb2\xd2\xa8@\x19\xee\xcd\xcf=\xed\x1e\x19\xd6qx\xdaT\xc3I\xf5\x99\x93\x8c\xb1\x93|\xdbx0\xcb\x10\xd2x\xa1-y*\x8c\x96\xe0\xbe[$(\x0e4\x19\xcd\x85\x1a\x9d\xa7\xda\x8ey\x17\x84\n7\xba\x13\xc6\xad\x93\xc6\x92\x13u\x0d`xm\x18\xcajL\x92\x99\x16t5\xb1/r\xfa\xaeB\xe0\xc0{\x19 \xaf\x07\xbb\n\x084\xaf\xa3T\xd47\xac\x9d]f\x9ej`[i\x9f\xab\x83\xc4\xb8b\x88\xe1):8 90/$s\xfb\x08.k\xa8\xa60\x18\x8cq8{\x86%F,\xdb\xed\xb3y\xc3\xd2\\/\x15+\xed\x086\n\x9d\xaaUY\xd8\x0b>\x14\xa9u\xc1V\xf4\xb0\x0c7\x03\xa1@t^ C\x18\x90:\xa2\xd7\xc1\xc2\xac.\x9f\x91K\x10\xab\xdaE\xec\xa4\xda&\x01\x83\x81\xef\xfdI\x0bXt\x94\xa2\xd9\x91\xb3\xb1\xfa\xdd\xb39\xbc\xb9\x9f\xf7\xbe\xc3\x19\xa1\n\xdf\xcf\xce2\xfdQ\xd6\x8b5\x9a5@\xd8+\xd4\xb1\x93\xa83\xf0\x10V\xb9v\xc2\x99~>\xce\xee{\x89(\xc9\x1fM\x19\xc50?\xbf6\xb7\x88\xcai\x1b\xa9\xdcg*\x0e\xf9\xf6\x96Bp\x08=\x0coF9\x9bz\xd4\xa8\xef\xc77\xc0d\x8e\\B\x1a*158\xa1\xc6\xc4\xd3\xf2I\x8dL\xa4]fh\xaf\x8dVj\xf2\xcb\x91\xd8w\xc6 \xf3\xa5\x1f\xe6\xf2\x92\xb8\x95\x15\xbff\xc9y\x0f\xfe%C<=\x97\xc78\xe8\xa2l\xe9\xec,h\xba2Z\xa6\xfb\x88\x8c\xd8~\xe7{,)d\x8b\xd6\xb3\n\npZn$\xf4|@}x\xeaH\x98\xd1\xb5\xbb\xdc\xf2s\x1aL\xd9\x8c{rl\x11\xd0\xac\xd1\nI\x84\xf7\xde\xbeXC~:#m\x91WE\xc75\xb8sJ\xb9##j!3\x1d\xdb\x93bh\xab,\"SJ`1\xb53\xb3\xef\xa8a\x9a\x1cf\xf6\x82,\x98\xb5\xc0\xb6\xcah\xbcs\xf3\x8c \xe7+\x9e\xaf\xb9\xa3\xad\xa2P\x1dz\xcf\xc9\xf2\xfe\xce\x13\xfb\xe9z\xd1\xad\xa4!\xfbb6\xb7\xd30\x9c.\xf2\xb6\xc1\xd0WOC3`\xc2o\x05\x90\x89]\x07\x9c\x80\x82g\x9a\xe5\xf2\xc9s\xde\xde\xf4DU|\xff+\xbeH\x0c\x87%m`\xf4\x99\x8ds\xd3\xa3=\xf5\xcamL\x1f\xca\x8aq\x91\xf8uA\xce\xc7\xe8\x84\xfeB ;-\x9b<\x7f\x16:a\x08F<4\xcc6\xd3\\8.\xec@\x94\x13\xa3\xf93y\x945\x10 \x9f'\xbeg_\xd5\xe5\x80\xe4[\xe2\xd7\x155\xb9;\x9aEo\x1e\x11\xe7Dtk7|\xe8|\xc3^\x0f\x86\x08\xa2\xd7\xd3\n\xf5\x12\x88\xb3\xef(\xf2Z\x837_\x14\xc7|1\xc7\x8b\xff\x16\xd7-\x0c\xee\x91\xdc\x06N\xcfk\xe7\x12\x89i\xc6Lt\xe8Q\x9c\x1f\x0dl3\\\x1eY\xe1\x10w\xdc\x8dm;\xfb(\xae\x19\x04Sf\x0dsK\xb4\xda\xe5\x0cJh\xab\xe9J\xe4\xc9\xb7\x0c\xcb\xeb\xb2\xb3*\xf7*1g\xba\x14\xc9q\x85,\x92\xb8E\xda\xf2fvk\xbe\\\x04\xaf$\x12\xa2\xeevh\xf2\xa3\xda\x92\xadK\xb46Y\xc2\xa7\x8d\x90\xb8\\\xf6-F\x8f\x18\xa5x\xf6]e\x80\xbe\x12\x14%\x0f\xaf8\xfa|I\xfc\x0eS\xd0\x01\x13\xc1\xa0\xaf\x8b\xa8\x99\x14\xcc\x1c\xc1\xef;v\xec*\xcf\x1cA\xa5\x80\xa97Br5\x96\xfd\x1e_\xf9\xd6\x95\xda\xaeu\xba,\x0e\xf1\x0e\xa1Tdn;\x11j\x13\xc1(\xa7\xaer\x9b\x97\xc5\xb0\x9b\x8bMe\xb3\xdc\x90H\xae=\xea\x95\xcfaCvB{h.%\xc3^\x8f\xcbl\xda\xb4\xdc\x06|e\xa3\xe2tC\x89m\xbb\x10B\xf4\x1b\xac\xe8\xb8[\x96\xc7\xe5\xe6\xd4\xdd\xd2\x1b\x7fI\xcfW\x0f\x9al\xeb\xe3G`\x9e[\x17.\x7f\xb4\"O!\xd8.\xc9\x01\x11\xf6\xe5Z\xca'\xbc\xd0\x93\xf9\xe5QChx\xb2,\x9a\xb2\x08.A5\xe3S\xc4\x06I4[h\xa5\xd5J\x11UUh\xa8w\xeb\xd0W\x15VR\xdb\xd0\xa3V\xc4\xbb\x80\xf4\xf7\xc4c\x16<\xb3\x10\xf2\xa5x^\x128J2B\x15*\xde\xe4\x18\x9cI\x828\xbd\xd5\xb8g\xad\x1bf\x99\xeeR\xdc\xd3\xd2\xb3\xdb\x86_\xcb\xda\xd9\xcb\x97\x9a\x87\x96\x97\xd2\x0bF\xbe\x85\x82~.]x\xa8`\x0dO&/l\x83\x88\xbb\\\xb7\x0dd\xe6\x0cUL\xaa\";\x10\xa6[\x96\xec\x03\xde\x02\x08j<\x11\x8a\xf0\x9a\x16U\xb6+n\x9b`D\"\x886\x9d;g\x1d_\xfaL#\xe8[7\xc5\x83\x17\xbf\xa3R\xe4\xab\x82j\x88\x90\xba\xf9}F\xf3}\xa9\xafb\xe7e!\xdd\xb3\x0c`\x99\xb1\xf6\xdfhL\xe4\xa1\x91\xbdN'\xec\xaa\x8aNmW+\xff^\x1a.\x0e1X\x1c/\xf2\xcd\x91\x04\x92\xf8\xea9\xf3\xb5\xfb\x852\x1cW\x94\x05_\xdf9\xc4\x1e>\xfd6\xdd\x8c\xa5ZAu\xd2o+\xafJ \xa1\x1a\xedJ\x0b,JC\x99i\xd5\xfa=R\x98h!\xb6`\xee R\x8b\x15\x9e\xd5{\x1e\x9d\x80\x87\xa1\xde\xe6\xf2\xb0\xabk\xd4\x87L\xe9\xeb\x12>'\xc3\xd5\xe4\xe9\x18\xee#\x98\x8c.p\xc5FQ?\xd6\x1e\x9b\xc2E\xd0\xd9\x98PG\xfb\x14\xd5\xcdt3\xf6p\xae\xfem\xa6vkO\xc8%\xfd\xaa\xd0\xed\xcb%\x98U\x81\x87V\x91\x0d\xd1\xbf-\x90\x7f\xf5Y\x12\xc1\xd8\xddJ^\xe5\x90\xf7*^4\xc3\xd8\x9b&\xaf\x96\x9f\x10\xb2\xd2]X\xb1\x80r\xc9\xe0s\xbb\xc1\xba['\xeel3\xd3\xf5\xc2\x03\x88*C\xbdo\x7f\xec\xf3tF\xa3\x00t\xfc\xad/\x8b\xdd\x9c\xca\x18\x0e.J,\xee\xd5\xc6\xf2Y\x7f\x9a\xad\xd1\xda;0\x95\xafRX\x1e\xecdC\"[A\xae\xb40Q\x18\xafy%\xb6\x82\x1e\xa8\x1c\x8b\xa7\xf7\x99\xcc\xd09\xe7\xfdW\xa1Z\xb6\xb5\xa4\x84?\xcb0X\x0d8\\\xf8\xd7\x05ZE\xba\xa1o\xd7Q\xad\xf7\x89*n\x9bIi\xb0\x1327S+\xeeb$\xab\xd4\x05\"\x02\xf7(\x86\x04\xed^/HF\x1bJ\xf0\x00\x19\xa0e\xa42\xf7\xecb\x9c2\xd1P\xc8\xe8\xc8\xc4\x13#u};\xc0\xe5v\xe3s\x01Gv:\xc7k\xa9[\x08\x81\xd7?\x0e\x8a\xd6\xcd\xb6\xde\xe0\xa5\xbc\x13\xe7\xb2\xc7\x98\x9a\xb3\xfd\xa1\xe54\xce\x84aC\x08d \x0e\xe0n\x0c\xd4\\\x06t\x0f\x1d\xaf8\x81\xad\xf4{>\x00~\xc8Q\xc7pG\x99\x8b\xa6\x96lv&\xc2\xbd\xb2\x93&\x7f\xbc\xd2\xbe\x0d\xeb\xb8f\xaa\x02\xad\xb4\x1e\xb9\xcd\x95?u/vL\xbc\x9c\xa0\xb8.5\x89\x13\x02X\xb1\x98l\x98\x97\xe9b\x86L*\xdb\\\xa8(-\x88\x03\xe2\xccc(\xbb\x91\xa8\xde\xb1\xfb\xd0\x9b-7\xaa\x9e4\xaf\xcb\x9cv:i\xcdbB\xb2\xac\x9bd\xfauKP\xcb\xea\xdf\xf9\x84O\xf7\x82\xc0\xe0\xb9\xb4\xaeo#\xb0\x84\xd5\xbf\x03_z2\x99\xd2\x02\xa15\x97q\xbb\x0e\xacgu\x15\xc93Y\x04\xd8Y\xfa\x9eR%\xa3\xb1\xd8\x9f\x1dE\x84\xad\x07\xe4r\xa9K\x92\x04:\xde\xc0~\x04\x9fc\xf8\xd5Z\xbe\xd25^\\\xb7V1\xc8\xd3\x99L\xbeK\xc45\x99\xcfFR|\xc3\x8cw\xebh\xec\xfe+\xdb\xfe\xeb\xc3\xe1\xe8~\xcdj\xec8\x9c\xe1Gi\xc8\xf2\x10s\x18\x03\xe7\xb6^\x1a\xbb\xa5Wv\xa1\xc6\xf6\x9f{.\x03\xca\x7fP\x85\xdc\xdf\x1f\x99\x8c C\x15<\xa7\x8a\xf32\xd0\xf6\x11|>\xa4\x82\x8cH\x83Q\x96\xd0M\xa7\xd3#ix\xe36\xa4\x84=dm0\xad\x8e\xcag\xbeK\x86c\xfb\x926i\x16\xda\xfa\n\xbb\x116\x94\x84\xcf\xdbY\xe7\x15\xbeFN\xd9\xad\x17\xe38s,s\xef\xbd\x08/.\xbd=\xd2\x9fO\xa8\xd3\xbbD\xd2v\x19n\x07\xe6\xc4\xe9\x85\x1f \x12#\x81Q\x82\xb3A\xfb\xbe\xcf\xe2e.\x89W\xf76\x15\xc6\xdf\xca\xc6\x10\x04\xf8\x9a\x88\xa8\xe4\x00\xdd\xa6\x1d.z\x9e+\xb2\x8e\x81\n\x12\x01\xd5_\xd7\x8ea9N?\xc9\xa8\xcf\xc2\\\xa7\x92\x94\xb5db\x90\xef1\xf1\x92\xe6\xc2\xaf 2\xbeU\xd9\xe3\x92d\xbfQ9\xea\x0f\x85|\xab2\xb7\xd9\x11\x14\xccX\xecY&\x01[\xd9\xcb\xab\x0e\xd8*\x85Z\xe4\xe7\xbd\xedk{J\x81\xe4\xa6\xa6U\xa9\x0f\x9d\xe5\xfd2\xc3\x11\xaew6\x11X\xf7\xbb\xea'd\x7f\x15'\x88\xbf3W\xeb\xbeU\xdaM\xb6S\x93\xd7O\xaa\x16\xe32\xa2-\xb0}\x9f\xe6\xd0\x8d\xed\x9d\xdb+\x92\x0c\x1fq\x91\x1b\x03\x83\x1b\xd0\xd9U\xf6\x1e\xce\xa9\xc0\xd0r\xa2\xe5*$\xb7)[%\xc6$=r\xc1:\"*3\x1ar\x90\xbd\xcd\\\x08k\xf7\xe8\xcfr\x7f\x9b\xc5S\xe1z\x06\xa0\xdb\xc8\x0e\xa2\xe3\xb7\x00?=\xd2\xc4\x02\xd2,\xc8\xd6\xdf\x97Q\xc1\xe9\x17\xb9Db\x80\xc7\xae\x1c\xcd\x94\x8b`\x1e\x9c6w\x8b\xc1\xc6\xc4\xc1\xe4\x12\x1a\xd4\x87b\x0c\xd5\xde\x85\xea\xf5\x91)\x14c\xdf\xf2\xc1$\x16sU\xe4\xf7\xa1?\xe5I\xac\"z\xc3\x90H,\x85\xe9X\x96\xda\xa5\xe3\xc7\xf6\x18\x01\xe5D\xe2\x1e\xb1\x93\xce\xcb<:D\x0c\xfe\x8a\\\xc3\xa20\"\x98\x8f\xcf\xde\xc4W\xeb\x1d\x99U\xe9YJ\xb2Q\x86mF\xa5\xa0\x0f\xceZ%\x82P\xbe\x17v\xc45#\xad\xe3\x15\xb3|\xf5\xcc|1\xa6\x04\xca)\xff\xd2P\x90\x91\x0f\x01Mu\x9a\xd6\xe7a\xb0k\xc9\x16\xae\x8e\xf3\xe5\x02\x95\x0c\x176lL\x08u\x96.O\xb1 l\x18#Ee\\\xf6\xea\xedak\x9fVW)d\xe3\x97\xc5\xfa \xe1q.]\xd2\xc1\x16\x86\xb0'/U\xfa\x81lPP_]\xb3\xa5\x04qY\x88\x1c\x86\x98\xf2\xaauT\x0d\\\xe2\xd6\x16\x0b\xd2\xaap-\x9e\xd1j\xef\xf0\x93\xbb#\xfe\xda\xdd\xd8\x0e\xcb\x81\xech\xea\xeb\xb4V\xcc#'t|`\x13\x9e\xc7\n\x97\xf9|\xa0D\xe7\x10\xd9e8\x06\x0bI/\xa9\xd6\xd4{\x19 )D\xc5N\x87\x91\xe6gU\x10\xacx\xfa* 3+\xa1\xe4\xe3\x9c\xc4\x18\xdbo\xf3z\xa6\xaf\xdb\xf5>\x0ffZ\x96\xcf\x1f]\xb7m\x1b6\x87\xc7\xbfs\xa7\"\xd2\xcd\xeb\xd9\xd0v\xf9X\xa6\xc1\xa874\x9d.\xe7ii\xbc^N\xdb\xcf\xc7\xd7`\x84\x84\xd4\x93z\xf3\xfe\xf6\xf9g\x99\xe8c0\xbf\xadE\xce\xa4#\x07\x04%a+\xe4\xe1\xfb\xb2[\x99\x0bR\xc5\xf6\xf0!\xf0\x81\xdc\xd0\xca~s\x88\x96\"\x9c\x1f-$\x91\x90Ev\xe8c\xdd\xdbR4I\xfc\x14\xf9\xe5p\xbbR\x85b\xf3\xcbf\x88\xad\xeeS\xfd\xbe\xb9\xdf\xb8H\xa1\x1f-Jl\x85+q%6\xaaK\xf9b\x8d\xde\xde\xf9\xe5\xcc\x15\xab\xc3/g,\x198[\x82\x84\xef\xa3\x95\xf6\x9bP\x86u\xabC\n#+<#\x05\xf6p\xac\x88\x1c`={\xe4\xd8\xeeZX%\x87\x98\xd8\"f\xaan!\x84V\x04\x85\xf4\xe5G\xa0Zt\x8f\xbe\x9c\xcc\xa4\x10= \xf6\xfb\xb3722\xb4\x94\x07\xa1\xd5\x03)3\xc4\xaf\xec!\xa9&\xed\x95\x14\xd9\xb3\xc2\xba\xd2\xbf\x06G#\x14Y\xe129\xf4\x9e,l\xcd/\x159\x95\x08\x91\x02$s\x82\x0f\x95v\xd3\x1d\xf3\xf1\xf7r\xbd\x0e\xe6\xfe\xb8\x9fL\x9d\x15!\x99\xd4\xaf\xea\x7fI=T\xc6\xee\xfd\x7f\x00\x00\x00\xff\xffPK\x07\x08uq\x02\xd2d\x01\x00\x00\x9e\x02\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00\x99\x06vP\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00 \x00style/main.cssUT\x05\x00\x01\xe3\xb6v^\xbcX[o\xdc\xbc\x11}\xf7\xaf`\xd7\xf8\x00;\x10\x15]V\xbb\x8e\xf2\xd6\x14A\x0b\xd4yH\xd0\x87>R\xd2h\xc5\x9a\"\x05\x92\xf2\xaeS\xe4\xbf\x17\xbc\xe8\xba\xbb\xb6\x9b\x87/\x88\x01\x8b\xd7\x993g\xce\x0c\xfd\x01\xfd\xf7\x06\xa1\x96\xc8\x03\xe59\x8a>\xdf \xd4\x91\xaa\xa2\xfc\xe0\xbf\xf0\x11\x8a'\xaaq-\xb8\xc6\xaa\x15B7v\x92pM \xa3DAe\x97\xb5\xe2'\x16\xeat\xb6\xee \xc9\x8b* \x83\xf9a\x1aN\x1a+\xfa\x130\xa9\xfe\xd3+\x9d#.\xb8]Q\x88\x93\x99\xb0[\x0b!+\x90\xb8\x10'3c\x0f\xaeIK\xd9K\x8e0\xe9:\x06X\xbd(\x0dm\x80\xfe\xca(\x7fz$\xe5\x0f\xfb\xfdUp\x1d\xa0\xcd\x0f8\x08@\xff\xfa\xc7&@\xdfE!\xb4\x08n\x10Bh\xf3w`\xcf\xa0iI\xd07\xe8a\x13 E\xb8\xc2\n$\xad\xc7{\x8cm9\x8a%\xb4f\x88Q\x0e\xb8\x01zht\x8e\xe2pkF\x7f\xdd\xdc\x84\x9d\xa4-\x91/\x16\xc2R0!st\xbb\x83m\n\x0ff>df\x83\x9d\xfc\xf8\x01\x91l\x1f\xd75\xfa\xf0qZ+\x0f\xc5]\xbc\xcb\x02\x14\xc7i\x80\x92,\xbb\xb7\xdb*\"\x9f\x86]\xb7\xdb$\xf9\xdbnw\xb6m\xb7\x0b\xd0\xd6\xec\x8c\x92{g\x8b\x85\xb4\x15\\\xa8\x8e\x94`\xf7\xcf<\x89\xc2\x87\xcc;\xb3\xc0\xf1\xc7\xd7G\xc1\x05\xfe\x0e\x87\x9e\x11\x19\xa0G\xe0L\x04\xe8QpR\x8a\x00}\x11\\ FT\x806\xff\xa4\x05H\xa2\xa9\xe0fVl<\x98_D/)H\xf4\x0d\x8e\x9b\x00M\xf7\xff\x85\xb6\x9d\x90\x9apm\xcd+D\xe5`\xaa\xa8\xea\x18y\xc9Q\xcd\xc0\x85\x95\xc1 WTBi\xce\xce\x91\x14G3L\x18=pL5\xb4*G%p\x0d\xd2\xd2\x83\x94O\x07)z^Y\x1c\xc8\x12\xbf\x00EadP\\`\x95\xa6\x01J\xf7\x01\xda\xc6v\xc6\xf0\x8d\xd6/\xb8\x14\\\x03\xd79\xb2\x06\xe3\x02\xf4\x11\x80[ko[B\xf9\xfb\xcc-\x05\xeb[~\xf1\xdc\xc9\xea#\xadt\x93\xa38\x8a\xfe0\x9f-\xe5\x13\x9b\xa2\xe8\xb9q\x97R^\x0bCv{\xf1+\xa7]\x81\xe6\xccT\x9f\xc7\xb8\x10Z\x8b6GI\x98H\xcf\\\xe5\xcc\xff\x7f]\xec\x84\xa2>J\xc0\x88\xa6\xcf6e-\xf3\xacQ9bP\xeb3\x17\x13\x7fk\x13\xafi\x99\x84\x0bV\x1e\xfd\x8em\x14\xad\x0f\x9e\xfcd\xa05Hl\xa2\xe6T*L\xbb\xd3\xb8\\K\xc2U-d\x9b\xa3\xbe\xeb@\x96D\xc1\x8a\x0fq\x1c\x05h\xb7\x0fP\x92\xfa\xe4i\xe2PS\xcd\\\xd6\\\xbeu\x12\xc5polFq\x98d\x03\x9cMr\xb6s\x00b\x90\x85\xd4\xfe{\xd3\xcc\xab\xce\xcdUi\xb8y\x85\xda.\x8b\x9c\x14\x94DV\xd6 /\xa0\x92T\xb4W\xe6\xb0a\xdfjb\xeb\xeep\xa39\x8a\xbb\x13R\x82\xd1\xca\xa5X\x14 \xff?\x8c\x13\x97]\x96\"\x07)\x8e9\x8a\xc7o\xd5H\xca\x9f\xfc\xc8XQ\x10N#w|KN\xd8g\xc2vJ\x84a\xe4\xc1\xaf\x1a\x81\xf6n\xce\xdd\xb5\xc5\xa1!\x95\xb97r\xe6\x98\x15\x91_\xb9\xb66\xda;E\xbd\xa9)\xb0J\x81\x9e\xd5\xba)+\xfc\xbdse\xb9\xad\xcb\xba\xac\xeb\xac\xfa\xedK\xdf\x82x(ws\x89^\x9a\xcaH\x01\xcc\x1a|9\xeb\xdeL[/\xa4C\x16n\x935\xbeQwr%\xfe\x8a\xa0\xbc\"A\xe7\xd9zfz\xce\x85\xbe\xcb\x19Q\x1a\x97\x0de\xd5\xfd\x9c\x91\x03\xf8\xaf0\xed\xfe\x12\x1c\xaa#N\xb4f\xcc\x89\x93l\xe5Y\x84\xe2l& >#\xa5\xb1\xd6\x1eJ\xdbCHK/\x7f\xfe\x14\xd2k1\x87+\xdd\xcd\x036\x8613\xc459\xd6\x82R\xe4\xe0\x14c\xbc8 3\xcf\x8d\xfd\xa8\x0d\xa1u\xc1 \x1f\x83\x93O\x8f\x0b\xb6^R\xc0\xab\x15\xff\x15\xbe\x8a^\x9b~e\xe2X\xd9KeN1`\x98\xefcC5X\x89\xb1\x8b\x8e\x92tv\xdf3\xc8\x9a\x19\x967\xb4\xaa\x80\x8f\xf8M\x13\xc0\x18\xed\x14U\xcb\xd0\x84\n\x18\x94:\xcfI\xadA\xfaf\xc8Sf\xb3\xf9\xbc`0)\x94`\xbd\xb6vy\xe4?9\xf7\x07\xe0=\x1a\xd2}\x0d\xd9\xa9E\xe7\xc1\x1f\x13\xd8\x0e\xe1\x81\xd5\x82\x1anbx\x06\xae\xd5\xe0\xbb\x895\xefz=\xa7\x9e\xd2/l\x06\xce\n-[\x1b\x8d7\xeb\x80\xbdk\xf7\xd4\xe0\x92\xae\x03\" /\xe7s\xa6E\xbe4qi\xec<\x8aC!\x89\xd3\"\xad\xe7q\xf5\xbe\xafIaKLG$\xf8\x16,4\xde\xbc\xaf\xe2{\xe9\xb0\xc3\x86\x1e9\x1aH\xf2\x8e\xe6),z\xad}r\x0dF;~\x9e\xd1v\xe8\x95\xd7\"\xbb\xedNh\xd7\x9d\x9c(dQ\x80\xcc\xcf\xa7\xd4\xeaB|\x1f\x98\xa4\xe9N(\x1dV\xcc\xd5\xf7\xe1\x1d\xe2\x1b\x9d\xa5\xdb\xde\xa5\xdb\xa8\x95\x9euWSi\x06\xf9BO\xc7\x9c\xa9\xa0\x14\xae[\x9e1\xcb#\x136\x84\xd5#\xbf|\x19\x8d>\xaf\xcb\xe84R\x10E\x8d2\x13V\xdee\xd1\x1f\x08\xdb\xbb\xee\x17g\xd6=c\xeb3\xe3\xf9\x8a\xbc1y\xec\xfa\x94\xa9\xf9\xb0\xbf2\xa2\xe1\xdfw8\xb6g\xaec\xb1\xefN(6\x01\x89.\x86\xe3\xde\xbd\x03\"\x1b\x8d\xdd\xb4l\x1d\x93_7\xa1\xa8kl \xe1\xd2qN\x84l\x9ffE\xe6i*\x84\xc9\xe4Q\xa0G\xb6Rn\xdfa\x9a\x14\xee1\xb9\xd0\x818Y\xcaH\xe2c\xbe\x92\xf7g\x90\xe6\xe5\xc7\x86\xa2\xa0E7\x7f>\xf5\x1a\xaa\xe5k\xae\xdcg\xfbj\xf1\x9aY\xbd\xabfJo\xfa.\xec\xcc\xbf\x9cgoe\xcf\xaa\x14\x8f\xcd\xf4\xcc\xd1\xb1\x97\xb2C^%\xf1\xacGZ\xb57\x17\xa6\xcc\xb1\xcb\x89\xeb\xcd\xed\xd5\xaf2\x86\x9f-\x1a\x81o\x80T\xd7\x80\x7fo\x87s\x16\x8f\xdf\x81\xde\x13\xf2O\xc6\xfd\x1d-\xd5\x12z-\xba\xb7p7K\xae\x82n3F\x8e\x7f\xe2\xb8\xd8j\xfd/\x00\x00\xff\xffPK\x07\x08L\xbb\xd3^\xd3\x05\x00\x00^\x12\x00\x00PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00v {P\x06\xa8c9W\x04\x00\x00\x11\x1f\x00\x00\x16\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x00\x00\x00\x00html/dashboard.go.htmlUT\x05\x00\x01!{}^PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\x99\x06vP\xe4\x92\xc0\x7fU\x02\x00\x00\x96\x06\x00\x00\x12\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xa4\x04\x00\x00html/error.go.htmlUT\x05\x00\x01\xe3\xb6v^PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\x99\x06vP\x9c\xd5a\xdc\xaa\x00\x00\x00\xe7\x00\x00\x00\x13\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81B\x07\x00\x00html/header.go.htmlUT\x05\x00\x01\xe3\xb6v^PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\x17\x029P\x83\xba\x83\xe4\xf5\x00\x00\x00|\x01\x00\x00\x1b\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x816\x08\x00\x00img/account_circle-24px.svgUT\x05\x00\x01\xee\x88+^PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\x99\x06vP\xfc\xc6x\x8f\xb7\x00\x00\x00\xf9\x00\x00\x00\x12\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81} \x00\x00img/error-24px.svgUT\x05\x00\x01\xe3\xb6v^PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\x17\x029PK\xfe\x8b#M\x03\x00\x00d\x08\x00\x00\x10\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81}\n\x00\x00img/pomerium.svgUT\x05\x00\x01\xee\x88+^PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\x99\x06vP\xf9\xfe\x13#\x13\x0f\x00\x00\xe5\x13\x00\x00\x1a\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x11\x0e\x00\x00img/pomerium_circle_96.svgUT\x05\x00\x01\xe3\xb6v^PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\x99\x06vPuq\x02\xd2d\x01\x00\x00\x9e\x02\x00\x00#\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81u\x1d\x00\x00img/supervised_user_circle-24px.svgUT\x05\x00\x01\xe3\xb6v^PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\x99\x06vPL\xbb\xd3^\xd3\x05\x00\x00^\x12\x00\x00\x0e\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x813\x1f\x00\x00style/main.cssUT\x05\x00\x01\xe3\xb6v^PK\x05\x06\x00\x00\x00\x00 \x00 \x00\xb2\x02\x00\x00K%\x00\x00\x00\x00" + data := "PK\x03\x04\x14\x00\x08\x00\x08\x00r\x9b\xb2P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x16\x00 \x00html/dashboard.go.htmlUT\x05\x00\x01\xa9\xe1\xc2^\xecYKo\xe36\x10\xbe\xe7WL\x85\x1ck1H{(\x16\xb2\xd1`\x1f\xc5^\x9a\x00\xc9\x1e\xf6\x14\xd0\xd2X\x9e\x82\x0f\x95\xa4\x1c\x07F\xfe{\xa1\x87\x1d\xbd\x1c\xcb\x8a\x925\x8a\xf5%\x129\x1f9\xf3}$\x87\x19m6\x11.H!x\x11\xb7\xcb\xb9\xe6&\xf2\x97N\n\xef\xe9\xe9,\xf8\xe5\xd3\xf5\xc7\xbb\xef7\x9f!k\x99\x9d\x05\xd9\x1f\x10\\\xc5S\x0f\x95\x07\xe1\x92\x1b\x8bn\xea\xa5n1\xf9\xc3\x9b\x9d\x01\x04K\xe4Q\xf6\x00\x108r\x02g7Z\xa2\xa1T\x06\xacx\xcf\xfb6\x1b\x872\x11\xdc!x\x19\x02\xcdnR\x80\x80\x15\x83d\x8fs\x1d=\x96\xc3E\xb4\x02\x8a\xa6\x9e\xe4\xa4\xbc\xa2\xad\xd2Jj\xa1's\xbd\xde\xf5\x94}\xa1\xe0\xd6N\xbd\x90\x9b\xa8\xd2\xd5\xee\x9c\x14n\xd4l\xb2p.g\x1fScP9H-\x9a\x80-/\xeb\x16\x9b\x0d-\xc0\xbfEkI+\xff\x86B\x97\x1a\x84<\x8e\xca0$\xe3\xedd\x14j\xe5\x815\xe1\xd4\xdbl\x9a\xc0\xa7'\x0f\xb8\xc8\x18\xb5h\x80$\x8f\xd1\x03\xd6\x9c\x11\x85\xc5\x8e\x19j\x0dP\x9b\xaf\xd1\x95\xcf\xce\xfc\xa4\x94\x86qk\xd1YF2f<\x0cu\xaa\xdc}H&\x148\xb9\xfc=Y\xfbv\x157GXK\xa1\xec\xd4[:\x97|`\xec\xe1\xe1\xc1\x7f\xf8\xcd\xd7&f\x97\x17\x17\x17\xac\x05h\x87\xa0\xa2Z\x04\x01\x8bh\x95K\xbekYh#A\xa2[\xeah\xea\xdd\\\xdf\xdey\xc0CGZ\xd5\\\xb7\x14\xab{\x9d\xba\xa6p\x16s\xdbY\xc3\xef \xd9\xf2\"\xd1\xda\x8c\xde\xd9w\x9d\x1a\x08K\x91m\xa1\x07D\xe88 \xeb\x07,i\x0d\xb1 \x14\x91E\xd7\xech.\x86\xbf\xb9l\xca\x94\xe3\x05\x9f\xa3h\x833\xa7\x13\xaef\x19,`\xf9c\x97\x0d\xa9$u\x1d\x1d\x00\xee1\xc1\xa9\xe7p\xed\x9aj\x15\xbf2\xf0\xdc\xfdn\x8b\x15\x17)\xd6\xd6e\x11C\xb7u\xbe\x9f{[Gd\xf9\\`\xd4\xd1\xc9\xda\x81\x06l\x0fK\xdb\xd5\xdf\xe0\xfa/Z\xa1\x1aHx\x8e\x85\x93\xa2\xbd\x12N_\xee\x0fB\xc6\x13 \xdb\xbb\x0d\xfe\xbfpI\xe2q\xa0\x00\x05\xf8\xb4\x14\xa8\x06\xd4W\x82\xc3\x98\xd15\xe8\xd2\xe26\x9d\xff\x83\xa1\x1b \xc47\x8b\xe6\xeb\xa7\x93\xd1`\x17H_\x01\x0e\x00Fg\xbf\xca\xfag\xc9I\x0c\xe0<\xc7\xbd\x82r\xcc\xf0\xe3q^\x86\xd1\x97\xf1\x17\xcd\xdf\x94\xefl\xad\x0e\xa0\xfb[~\x8b\x1b\xcc\xf6\xb8\x0b\xbc\x88\xa1/\xd7/Y\x8fN\xb5\xe1*F8\xa7_\xcf\xef?L+y\xd6\xe84\xb1G\xf1\x9e\xcb\x86\xff\xc29\xc1E\x07p\x97\x85\xb3\x91\xf7+\xd3y\xeb\xad\xe2_\x846n\x9b;\xe4\xfb\xc8}X\xe2w\x93\xb5vb\xad\x132\x8fGiY\x1eY9\xf0dvQ\xe1\x8e\x7fG\xc7\xe4\xea\x1e\xa07%\xff\xab\xb5)FWC\xb2t\x01=\x19\xfa\xb7\x91\x1c)@/\xd8\xdbK0$\x87\x14\xc0\x1f-@;\x90\x03\xd4\xf7\x05\x8cN\xfa.\x97@=\x99\\\xa5\x11\xa1\n\x8f\xfb\x97\xa1g:\xd9\x8e\xfd\x7f\xca(\x15%a\x9fx\x95\xe5\xb1\xd7ft\x81k\xbbJ&h\xacV\xdc\xe1\xd0+\xf1\xf3\x10\xa4bx\xed\x05\xf95\x8cw\x9cv\xad\xf0\xde\x89\xe3=\x17\xb2\x8a?ow7\xab\x0b\xf2\xf3\xa6\xd6\xf1\x1bW\xef\xb3\xa6yw\xcd1`\x9de\xcejm{!p\xed5\xe7\xc9N\x87\xd0\x9a\xc5\x97l\xd4f\xb1\x1a \x98\xa7\xcei\xb5\x1d\xa2|[\xa4Bx%\xdb6\x9dKr\xde\xec\x96b\x05\xd7\xa9\x0bXa\xd4t//\xeaV\x1b\x16\xda\xc8\xe7\x96\x9aA\xbd\x04\xfc\x0e\x85\xfd\xcc\xf9 \xa9 \xb7\xed\xb2\xfeh\xf5t\x9b&hVd1\xbaO-\x9a\xb7\xac\xab\x0f\xae\xa1\xf3H\x92b\xf4|\x92\x0c/\xa6\xb7\x16\xf4U66Yg\xb8\xd3\xc6B\xc8\x158\x94\x896\xdc\x90x\x84\xca\xa4\xc0\x95vK4\xf9\x87\x16\xbf\xb5\x03\x8e\xa9\xc3\x1fJ0\xc3S\x8a\xe2\xb2\xcc\xae\xcdD\xb07\xdb\x8eW\xa6\xe9\xeeL\x04\x0fq\xa9E\x84\xa6\xf8n\xf4'\xae\xb9L\x04\xfa\xa1\x96]\x90c\xce\xa3CL\x1e\xc8\x05\xc72Y\xa4\xb0\x03T\x8ep\xd4\xf7`\x12UL\n\xd1\x90jmR8\x8a\xc4\x1fsx\xb7\xbc\xeb\xe4\xfb*w\xa0\x93\xef\x92)\x8b\x1d\\w$\x86\x96M-Q4z\xdb\xdcU|j\xd17Zj\xa9=\x06\xac\xf8\xb6\x1c\xb0\xe2\xe3\xf66\xf3\xfe\x17\x00\x00\xff\xffPK\x07\x08\x06\xa8c9W\x04\x00\x00\x11\x1f\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00r\x9b\xb2P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x12\x00 \x00html/error.go.htmlUT\x05\x00\x01\xa9\xe1\xc2^\x9cUMo\xdb8\x10\xbd\xe7W\xcc\xf2\x1c\x8b\x81w\xb1h\x0b\xda\x97$\x87\x00\x05\x1a\xa4i\x81\x9e\x02\x9a\x1cI\x04D\xd2%G\xfe\x80\xa0\xff^P\xb6S\x8br\x02\xb4'K\xf3\xc5\xf7F\xef\xd1]\xa7\xb14\x0e\x81a\x08>\x145\xd9\x86\xf5\xfd\x95\xf8\xe7\xee\xcb\xed\xf3\x8f\xc7{H\x91\xe5\x95H?\xd0HW-\x18:\x06\xaa\x96!\"-XK\xe5\xec\x03[^\x01\x88\x1a\xa5N\x0f\x00\x82\x0c5\xb8\xec\xba\xe2+Ijc\xdf\xc3\x0c^\xdf\x9eqG}/\xf8\xa1hh\xe8:B\xbbn$!\xb04\x06\x7f#\x01\x10\xfc4Y\xac\xbc\xde\x1f\x8f\xd0f\x03F/\x98\x95\xc6\xb1C\xec,j\\\xe9g+\xbf{\xcd\x1cs\xaa\x911.\x98\x92A\x9f\xa5\xa6\xc9\xd9\x01\xc5\xa8\x06@\x18[\x8d\x02p\xea1\xca;\x96\xa5bP\x0b\xc6\x8b\xb5\xb7\x18Lk\xb9\x8c\x11)rc+>,{6\xffo\xbd+\xe2\xa6\xca\x1bw\xb6qq\xc1j\xa2\xf5'\xce\xb7\xdbm\xb1\xfd\xb7\xf0\xa1\xe2\xf3\x9b\x9b\x1b>i\xe0\x19\xc8z\xbe\x9c\xec\xba\x9e\xbfS4-\x10\\\x9b\xcd(\x10Q\x91\xf1.\x9br\xb65\x8b1\xca\n\xb3\x8d\x8dk\x08w4\xb3\xde\xf9\xb8\x96\nYBp\x9f6\x91\x00d\x07^\xc0\x90dbJ(n\xa5\xbb\xc3U[\x0d\xe2\xb8|\xd0[`\x1eJ\xd8\xfb\x16b\xed\xdbFC-7\x08R)\x8c\xf1\x1a\x94w$\x15\xa5|\x00\xa9\xadq&R\x90\xe4\x03H\xa7a\x1d\xfc\xc6h\xcc\xe6Q\x8d\x16\xb6\x86\xea\xa1-g.\xa1\x0eX\x8e4\xc0\x96\x01\x7f\xb6\x18 4\x924M\x14\\.\x8bK\xbc3\xe2\xe8t\xdf\x0f\x0b(\x9e\x90\xc2\xfe\xdb\xd3\xe7\xbf\xe7\xbf\xc2\xc6\xe0\x06\x13|\x18\xa4\x08&Br\xa0\x0f2\xec\xaf\x87\x1a%\xdd[|\xba\xee\x0cCbDa\x9fx\x0c\xf3\x8e\xfc.r:\x0f\x1d)\x8dDwAd\x13c\x96\xde\xd3\xd4\x98'h\xc93\xf1h\x9a\xd3\xd2\x0b\xe3\xa7\xaa\x9c:\xf9]\xc3\x9eb/\xca\x04\xd5\xe0\xcb\xc7\xff/\x19\xf7\x8f\xad\x0b\xef\xdf \xb9\xb3\xb9\xcct11W0UMp\xf0YK\xa8!Z\xd94\x13\xfa\xc3\x17\x1c\xbe\xd3\xc3]\xdf\x83X\x85\xfc,\x80\xc7#\xe5T\xfc\x1dC4\xde\xe5\x8a\x9b\xdc\x12\xe3\xc0\xe8\xf5\xec\xe5\xf5Q\xf0\xc3\x85.\xf8\xe1_\xe6\xa4\x89_\x01\x00\x00\xff\xffPK\x07\x08\xe4\x92\xc0\x7fU\x02\x00\x00\x96\x06\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00r\x9b\xb2P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x13\x00 \x00html/header.go.htmlUT\x05\x00\x01\xa9\xe1\xc2^D\x8dKn\xc30\x0cD\xf7:\x85\xa0ub\xa1\xfb(wa\xed DT\xa4\x0d\x91\xce\x07\x86\xef^T\x9b.\xdf\xc3\x0c\xdeq,x\xb0\"\xa6\nZ\xd0\xa7\xea\xd2\xd2y\x86\x9b\xc0)\xc4\xa8$(\xe9\xc9xmk\xf7\x14b\x9cWu\xa8\x97\xf4\xe2\xc5kY\xf0\xe4\x19\xd7\x01\x97\xc8\xca\xce\xd4\xae6SC\xf9\xbaD\xa17\xcb.\xffb7\xf4A\xf4\xddPtM!\xdf\xc3\xad\xb1\xfe\x84\x18;ZI\xe6\x9f\x06\xab\xc0\xc8\xf9gCI\x8e\xb7\xe7\xd9\xec\xcf\xd4\x8eGIy\xdaVA\xe7]2\x99\xc1-\x8f_\x16b\x9d\xc62\xdf\xc3q@\x97\xf3\x0c\xbf\x01\x00\x00\xff\xffPK\x07\x08\x9c\xd5a\xdc\xaa\x00\x00\x00\xe7\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00r\x9b\xb2P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1b\x00 \x00img/account_circle-24px.svgUT\x05\x00\x01\xa9\xe1\xc2^<\x90Ao\x830\x0c\x85\xff\xcaSv\x8eq\x9c\x90\xc2Tz\xd8.\xbb\xec\xb4_0\xb5\x8c \xb5\xa5\x1a\x88T\xfc\xfa\xc9\x80&\x81\xfc\xf9\xe5=K\xf6q\x9c;\xcd\xcb\xeb\xa5\xd1\xf5/\x00\x00\xff\xffPK\x07\x08\xfc\xc6x\x8f\xb7\x00\x00\x00\xf9\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00r\x9b\xb2P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00 \x00img/pomerium.svgUT\x05\x00\x01\xa9\xe1\xc2^\xc4UMo#7\x0c\xfd+\x84{i\x0fz\x16I}\x16\xeb\x1c\xfaO\x06\x89\xe3Y\xc0N\x16q:Y\xf8\xd7\x17\xa4\xc6N\x8a\xdd\xf4\xd4\xa2\x08\xc2\x91\x1f)\x0d\xf9\x9e\xc8\xf9r^\x0e\xb4|\xdd\xbf\xfd\xf1\xfc}\xb7\x89\x14\x89\xb5\x93\xc6\x0d}?\x1d\x9f\xce\xbb\xcd\xfc\xfa\xfa\xed\xf7\xed\xf6\xed\xed\x0do\x8a\xe7\x97\xc3Vb\x8c\xdb\xf3r\xd8\xdc}9\xd0\xeb\xcb\xf4t~|~9\xed6\xbe\xe9\xbe\x8d\x9f\xe1\xe5\xcf\xe3~\xb7\xd9/\xfb\xa7\xe7\x87\x87\xcd\xd6\xaaZ\xc3\x1e\x1f\x1f?\x96P3\x12IA\x9190R92X\x83\xa0\xf6 \x90l\x89\xf4\x99\x91\xd3\x91-\x01E+\xc4H\x1a\x14\xad\xba\xe3r\xb2 \xb6\x04\xd3\xb4f\x99\x84\x9cv\n\x8c\x1c\x90\xf3\x02\xad\xf6\x82\xccKhHi\xf6\xa5Z|#7#~\x0d\xbfgd\x83\x04\xc5^'m\xac\x8c\xa2s`\xc4f\xcb\x16\xae`\xbb\x9c\x02TBBk\x13\xa34r\x13\xfd/0\xb8!\xeb\"\xc8b\xdeLn\x86w8\xd3=Z\xf6_)\x05T\x1d\x0bF;\x07\xe4\x1e\xd8\xe8X\xa1\xda/\xa7\xd0\xd0L<\xcb_-7m\x1f\xea\xb5\xfc\xedY)z\x86\x81!u\xac\xbc\x00F\xecC\xed\x1bf<$rs\xe3\xc1X\x0bjD\xc9\xd2\x90\xb2\xd3'\x97S4\xea\xad\x0e\xcb\xb2\xe8\x87*k@\xd6\xfb`\x12EK7\xa3\xaa?\xc9\xd2>#wb\x187\x8e\xf8\x19v\xc4;\x13v\x80\xe9\xd9\x9d?\xf3\xfb\xaa\xdf\"|s'E\xce\xc3\\\x1d\xb5\x04\xe4\xb20\xa4L\xc9\x08t\xb3r\"\x84TH\xe9\x06\xd8\xeda%A\xab\xc3\x0c\\\xd0\x9a\xd1\xa1Cm7WO\xee\xa4\xd6j\xab\x19p$\xe4r91\xd4\xf8\xd54\xb1)\xc1W9\x9c\x15\xad\x06\x94Y\x90\xeb\xe4W\xc9\xcd;k\xa6\xac\x16\xbf\xc5\x9a\xd0uQt\xe7z \x19}\\T\xe4> \xc9M\x9c\x94\xc3\xca\xcd\x87\xcb\xc4\x84\xd4\x11\x8d\x05\xcd\x13\xa3\x99@\xed&\x10\xb2\x06D\xcb\xa6\xad\xe6\x96C2\x02/\xa7P`\x9e\xc4?a\xbe\x96\xff\x8d\xfa\xf2\x1f1/ze\xbe\xa0TJh\x06H\xb0.\x13J\xa6\x81\x1e}\x16\x0e\x15l4uJ\xe4\x9a\xd9\x04\xd1\xfa\x03\x94\xf9\x18l\xf6Z\x93v\x88\x82m&u\xeb\xad\x9e>r\x10\xfb\xca\n\x13S1]\"\xfd\x18h\xb8\xf1e]\x97\xd0\xc5\x9a\xab\x8d\xe6\xea\xa8\xd9\x9f.\xf2\x19\x85i\x95|\x85\xac\x875\xb8\xa2Y\xc7\x08A\xb1-^\xa6CMG\xf1\xe2\xf7\xc1\x0b.\x8b\xa0\xb3\xcf\xe2n\xa32\xd6\xd9d\x9a\xc4^\xe0f\xa4l\x9f\x19\xb1\xb1'\x12\x9c*N\x8b\xa0\xf1l\xab\xbf\xa9a\xb16\x88j\xbe|\xf6qx\xff\xa0\xa9\xa02qB\x9f\x14]\xc9\xcd\xaa\x98\xad\x837\x87&\xf0'\x01\x06\xc8\xc2\x05\x9cf;k \x99\xa7l\xac\xb8\x19\xe93S\\\xcc3\xcb\xbf\x11\x81T/\xa7\xa0\x1d\x9a(\xe1\xe7\xa1\x97\x13\x0bz\xa6\xf8\x0f\xde\xf4\x99\xd7X\xda\x1e\xd6\xff\xf3r\xb8\xfb+\x00\x00\xff\xffPK\x07\x08K\xfe\x8b#M\x03\x00\x00d\x08\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00r\x9b\xb2P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1a\x00 \x00img/pomerium_circle_96.svgUT\x05\x00\x01\xa9\xe1\xc2^l\x95\xc7\x0e\xe4HvE\xf7\xf3\x15\x85\xdaR\x9a\xa47\xad\xa9\x06\x82Lz\x9f\xf4\xb9\x19\xd0{\xef\xf9\xf5B\xb5\xd0\x82\x16\n \x16\x07\xe7\xe2\xdd\xb7{\xffZ\x8f\xf2\xc7\xd5w\xc3\xfa\xebg\xb5m\xd3\x1f\xaf\xd7y\x9e\xff<\xb1\x7f\x8eK\xf9Ba\x18~\xadG\xf9\xf3\x7f\"\x7f\\]=\xb4\xff_\x10a\x18\xe6\xf5\x97\xfd\xf9\xe3\xac\xb3\xad\xfa\xf5\x93!\xff c?\x7fTy]V\xdb\xff\xe2Q\xe7';^\xbf~\xc2?\xe0\x1f\x0c\xfc\xd7\xff\xf9\xe7?~\xfc\xf8W\xdd\xc7e\xfe\xa3\xce~\xfd\xf4\xf3t\x1b\x97\x7f;}\xbcl\xff6\x93&O\xb7\x9f?\xb2x\x8b\xffs\x88\xfb\xfco\xff\xe3/\xff\xe3o\xffw+\x0c\xff\x9f\xce\xdf\xf0\xd7V\x7fTK^\xfc\xfa\xf9{\xc8\x1fu_\xbe\xa6\xa1\xfc\xaf$^s\x12\xff\x8f\xdag\xcd\xcf \xabb9\x02\x00\x80\xe1x\x15\xef\x95\x00\x00\xf17\xb2%\x07\"\x00\xc0\xbb\xee\xa5\x14\xff\xedI\xbe\xe3m\xff\x83\x0f&\x9aq!\xeb\xf9\x9fX<0\x9bs8n\xb5GPQ\xb2\x8d\x8f\\S\x86#I5{\xa9\x93K\x95\xd8j\xdb\xe8\x8f/\xec\xd1\x93\x98\x1c\xd9!{]v\xf2x\xd6S\xa2\xee:?\xce$\x18\xe5\xc6\xe4\xd6\xb1/9 \n\xab\xde\xf6v+s\x9c-\xdeL\xe6N\xf9\xb3\x11d\xe8b\x01u\xb8N1\x1fd;\xf9\x13\x031\x04A=\xd4\x82\x0d/3\xa7!D\x13\xdfV\xe0\x8eb)\x0d@\x83\xd9dd\x8b\x08\xac2k\x8c\x00\xb5\xc1\x93\x84\xa3\xe0\x81+eK\x1c\x98\xa7\xb8\x95\xc0\xe9\xc1a5X\xd2\x1e\xcf\x81*p\x95\xbe_g\xb8;\xe0\xddq\xc4\xc8\x1a\xa7^\x00\x86\x06\x11\xa4}\xf3\xe4\xd4S\x16;\x87\x84\xd2\x9e\x91q\x07\x18?\xdfRi\xa0\xc0M\x85\x916#\x14\x05:\xce\x1f%;\x80\xd9\x95\xf6\x1b\xdeZ\x80Xp\xa3\x95xWa\xc2]\xbe\xa7S>\x80\xb3c\xbe!\xb8r#\xd4m\xf3\xe5\xbf\xf5#\xf1g\xc9\xbd\x10\xdf\xc2\x87\xaa\x86C\x1f\xc2\xdd\xe85}\xe7\x17\xc50#xN\xb1\x00\x1b\xcc$M\xfa\x81\xd98b\x97g\x9a\x9c+\x17c.\x0b\xf7\xb5\xa0\xd5\x9a\xf9\xe8\xc9v\ns\xd9]\xe9\x9d\xf7<\xa8\xfd\xe9\x11_U\x9c\\\xf9\xbb\xb4\x91\xbc1\xfa\x0d8\x94M\xed\xc1\x81\x81\xc5\x01\x8a\xb8\x7f_(\x92\xb4\x1f,\xc2\xc1\xb2\xd9\xef\xeb\xbdG\xce;\xb1\xd2\x0b\\f\x17\xe6DS\xdd\xae\xc7\x02,\x0b\x1d\xfam\x81$\xe2\x05\x87\x0e\xc3S\x188\x06\x8b\x8eU\xac\x81\x14\x98\x8a~\xd41K\x86\xf1$4\xab\x18\xab(\xc1pN\x8dc\x0ch\x902\x8a8\xc9\xd6\xe3\x8e\xf6^5\x9c\xa2\x1aq!\xd1\x19\xd6\x99\xde.@t\xbe\x10D\x11\x1e\xfb\xe5Q\xca\";\x1c\xa5\x9a\x0f7d\xed\xb7|+b\nm\xd2\xabM\xd8\x8c\nr4\xe3{v(?\x1e\xc2)9\xaf\xb7=B=d\xcb^\xa6\x97\xf6\x88C\xc7\xf8c\x9b\x05w\xd6b\x0c^\xb7\x0b\xa1\xec\xa8Om\xe4\x18\x8a\xcc.\xfdU\xaa\xe9\x87\xff\\E\xa9AB\xdb\x88\x0ep<$\xf0\x13\xe9\xbdK`\xab\x1e\x0bP\xb8\x94\xbfC\xc7\xae\xa3[O\xa5m\xdeuu\xdd\xdc\xa5\xc2A\x03\xcdk\xbc]\x9f\x0fFX+4\xaa\xc7U3\xef\x18\x16\x05\xb4\xfbB\xcex\xe09\x06\xf2\xae\xbaE\xa4\x0f\xd6\x8a\xbf2h\xe6jI\xd8\xbb3jR\xc4\x9dV,ds\xe6\\O\xad\xa8\x84\xb9\xd4\x8emS\xe2\x98\xad}\xa1\xd5\xe8\xb0\xeaN\xb9\xa4!\xe9\x11H\xbbk\xcb\xee\xb8\xc8z.\x17\x94\xc2\xb5\xf4\x9b\xb03Hl\x86\xcf&\xc4k\xe3\xab,v\x83m\xc0n\xc7\x08\\\x87\xb9\xda%_z\x0e\xc1(\xe1xVf\xa2u\xe2\xf5\x99cOs\xe5\x99\x1b\xa5s\x83[\xd9\xbb\xba ;\x08\x84e\x16z\xb2\xd2\xa8@\x19\xee\xcd\xcf=\xed\x1e\x19\xd6qx\xdaT\xc3I\xf5\x99\x93\x8c\xb1\x93|\xdbx0\xcb\x10\xd2x\xa1-y*\x8c\x96\xe0\xbe[$(\x0e4\x19\xcd\x85\x1a\x9d\xa7\xda\x8ey\x17\x84\n7\xba\x13\xc6\xad\x93\xc6\x92\x13u\x0d`xm\x18\xcajL\x92\x99\x16t5\xb1/r\xfa\xaeB\xe0\xc0{\x19 \xaf\x07\xbb\n\x084\xaf\xa3T\xd47\xac\x9d]f\x9ej`[i\x9f\xab\x83\xc4\xb8b\x88\xe1):8 90/$s\xfb\x08.k\xa8\xa60\x18\x8cq8{\x86%F,\xdb\xed\xb3y\xc3\xd2\\/\x15+\xed\x086\n\x9d\xaaUY\xd8\x0b>\x14\xa9u\xc1V\xf4\xb0\x0c7\x03\xa1@t^ C\x18\x90:\xa2\xd7\xc1\xc2\xac.\x9f\x91K\x10\xab\xdaE\xec\xa4\xda&\x01\x83\x81\xef\xfdI\x0bXt\x94\xa2\xd9\x91\xb3\xb1\xfa\xdd\xb39\xbc\xb9\x9f\xf7\xbe\xc3\x19\xa1\n\xdf\xcf\xce2\xfdQ\xd6\x8b5\x9a5@\xd8+\xd4\xb1\x93\xa83\xf0\x10V\xb9v\xc2\x99~>\xce\xee{\x89(\xc9\x1fM\x19\xc50?\xbf6\xb7\x88\xcai\x1b\xa9\xdcg*\x0e\xf9\xf6\x96Bp\x08=\x0coF9\x9bz\xd4\xa8\xef\xc77\xc0d\x8e\\B\x1a*158\xa1\xc6\xc4\xd3\xf2I\x8dL\xa4]fh\xaf\x8dVj\xf2\xcb\x91\xd8w\xc6 \xf3\xa5\x1f\xe6\xf2\x92\xb8\x95\x15\xbff\xc9y\x0f\xfe%C<=\x97\xc78\xe8\xa2l\xe9\xec,h\xba2Z\xa6\xfb\x88\x8c\xd8~\xe7{,)d\x8b\xd6\xb3\n\npZn$\xf4|@}x\xeaH\x98\xd1\xb5\xbb\xdc\xf2s\x1aL\xd9\x8c{rl\x11\xd0\xac\xd1\nI\x84\xf7\xde\xbeXC~:#m\x91WE\xc75\xb8sJ\xb9##j!3\x1d\xdb\x93bh\xab,\"SJ`1\xb53\xb3\xef\xa8a\x9a\x1cf\xf6\x82,\x98\xb5\xc0\xb6\xcah\xbcs\xf3\x8c \xe7+\x9e\xaf\xb9\xa3\xad\xa2P\x1dz\xcf\xc9\xf2\xfe\xce\x13\xfb\xe9z\xd1\xad\xa4!\xfbb6\xb7\xd30\x9c.\xf2\xb6\xc1\xd0WOC3`\xc2o\x05\x90\x89]\x07\x9c\x80\x82g\x9a\xe5\xf2\xc9s\xde\xde\xf4DU|\xff+\xbeH\x0c\x87%m`\xf4\x99\x8ds\xd3\xa3=\xf5\xcamL\x1f\xca\x8aq\x91\xf8uA\xce\xc7\xe8\x84\xfeB ;-\x9b<\x7f\x16:a\x08F<4\xcc6\xd3\\8.\xec@\x94\x13\xa3\xf93y\x945\x10 \x9f'\xbeg_\xd5\xe5\x80\xe4[\xe2\xd7\x155\xb9;\x9aEo\x1e\x11\xe7Dtk7|\xe8|\xc3^\x0f\x86\x08\xa2\xd7\xd3\n\xf5\x12\x88\xb3\xef(\xf2Z\x837_\x14\xc7|1\xc7\x8b\xff\x16\xd7-\x0c\xee\x91\xdc\x06N\xcfk\xe7\x12\x89i\xc6Lt\xe8Q\x9c\x1f\x0dl3\\\x1eY\xe1\x10w\xdc\x8dm;\xfb(\xae\x19\x04Sf\x0dsK\xb4\xda\xe5\x0cJh\xab\xe9J\xe4\xc9\xb7\x0c\xcb\xeb\xb2\xb3*\xf7*1g\xba\x14\xc9q\x85,\x92\xb8E\xda\xf2fvk\xbe\\\x04\xaf$\x12\xa2\xeevh\xf2\xa3\xda\x92\xadK\xb46Y\xc2\xa7\x8d\x90\xb8\\\xf6-F\x8f\x18\xa5x\xf6]e\x80\xbe\x12\x14%\x0f\xaf8\xfa|I\xfc\x0eS\xd0\x01\x13\xc1\xa0\xaf\x8b\xa8\x99\x14\xcc\x1c\xc1\xef;v\xec*\xcf\x1cA\xa5\x80\xa97Br5\x96\xfd\x1e_\xf9\xd6\x95\xda\xaeu\xba,\x0e\xf1\x0e\xa1Tdn;\x11j\x13\xc1(\xa7\xaer\x9b\x97\xc5\xb0\x9b\x8bMe\xb3\xdc\x90H\xae=\xea\x95\xcfaCvB{h.%\xc3^\x8f\xcbl\xda\xb4\xdc\x06|e\xa3\xe2tC\x89m\xbb\x10B\xf4\x1b\xac\xe8\xb8[\x96\xc7\xe5\xe6\xd4\xdd\xd2\x1b\x7fI\xcfW\x0f\x9al\xeb\xe3G`\x9e[\x17.\x7f\xb4\"O!\xd8.\xc9\x01\x11\xf6\xe5Z\xca'\xbc\xd0\x93\xf9\xe5QChx\xb2,\x9a\xb2\x08.A5\xe3S\xc4\x06I4[h\xa5\xd5J\x11UUh\xa8w\xeb\xd0W\x15VR\xdb\xd0\xa3V\xc4\xbb\x80\xf4\xf7\xc4c\x16<\xb3\x10\xf2\xa5x^\x128J2B\x15*\xde\xe4\x18\x9cI\x828\xbd\xd5\xb8g\xad\x1bf\x99\xeeR\xdc\xd3\xd2\xb3\xdb\x86_\xcb\xda\xd9\xcb\x97\x9a\x87\x96\x97\xd2\x0bF\xbe\x85\x82~.]x\xa8`\x0dO&/l\x83\x88\xbb\\\xb7\x0dd\xe6\x0cUL\xaa\";\x10\xa6[\x96\xec\x03\xde\x02\x08j<\x11\x8a\xf0\x9a\x16U\xb6+n\x9b`D\"\x886\x9d;g\x1d_\xfaL#\xe8[7\xc5\x83\x17\xbf\xa3R\xe4\xab\x82j\x88\x90\xba\xf9}F\xf3}\xa9\xafb\xe7e!\xdd\xb3\x0c`\x99\xb1\xf6\xdfhL\xe4\xa1\x91\xbdN'\xec\xaa\x8aNmW+\xff^\x1a.\x0e1X\x1c/\xf2\xcd\x91\x04\x92\xf8\xea9\xf3\xb5\xfb\x852\x1cW\x94\x05_\xdf9\xc4\x1e>\xfd6\xdd\x8c\xa5ZAu\xd2o+\xafJ \xa1\x1a\xedJ\x0b,JC\x99i\xd5\xfa=R\x98h!\xb6`\xee R\x8b\x15\x9e\xd5{\x1e\x9d\x80\x87\xa1\xde\xe6\xf2\xb0\xabk\xd4\x87L\xe9\xeb\x12>'\xc3\xd5\xe4\xe9\x18\xee#\x98\x8c.p\xc5FQ?\xd6\x1e\x9b\xc2E\xd0\xd9\x98PG\xfb\x14\xd5\xcdt3\xf6p\xae\xfem\xa6vkO\xc8%\xfd\xaa\xd0\xed\xcb%\x98U\x81\x87V\x91\x0d\xd1\xbf-\x90\x7f\xf5Y\x12\xc1\xd8\xddJ^\xe5\x90\xf7*^4\xc3\xd8\x9b&\xaf\x96\x9f\x10\xb2\xd2]X\xb1\x80r\xc9\xe0s\xbb\xc1\xba['\xeel3\xd3\xf5\xc2\x03\x88*C\xbdo\x7f\xec\xf3tF\xa3\x00t\xfc\xad/\x8b\xdd\x9c\xca\x18\x0e.J,\xee\xd5\xc6\xf2Y\x7f\x9a\xad\xd1\xda;0\x95\xafRX\x1e\xecdC\"[A\xae\xb40Q\x18\xafy%\xb6\x82\x1e\xa8\x1c\x8b\xa7\xf7\x99\xcc\xd09\xe7\xfdW\xa1Z\xb6\xb5\xa4\x84?\xcb0X\x0d8\\\xf8\xd7\x05ZE\xba\xa1o\xd7Q\xad\xf7\x89*n\x9bIi\xb0\x1327S+\xeeb$\xab\xd4\x05\"\x02\xf7(\x86\x04\xed^/HF\x1bJ\xf0\x00\x19\xa0e\xa42\xf7\xecb\x9c2\xd1P\xc8\xe8\xc8\xc4\x13#u};\xc0\xe5v\xe3s\x01Gv:\xc7k\xa9[\x08\x81\xd7?\x0e\x8a\xd6\xcd\xb6\xde\xe0\xa5\xbc\x13\xe7\xb2\xc7\x98\x9a\xb3\xfd\xa1\xe54\xce\x84aC\x08d \x0e\xe0n\x0c\xd4\\\x06t\x0f\x1d\xaf8\x81\xad\xf4{>\x00~\xc8Q\xc7pG\x99\x8b\xa6\x96lv&\xc2\xbd\xb2\x93&\x7f\xbc\xd2\xbe\x0d\xeb\xb8f\xaa\x02\xad\xb4\x1e\xb9\xcd\x95?u/vL\xbc\x9c\xa0\xb8.5\x89\x13\x02X\xb1\x98l\x98\x97\xe9b\x86L*\xdb\\\xa8(-\x88\x03\xe2\xccc(\xbb\x91\xa8\xde\xb1\xfb\xd0\x9b-7\xaa\x9e4\xaf\xcb\x9cv:i\xcdbB\xb2\xac\x9bd\xfauKP\xcb\xea\xdf\xf9\x84O\xf7\x82\xc0\xe0\xb9\xb4\xaeo#\xb0\x84\xd5\xbf\x03_z2\x99\xd2\x02\xa15\x97q\xbb\x0e\xacgu\x15\xc93Y\x04\xd8Y\xfa\x9eR%\xa3\xb1\xd8\x9f\x1dE\x84\xad\x07\xe4r\xa9K\x92\x04:\xde\xc0~\x04\x9fc\xf8\xd5Z\xbe\xd25^\\\xb7V1\xc8\xd3\x99L\xbeK\xc45\x99\xcfFR|\xc3\x8cw\xebh\xec\xfe+\xdb\xfe\xeb\xc3\xe1\xe8~\xcdj\xec8\x9c\xe1Gi\xc8\xf2\x10s\x18\x03\xe7\xb6^\x1a\xbb\xa5Wv\xa1\xc6\xf6\x9f{.\x03\xca\x7fP\x85\xdc\xdf\x1f\x99\x8c C\x15<\xa7\x8a\xf32\xd0\xf6\x11|>\xa4\x82\x8cH\x83Q\x96\xd0M\xa7\xd3#ix\xe36\xa4\x84=dm0\xad\x8e\xcag\xbeK\x86c\xfb\x926i\x16\xda\xfa\n\xbb\x116\x94\x84\xcf\xdbY\xe7\x15\xbeFN\xd9\xad\x17\xe38s,s\xef\xbd\x08/.\xbd=\xd2\x9fO\xa8\xd3\xbbD\xd2v\x19n\x07\xe6\xc4\xe9\x85\x1f \x12#\x81Q\x82\xb3A\xfb\xbe\xcf\xe2e.\x89W\xf76\x15\xc6\xdf\xca\xc6\x10\x04\xf8\x9a\x88\xa8\xe4\x00\xdd\xa6\x1d.z\x9e+\xb2\x8e\x81\n\x12\x01\xd5_\xd7\x8ea9N?\xc9\xa8\xcf\xc2\\\xa7\x92\x94\xb5db\x90\xef1\xf1\x92\xe6\xc2\xaf 2\xbeU\xd9\xe3\x92d\xbfQ9\xea\x0f\x85|\xab2\xb7\xd9\x11\x14\xccX\xecY&\x01[\xd9\xcb\xab\x0e\xd8*\x85Z\xe4\xe7\xbd\xedk{J\x81\xe4\xa6\xa6U\xa9\x0f\x9d\xe5\xfd2\xc3\x11\xaew6\x11X\xf7\xbb\xea'd\x7f\x15'\x88\xbf3W\xeb\xbeU\xdaM\xb6S\x93\xd7O\xaa\x16\xe32\xa2-\xb0}\x9f\xe6\xd0\x8d\xed\x9d\xdb+\x92\x0c\x1fq\x91\x1b\x03\x83\x1b\xd0\xd9U\xf6\x1e\xce\xa9\xc0\xd0r\xa2\xe5*$\xb7)[%\xc6$=r\xc1:\"*3\x1ar\x90\xbd\xcd\\\x08k\xf7\xe8\xcfr\x7f\x9b\xc5S\xe1z\x06\xa0\xdb\xc8\x0e\xa2\xe3\xb7\x00?=\xd2\xc4\x02\xd2,\xc8\xd6\xdf\x97Q\xc1\xe9\x17\xb9Db\x80\xc7\xae\x1c\xcd\x94\x8b`\x1e\x9c6w\x8b\xc1\xc6\xc4\xc1\xe4\x12\x1a\xd4\x87b\x0c\xd5\xde\x85\xea\xf5\x91)\x14c\xdf\xf2\xc1$\x16sU\xe4\xf7\xa1?\xe5I\xac\"z\xc3\x90H,\x85\xe9X\x96\xda\xa5\xe3\xc7\xf6\x18\x01\xe5D\xe2\x1e\xb1\x93\xce\xcb<:D\x0c\xfe\x8a\\\xc3\xa20\"\x98\x8f\xcf\xde\xc4W\xeb\x1d\x99U\xe9YJ\xb2Q\x86mF\xa5\xa0\x0f\xceZ%\x82P\xbe\x17v\xc45#\xad\xe3\x15\xb3|\xf5\xcc|1\xa6\x04\xca)\xff\xd2P\x90\x91\x0f\x01Mu\x9a\xd6\xe7a\xb0k\xc9\x16\xae\x8e\xf3\xe5\x02\x95\x0c\x176lL\x08u\x96.O\xb1 l\x18#Ee\\\xf6\xea\xedak\x9fVW)d\xe3\x97\xc5\xfa \xe1q.]\xd2\xc1\x16\x86\xb0'/U\xfa\x81lPP_]\xb3\xa5\x04qY\x88\x1c\x86\x98\xf2\xaauT\x0d\\\xe2\xd6\x16\x0b\xd2\xaap-\x9e\xd1j\xef\xf0\x93\xbb#\xfe\xda\xdd\xd8\x0e\xcb\x81\xech\xea\xeb\xb4V\xcc#'t|`\x13\x9e\xc7\n\x97\xf9|\xa0D\xe7\x10\xd9e8\x06\x0bI/\xa9\xd6\xd4{\x19 )D\xc5N\x87\x91\xe6gU\x10\xacx\xfa* 3+\xa1\xe4\xe3\x9c\xc4\x18\xdbo\xf3z\xa6\xaf\xdb\xf5>\x0ffZ\x96\xcf\x1f]\xb7m\x1b6\x87\xc7\xbfs\xa7\"\xd2\xcd\xeb\xd9\xd0v\xf9X\xa6\xc1\xa874\x9d.\xe7ii\xbc^N\xdb\xcf\xc7\xd7`\x84\x84\xd4\x93z\xf3\xfe\xf6\xf9g\x99\xe8c0\xbf\xadE\xce\xa4#\x07\x04%a+\xe4\xe1\xfb\xb2[\x99\x0bR\xc5\xf6\xf0!\xf0\x81\xdc\xd0\xca~s\x88\x96\"\x9c\x1f-$\x91\x90Ev\xe8c\xdd\xdbR4I\xfc\x14\xf9\xe5p\xbbR\x85b\xf3\xcbf\x88\xad\xeeS\xfd\xbe\xb9\xdf\xb8H\xa1\x1f-Jl\x85+q%6\xaaK\xf9b\x8d\xde\xde\xf9\xe5\xcc\x15\xab\xc3/g,\x198[\x82\x84\xef\xa3\x95\xf6\x9bP\x86u\xabC\n#+<#\x05\xf6p\xac\x88\x1c`={\xe4\xd8\xeeZX%\x87\x98\xd8\"f\xaan!\x84V\x04\x85\xf4\xe5G\xa0Zt\x8f\xbe\x9c\xcc\xa4\x10= \xf6\xfb\xb3722\xb4\x94\x07\xa1\xd5\x03)3\xc4\xaf\xec!\xa9&\xed\x95\x14\xd9\xb3\xc2\xba\xd2\xbf\x06G#\x14Y\xe129\xf4\x9e,l\xcd/\x159\x95\x08\x91\x02$s\x82\x0f\x95v\xd3\x1d\xf3\xf1\xf7r\xbd\x0e\xe6\xfe\xb8\x9fL\x9d\x15!\x99\xd4\xaf\xea\x7fI=T\xc6\xee\xfd\x7f\x00\x00\x00\xff\xffPK\x07\x08uq\x02\xd2d\x01\x00\x00\x9e\x02\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00r\x9b\xb2P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00 \x00style/main.cssUT\x05\x00\x01\xa9\xe1\xc2^\xbcX[o\xdc\xbc\x11}\xf7\xaf`\xd7\xf8\x00;\x10\x15]V\xbb\x8e\xf2\xd6\x14A\x0b\xd4yH\xd0\x87>R\xd2h\xc5\x9a\"\x05\x92\xf2\xaeS\xe4\xbf\x17\xbc\xe8\xba\xbb\xb6\x9b\x87/\x88\x01\x8b\xd7\x993g\xce\x0c\xfd\x01\xfd\xf7\x06\xa1\x96\xc8\x03\xe59\x8a>\xdf \xd4\x91\xaa\xa2\xfc\xe0\xbf\xf0\x11\x8a'\xaaq-\xb8\xc6\xaa\x15B7v\x92pM \xa3DAe\x97\xb5\xe2'\x16\xeat\xb6\xee \xc9\x8b* \x83\xf9a\x1aN\x1a+\xfa\x130\xa9\xfe\xd3+\x9d#.\xb8]Q\x88\x93\x99\xb0[\x0b!+\x90\xb8\x10'3c\x0f\xaeIK\xd9K\x8e0\xe9:\x06X\xbd(\x0dm\x80\xfe\xca(\x7fz$\xe5\x0f\xfb\xfdUp\x1d\xa0\xcd\x0f8\x08@\xff\xfa\xc7&@\xdfE!\xb4\x08n\x10Bh\xf3w`\xcf\xa0iI\xd07\xe8a\x13 E\xb8\xc2\n$\xad\xc7{\x8cm9\x8a%\xb4f\x88Q\x0e\xb8\x01zht\x8e\xe2pkF\x7f\xdd\xdc\x84\x9d\xa4-\x91/\x16\xc2R0!st\xbb\x83m\n\x0ff>df\x83\x9d\xfc\xf8\x01\x91l\x1f\xd75\xfa\xf0qZ+\x0f\xc5]\xbc\xcb\x02\x14\xc7i\x80\x92,\xbb\xb7\xdb*\"\x9f\x86]\xb7\xdb$\xf9\xdbnw\xb6m\xb7\x0b\xd0\xd6\xec\x8c\x92{g\x8b\x85\xb4\x15\\\xa8\x8e\x94`\xf7\xcf<\x89\xc2\x87\xcc;\xb3\xc0\xf1\xc7\xd7G\xc1\x05\xfe\x0e\x87\x9e\x11\x19\xa0G\xe0L\x04\xe8QpR\x8a\x00}\x11\\ FT\x806\xff\xa4\x05H\xa2\xa9\xe0fVl<\x98_D/)H\xf4\x0d\x8e\x9b\x00M\xf7\xff\x85\xb6\x9d\x90\x9apm\xcd+D\xe5`\xaa\xa8\xea\x18y\xc9Q\xcd\xc0\x85\x95\xc1 WTBi\xce\xce\x91\x14G3L\x18=pL5\xb4*G%p\x0d\xd2\xd2\x83\x94O\x07)z^Y\x1c\xc8\x12\xbf\x00EadP\\`\x95\xa6\x01J\xf7\x01\xda\xc6v\xc6\xf0\x8d\xd6/\xb8\x14\\\x03\xd79\xb2\x06\xe3\x02\xf4\x11\x80[ko[B\xf9\xfb\xcc-\x05\xeb[~\xf1\xdc\xc9\xea#\xadt\x93\xa38\x8a\xfe0\x9f-\xe5\x13\x9b\xa2\xe8\xb9q\x97R^\x0bCv{\xf1+\xa7]\x81\xe6\xccT\x9f\xc7\xb8\x10Z\x8b6GI\x98H\xcf\\\xe5\xcc\xff\x7f]\xec\x84\xa2>J\xc0\x88\xa6\xcf6e-\xf3\xacQ9bP\xeb3\x17\x13\x7fk\x13\xafi\x99\x84\x0bV\x1e\xfd\x8em\x14\xad\x0f\x9e\xfcd\xa05Hl\xa2\xe6T*L\xbb\xd3\xb8\\K\xc2U-d\x9b\xa3\xbe\xeb@\x96D\xc1\x8a\x0fq\x1c\x05h\xb7\x0fP\x92\xfa\xe4i\xe2PS\xcd\\\xd6\\\xbeu\x12\xc5polFq\x98d\x03\x9cMr\xb6s\x00b\x90\x85\xd4\xfe{\xd3\xcc\xab\xce\xcdUi\xb8y\x85\xda.\x8b\x9c\x14\x94DV\xd6 /\xa0\x92T\xb4W\xe6\xb0a\xdfjb\xeb\xeep\xa39\x8a\xbb\x13R\x82\xd1\xca\xa5X\x14 \xff?\x8c\x13\x97]\x96\"\x07)\x8e9\x8a\xc7o\xd5H\xca\x9f\xfc\xc8XQ\x10N#w|KN\xd8g\xc2vJ\x84a\xe4\xc1\xaf\x1a\x81\xf6n\xce\xdd\xb5\xc5\xa1!\x95\xb97r\xe6\x98\x15\x91_\xb9\xb66\xda;E\xbd\xa9)\xb0J\x81\x9e\xd5\xba)+\xfc\xbdse\xb9\xad\xcb\xba\xac\xeb\xac\xfa\xedK\xdf\x82x(ws\x89^\x9a\xcaH\x01\xcc\x1a|9\xeb\xdeL[/\xa4C\x16n\x935\xbeQwr%\xfe\x8a\xa0\xbc\"A\xe7\xd9zfz\xce\x85\xbe\xcb\x19Q\x1a\x97\x0de\xd5\xfd\x9c\x91\x03\xf8\xaf0\xed\xfe\x12\x1c\xaa#N\xb4f\xcc\x89\x93l\xe5Y\x84\xe2l& >#\xa5\xb1\xd6\x1eJ\xdbCHK/\x7f\xfe\x14\xd2k1\x87+\xdd\xcd\x036\x8613\xc459\xd6\x82R\xe4\xe0\x14c\xbc8 3\xcf\x8d\xfd\xa8\x0d\xa1u\xc1 \x1f\x83\x93O\x8f\x0b\xb6^R\xc0\xab\x15\xff\x15\xbe\x8a^\x9b~e\xe2X\xd9KeN1`\x98\xefcC5X\x89\xb1\x8b\x8e\x92tv\xdf3\xc8\x9a\x19\x967\xb4\xaa\x80\x8f\xf8M\x13\xc0\x18\xed\x14U\xcb\xd0\x84\n\x18\x94:\xcfI\xadA\xfaf\xc8Sf\xb3\xf9\xbc`0)\x94`\xbd\xb6vy\xe4?9\xf7\x07\xe0=\x1a\xd2}\x0d\xd9\xa9E\xe7\xc1\x1f\x13\xd8\x0e\xe1\x81\xd5\x82\x1anbx\x06\xae\xd5\xe0\xbb\x895\xefz=\xa7\x9e\xd2/l\x06\xce\n-[\x1b\x8d7\xeb\x80\xbdk\xf7\xd4\xe0\x92\xae\x03\" /\xe7s\xa6E\xbe4qi\xec<\x8aC!\x89\xd3\"\xad\xe7q\xf5\xbe\xafIaKLG$\xf8\x16,4\xde\xbc\xaf\xe2{\xe9\xb0\xc3\x86\x1e9\x1aH\xf2\x8e\xe6),z\xad}r\x0dF;~\x9e\xd1v\xe8\x95\xd7\"\xbb\xedNh\xd7\x9d\x9c(dQ\x80\xcc\xcf\xa7\xd4\xeaB|\x1f\x98\xa4\xe9N(\x1dV\xcc\xd5\xf7\xe1\x1d\xe2\x1b\x9d\xa5\xdb\xde\xa5\xdb\xa8\x95\x9euWSi\x06\xf9BO\xc7\x9c\xa9\xa0\x14\xae[\x9e1\xcb#\x136\x84\xd5#\xbf|\x19\x8d>\xaf\xcb\xe84R\x10E\x8d2\x13V\xdee\xd1\x1f\x08\xdb\xbb\xee\x17g\xd6=c\xeb3\xe3\xf9\x8a\xbc1y\xec\xfa\x94\xa9\xf9\xb0\xbf2\xa2\xe1\xdfw8\xb6g\xaec\xb1\xefN(6\x01\x89.\x86\xe3\xde\xbd\x03\"\x1b\x8d\xdd\xb4l\x1d\x93_7\xa1\xa8kl \xe1\xd2qN\x84l\x9ffE\xe6i*\x84\xc9\xe4Q\xa0G\xb6Rn\xdfa\x9a\x14\xee1\xb9\xd0\x818Y\xcaH\xe2c\xbe\x92\xf7g\x90\xe6\xe5\xc7\x86\xa2\xa0E7\x7f>\xf5\x1a\xaa\xe5k\xae\xdcg\xfbj\xf1\x9aY\xbd\xabfJo\xfa.\xec\xcc\xbf\x9cgoe\xcf\xaa\x14\x8f\xcd\xf4\xcc\xd1\xb1\x97\xb2C^%\xf1\xacGZ\xb57\x17\xa6\xcc\xb1\xcb\x89\xeb\xcd\xed\xd5\xaf2\x86\x9f-\x1a\x81o\x80T\xd7\x80\x7fo\x87s\x16\x8f\xdf\x81\xde\x13\xf2O\xc6\xfd\x1d-\xd5\x12z-\xba\xb7p7K\xae\x82n3F\x8e\x7f\xe2\xb8\xd8j\xfd/\x00\x00\xff\xffPK\x07\x08L\xbb\xd3^\xd3\x05\x00\x00^\x12\x00\x00PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00r\x9b\xb2P\x06\xa8c9W\x04\x00\x00\x11\x1f\x00\x00\x16\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xb4\x81\x00\x00\x00\x00html/dashboard.go.htmlUT\x05\x00\x01\xa9\xe1\xc2^PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00r\x9b\xb2P\xe4\x92\xc0\x7fU\x02\x00\x00\x96\x06\x00\x00\x12\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xb4\x81\xa4\x04\x00\x00html/error.go.htmlUT\x05\x00\x01\xa9\xe1\xc2^PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00r\x9b\xb2P\x9c\xd5a\xdc\xaa\x00\x00\x00\xe7\x00\x00\x00\x13\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xb4\x81B\x07\x00\x00html/header.go.htmlUT\x05\x00\x01\xa9\xe1\xc2^PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00r\x9b\xb2P\x83\xba\x83\xe4\xf5\x00\x00\x00|\x01\x00\x00\x1b\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xb4\x816\x08\x00\x00img/account_circle-24px.svgUT\x05\x00\x01\xa9\xe1\xc2^PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00r\x9b\xb2P\xfc\xc6x\x8f\xb7\x00\x00\x00\xf9\x00\x00\x00\x12\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xb4\x81} \x00\x00img/error-24px.svgUT\x05\x00\x01\xa9\xe1\xc2^PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00r\x9b\xb2PK\xfe\x8b#M\x03\x00\x00d\x08\x00\x00\x10\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xb4\x81}\n\x00\x00img/pomerium.svgUT\x05\x00\x01\xa9\xe1\xc2^PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00r\x9b\xb2P\xf9\xfe\x13#\x13\x0f\x00\x00\xe5\x13\x00\x00\x1a\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xb4\x81\x11\x0e\x00\x00img/pomerium_circle_96.svgUT\x05\x00\x01\xa9\xe1\xc2^PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00r\x9b\xb2Puq\x02\xd2d\x01\x00\x00\x9e\x02\x00\x00#\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xb4\x81u\x1d\x00\x00img/supervised_user_circle-24px.svgUT\x05\x00\x01\xa9\xe1\xc2^PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00r\x9b\xb2PL\xbb\xd3^\xd3\x05\x00\x00^\x12\x00\x00\x0e\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xb4\x813\x1f\x00\x00style/main.cssUT\x05\x00\x01\xa9\xe1\xc2^PK\x05\x06\x00\x00\x00\x00 \x00 \x00\xb2\x02\x00\x00K%\x00\x00\x00\x00" fs.RegisterWithNamespace("web", data) } - \ No newline at end of file diff --git a/internal/grpc/authorize/authorize.pb.go b/internal/grpc/authorize/authorize.pb.go index 16885a3fd..c7cf7e9c2 100644 --- a/internal/grpc/authorize/authorize.pb.go +++ b/internal/grpc/authorize/authorize.pb.go @@ -1,30 +1,39 @@ // Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.23.0 +// protoc v3.12.1 // source: authorize.proto package authorize import ( context "context" - fmt "fmt" proto "github.com/golang/protobuf/proto" grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" - math "math" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" ) -// Reference imports to suppress errors if they are not otherwise used. -var _ = proto.Marshal -var _ = fmt.Errorf -var _ = math.Inf +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) -// This is a compile-time assertion to ensure that this generated file -// is compatible with the proto package it is being compiled against. -// A compilation error at this line likely means your copy of the -// proto package needs to be updated. -const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package +// This is a compile-time assertion that a sufficiently up-to-date version +// of the legacy proto package is being used. +const _ = proto.ProtoPackageIsVersion4 type IsAuthorizedRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + // User Context // UserToken string `protobuf:"bytes,1,opt,name=user_token,json=userToken,proto3" json:"user_token,omitempty"` @@ -41,254 +50,486 @@ type IsAuthorizedRequest struct { RequestRequestUri string `protobuf:"bytes,5,opt,name=request_request_uri,json=requestRequestUri,proto3" json:"request_request_uri,omitempty"` // RemoteAddr allows HTTP servers and other software to record // the network address that sent the request, usually for - RequestRemoteAddr string `protobuf:"bytes,6,opt,name=request_remote_addr,json=requestRemoteAddr,proto3" json:"request_remote_addr,omitempty"` - RequestHeaders map[string]*IsAuthorizedRequest_Headers `protobuf:"bytes,7,rep,name=request_headers,json=requestHeaders,proto3" json:"request_headers,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` + RequestRemoteAddr string `protobuf:"bytes,6,opt,name=request_remote_addr,json=requestRemoteAddr,proto3" json:"request_remote_addr,omitempty"` + RequestHeaders map[string]*IsAuthorizedRequest_Headers `protobuf:"bytes,7,rep,name=request_headers,json=requestHeaders,proto3" json:"request_headers,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` } -func (m *IsAuthorizedRequest) Reset() { *m = IsAuthorizedRequest{} } -func (m *IsAuthorizedRequest) String() string { return proto.CompactTextString(m) } -func (*IsAuthorizedRequest) ProtoMessage() {} +func (x *IsAuthorizedRequest) Reset() { + *x = IsAuthorizedRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_authorize_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *IsAuthorizedRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*IsAuthorizedRequest) ProtoMessage() {} + +func (x *IsAuthorizedRequest) ProtoReflect() protoreflect.Message { + mi := &file_authorize_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use IsAuthorizedRequest.ProtoReflect.Descriptor instead. func (*IsAuthorizedRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_ffbc3c71370bee9a, []int{0} + return file_authorize_proto_rawDescGZIP(), []int{0} } -func (m *IsAuthorizedRequest) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_IsAuthorizedRequest.Unmarshal(m, b) -} -func (m *IsAuthorizedRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_IsAuthorizedRequest.Marshal(b, m, deterministic) -} -func (m *IsAuthorizedRequest) XXX_Merge(src proto.Message) { - xxx_messageInfo_IsAuthorizedRequest.Merge(m, src) -} -func (m *IsAuthorizedRequest) XXX_Size() int { - return xxx_messageInfo_IsAuthorizedRequest.Size(m) -} -func (m *IsAuthorizedRequest) XXX_DiscardUnknown() { - xxx_messageInfo_IsAuthorizedRequest.DiscardUnknown(m) -} - -var xxx_messageInfo_IsAuthorizedRequest proto.InternalMessageInfo - -func (m *IsAuthorizedRequest) GetUserToken() string { - if m != nil { - return m.UserToken +func (x *IsAuthorizedRequest) GetUserToken() string { + if x != nil { + return x.UserToken } return "" } -func (m *IsAuthorizedRequest) GetRequestMethod() string { - if m != nil { - return m.RequestMethod +func (x *IsAuthorizedRequest) GetRequestMethod() string { + if x != nil { + return x.RequestMethod } return "" } -func (m *IsAuthorizedRequest) GetRequestUrl() string { - if m != nil { - return m.RequestUrl +func (x *IsAuthorizedRequest) GetRequestUrl() string { + if x != nil { + return x.RequestUrl } return "" } -func (m *IsAuthorizedRequest) GetRequestHost() string { - if m != nil { - return m.RequestHost +func (x *IsAuthorizedRequest) GetRequestHost() string { + if x != nil { + return x.RequestHost } return "" } -func (m *IsAuthorizedRequest) GetRequestRequestUri() string { - if m != nil { - return m.RequestRequestUri +func (x *IsAuthorizedRequest) GetRequestRequestUri() string { + if x != nil { + return x.RequestRequestUri } return "" } -func (m *IsAuthorizedRequest) GetRequestRemoteAddr() string { - if m != nil { - return m.RequestRemoteAddr +func (x *IsAuthorizedRequest) GetRequestRemoteAddr() string { + if x != nil { + return x.RequestRemoteAddr } return "" } -func (m *IsAuthorizedRequest) GetRequestHeaders() map[string]*IsAuthorizedRequest_Headers { - if m != nil { - return m.RequestHeaders +func (x *IsAuthorizedRequest) GetRequestHeaders() map[string]*IsAuthorizedRequest_Headers { + if x != nil { + return x.RequestHeaders + } + return nil +} + +type IsAuthorizedReply struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Allow bool `protobuf:"varint,1,opt,name=allow,proto3" json:"allow,omitempty"` + SessionExpired bool `protobuf:"varint,2,opt,name=session_expired,json=sessionExpired,proto3" json:"session_expired,omitempty"` // special case + DenyReasons []string `protobuf:"bytes,3,rep,name=deny_reasons,json=denyReasons,proto3" json:"deny_reasons,omitempty"` + SignedJwt string `protobuf:"bytes,4,opt,name=signed_jwt,json=signedJwt,proto3" json:"signed_jwt,omitempty"` + User string `protobuf:"bytes,5,opt,name=user,proto3" json:"user,omitempty"` + Email string `protobuf:"bytes,6,opt,name=email,proto3" json:"email,omitempty"` + Groups []string `protobuf:"bytes,7,rep,name=groups,proto3" json:"groups,omitempty"` + HttpStatus *HTTPStatus `protobuf:"bytes,8,opt,name=http_status,json=httpStatus,proto3" json:"http_status,omitempty"` +} + +func (x *IsAuthorizedReply) Reset() { + *x = IsAuthorizedReply{} + if protoimpl.UnsafeEnabled { + mi := &file_authorize_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *IsAuthorizedReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*IsAuthorizedReply) ProtoMessage() {} + +func (x *IsAuthorizedReply) ProtoReflect() protoreflect.Message { + mi := &file_authorize_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use IsAuthorizedReply.ProtoReflect.Descriptor instead. +func (*IsAuthorizedReply) Descriptor() ([]byte, []int) { + return file_authorize_proto_rawDescGZIP(), []int{1} +} + +func (x *IsAuthorizedReply) GetAllow() bool { + if x != nil { + return x.Allow + } + return false +} + +func (x *IsAuthorizedReply) GetSessionExpired() bool { + if x != nil { + return x.SessionExpired + } + return false +} + +func (x *IsAuthorizedReply) GetDenyReasons() []string { + if x != nil { + return x.DenyReasons + } + return nil +} + +func (x *IsAuthorizedReply) GetSignedJwt() string { + if x != nil { + return x.SignedJwt + } + return "" +} + +func (x *IsAuthorizedReply) GetUser() string { + if x != nil { + return x.User + } + return "" +} + +func (x *IsAuthorizedReply) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +func (x *IsAuthorizedReply) GetGroups() []string { + if x != nil { + return x.Groups + } + return nil +} + +func (x *IsAuthorizedReply) GetHttpStatus() *HTTPStatus { + if x != nil { + return x.HttpStatus + } + return nil +} + +type HTTPStatus struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"` + Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` + Headers map[string]string `protobuf:"bytes,3,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` +} + +func (x *HTTPStatus) Reset() { + *x = HTTPStatus{} + if protoimpl.UnsafeEnabled { + mi := &file_authorize_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *HTTPStatus) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HTTPStatus) ProtoMessage() {} + +func (x *HTTPStatus) ProtoReflect() protoreflect.Message { + mi := &file_authorize_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HTTPStatus.ProtoReflect.Descriptor instead. +func (*HTTPStatus) Descriptor() ([]byte, []int) { + return file_authorize_proto_rawDescGZIP(), []int{2} +} + +func (x *HTTPStatus) GetCode() int32 { + if x != nil { + return x.Code + } + return 0 +} + +func (x *HTTPStatus) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *HTTPStatus) GetHeaders() map[string]string { + if x != nil { + return x.Headers } return nil } // headers represents key-value pairs in an HTTP header; map[string][]string type IsAuthorizedRequest_Headers struct { - Value []string `protobuf:"bytes,1,rep,name=value,proto3" json:"value,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Value []string `protobuf:"bytes,1,rep,name=value,proto3" json:"value,omitempty"` } -func (m *IsAuthorizedRequest_Headers) Reset() { *m = IsAuthorizedRequest_Headers{} } -func (m *IsAuthorizedRequest_Headers) String() string { return proto.CompactTextString(m) } -func (*IsAuthorizedRequest_Headers) ProtoMessage() {} +func (x *IsAuthorizedRequest_Headers) Reset() { + *x = IsAuthorizedRequest_Headers{} + if protoimpl.UnsafeEnabled { + mi := &file_authorize_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *IsAuthorizedRequest_Headers) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*IsAuthorizedRequest_Headers) ProtoMessage() {} + +func (x *IsAuthorizedRequest_Headers) ProtoReflect() protoreflect.Message { + mi := &file_authorize_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use IsAuthorizedRequest_Headers.ProtoReflect.Descriptor instead. func (*IsAuthorizedRequest_Headers) Descriptor() ([]byte, []int) { - return fileDescriptor_ffbc3c71370bee9a, []int{0, 0} + return file_authorize_proto_rawDescGZIP(), []int{0, 0} } -func (m *IsAuthorizedRequest_Headers) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_IsAuthorizedRequest_Headers.Unmarshal(m, b) -} -func (m *IsAuthorizedRequest_Headers) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_IsAuthorizedRequest_Headers.Marshal(b, m, deterministic) -} -func (m *IsAuthorizedRequest_Headers) XXX_Merge(src proto.Message) { - xxx_messageInfo_IsAuthorizedRequest_Headers.Merge(m, src) -} -func (m *IsAuthorizedRequest_Headers) XXX_Size() int { - return xxx_messageInfo_IsAuthorizedRequest_Headers.Size(m) -} -func (m *IsAuthorizedRequest_Headers) XXX_DiscardUnknown() { - xxx_messageInfo_IsAuthorizedRequest_Headers.DiscardUnknown(m) -} - -var xxx_messageInfo_IsAuthorizedRequest_Headers proto.InternalMessageInfo - -func (m *IsAuthorizedRequest_Headers) GetValue() []string { - if m != nil { - return m.Value +func (x *IsAuthorizedRequest_Headers) GetValue() []string { + if x != nil { + return x.Value } return nil } -type IsAuthorizedReply struct { - Allow bool `protobuf:"varint,1,opt,name=allow,proto3" json:"allow,omitempty"` - SessionExpired bool `protobuf:"varint,2,opt,name=session_expired,json=sessionExpired,proto3" json:"session_expired,omitempty"` - DenyReasons []string `protobuf:"bytes,3,rep,name=deny_reasons,json=denyReasons,proto3" json:"deny_reasons,omitempty"` - SignedJwt string `protobuf:"bytes,4,opt,name=signed_jwt,json=signedJwt,proto3" json:"signed_jwt,omitempty"` - User string `protobuf:"bytes,5,opt,name=user,proto3" json:"user,omitempty"` - Email string `protobuf:"bytes,6,opt,name=email,proto3" json:"email,omitempty"` - Groups []string `protobuf:"bytes,7,rep,name=groups,proto3" json:"groups,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` +var File_authorize_proto protoreflect.FileDescriptor + +var file_authorize_proto_rawDesc = []byte{ + 0x0a, 0x0f, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x12, 0x09, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x22, 0xe8, 0x03, 0x0a, + 0x13, 0x49, 0x73, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x74, 0x6f, 0x6b, + 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x75, 0x73, 0x65, 0x72, 0x54, 0x6f, + 0x6b, 0x65, 0x6e, 0x12, 0x25, 0x0a, 0x0e, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x6d, + 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x72, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0a, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x55, 0x72, 0x6c, 0x12, 0x21, 0x0a, 0x0c, 0x72, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x68, 0x6f, 0x73, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0b, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x6f, 0x73, 0x74, 0x12, 0x2e, + 0x0a, 0x13, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x5f, 0x75, 0x72, 0x69, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x72, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x55, 0x72, 0x69, 0x12, 0x2e, + 0x0a, 0x13, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, + 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x72, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x41, 0x64, 0x64, 0x72, 0x12, 0x5b, + 0x0a, 0x0f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, + 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, + 0x69, 0x7a, 0x65, 0x2e, 0x49, 0x73, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, + 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0e, 0x72, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x1a, 0x1f, 0x0a, 0x07, 0x48, + 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x69, 0x0a, 0x13, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x3c, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, + 0x2e, 0x49, 0x73, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x2e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x52, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x8e, 0x02, 0x0a, 0x11, 0x49, 0x73, 0x41, 0x75, + 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x14, 0x0a, + 0x05, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x61, 0x6c, + 0x6c, 0x6f, 0x77, 0x12, 0x27, 0x0a, 0x0f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x65, + 0x78, 0x70, 0x69, 0x72, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x73, 0x65, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x64, 0x12, 0x21, 0x0a, 0x0c, + 0x64, 0x65, 0x6e, 0x79, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x6e, 0x79, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x73, 0x12, + 0x1d, 0x0a, 0x0a, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x6a, 0x77, 0x74, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x4a, 0x77, 0x74, 0x12, 0x12, + 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, + 0x65, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x06, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x16, 0x0a, 0x06, 0x67, 0x72, 0x6f, 0x75, + 0x70, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, + 0x12, 0x36, 0x0a, 0x0b, 0x68, 0x74, 0x74, 0x70, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, + 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, + 0x65, 0x2e, 0x48, 0x54, 0x54, 0x50, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x0a, 0x68, 0x74, + 0x74, 0x70, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0xb4, 0x01, 0x0a, 0x0a, 0x48, 0x54, 0x54, + 0x50, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, + 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x3c, 0x0a, 0x07, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, + 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, + 0x7a, 0x65, 0x2e, 0x48, 0x54, 0x54, 0x50, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2e, 0x48, 0x65, + 0x61, 0x64, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x68, 0x65, 0x61, 0x64, + 0x65, 0x72, 0x73, 0x1a, 0x3a, 0x0a, 0x0c, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x32, + 0x5c, 0x0a, 0x0a, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x72, 0x12, 0x4e, 0x0a, + 0x0c, 0x49, 0x73, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x12, 0x1e, 0x2e, + 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x2e, 0x49, 0x73, 0x41, 0x75, 0x74, 0x68, + 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, + 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x2e, 0x49, 0x73, 0x41, 0x75, 0x74, 0x68, + 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, } -func (m *IsAuthorizedReply) Reset() { *m = IsAuthorizedReply{} } -func (m *IsAuthorizedReply) String() string { return proto.CompactTextString(m) } -func (*IsAuthorizedReply) ProtoMessage() {} -func (*IsAuthorizedReply) Descriptor() ([]byte, []int) { - return fileDescriptor_ffbc3c71370bee9a, []int{1} +var ( + file_authorize_proto_rawDescOnce sync.Once + file_authorize_proto_rawDescData = file_authorize_proto_rawDesc +) + +func file_authorize_proto_rawDescGZIP() []byte { + file_authorize_proto_rawDescOnce.Do(func() { + file_authorize_proto_rawDescData = protoimpl.X.CompressGZIP(file_authorize_proto_rawDescData) + }) + return file_authorize_proto_rawDescData } -func (m *IsAuthorizedReply) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_IsAuthorizedReply.Unmarshal(m, b) +var file_authorize_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_authorize_proto_goTypes = []interface{}{ + (*IsAuthorizedRequest)(nil), // 0: authorize.IsAuthorizedRequest + (*IsAuthorizedReply)(nil), // 1: authorize.IsAuthorizedReply + (*HTTPStatus)(nil), // 2: authorize.HTTPStatus + (*IsAuthorizedRequest_Headers)(nil), // 3: authorize.IsAuthorizedRequest.Headers + nil, // 4: authorize.IsAuthorizedRequest.RequestHeadersEntry + nil, // 5: authorize.HTTPStatus.HeadersEntry } -func (m *IsAuthorizedReply) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_IsAuthorizedReply.Marshal(b, m, deterministic) -} -func (m *IsAuthorizedReply) XXX_Merge(src proto.Message) { - xxx_messageInfo_IsAuthorizedReply.Merge(m, src) -} -func (m *IsAuthorizedReply) XXX_Size() int { - return xxx_messageInfo_IsAuthorizedReply.Size(m) -} -func (m *IsAuthorizedReply) XXX_DiscardUnknown() { - xxx_messageInfo_IsAuthorizedReply.DiscardUnknown(m) +var file_authorize_proto_depIdxs = []int32{ + 4, // 0: authorize.IsAuthorizedRequest.request_headers:type_name -> authorize.IsAuthorizedRequest.RequestHeadersEntry + 2, // 1: authorize.IsAuthorizedReply.http_status:type_name -> authorize.HTTPStatus + 5, // 2: authorize.HTTPStatus.headers:type_name -> authorize.HTTPStatus.HeadersEntry + 3, // 3: authorize.IsAuthorizedRequest.RequestHeadersEntry.value:type_name -> authorize.IsAuthorizedRequest.Headers + 0, // 4: authorize.Authorizer.IsAuthorized:input_type -> authorize.IsAuthorizedRequest + 1, // 5: authorize.Authorizer.IsAuthorized:output_type -> authorize.IsAuthorizedReply + 5, // [5:6] is the sub-list for method output_type + 4, // [4:5] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name } -var xxx_messageInfo_IsAuthorizedReply proto.InternalMessageInfo - -func (m *IsAuthorizedReply) GetAllow() bool { - if m != nil { - return m.Allow +func init() { file_authorize_proto_init() } +func file_authorize_proto_init() { + if File_authorize_proto != nil { + return } - return false -} - -func (m *IsAuthorizedReply) GetSessionExpired() bool { - if m != nil { - return m.SessionExpired + if !protoimpl.UnsafeEnabled { + file_authorize_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*IsAuthorizedRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_authorize_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*IsAuthorizedReply); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_authorize_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*HTTPStatus); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_authorize_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*IsAuthorizedRequest_Headers); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } - return false -} - -func (m *IsAuthorizedReply) GetDenyReasons() []string { - if m != nil { - return m.DenyReasons - } - return nil -} - -func (m *IsAuthorizedReply) GetSignedJwt() string { - if m != nil { - return m.SignedJwt - } - return "" -} - -func (m *IsAuthorizedReply) GetUser() string { - if m != nil { - return m.User - } - return "" -} - -func (m *IsAuthorizedReply) GetEmail() string { - if m != nil { - return m.Email - } - return "" -} - -func (m *IsAuthorizedReply) GetGroups() []string { - if m != nil { - return m.Groups - } - return nil -} - -func init() { - proto.RegisterType((*IsAuthorizedRequest)(nil), "authorize.IsAuthorizedRequest") - proto.RegisterMapType((map[string]*IsAuthorizedRequest_Headers)(nil), "authorize.IsAuthorizedRequest.RequestHeadersEntry") - proto.RegisterType((*IsAuthorizedRequest_Headers)(nil), "authorize.IsAuthorizedRequest.Headers") - proto.RegisterType((*IsAuthorizedReply)(nil), "authorize.IsAuthorizedReply") -} - -func init() { - proto.RegisterFile("authorize.proto", fileDescriptor_ffbc3c71370bee9a) -} - -var fileDescriptor_ffbc3c71370bee9a = []byte{ - // 431 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x93, 0xcf, 0x6e, 0x13, 0x31, - 0x10, 0xc6, 0xd9, 0x6e, 0x9b, 0x76, 0x27, 0xa5, 0xa1, 0x13, 0x84, 0xac, 0x08, 0x68, 0x88, 0x04, - 0xe4, 0x94, 0x43, 0xb8, 0x20, 0xc4, 0xa5, 0x87, 0x4a, 0x05, 0x09, 0x0e, 0x16, 0x9c, 0x40, 0x5a, - 0x2d, 0xf2, 0xa8, 0x59, 0xea, 0xac, 0x17, 0xdb, 0x4b, 0x58, 0x1e, 0x94, 0x67, 0xe0, 0x31, 0x90, - 0xff, 0xa5, 0x14, 0x15, 0x38, 0xad, 0xe7, 0xe7, 0xcf, 0xe3, 0x99, 0xf9, 0xbc, 0x30, 0xaa, 0x3a, - 0xbb, 0x52, 0xba, 0xfe, 0x4e, 0x8b, 0x56, 0x2b, 0xab, 0xb0, 0xd8, 0x82, 0xd9, 0xcf, 0x1c, 0xc6, - 0xaf, 0xcc, 0x69, 0x8a, 0x05, 0xa7, 0x2f, 0x1d, 0x19, 0x8b, 0x0f, 0x00, 0x3a, 0x43, 0xba, 0xb4, - 0xea, 0x92, 0x1a, 0x96, 0x4d, 0xb3, 0x79, 0xc1, 0x0b, 0x47, 0xde, 0x39, 0x80, 0x8f, 0xe1, 0x48, - 0x07, 0x65, 0xb9, 0x26, 0xbb, 0x52, 0x82, 0xed, 0x78, 0xc9, 0xed, 0x48, 0xdf, 0x78, 0x88, 0x27, - 0x30, 0x4c, 0xb2, 0x4e, 0x4b, 0x96, 0x7b, 0x0d, 0x44, 0xf4, 0x5e, 0x4b, 0x7c, 0x04, 0x87, 0x49, - 0xb0, 0x52, 0xc6, 0xb2, 0x5d, 0xaf, 0x48, 0x87, 0xce, 0x95, 0xb1, 0xb8, 0x80, 0x71, 0x92, 0x5c, - 0xe5, 0xaa, 0xd9, 0x9e, 0x57, 0x1e, 0x47, 0xc4, 0x53, 0xca, 0xfa, 0xba, 0x7e, 0xad, 0x2c, 0x95, - 0x95, 0x10, 0x9a, 0x0d, 0xfe, 0xd0, 0xbb, 0x9d, 0x53, 0x21, 0x34, 0x7e, 0x80, 0xd1, 0xb6, 0x04, - 0xaa, 0x04, 0x69, 0xc3, 0xf6, 0xa7, 0xf9, 0x7c, 0xb8, 0x5c, 0x2e, 0xae, 0xe6, 0x76, 0xc3, 0x88, - 0x16, 0xf1, 0x7b, 0x1e, 0x0e, 0x9d, 0x35, 0x56, 0xf7, 0x3c, 0x4d, 0x25, 0xc2, 0xc9, 0x09, 0xec, - 0xc7, 0x25, 0xde, 0x85, 0xbd, 0xaf, 0x95, 0xec, 0x88, 0x65, 0xd3, 0x7c, 0x5e, 0xf0, 0x10, 0x4c, - 0x6a, 0x18, 0xdf, 0x90, 0x07, 0xef, 0x40, 0x7e, 0x49, 0x7d, 0x9c, 0xbb, 0x5b, 0xe2, 0xcb, 0x74, - 0xdc, 0x0d, 0x7a, 0xb8, 0x7c, 0xf2, 0x9f, 0xe2, 0x62, 0xb6, 0x78, 0xcd, 0x8b, 0x9d, 0xe7, 0xd9, - 0xec, 0x47, 0x06, 0xc7, 0xd7, 0xa5, 0xad, 0xec, 0x5d, 0x59, 0x95, 0x94, 0x6a, 0xe3, 0xef, 0x3a, - 0xe0, 0x21, 0xc0, 0xa7, 0x30, 0x32, 0x64, 0x4c, 0xad, 0x9a, 0x92, 0xbe, 0xb5, 0xb5, 0xa6, 0x60, - 0xf0, 0x01, 0x3f, 0x8a, 0xf8, 0x2c, 0x50, 0x67, 0xa0, 0xa0, 0xa6, 0x2f, 0x35, 0x55, 0x46, 0x35, - 0x86, 0xe5, 0xbe, 0xb9, 0xa1, 0x63, 0x3c, 0x20, 0xf7, 0x94, 0x4c, 0x7d, 0xd1, 0x90, 0x28, 0x3f, - 0x6f, 0x92, 0xc3, 0x45, 0x20, 0xaf, 0x37, 0x16, 0x11, 0x76, 0xdd, 0xbb, 0x8a, 0x86, 0xfa, 0xb5, - 0x2b, 0x8a, 0xd6, 0x55, 0x2d, 0xa3, 0x6b, 0x21, 0xc0, 0x7b, 0x30, 0xb8, 0xd0, 0xaa, 0x6b, 0x83, - 0x41, 0x05, 0x8f, 0xd1, 0xf2, 0x23, 0xc0, 0xb6, 0x2b, 0x8d, 0x6f, 0xe1, 0xf0, 0xf7, 0x2e, 0xf1, - 0xe1, 0xbf, 0x27, 0x35, 0xb9, 0xff, 0xd7, 0xfd, 0x56, 0xf6, 0xb3, 0x5b, 0x9f, 0x06, 0xfe, 0x9f, - 0x79, 0xf6, 0x2b, 0x00, 0x00, 0xff, 0xff, 0x8b, 0x10, 0x59, 0xee, 0x46, 0x03, 0x00, 0x00, + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_authorize_proto_rawDesc, + NumEnums: 0, + NumMessages: 6, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_authorize_proto_goTypes, + DependencyIndexes: file_authorize_proto_depIdxs, + MessageInfos: file_authorize_proto_msgTypes, + }.Build() + File_authorize_proto = out.File + file_authorize_proto_rawDesc = nil + file_authorize_proto_goTypes = nil + file_authorize_proto_depIdxs = nil } // Reference imports to suppress errors if they are not otherwise used. @@ -332,7 +573,7 @@ type AuthorizerServer interface { type UnimplementedAuthorizerServer struct { } -func (*UnimplementedAuthorizerServer) IsAuthorized(ctx context.Context, req *IsAuthorizedRequest) (*IsAuthorizedReply, error) { +func (*UnimplementedAuthorizerServer) IsAuthorized(context.Context, *IsAuthorizedRequest) (*IsAuthorizedReply, error) { return nil, status.Errorf(codes.Unimplemented, "method IsAuthorized not implemented") } diff --git a/internal/grpc/authorize/authorize.proto b/internal/grpc/authorize/authorize.proto index a34e90105..0f8beaf1d 100644 --- a/internal/grpc/authorize/authorize.proto +++ b/internal/grpc/authorize/authorize.proto @@ -37,5 +37,11 @@ message IsAuthorizedReply { string user = 5; string email = 6; repeated string groups = 7; + HTTPStatus http_status = 8; } +message HTTPStatus { + int32 code = 1; + string message = 2; + map headers = 3; +} diff --git a/internal/grpc/cache/cache.pb.go b/internal/grpc/cache/cache.pb.go index 6cd3c7aff..9c396e558 100644 --- a/internal/grpc/cache/cache.pb.go +++ b/internal/grpc/cache/cache.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.21.0 -// protoc v3.11.4 +// protoc-gen-go v1.23.0 +// protoc v3.12.1 // source: cache.proto package cache diff --git a/internal/httputil/httputil.go b/internal/httputil/httputil.go new file mode 100644 index 000000000..50444f3c9 --- /dev/null +++ b/internal/httputil/httputil.go @@ -0,0 +1,6 @@ +package httputil + +// StatusInvalidClientCertificate is the status code returned when a +// client's certificate is invalid. This is the same status code used +// by nginx for this purpose. +const StatusInvalidClientCertificate = 495 diff --git a/scripts/protoc b/scripts/protoc new file mode 100755 index 000000000..7f0ed0ce9 --- /dev/null +++ b/scripts/protoc @@ -0,0 +1,22 @@ +#!/bin/bash +set -euo pipefail + +_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +_protoc_version="3.12.1" +_protoc_path="/tmp/pomerium-protoc/protoc-$_protoc_version" +_os="linux" +if [ "$(uname -s)" == "Darwin" ]; then + _os="osx" +fi + +if [ ! -f "$_protoc_path" ]; then + echo "downloading protoc" + mkdir -p "/tmp/pomerium-protoc" + curl -L \ + -o protoc.zip \ + "https://github.com/protocolbuffers/protobuf/releases/download/v$_protoc_version/protoc-$_protoc_version-$_os-x86_64.zip" + unzip -p protoc.zip bin/protoc >"$_protoc_path" +fi +chmod +x "$_protoc_path" + +exec "$_protoc_path" --plugin="protoc-gen-go=$_dir/protoc-gen-go" "$@" diff --git a/scripts/protoc-gen-go b/scripts/protoc-gen-go new file mode 100755 index 000000000..b4065945f --- /dev/null +++ b/scripts/protoc-gen-go @@ -0,0 +1,3 @@ +#!/bin/bash +set -euo pipefail +exec go run github.com/golang/protobuf/protoc-gen-go "$@"