From 2f13488598af0e30c75c1a80f0356a42a592e9bf Mon Sep 17 00:00:00 2001 From: Bobby DeSimone Date: Sun, 2 Feb 2020 11:18:22 -0800 Subject: [PATCH] authorize: use opa for policy engine (#474) Signed-off-by: Bobby DeSimone --- authenticate/handlers.go | 12 +- authenticate/handlers_test.go | 20 +- authorize/authorize.go | 92 +++-- authorize/authorize_test.go | 55 +-- authorize/evaluator/evaluator.go | 51 +++ authorize/evaluator/mock/mock.go | 35 ++ authorize/evaluator/opa/opa.go | 155 ++++++++ .../opa/opa_test.go} | 128 ++++--- authorize/evaluator/opa/rego.go | 87 +++++ authorize/grpc.go | 59 ++- authorize/grpc_test.go | 55 +-- authorize/identity.go | 187 --------- config/options_test.go | 2 +- config/policy.go | 29 +- config/policy_test.go | 27 +- go.mod | 5 +- go.sum | 46 ++- internal/frontend/statik/statik.go | 2 +- internal/grpc/authorize/authorize.pb.go | 354 ++++++++++++------ internal/grpc/authorize/authorize.proto | 42 ++- .../grpc/authorize/client/authorize_client.go | 56 +-- .../authorize/client/authorize_client_test.go | 78 ---- internal/grpc/authorize/client/mock.go | 7 +- .../client/mock_authorize/mock_authorize.go | 130 ------- internal/sessions/cache/cache_store.go | 12 +- internal/sessions/cache/cache_store_test.go | 2 +- internal/sessions/cookie/cookie_store.go | 12 +- internal/sessions/cookie/cookie_store_test.go | 2 +- internal/sessions/cookie/middleware_test.go | 2 +- internal/sessions/header/header_store.go | 14 +- internal/sessions/header/middleware_test.go | 2 +- internal/sessions/middleware.go | 28 +- internal/sessions/middleware_test.go | 10 +- internal/sessions/mock/mock_store.go | 5 +- internal/sessions/mock/mock_store_test.go | 2 +- .../sessions/queryparam/middleware_test.go | 2 +- internal/sessions/queryparam/query_store.go | 14 +- internal/sessions/store.go | 2 +- proxy/forward_auth.go | 5 +- proxy/forward_auth_test.go | 4 +- proxy/handlers.go | 8 +- proxy/handlers_test.go | 8 +- proxy/middleware.go | 22 +- proxy/middleware_test.go | 12 +- proxy/proxy.go | 12 +- 45 files changed, 1022 insertions(+), 872 deletions(-) create mode 100644 authorize/evaluator/evaluator.go create mode 100644 authorize/evaluator/mock/mock.go create mode 100644 authorize/evaluator/opa/opa.go rename authorize/{identity_test.go => evaluator/opa/opa_test.go} (57%) create mode 100644 authorize/evaluator/opa/rego.go delete mode 100644 authorize/identity.go delete mode 100644 internal/grpc/authorize/client/authorize_client_test.go delete mode 100644 internal/grpc/authorize/client/mock_authorize/mock_authorize.go diff --git a/authenticate/handlers.go b/authenticate/handlers.go index 535d9ab54..75fc940ee 100644 --- a/authenticate/handlers.go +++ b/authenticate/handlers.go @@ -74,7 +74,7 @@ func (a *Authenticate) Handler() http.Handler { // session state is attached to the users's request context. func (a *Authenticate) VerifySession(next http.Handler) http.Handler { return httputil.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { - state, err := sessions.FromContext(r.Context()) + state, _, err := sessions.FromContext(r.Context()) if errors.Is(err, sessions.ErrExpired) { ctx, err := a.refresh(w, r, state) if err != nil { @@ -103,7 +103,7 @@ func (a *Authenticate) refresh(w http.ResponseWriter, r *http.Request, s *sessio return nil, fmt.Errorf("authenticate: refresh save failed: %w", err) } // return the new session and add it to the current request context - return sessions.NewContext(ctx, newSession, err), nil + return sessions.NewContext(ctx, newSession, "", err), nil } // RobotsTxt handles the /robots.txt route. @@ -142,7 +142,7 @@ func (a *Authenticate) SignIn(w http.ResponseWriter, r *http.Request) error { jwtAudience = append(jwtAudience, fwdAuth) } - s, err := sessions.FromContext(r.Context()) + s, _, err := sessions.FromContext(r.Context()) if err != nil { return httputil.NewError(http.StatusBadRequest, err) } @@ -197,7 +197,7 @@ func (a *Authenticate) SignIn(w http.ResponseWriter, r *http.Request) error { // SignOut signs the user out and attempts to revoke the user's identity session // Handles both GET and POST. func (a *Authenticate) SignOut(w http.ResponseWriter, r *http.Request) error { - session, err := sessions.FromContext(r.Context()) + session, _, err := sessions.FromContext(r.Context()) if err != nil { return httputil.NewError(http.StatusBadRequest, err) } @@ -318,7 +318,7 @@ func (a *Authenticate) getOAuthCallback(w http.ResponseWriter, r *http.Request) // tokens and state with the identity provider. If successful, a new signed JWT // and refresh token (`refresh_token`) are returned as JSON func (a *Authenticate) RefreshAPI(w http.ResponseWriter, r *http.Request) error { - s, err := sessions.FromContext(r.Context()) + s, _, err := sessions.FromContext(r.Context()) if err != nil && !errors.Is(err, sessions.ErrExpired) { return httputil.NewError(http.StatusBadRequest, err) } @@ -359,7 +359,7 @@ func (a *Authenticate) RefreshAPI(w http.ResponseWriter, r *http.Request) error // middleware. This handler is responsible for creating a new route scoped // session and returning it. func (a *Authenticate) Refresh(w http.ResponseWriter, r *http.Request) error { - s, err := sessions.FromContext(r.Context()) + s, _, err := sessions.FromContext(r.Context()) if err != nil { return httputil.NewError(http.StatusBadRequest, err) } diff --git a/authenticate/handlers_test.go b/authenticate/handlers_test.go index 5555373fe..53c7d49a4 100644 --- a/authenticate/handlers_test.go +++ b/authenticate/handlers_test.go @@ -151,9 +151,9 @@ func TestAuthenticate_SignIn(t *testing.T) { uri.RawQuery = queryString.Encode() r := httptest.NewRequest(http.MethodGet, uri.String(), nil) r.Header.Set("Accept", "application/json") - state, err := tt.session.LoadSession(r) + state, _, err := tt.session.LoadSession(r) ctx := r.Context() - ctx = sessions.NewContext(ctx, state, err) + ctx = sessions.NewContext(ctx, state, "", err) r = r.WithContext(ctx) w := httptest.NewRecorder() @@ -206,9 +206,9 @@ func TestAuthenticate_SignOut(t *testing.T) { params.Add(urlutil.QueryRedirectURI, tt.redirectURL) u.RawQuery = params.Encode() r := httptest.NewRequest(tt.method, u.String(), nil) - state, _ := tt.sessionStore.LoadSession(r) + state, _, _ := tt.sessionStore.LoadSession(r) ctx := r.Context() - ctx = sessions.NewContext(ctx, state, tt.ctxError) + ctx = sessions.NewContext(ctx, state, "", tt.ctxError) r = r.WithContext(ctx) r.Header.Set("Accept", "application/json") @@ -349,9 +349,9 @@ func TestAuthenticate_SessionValidatorMiddleware(t *testing.T) { cookieCipher: aead, } r := httptest.NewRequest("GET", "/", nil) - state, _ := tt.session.LoadSession(r) + state, _, _ := tt.session.LoadSession(r) ctx := r.Context() - ctx = sessions.NewContext(ctx, state, tt.ctxError) + ctx = sessions.NewContext(ctx, state, "", tt.ctxError) r = r.WithContext(ctx) r.Header.Set("Accept", "application/json") @@ -408,9 +408,9 @@ func TestAuthenticate_RefreshAPI(t *testing.T) { cookieCipher: aead, } r := httptest.NewRequest("GET", "/", nil) - state, _ := tt.session.LoadSession(r) + state, _, _ := tt.session.LoadSession(r) ctx := r.Context() - ctx = sessions.NewContext(ctx, state, tt.ctxError) + ctx = sessions.NewContext(ctx, state, "", tt.ctxError) r = r.WithContext(ctx) r.Header.Set("Accept", "application/json") @@ -459,9 +459,9 @@ func TestAuthenticate_Refresh(t *testing.T) { cookieCipher: aead, } r := httptest.NewRequest("GET", "/", nil) - state, _ := tt.session.LoadSession(r) + state, _, _ := tt.session.LoadSession(r) ctx := r.Context() - ctx = sessions.NewContext(ctx, state, tt.ctxError) + ctx = sessions.NewContext(ctx, state, "", tt.ctxError) r = r.WithContext(ctx) r.Header.Set("Accept", "application/json") diff --git a/authorize/authorize.go b/authorize/authorize.go index cd3ce3076..2314aaff2 100644 --- a/authorize/authorize.go +++ b/authorize/authorize.go @@ -3,69 +3,67 @@ package authorize // import "github.com/pomerium/pomerium/authorize" import ( - "encoding/base64" + "context" "fmt" + "github.com/pomerium/pomerium/authorize/evaluator" + "github.com/pomerium/pomerium/authorize/evaluator/opa" "github.com/pomerium/pomerium/config" + "github.com/pomerium/pomerium/internal/cryptutil" "github.com/pomerium/pomerium/internal/log" "github.com/pomerium/pomerium/internal/telemetry/metrics" + "github.com/pomerium/pomerium/internal/telemetry/trace" ) -// ValidateOptions checks to see if configuration values are valid for the -// authorize service. Returns first error, if found. -func ValidateOptions(o config.Options) error { - decoded, err := base64.StdEncoding.DecodeString(o.SharedKey) - if err != nil { - return fmt.Errorf("authorize: `SHARED_SECRET` malformed base64: %w", err) - } - if len(decoded) != 32 { - return fmt.Errorf("authorize: `SHARED_SECRET` want 32 but got %d bytes", len(decoded)) - } - return nil -} - // Authorize struct holds type Authorize struct { - SharedKey string - - identityAccess IdentityValidator - // contextValidator - // deviceValidator + pe evaluator.Evaluator } -// New validates and creates a new Authorize service from a set of Options +// New validates and creates a new Authorize service from a set of config options. func New(opts config.Options) (*Authorize, error) { - if err := ValidateOptions(opts); err != nil { + if err := validateOptions(opts); err != nil { + return nil, fmt.Errorf("authorize: bad options: %w", err) + } + var a Authorize + var err error + if a.pe, err = newPolicyEvaluator(&opts); err != nil { return nil, err } - // errors handled by validate - sharedKey, _ := base64.StdEncoding.DecodeString(opts.SharedKey) - return &Authorize{ - SharedKey: string(sharedKey), - identityAccess: NewIdentityWhitelist(opts.Policies, opts.Administrators), - }, nil + return &a, nil } -// NewIdentityWhitelist returns an indentity validator. -// todo(bdd) : a radix-tree implementation is probably more efficient -func NewIdentityWhitelist(policies []config.Policy, admins []string) IdentityValidator { - metrics.AddPolicyCountCallback("authorize", func() int64 { - return int64(len(policies)) - }) - return newIdentityWhitelistMap(policies, admins) -} - -// ValidIdentity returns if an identity is authorized to access a route resource. -func (a *Authorize) ValidIdentity(route string, identity *Identity) bool { - return a.identityAccess.Valid(route, identity) -} - -// UpdateOptions updates internal structures based on config.Options -func (a *Authorize) UpdateOptions(o config.Options) error { - if a == nil { - return nil +func validateOptions(o config.Options) error { + if _, err := cryptutil.NewAEADCipherFromBase64(o.SharedKey); err != nil { + return fmt.Errorf("bad shared_secret: %w", err) + } + return nil +} + +// newPolicyEvaluator returns an policy evaluator. +func newPolicyEvaluator(opts *config.Options) (evaluator.Evaluator, error) { + metrics.AddPolicyCountCallback("authorize", func() int64 { + return int64(len(opts.Policies)) + }) + ctx := context.Background() + ctx, span := trace.StartSpan(ctx, "authorize.newPolicyEvaluator") + defer span.End() + + data := map[string]interface{}{ + "shared_key": opts.SharedKey, + "route_policies": opts.Policies, + "admins": opts.Administrators, + } + return opa.New(ctx, &opa.Options{Data: data}) +} + +// UpdateOptions implements the OptionsUpdater interface and updates internal +// structures based on config.Options +func (a *Authorize) UpdateOptions(opts config.Options) error { + log.Info().Str("checksum", opts.Checksum()).Msg("authorize: updating options") + var err error + if a.pe, err = newPolicyEvaluator(&opts); err != nil { + return err } - log.Info().Msg("authorize: updating options") - a.identityAccess = NewIdentityWhitelist(o.Policies, o.Administrators) return nil } diff --git a/authorize/authorize_test.go b/authorize/authorize_test.go index e52b29edb..b701abddb 100644 --- a/authorize/authorize_test.go +++ b/authorize/authorize_test.go @@ -3,14 +3,14 @@ package authorize import ( "testing" + "github.com/pomerium/pomerium/authorize/evaluator" + "github.com/pomerium/pomerium/authorize/evaluator/mock" "github.com/pomerium/pomerium/config" ) func TestNew(t *testing.T) { t.Parallel() - policies := testPolicies(t) - tests := []struct { name string SharedKey string @@ -22,6 +22,7 @@ func TestNew(t *testing.T) { {"really bad shared secret", "sup", policies, true}, {"validation error, short secret", "AZA85podM73CjLCjViDNz1EUvvejKpWp7Hysr0knXA==", policies, true}, {"empty options", "", []config.Policy{}, true}, // special case + } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -39,7 +40,7 @@ func TestNew(t *testing.T) { } func testPolicies(t *testing.T) []config.Policy { - testPolicy := config.Policy{From: "https://pomerium.io", To: "http://httpbin.org", AllowedEmails: []string{"test@gmail.com"}} + testPolicy := config.Policy{From: "https://pomerium.io", To: "http://httpbin.org", AllowedUsers: []string{"test@gmail.com"}} err := testPolicy.Validate() if err != nil { t.Fatal(err) @@ -47,55 +48,27 @@ func testPolicies(t *testing.T) []config.Policy { policies := []config.Policy{ testPolicy, } - return policies } -func Test_UpdateOptions(t *testing.T) { +func TestAuthorize_UpdateOptions(t *testing.T) { t.Parallel() - policies := testPolicies(t) - newPolicy := config.Policy{From: "https://source.example", To: "http://destination.example", AllowedEmails: []string{"test@gmail.com"}} - if err := newPolicy.Validate(); err != nil { - t.Fatal(err) - } - - newPolicies := []config.Policy{ - newPolicy, - } - identity := &Identity{Email: "test@gmail.com"} tests := []struct { - name string - SharedKey string - Policies []config.Policy - newPolices []config.Policy - route string - wantAllowed bool + name string + pe evaluator.Evaluator + opts config.Options + wantErr bool }{ - {"good", "gXK6ggrlIW2HyKyUF9rUO4azrDgxhDPWqw9y+lJU7B8=", policies, policies, "pomerium.io", true}, - {"changed", "gXK6ggrlIW2HyKyUF9rUO4azrDgxhDPWqw9y+lJU7B8=", policies, newPolicies, "source.example", true}, - {"changed and missing", "gXK6ggrlIW2HyKyUF9rUO4azrDgxhDPWqw9y+lJU7B8=", policies, newPolicies, "pomerium.io", false}, + {"good", &mock.PolicyEvaluator{}, config.Options{}, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - o := config.Options{SharedKey: tt.SharedKey, Policies: tt.Policies} - authorize, err := New(o) - if err != nil { - t.Fatal(err) + a := &Authorize{ + pe: tt.pe, } - o.Policies = tt.newPolices - if err := authorize.UpdateOptions(o); err != nil { - t.Fatal(err) - } - - allowed := authorize.ValidIdentity(tt.route, identity) - if allowed != tt.wantAllowed { - t.Errorf("New() allowed = %v, wantAllowed %v", allowed, tt.wantAllowed) - return + if err := a.UpdateOptions(tt.opts); (err != nil) != tt.wantErr { + t.Errorf("Authorize.UpdateOptions() error = %v, wantErr %v", err, tt.wantErr) } }) } - - // Test nil - var a *Authorize - a.UpdateOptions(config.Options{}) } diff --git a/authorize/evaluator/evaluator.go b/authorize/evaluator/evaluator.go new file mode 100644 index 000000000..398692be7 --- /dev/null +++ b/authorize/evaluator/evaluator.go @@ -0,0 +1,51 @@ +// Package evaluator defines a Evaluator interfaces that can be implemented by +// a policy evaluator framework. +package evaluator + +import ( + "context" +) + +// Evaluator specifies the interface for a policy engine. +type Evaluator interface { + IsAuthorized(ctx context.Context, input interface{}) (bool, error) + IsAdmin(ctx context.Context, input interface{}) (bool, error) + PutData(ctx context.Context, data map[string]interface{}) error +} + +// A Request represents an evaluable request with an associated user, device, +// and request context. +type Request struct { + // User context + // + // User contains the associated user's JWT created by the authenticate + // service + User string `json:"user,omitempty"` + + // Request context + // + // Method specifies the HTTP method (GET, POST, PUT, etc.). + Method string `json:"method,omitempty"` + // URL specifies either the URI being requested. + URL string `json:"url,omitempty"` + // The protocol version for incoming server requests. + Proto string `json:"proto,omitempty"` // "HTTP/1.0" + // Header contains the request header fields either received + // by the server or to be sent by the client. + Header map[string][]string `json:"headers,omitempty"` + // Host specifies the host on which the URL is sought. + Host string `json:"host,omitempty"` + // RemoteAddr is the network address that sent the request. + RemoteAddr string `json:"remote_addr,omitempty"` + // RequestURI is the unmodified request-target of the + // Request-Line (RFC 7230, Section 3.1.1) as sent by the client + // to a server. Usually the URL field should be used instead. + // It is an error to set this field in an HTTP client request. + RequestURI string `json:"request_uri,omitempty"` + + // Device context + // + // todo(bdd): + // Use the peer TLS certificate as the basis for binding device + // identity with a request context ! +} diff --git a/authorize/evaluator/mock/mock.go b/authorize/evaluator/mock/mock.go new file mode 100644 index 000000000..f17ae6943 --- /dev/null +++ b/authorize/evaluator/mock/mock.go @@ -0,0 +1,35 @@ +// Package mock implements the policy evaluator interface to make authorization +// decisions. +package mock + +import ( + "context" + + "github.com/pomerium/pomerium/authorize/evaluator" +) + +var _ evaluator.Evaluator = &PolicyEvaluator{} + +// PolicyEvaluator is the mock implementation of Evaluator +type PolicyEvaluator struct { + IsAuthorizedResponse bool + IsAuthorizedErr error + IsAdminResponse bool + IsAdminErr error + PutDataErr error +} + +// IsAuthorized is the mock implementation of IsAuthorized +func (pe PolicyEvaluator) IsAuthorized(ctx context.Context, input interface{}) (bool, error) { + return pe.IsAuthorizedResponse, pe.IsAuthorizedErr +} + +// IsAdmin is the mock implementation of IsAdmin +func (pe PolicyEvaluator) IsAdmin(ctx context.Context, input interface{}) (bool, error) { + return pe.IsAdminResponse, pe.IsAdminErr +} + +// PutData is the mock implementation of PutData +func (pe PolicyEvaluator) PutData(ctx context.Context, data map[string]interface{}) error { + return pe.PutDataErr +} diff --git a/authorize/evaluator/opa/opa.go b/authorize/evaluator/opa/opa.go new file mode 100644 index 000000000..9bf148d67 --- /dev/null +++ b/authorize/evaluator/opa/opa.go @@ -0,0 +1,155 @@ +// Package opa implements the policy evaluator interface to make authorization +// decisions. +package opa + +import ( + "context" + "errors" + "fmt" + "sync" + + "github.com/open-policy-agent/opa/rego" + "github.com/open-policy-agent/opa/storage" + "github.com/open-policy-agent/opa/storage/inmem" + "github.com/pomerium/pomerium/authorize/evaluator" + "github.com/pomerium/pomerium/internal/telemetry/trace" +) + +var _ evaluator.Evaluator = &PolicyEvaluator{} + +// PolicyEvaluator implements the evaluator interface using the open policy +// agent framework. The Open Policy Agent (OPA, pronounced “oh-pa”) is an open +// source, general-purpose policy engine that unifies policy enforcement across +// the stack. +// https://www.openpolicyagent.org/docs/latest/ +type PolicyEvaluator struct { + // The in-memory store supports multi-reader/single-writer concurrency with + // rollback so we leverage a RWMutex. + mu sync.RWMutex + store storage.Store + isAuthorized rego.PreparedEvalQuery + isAdmin rego.PreparedEvalQuery +} + +// Options represent OPA's evaluator configurations. +type Options struct { + // AuthorizationPolicy accepts custom rego code which can be used to + // apply custom authorization policy. + // Defaults to authorization policy defined in config.yaml's policy. + AuthorizationPolicy string + // PAMPolicy accepts custom rego code which can be used to + // apply custom privileged access management policy. + // Defaults to users whose emails match those defined in config.yaml. + PAMPolicy string + // Data maps data that will be bound and + Data map[string]interface{} +} + +// New creates a new OPA policy evaluator. +func New(ctx context.Context, opts *Options) (*PolicyEvaluator, error) { + var pe PolicyEvaluator + pe.store = inmem.New() + if opts.Data == nil { + return nil, errors.New("opa: cannot create new evaluator without data") + } + if opts.AuthorizationPolicy == "" { + opts.AuthorizationPolicy = defaultAuthorization + } + if opts.PAMPolicy == "" { + opts.PAMPolicy = defaultPAM + } + if err := pe.PutData(ctx, opts.Data); err != nil { + return nil, err + } + if err := pe.UpdatePolicy(ctx, opts.AuthorizationPolicy, opts.PAMPolicy); err != nil { + return nil, err + } + return &pe, nil +} + +// UpdatePolicy takes authorization and privilege access management rego code +// as an input and updates the prepared policy evaluator. +func (pe *PolicyEvaluator) UpdatePolicy(ctx context.Context, authz, pam string) error { + ctx, span := trace.StartSpan(ctx, "authorize.evaluator.opa.UpdatePolicy") + defer span.End() + + var err error + pe.mu.Lock() + defer pe.mu.Unlock() + + r := rego.New( + rego.Store(pe.store), + rego.Module("pomerium.authz", authz), + // rego.Query("data.pomerium.authz"), + rego.Query("result = data.pomerium.authz.allow"), + ) + pe.isAuthorized, err = r.PrepareForEval(ctx) + if err != nil { + return fmt.Errorf("opa: prepare policy: %w", err) + } + r = rego.New( + rego.Store(pe.store), + rego.Module("pomerium.pam", pam), + rego.Query("result = data.pomerium.pam.is_admin"), + ) + pe.isAdmin, err = r.PrepareForEval(ctx) + if err != nil { + return fmt.Errorf("opa: prepare policy: %w", err) + } + return nil +} + +// IsAuthorized determines if a given request input is authorized. +func (pe *PolicyEvaluator) IsAuthorized(ctx context.Context, input interface{}) (bool, error) { + ctx, span := trace.StartSpan(ctx, "authorize.evaluator.opa.PutData") + defer span.End() + return pe.runBoolQuery(ctx, input, pe.isAuthorized) + +} + +// IsAdmin determines if a given input user has super user privleges. +func (pe *PolicyEvaluator) IsAdmin(ctx context.Context, input interface{}) (bool, error) { + ctx, span := trace.StartSpan(ctx, "authorize.evaluator.opa.IsAdmin") + defer span.End() + return pe.runBoolQuery(ctx, input, pe.isAdmin) +} + +// PutData adds (or replaces if the mapping key is the same) contextual data +// for making policy decisions. +func (pe *PolicyEvaluator) PutData(ctx context.Context, data map[string]interface{}) error { + ctx, span := trace.StartSpan(ctx, "authorize.evaluator.opa.PutData") + defer span.End() + + pe.mu.Lock() + defer pe.mu.Unlock() + txn, err := pe.store.NewTransaction(ctx, storage.WriteParams) + if err != nil { + return fmt.Errorf("opa: bad transaction: %w", err) + } + if err := pe.store.Write(ctx, txn, storage.ReplaceOp, storage.Path{}, data); err != nil { + pe.store.Abort(ctx, txn) + return fmt.Errorf("opa: write failed %v : %w", data, err) + } + if err := pe.store.Commit(ctx, txn); err != nil { + return fmt.Errorf("opa: commit failed: %w", err) + } + return nil +} + +func (pe *PolicyEvaluator) runBoolQuery(ctx context.Context, input interface{}, q rego.PreparedEvalQuery) (bool, error) { + pe.mu.RLock() + defer pe.mu.RUnlock() + rs, err := q.Eval( + ctx, + rego.EvalInput(input), + ) + if err != nil { + return false, fmt.Errorf("opa: eval query: %w", err) + } else if len(rs) != 1 { + return false, fmt.Errorf("opa: eval result set: %v, expected len 1", rs) + } else if result, ok := rs[0].Bindings["result"].(bool); !ok { + return false, fmt.Errorf("opa: expected bool, got: %v", rs) + } else { + return result, nil + } +} diff --git a/authorize/identity_test.go b/authorize/evaluator/opa/opa_test.go similarity index 57% rename from authorize/identity_test.go rename to authorize/evaluator/opa/opa_test.go index fc1ed01a9..4d45cca3a 100644 --- a/authorize/identity_test.go +++ b/authorize/evaluator/opa/opa_test.go @@ -1,68 +1,60 @@ -package authorize +package opa import ( + "context" "testing" + "time" "github.com/pomerium/pomerium/config" + "gopkg.in/square/go-jose.v2" + "gopkg.in/square/go-jose.v2/jwt" ) -func TestIdentity_EmailDomain(t *testing.T) { +func Test_Eval(t *testing.T) { t.Parallel() - tests := []struct { - name string - Email string - want string - }{ - {"simple", "user@pomerium.io", "pomerium.io"}, - {"period malformed", "user@.io", ".io"}, - {"empty", "", ""}, - {"empty first part", "@uhoh.com", ""}, - {"empty second part", "uhoh@", ""}, + type Identity struct { + User string `json:"user,omitempty"` + Email string `json:"email,omitempty"` + Groups []string `json:"groups,omitempty"` + ImpersonateEmail string `json:"impersonate_email,omitempty"` + ImpersonateGroups []string `json:"impersonate_groups,omitempty"` } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := EmailDomain(tt.Email); got != tt.want { - t.Errorf("Identity.EmailDomain() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_IdentityWhitelistMap(t *testing.T) { - t.Parallel() tests := []struct { name string policies []config.Policy route string Identity *Identity admins []string + secret string want bool }{ - {"valid domain", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedDomains: []string{"example.com"}}}, "from.example", &Identity{Email: "user@example.com"}, nil, true}, - {"valid domain with admins", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedDomains: []string{"example.com"}}}, "from.example", &Identity{Email: "user@example.com"}, []string{"admin@example.com"}, true}, - {"invalid domain prepend", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedDomains: []string{"example.com"}}}, "from.example", &Identity{Email: "a@1example.com"}, nil, false}, - {"invalid domain postpend", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedDomains: []string{"example.com"}}}, "from.example", &Identity{Email: "user@example.com2"}, nil, false}, - {"valid group", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedGroups: []string{"admin"}}}, "from.example", &Identity{Email: "user@example.com", Groups: []string{"admin"}}, nil, true}, - {"invalid group", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedGroups: []string{"admin"}}}, "from.example", &Identity{Email: "user@example.com", Groups: []string{"everyone"}}, nil, false}, - {"invalid empty", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedGroups: []string{"admin"}}}, "from.example", &Identity{Email: "user@example.com", Groups: []string{""}}, nil, false}, - {"valid group multiple", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedGroups: []string{"admin"}}}, "from.example", &Identity{Email: "user@example.com", Groups: []string{"everyone", "admin"}}, nil, true}, - {"invalid group multiple", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedGroups: []string{"admin"}}}, "from.example", &Identity{Email: "user@example.com", Groups: []string{"everyones", "sadmin"}}, nil, false}, - {"valid user email", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedEmails: []string{"user@example.com"}}}, "from.example", &Identity{Email: "user@example.com"}, nil, true}, - {"invalid user email", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedEmails: []string{"user@example.com"}}}, "from.example", &Identity{Email: "user2@example.com"}, nil, false}, - {"empty everything", []config.Policy{{From: "https://from.example", To: "https://to.example"}}, "from.example", &Identity{Email: "user2@example.com"}, nil, false}, - {"empty policy", []config.Policy{}, "from.example", &Identity{Email: "user2@example.com"}, nil, false}, + {"valid domain", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedDomains: []string{"example.com"}}}, "from.example", &Identity{Email: "user@example.com"}, nil, "secret", true}, + {"valid domain with admins", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedDomains: []string{"example.com"}}}, "from.example", &Identity{Email: "user@example.com"}, []string{"admin@example.com"}, "secret", true}, + {"invalid domain prepend", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedDomains: []string{"example.com"}}}, "from.example", &Identity{Email: "a@1example.com"}, nil, "secret", false}, + {"invalid domain postpend", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedDomains: []string{"example.com"}}}, "from.example", &Identity{Email: "user@example.com2"}, nil, "secret", false}, + {"valid group", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedGroups: []string{"admin"}}}, "from.example", &Identity{Email: "user@example.com", Groups: []string{"admin"}}, nil, "secret", true}, + {"invalid group", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedGroups: []string{"admin"}}}, "from.example", &Identity{Email: "user@example.com", Groups: []string{"everyone"}}, nil, "secret", false}, + {"invalid empty", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedGroups: []string{"admin"}}}, "from.example", &Identity{Email: "user@example.com", Groups: []string{""}}, nil, "secret", false}, + {"valid group multiple", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedGroups: []string{"admin"}}}, "from.example", &Identity{Email: "user@example.com", Groups: []string{"everyone", "admin"}}, nil, "secret", true}, + {"invalid group multiple", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedGroups: []string{"admin"}}}, "from.example", &Identity{Email: "user@example.com", Groups: []string{"everyones", "sadmin"}}, nil, "secret", false}, + {"valid user email", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedUsers: []string{"user@example.com"}}}, "from.example", &Identity{Email: "user@example.com"}, nil, "secret", true}, + {"invalid user email", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedUsers: []string{"user@example.com"}}}, "from.example", &Identity{Email: "user2@example.com"}, nil, "secret", false}, + {"empty everything", []config.Policy{{From: "https://from.example", To: "https://to.example"}}, "from.example", &Identity{Email: "user2@example.com"}, nil, "secret", false}, + {"empty policy", []config.Policy{}, "from.example", &Identity{Email: "user2@example.com"}, nil, "secret", false}, // impersonation related - {"admin not impersonating allowed", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedDomains: []string{"example.com"}}}, "from.example", &Identity{Email: "admin@example.com"}, []string{"admin@example.com"}, true}, - {"admin not impersonating denied", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedDomains: []string{"example.com"}}}, "from.example", &Identity{Email: "admin@admin-domain.com"}, []string{"admin@admin-domain.com"}, false}, - {"impersonating match domain", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedDomains: []string{"example.com"}}}, "from.example", &Identity{Email: "admin@admin-domain.com", ImpersonateEmail: "user@example.com"}, []string{"admin@admin-domain.com"}, true}, - {"impersonating does not match domain", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedDomains: []string{"example.com"}}}, "from.example", &Identity{Email: "admin@admin-domain.com", ImpersonateEmail: "user@not-example.com"}, []string{"admin@admin-domain.com"}, false}, - {"impersonating match email", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedEmails: []string{"user@example.com"}}}, "from.example", &Identity{Email: "admin@admin-domain.com", ImpersonateEmail: "user@example.com"}, []string{"admin@admin-domain.com"}, true}, - {"impersonating does not match email", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedEmails: []string{"user@example.com"}}}, "from.example", &Identity{Email: "admin@admin-domain.com", ImpersonateEmail: "user@not-example.com"}, []string{"admin@admin-domain.com"}, false}, - {"impersonating match groups", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedGroups: []string{"support"}}}, "from.example", &Identity{Email: "admin@admin-domain.com", ImpersonateGroups: []string{"support"}}, []string{"admin@admin-domain.com"}, true}, - {"impersonating match many groups", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedGroups: []string{"support"}}}, "from.example", &Identity{Email: "admin@admin-domain.com", ImpersonateGroups: []string{"a", "b", "c", "support"}}, []string{"admin@admin-domain.com"}, true}, - {"impersonating does not match groups", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedGroups: []string{"support"}}}, "from.example", &Identity{Email: "admin@admin-domain.com", ImpersonateGroups: []string{"not support"}}, []string{"admin@admin-domain.com"}, false}, - {"impersonating does not match many groups", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedGroups: []string{"support"}}}, "from.example", &Identity{Email: "admin@admin-domain.com", ImpersonateGroups: []string{"not support", "b", "c"}}, []string{"admin@admin-domain.com"}, false}, - {"impersonating does not match empty groups", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedGroups: []string{"support"}}}, "from.example", &Identity{Email: "admin@admin-domain.com", ImpersonateGroups: []string{""}}, []string{"admin@admin-domain.com"}, false}, + {"admin not impersonating allowed", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedDomains: []string{"example.com"}}}, "from.example", &Identity{Email: "admin@example.com"}, []string{"admin@example.com"}, "secret", true}, + {"admin not impersonating denied", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedDomains: []string{"example.com"}}}, "from.example", &Identity{Email: "admin@admin-domain.com"}, []string{"admin@admin-domain.com"}, "secret", false}, + {"impersonating match domain", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedDomains: []string{"example.com"}}}, "from.example", &Identity{Email: "admin@admin-domain.com", ImpersonateEmail: "user@example.com"}, []string{"admin@admin-domain.com"}, "secret", true}, + {"impersonating does not match domain", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedDomains: []string{"example.com"}}}, "from.example", &Identity{Email: "admin@admin-domain.com", ImpersonateEmail: "user@not-example.com"}, []string{"admin@admin-domain.com"}, "secret", false}, + {"impersonating match email", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedUsers: []string{"user@example.com"}}}, "from.example", &Identity{Email: "admin@admin-domain.com", ImpersonateEmail: "user@example.com"}, []string{"admin@admin-domain.com"}, "secret", true}, + {"impersonating does not match email", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedUsers: []string{"user@example.com"}}}, "from.example", &Identity{Email: "admin@admin-domain.com", ImpersonateEmail: "user@not-example.com"}, []string{"admin@admin-domain.com"}, "secret", false}, + {"impersonating match groups", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedGroups: []string{"support"}}}, "from.example", &Identity{Email: "admin@admin-domain.com", ImpersonateGroups: []string{"support"}}, []string{"admin@admin-domain.com"}, "secret", true}, + {"impersonating match many groups", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedGroups: []string{"support"}}}, "from.example", &Identity{Email: "admin@admin-domain.com", ImpersonateGroups: []string{"a", "b", "c", "support"}}, []string{"admin@admin-domain.com"}, "secret", true}, + {"impersonating does not match groups", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedGroups: []string{"support"}}}, "from.example", &Identity{Email: "admin@admin-domain.com", ImpersonateGroups: []string{"not support"}}, []string{"admin@admin-domain.com"}, "secret", false}, + {"impersonating does not match many groups", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedGroups: []string{"support"}}}, "from.example", &Identity{Email: "admin@admin-domain.com", ImpersonateGroups: []string{"not support", "b", "c"}}, []string{"admin@admin-domain.com"}, "secret", false}, + {"impersonating does not match empty groups", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedGroups: []string{"support"}}}, "from.example", &Identity{Email: "admin@admin-domain.com", ImpersonateGroups: []string{""}}, []string{"admin@admin-domain.com"}, "secret", false}, + // jwt validation + {"bad jwt shared secret", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedDomains: []string{"example.com"}}}, "from.example", &Identity{Email: "user@example.com"}, nil, "bad-secret", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -71,12 +63,46 @@ func Test_IdentityWhitelistMap(t *testing.T) { t.Fatal(err) } } - - wl := NewIdentityWhitelist(tt.policies, tt.admins) - if got := wl.Valid(tt.route, tt.Identity); got != tt.want { - t.Errorf("wl.Valid() = %v, want %v", got, tt.want) + key := []byte("secret") + sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.HS256, Key: key}, + (&jose.SignerOptions{}).WithType("JWT")) + if err != nil { + t.Fatal(err) } + cl := jwt.Claims{ + NotBefore: jwt.NewNumericDate(time.Now()), + Expiry: jwt.NewNumericDate(time.Now().Add(time.Hour)), + Audience: jwt.Audience{tt.route}, + } + rawJWT, err := jwt.Signed(sig).Claims(cl).Claims(tt.Identity).CompactSerialize() + if err != nil { + t.Fatal(err) + } + + data := map[string]interface{}{ + "route_policies": tt.policies, + "admins": tt.admins, + "shared_key": tt.secret, + } + pe, err := New(context.Background(), &Options{Data: data}) + if err != nil { + t.Fatal(err) + } + req := struct { + Host string `json:"host,omitempty"` + User string `json:"user,omitempty"` + }{ + Host: tt.route, + User: rawJWT, + } + got, err := pe.IsAuthorized(context.TODO(), req) + if err != nil { + t.Fatal(err) + } + if got != tt.want { + t.Errorf("pe.Eval() = %v, want %v", got, tt.want) + } }) } } diff --git a/authorize/evaluator/opa/rego.go b/authorize/evaluator/opa/rego.go new file mode 100644 index 000000000..72aff493d --- /dev/null +++ b/authorize/evaluator/opa/rego.go @@ -0,0 +1,87 @@ +package opa + +//todo(bdd): embed source files directly, and setup tests. + +const defaultAuthorization = ` +package pomerium.authz +import data.route_policies +import data.shared_key + +default allow = false + +# allow by email +allow { + some route + input.host = route_policies[route].source + jwt.payload.email = route_policies[route].allowed_users[_] + jwt.valid +} + +# allow group +allow { + some route + input.host = route_policies[route].source + jwt.payload.groups[_] = route_policies[route].allowed_groups[_] + jwt.valid +} + +# allow by impersonate email +allow { + some route + input.host = route_policies[route].source + jwt.payload.impersonate_email = route_policies[route].allowed_users[_] + jwt.valid + +} + +# allow by impersonate group +allow { + some route + input.host = route_policies[route].source + jwt.payload.impersonate_groups[_] = route_policies[route].allowed_groups[_] + jwt.valid +} + +# allow by domain +allow { + some route + input.host = route_policies[route].source + x := split(jwt.payload.email, "@") + count(x)=2 + x[1] = route_policies[route].allowed_domains[_] + jwt.valid +} + +# allow by impersonate domain +allow { + some route + input.host = route_policies[route].source + x := split(jwt.payload.impersonate_email, "@") + count(x)=2 + x[1] == route_policies[route].allowed_domains[_] + jwt.valid +} + + +jwt = {"payload": payload, "valid": valid} { + [valid, header, payload] := io.jwt.decode_verify( + input.user, { + "secret": shared_key, + "aud": input.host, + } + ) +} +` + +const defaultPAM = ` +package pomerium.pam +import data.admins +import data.shared_key + +default is_admin = false +is_admin{ + io.jwt.verify_hs256(input.user,shared_key) + jwt.payload.email = admins[_] +} +jwt = {"payload": payload} {[header, payload, signature] := io.jwt.decode(input.user)} +` diff --git a/authorize/grpc.go b/authorize/grpc.go index 93ed7af10..c88f60301 100644 --- a/authorize/grpc.go +++ b/authorize/grpc.go @@ -4,35 +4,54 @@ package authorize // import "github.com/pomerium/pomerium/authorize" import ( "context" + "github.com/pomerium/pomerium/authorize/evaluator" "github.com/pomerium/pomerium/internal/grpc/authorize" "github.com/pomerium/pomerium/internal/telemetry/trace" ) -// Authorize validates the user identity, device, and context of a request for -// a given route. Currently only checks identity. -func (a *Authorize) Authorize(ctx context.Context, in *authorize.Identity) (*authorize.AuthorizeReply, error) { +// IsAuthorized checks to see if a given user is authorized to make a request. +func (a *Authorize) IsAuthorized(ctx context.Context, in *authorize.IsAuthorizedRequest) (*authorize.IsAuthorizedReply, error) { _, span := trace.StartSpan(ctx, "authorize.grpc.Authorize") defer span.End() - ok := a.ValidIdentity(in.Route, - &Identity{ - User: in.User, - Email: in.Email, - Groups: in.Groups, - ImpersonateEmail: in.ImpersonateEmail, - ImpersonateGroups: in.ImpersonateGroups, - }) - return &authorize.AuthorizeReply{IsValid: ok}, nil + req := &evaluator.Request{ + User: in.GetUserToken(), + Header: cloneHeaders(in.GetRequestHeaders()), + Host: in.GetRequestHost(), + Method: in.GetRequestMethod(), + RequestURI: in.GetRequestRequestUri(), + RemoteAddr: in.GetRequestRemoteAddr(), + URL: in.GetRequestUrl(), + } + ok, err := a.pe.IsAuthorized(ctx, req) + if err != nil { + return nil, err + } + return &authorize.IsAuthorizedReply{IsValid: ok}, nil } -// IsAdmin validates the user is an administrative user. -func (a *Authorize) IsAdmin(ctx context.Context, in *authorize.Identity) (*authorize.IsAdminReply, error) { +// IsAdmin checks to see if a given user has super user privleges. +func (a *Authorize) IsAdmin(ctx context.Context, in *authorize.IsAdminRequest) (*authorize.IsAdminReply, error) { _, span := trace.StartSpan(ctx, "authorize.grpc.IsAdmin") defer span.End() - ok := a.identityAccess.IsAdmin( - &Identity{ - Email: in.Email, - Groups: in.Groups, - }) - return &authorize.IsAdminReply{IsAdmin: ok}, nil + req := &evaluator.Request{ + User: in.GetUserToken(), + } + ok, err := a.pe.IsAdmin(ctx, req) + if err != nil { + return nil, err + } + return &authorize.IsAdminReply{IsValid: ok}, nil +} + +type protoHeader map[string]*authorize.IsAuthorizedRequest_Headers + +func cloneHeaders(in protoHeader) map[string][]string { + out := make(map[string][]string, len(in)) + for key, values := range in { + newValues := make([]string, len(values.Value)) + copy(newValues, values.Value) + out[key] = newValues + } + return out } diff --git a/authorize/grpc_test.go b/authorize/grpc_test.go index 310740c67..6ca70d34b 100644 --- a/authorize/grpc_test.go +++ b/authorize/grpc_test.go @@ -1,36 +1,43 @@ +//go:generate protoc -I ../internal/grpc/authorize/ --go_out=plugins=grpc:../internal/grpc/authorize/ ../internal/grpc/authorize/authorize.proto + package authorize import ( "context" + "errors" "reflect" "testing" - pb "github.com/pomerium/pomerium/internal/grpc/authorize" + "github.com/pomerium/pomerium/authorize/evaluator" + "github.com/pomerium/pomerium/authorize/evaluator/mock" + "github.com/pomerium/pomerium/internal/grpc/authorize" ) -func TestAuthorize_Authorize(t *testing.T) { +func TestAuthorize_IsAuthorized(t *testing.T) { t.Parallel() tests := []struct { - name string - SharedKey string - identityAccess IdentityValidator - in *pb.Identity - want *pb.AuthorizeReply - wantErr bool + name string + pe evaluator.Evaluator + in *authorize.IsAuthorizedRequest + want *authorize.IsAuthorizedReply + wantErr bool }{ - {"valid authorization request", "gXK6ggrlIW2HyKyUF9rUO4azrDgxhDPWqw9y+lJU7B8=", &MockIdentityValidator{ValidResponse: true}, &pb.Identity{Route: "http://pomerium.io", User: "user@pomerium.io"}, &pb.AuthorizeReply{IsValid: true}, false}, - {"invalid authorization request", "gXK6ggrlIW2HyKyUF9rUO4azrDgxhDPWqw9y+lJU7B8=", &MockIdentityValidator{ValidResponse: false}, &pb.Identity{Route: "http://pomerium.io", User: "user@pomerium.io"}, &pb.AuthorizeReply{IsValid: false}, false}, + {"want false", &mock.PolicyEvaluator{}, &authorize.IsAuthorizedRequest{}, &authorize.IsAuthorizedReply{IsValid: false}, false}, + {"want true", &mock.PolicyEvaluator{IsAuthorizedResponse: true}, &authorize.IsAuthorizedRequest{}, &authorize.IsAuthorizedReply{IsValid: true}, false}, + {"want err", &mock.PolicyEvaluator{IsAuthorizedErr: errors.New("err")}, &authorize.IsAuthorizedRequest{}, nil, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - a := &Authorize{SharedKey: tt.SharedKey, identityAccess: tt.identityAccess} - got, err := a.Authorize(context.Background(), tt.in) + a := &Authorize{ + pe: tt.pe, + } + got, err := a.IsAuthorized(context.TODO(), tt.in) if (err != nil) != tt.wantErr { - t.Errorf("Authorize.Authorize() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("Authorize.IsAuthorized() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { - t.Errorf("Authorize.Authorize() = %v, want %v", got, tt.want) + t.Errorf("Authorize.IsAuthorized() = %v, want %v", got, tt.want) } }) } @@ -39,22 +46,22 @@ func TestAuthorize_Authorize(t *testing.T) { func TestAuthorize_IsAdmin(t *testing.T) { t.Parallel() tests := []struct { - name string - identityAccess IdentityValidator - in *pb.Identity - want *pb.IsAdminReply - wantErr bool + name string + pe evaluator.Evaluator + in *authorize.IsAdminRequest + want *authorize.IsAdminReply + wantErr bool }{ - {"valid authorization request", &MockIdentityValidator{IsAdminResponse: true}, &pb.Identity{Route: "http://pomerium.io", User: "user@pomerium.io"}, &pb.IsAdminReply{IsAdmin: true}, false}, - {"invalid authorization request", &MockIdentityValidator{IsAdminResponse: false}, &pb.Identity{Route: "http://pomerium.io", User: "user@pomerium.io"}, &pb.IsAdminReply{IsAdmin: false}, false}, + {"want false", &mock.PolicyEvaluator{}, &authorize.IsAdminRequest{}, &authorize.IsAdminReply{IsValid: false}, false}, + {"want true", &mock.PolicyEvaluator{IsAdminResponse: true}, &authorize.IsAdminRequest{}, &authorize.IsAdminReply{IsValid: true}, false}, + {"want err", &mock.PolicyEvaluator{IsAdminErr: errors.New("err")}, &authorize.IsAdminRequest{}, nil, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { a := &Authorize{ - SharedKey: "gXK6ggrlIW2HyKyUF9rUO4azrDgxhDPWqw9y", - identityAccess: tt.identityAccess, + pe: tt.pe, } - got, err := a.IsAdmin(context.Background(), tt.in) + got, err := a.IsAdmin(context.TODO(), tt.in) if (err != nil) != tt.wantErr { t.Errorf("Authorize.IsAdmin() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/authorize/identity.go b/authorize/identity.go deleted file mode 100644 index 3c5de2610..000000000 --- a/authorize/identity.go +++ /dev/null @@ -1,187 +0,0 @@ -package authorize // import "github.com/pomerium/pomerium/authorize" - -import ( - "fmt" - "strings" - "sync" - - "github.com/pomerium/pomerium/config" - "github.com/pomerium/pomerium/internal/log" -) - -// Identity contains a user's identity information. -type Identity struct { - User string - Email string - Groups []string - // Impersonation - ImpersonateEmail string - ImpersonateGroups []string -} - -// IsImpersonating returns whether the user is trying to impersonate another -// user email or group. -func (i *Identity) IsImpersonating() bool { - if i.ImpersonateEmail != "" || len(i.ImpersonateGroups) != 0 { - return true - } - return false -} - -// EmailDomain returns the domain portion of an email. -func EmailDomain(email string) string { - if email == "" { - return "" - } - comp := strings.Split(email, "@") - if len(comp) != 2 || comp[0] == "" { - return "" - } - return comp[1] -} - -// IdentityValidator provides an interface to check whether a user has access -// to a given route. -type IdentityValidator interface { - Valid(string, *Identity) bool - IsAdmin(*Identity) bool -} - -type whitelist struct { - sync.RWMutex - access map[string]bool - admins map[string]bool -} - -// newIdentityWhitelistMap takes a slice of policies and creates a hashmap of identity -// authorizations per-route for each allowed group, domain, and email. -func newIdentityWhitelistMap(policies []config.Policy, admins []string) *whitelist { - if len(policies) == 0 { - log.Warn().Msg("authorize: loaded configuration with no policies") - } - var wl whitelist - wl.access = make(map[string]bool, len(policies)*3) - for _, p := range policies { - for _, group := range p.AllowedGroups { - wl.PutGroup(p.Source.Host, group) - log.Debug().Str("route", p.Source.Host).Str("group", group).Msg("add group") - } - for _, domain := range p.AllowedDomains { - wl.PutDomain(p.Source.Host, domain) - log.Debug().Str("route", p.Source.Host).Str("domain", domain).Msg("add domain") - } - for _, email := range p.AllowedEmails { - wl.PutEmail(p.Source.Host, email) - log.Debug().Str("route", p.Source.Host).Str("email", email).Msg("add email") - } - } - - wl.admins = make(map[string]bool, len(admins)) - for _, admin := range admins { - wl.PutAdmin(admin) - log.Debug().Str("admin", admin).Msg("add administrator") - } - return &wl -} - -// Valid reports whether an identity has valid access for a given route. -func (wl *whitelist) Valid(route string, i *Identity) bool { - email := i.Email - domain := EmailDomain(email) - groups := i.Groups - - // if user is admin, and wants to impersonate, override values - if wl.IsAdmin(i) && i.IsImpersonating() { - email = i.ImpersonateEmail - domain = EmailDomain(email) - groups = i.ImpersonateGroups - } - - if ok := wl.Email(route, email); ok { - return ok - } - if ok := wl.Domain(route, domain); ok { - return ok - } - for _, group := range groups { - if ok := wl.Group(route, group); ok { - return ok - } - } - return false -} - -func (wl *whitelist) IsAdmin(i *Identity) bool { - if ok := wl.Admin(i.Email); ok { - return ok - } - return false -} - -// Group retrieves per-route access given a group name. -func (wl *whitelist) Group(route, group string) bool { - wl.RLock() - defer wl.RUnlock() - return wl.access[fmt.Sprintf("%s|group:%s", route, group)] -} - -// PutGroup adds an access entry for a route given a group name. -func (wl *whitelist) PutGroup(route, group string) { - wl.Lock() - wl.access[fmt.Sprintf("%s|group:%s", route, group)] = true - wl.Unlock() -} - -// Domain retrieves per-route access given a domain name. -func (wl *whitelist) Domain(route, domain string) bool { - wl.RLock() - defer wl.RUnlock() - return wl.access[fmt.Sprintf("%s|domain:%s", route, domain)] -} - -// PutDomain adds an access entry for a route given a domain name. -func (wl *whitelist) PutDomain(route, domain string) { - wl.Lock() - wl.access[fmt.Sprintf("%s|domain:%s", route, domain)] = true - wl.Unlock() -} - -// Email retrieves per-route access given a user's email. -func (wl *whitelist) Email(route, email string) bool { - wl.RLock() - defer wl.RUnlock() - return wl.access[fmt.Sprintf("%s|email:%s", route, email)] -} - -// PutEmail adds an access entry for a route given a user's email. -func (wl *whitelist) PutEmail(route, email string) { - wl.Lock() - wl.access[fmt.Sprintf("%s|email:%s", route, email)] = true - wl.Unlock() -} - -// PutEmail adds an admin entry -func (wl *whitelist) PutAdmin(admin string) { - wl.Lock() - wl.admins[admin] = true - wl.Unlock() -} - -// Admin checks if the email matches an admin -func (wl *whitelist) Admin(admin string) bool { - wl.RLock() - defer wl.RUnlock() - return wl.admins[admin] -} - -// MockIdentityValidator is a mock implementation of IdentityValidator -type MockIdentityValidator struct { - ValidResponse bool - IsAdminResponse bool -} - -// Valid is a mock implementation IdentityValidator's Valid method -func (mv *MockIdentityValidator) Valid(u string, i *Identity) bool { return mv.ValidResponse } - -// IsAdmin is a mock implementation IdentityValidator's IsAdmin method -func (mv *MockIdentityValidator) IsAdmin(i *Identity) bool { return mv.IsAdminResponse } diff --git a/config/options_test.go b/config/options_test.go index 4586bce75..e104defcb 100644 --- a/config/options_test.go +++ b/config/options_test.go @@ -151,7 +151,7 @@ func Test_parsePolicyFile(t *testing.T) { want []Policy wantErr bool }{ - {"simple json", []byte(fmt.Sprintf(`{"policy":[{"from": "%s","to":"%s"}]}`, source, dest)), []Policy{{From: source, To: dest, Source: sourceURL, Destination: destURL}}, false}, + {"simple json", []byte(fmt.Sprintf(`{"policy":[{"from": "%s","to":"%s"}]}`, source, dest)), []Policy{{From: source, To: dest, Source: &HostnameURL{sourceURL}, Destination: destURL}}, false}, {"bad from", []byte(`{"policy":[{"from": "%","to":"httpbin.org"}]}`), nil, true}, {"bad to", []byte(`{"policy":[{"from": "pomerium.io","to":"%"}]}`), nil, true}, } diff --git a/config/policy.go b/config/policy.go index 857cdb082..8f9cc7c85 100644 --- a/config/policy.go +++ b/config/policy.go @@ -3,6 +3,7 @@ package config // import "github.com/pomerium/pomerium/config" import ( "crypto/tls" "crypto/x509" + "encoding/json" "fmt" "net/url" "time" @@ -16,12 +17,12 @@ type Policy struct { From string `mapstructure:"from" yaml:"from"` To string `mapstructure:"to" yaml:"to"` // Identity related policy - AllowedEmails []string `mapstructure:"allowed_users" yaml:"allowed_users,omitempty"` - AllowedGroups []string `mapstructure:"allowed_groups" yaml:"allowed_groups,omitempty"` - AllowedDomains []string `mapstructure:"allowed_domains" yaml:"allowed_domains,omitempty"` + AllowedUsers []string `mapstructure:"allowed_users" yaml:"allowed_users,omitempty" json:"allowed_users,omitempty"` + AllowedGroups []string `mapstructure:"allowed_groups" yaml:"allowed_groups,omitempty" json:"allowed_groups,omitempty"` + AllowedDomains []string `mapstructure:"allowed_domains" yaml:"allowed_domains,omitempty" json:"allowed_domains,omitempty"` - Source *url.URL `yaml:",omitempty"` - Destination *url.URL `yaml:",omitempty"` + Source *HostnameURL `yaml:",omitempty" json:"source,omitempty"` + Destination *url.URL `yaml:",omitempty" json:"destination,omitempty"` // Allow unauthenticated HTTP OPTIONS requests as per the CORS spec // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Preflighted_requests @@ -80,10 +81,11 @@ type Policy struct { // Validate checks the validity of a policy. func (p *Policy) Validate() error { var err error - p.Source, err = urlutil.ParseAndValidateURL(p.From) + source, err := urlutil.ParseAndValidateURL(p.From) if err != nil { return fmt.Errorf("config: policy bad source url %w", err) } + p.Source = &HostnameURL{source} p.Destination, err = urlutil.ParseAndValidateURL(p.To) if err != nil { @@ -91,7 +93,7 @@ func (p *Policy) Validate() error { } // Only allow public access if no other whitelists are in place - if p.AllowPublicUnauthenticatedAccess && (p.AllowedDomains != nil || p.AllowedGroups != nil || p.AllowedEmails != nil) { + if p.AllowPublicUnauthenticatedAccess && (p.AllowedDomains != nil || p.AllowedGroups != nil || p.AllowedUsers != nil) { return fmt.Errorf("config: policy route marked as public but contains whitelists") } @@ -123,12 +125,23 @@ func (p *Policy) Validate() error { return fmt.Errorf("config: couldn't load custom ca file %w", err) } } - return nil } + func (p *Policy) String() string { if p.Source == nil || p.Destination == nil { return fmt.Sprintf("%s → %s", p.From, p.To) } return fmt.Sprintf("%s → %s", p.Source.String(), p.Destination.String()) } + +// HostnameURL wraps url but marshals only the host representation of that +// url struct. +type HostnameURL struct { + *url.URL +} + +// MarshalJSON returns the URLs host as json. +func (j *HostnameURL) MarshalJSON() ([]byte, error) { + return json.Marshal(j.Host) +} diff --git a/config/policy_test.go b/config/policy_test.go index 157c30fea..b64c6b9b1 100644 --- a/config/policy_test.go +++ b/config/policy_test.go @@ -1,7 +1,10 @@ package config import ( + "encoding/json" "testing" + + "github.com/google/go-cmp/cmp" ) func Test_PolicyValidate(t *testing.T) { @@ -19,8 +22,8 @@ func Test_PolicyValidate(t *testing.T) { {"empty to scheme", Policy{From: "https://httpbin.corp.example", To: "//httpbin.corp.example"}, true}, {"cors policy", Policy{From: "https://httpbin.corp.example", To: "https://httpbin.corp.notatld", CORSAllowPreflight: true}, false}, {"public policy", Policy{From: "https://httpbin.corp.example", To: "https://httpbin.corp.notatld", AllowPublicUnauthenticatedAccess: true}, false}, - {"public and whitelist", Policy{From: "https://httpbin.corp.example", To: "https://httpbin.corp.notatld", AllowPublicUnauthenticatedAccess: true, AllowedEmails: []string{"test@domain.example"}}, true}, - {"route must have", Policy{From: "https://httpbin.corp.example", To: "https://httpbin.corp.notatld", AllowPublicUnauthenticatedAccess: true, AllowedEmails: []string{"test@domain.example"}}, true}, + {"public and whitelist", Policy{From: "https://httpbin.corp.example", To: "https://httpbin.corp.notatld", AllowPublicUnauthenticatedAccess: true, AllowedUsers: []string{"test@domain.example"}}, true}, + {"route must have", Policy{From: "https://httpbin.corp.example", To: "https://httpbin.corp.notatld", AllowPublicUnauthenticatedAccess: true, AllowedUsers: []string{"test@domain.example"}}, true}, {"good client cert", Policy{From: "https://httpbin.corp.example", To: "https://httpbin.corp.notatld", TLSClientKey: "LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcGdJQkFBS0NBUUVBNjdLanFtUVlHcTBNVnRBQ1ZwZUNtWG1pbmxRYkRQR0xtc1pBVUV3dWVIUW5ydDNXCnR2cERPbTZBbGFKTVVuVytIdTU1ampva2FsS2VWalRLbWdZR2JxVXpWRG9NYlBEYUhla2x0ZEJUTUdsT1VGc1AKNFVKU0RyTzR6ZE4rem80MjhUWDJQbkcyRkNkVktHeTRQRThpbEhiV0xjcjg3MVlqVjUxZnc4Q0xEWDlQWkpOdQo4NjFDRjdWOWlFSm02c1NmUWxtbmhOOGozK1d6VmJQUU55MVdzUjdpOWU5ajYzRXFLdDIyUTlPWEwrV0FjS3NrCm9JU21DTlZSVUFqVThZUlZjZ1FKQit6UTM0QVFQbHowT3A1Ty9RTi9NZWRqYUY4d0xTK2l2L3p2aVM4Y3FQYngKbzZzTHE2Rk5UbHRrL1FreGVDZUtLVFFlLzNrUFl2UUFkbmw2NVFJREFRQUJBb0lCQVFEQVQ0eXN2V2pSY3pxcgpKcU9SeGFPQTJEY3dXazJML1JXOFhtQWhaRmRTWHV2MkNQbGxhTU1yelBmTG41WUlmaHQzSDNzODZnSEdZc3pnClo4aWJiYWtYNUdFQ0t5N3lRSDZuZ3hFS3pRVGpiampBNWR3S0h0UFhQUnJmamQ1Y2FMczVpcDcxaWxCWEYxU3IKWERIaXUycnFtaC9kVTArWGRMLzNmK2VnVDl6bFQ5YzRyUm84dnZueWNYejFyMnVhRVZ2VExsWHVsb2NpeEVrcgoySjlTMmxveWFUb2tFTnNlMDNpSVdaWnpNNElZcVowOGJOeG9IWCszQXVlWExIUStzRkRKMlhaVVdLSkZHMHUyClp3R2w3YlZpRTFQNXdiQUdtZzJDeDVCN1MrdGQyUEpSV3Frb2VxY3F2RVdCc3RFL1FEcDFpVThCOHpiQXd0Y3IKZHc5TXZ6Q2hBb0dCQVBObzRWMjF6MGp6MWdEb2tlTVN5d3JnL2E4RkJSM2R2Y0xZbWV5VXkybmd3eHVucnFsdwo2U2IrOWdrOGovcXEvc3VQSDhVdzNqSHNKYXdGSnNvTkVqNCt2b1ZSM3UrbE5sTEw5b21rMXBoU0dNdVp0b3huCm5nbUxVbkJUMGI1M3BURkJ5WGsveE5CbElreWdBNlg5T2MreW5na3RqNlRyVnMxUERTdnVJY0s1QW9HQkFQZmoKcEUzR2F6cVFSemx6TjRvTHZmQWJBdktCZ1lPaFNnemxsK0ZLZkhzYWJGNkdudFd1dWVhY1FIWFpYZTA1c2tLcApXN2xYQ3dqQU1iUXI3QmdlazcrOSszZElwL1RnYmZCYnN3Syt6Vng3Z2doeWMrdytXRWExaHByWTZ6YXdxdkFaCkhRU2lMUEd1UGp5WXBQa1E2ZFdEczNmWHJGZ1dlTmd4SkhTZkdaT05Bb0dCQUt5WTF3MUM2U3Y2c3VuTC8vNTcKQ2Z5NTAwaXlqNUZBOWRqZkRDNWt4K1JZMnlDV0ExVGsybjZyVmJ6dzg4czBTeDMrYS9IQW1CM2dMRXBSRU5NKwo5NHVwcENFWEQ3VHdlcGUxUnlrTStKbmp4TzlDSE41c2J2U25sUnBQWlMvZzJRTVhlZ3grK2trbkhXNG1ITkFyCndqMlRrMXBBczFXbkJ0TG9WaGVyY01jSkFvR0JBSTYwSGdJb0Y5SysvRUcyY21LbUg5SDV1dGlnZFU2eHEwK0IKWE0zMWMzUHE0amdJaDZlN3pvbFRxa2d0dWtTMjBraE45dC9ibkI2TmhnK1N1WGVwSXFWZldVUnlMejVwZE9ESgo2V1BMTTYzcDdCR3cwY3RPbU1NYi9VRm5Yd0U4OHlzRlNnOUF6VjdVVUQvU0lDYkI5ZHRVMWh4SHJJK0pZRWdWCkFrZWd6N2lCQW9HQkFJRncrQVFJZUIwM01UL0lCbGswNENQTDJEak0rNDhoVGRRdjgwMDBIQU9mUWJrMEVZUDEKQ2FLR3RDbTg2MXpBZjBzcS81REtZQ0l6OS9HUzNYRk00Qm1rRk9nY1NXVENPNmZmTGdLM3FmQzN4WDJudlpIOQpYZGNKTDQrZndhY0x4c2JJKzhhUWNOVHRtb3pkUjEzQnNmUmIrSGpUL2o3dkdrYlFnSkhCT0syegotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=", TLSClientCert: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUVJVENDQWdtZ0F3SUJBZ0lSQVBqTEJxS1lwcWU0ekhQc0dWdFR6T0F3RFFZSktvWklodmNOQVFFTEJRQXcKRWpFUU1BNEdBMVVFQXhNSFoyOXZaQzFqWVRBZUZ3MHhPVEE0TVRBeE9EUTVOREJhRncweU1UQXlNVEF4TnpRdwpNREZhTUJNeEVUQVBCZ05WQkFNVENIQnZiV1Z5YVhWdE1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBCk1JSUJDZ0tDQVFFQTY3S2pxbVFZR3EwTVZ0QUNWcGVDbVhtaW5sUWJEUEdMbXNaQVVFd3VlSFFucnQzV3R2cEQKT202QWxhSk1VblcrSHU1NWpqb2thbEtlVmpUS21nWUdicVV6VkRvTWJQRGFIZWtsdGRCVE1HbE9VRnNQNFVKUwpEck80emROK3pvNDI4VFgyUG5HMkZDZFZLR3k0UEU4aWxIYldMY3I4NzFZalY1MWZ3OENMRFg5UFpKTnU4NjFDCkY3VjlpRUptNnNTZlFsbW5oTjhqMytXelZiUFFOeTFXc1I3aTllOWo2M0VxS3QyMlE5T1hMK1dBY0tza29JU20KQ05WUlVBalU4WVJWY2dRSkIrelEzNEFRUGx6ME9wNU8vUU4vTWVkamFGOHdMUytpdi96dmlTOGNxUGJ4bzZzTApxNkZOVGx0ay9Ra3hlQ2VLS1RRZS8za1BZdlFBZG5sNjVRSURBUUFCbzNFd2J6QU9CZ05WSFE4QkFmOEVCQU1DCkE3Z3dIUVlEVlIwbEJCWXdGQVlJS3dZQkJRVUhBd0VHQ0NzR0FRVUZCd01DTUIwR0ExVWREZ1FXQkJRQ1FYbWIKc0hpcS9UQlZUZVhoQ0dpNjhrVy9DakFmQmdOVkhTTUVHREFXZ0JSNTRKQ3pMRlg0T0RTQ1J0dWNBUGZOdVhWegpuREFOQmdrcWhraUc5dzBCQVFzRkFBT0NBZ0VBcm9XL2trMllleFN5NEhaQXFLNDVZaGQ5ay9QVTFiaDlFK1BRCk5jZFgzTUdEY2NDRUFkc1k4dll3NVE1cnhuMGFzcSt3VGFCcGxoYS9rMi9VVW9IQ1RqUVp1Mk94dEF3UTdPaWIKVE1tMEorU3NWT3d4YnFQTW9rK1RqVE16NFdXaFFUTzVwRmNoZDZXZXNCVHlJNzJ0aG1jcDd1c2NLU2h3YktIegpQY2h1QTQ4SzhPdi96WkxmZnduQVNZb3VCczJjd1ZiRDI3ZXZOMzdoMGFzR1BrR1VXdm1PSDduTHNVeTh3TTdqCkNGL3NwMmJmTC9OYVdNclJnTHZBMGZMS2pwWTQrVEpPbkVxQmxPcCsrbHlJTEZMcC9qMHNybjRNUnlKK0t6UTEKR1RPakVtQ1QvVEFtOS9XSThSL0FlYjcwTjEzTytYNEtaOUJHaDAxTzN3T1Vqd3BZZ3lxSnNoRnNRUG50VmMrSQpKQmF4M2VQU3NicUcwTFkzcHdHUkpRNmMrd1lxdGk2Y0tNTjliYlRkMDhCNUk1N1RRTHhNcUoycTFnWmw1R1VUCmVFZGNWRXltMnZmd0NPd0lrbGNBbThxTm5kZGZKV1FabE5VaHNOVWFBMkVINnlDeXdaZm9aak9hSDEwTXowV20KeTNpZ2NSZFQ3Mi9NR2VkZk93MlV0MVVvRFZmdEcxcysrditUQ1lpNmpUQU05dkZPckJ4UGlOeGFkUENHR2NZZAowakZIc2FWOGFPV1dQQjZBQ1JteHdDVDdRTnRTczM2MlpIOUlFWWR4Q00yMDUrZmluVHhkOUcwSmVRRTd2Kyt6CldoeWo2ZmJBWUIxM2wvN1hkRnpNSW5BOGxpekdrVHB2RHMxeTBCUzlwV3ppYmhqbVFoZGZIejdCZGpGTHVvc2wKZzlNZE5sND0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo="}, false}, {"bad base64 client cert", Policy{From: "https://httpbin.corp.example", To: "https://httpbin.corp.notatld", TLSClientKey: "!=", TLSClientCert: "!="}, true}, {"bad one client cert empty", Policy{From: "https://httpbin.corp.example", To: "https://httpbin.corp.notatld", TLSClientKey: "", TLSClientCert: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUVJVENDQWdtZ0F3SUJBZ0lSQVBqTEJxS1lwcWU0ekhQc0dWdFR6T0F3RFFZSktvWklodmNOQVFFTEJRQXcKRWpFUU1BNEdBMVVFQXhNSFoyOXZaQzFqWVRBZUZ3MHhPVEE0TVRBeE9EUTVOREJhRncweU1UQXlNVEF4TnpRdwpNREZhTUJNeEVUQVBCZ05WQkFNVENIQnZiV1Z5YVhWdE1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBCk1JSUJDZ0tDQVFFQTY3S2pxbVFZR3EwTVZ0QUNWcGVDbVhtaW5sUWJEUEdMbXNaQVVFd3VlSFFucnQzV3R2cEQKT202QWxhSk1VblcrSHU1NWpqb2thbEtlVmpUS21nWUdicVV6VkRvTWJQRGFIZWtsdGRCVE1HbE9VRnNQNFVKUwpEck80emROK3pvNDI4VFgyUG5HMkZDZFZLR3k0UEU4aWxIYldMY3I4NzFZalY1MWZ3OENMRFg5UFpKTnU4NjFDCkY3VjlpRUptNnNTZlFsbW5oTjhqMytXelZiUFFOeTFXc1I3aTllOWo2M0VxS3QyMlE5T1hMK1dBY0tza29JU20KQ05WUlVBalU4WVJWY2dRSkIrelEzNEFRUGx6ME9wNU8vUU4vTWVkamFGOHdMUytpdi96dmlTOGNxUGJ4bzZzTApxNkZOVGx0ay9Ra3hlQ2VLS1RRZS8za1BZdlFBZG5sNjVRSURBUUFCbzNFd2J6QU9CZ05WSFE4QkFmOEVCQU1DCkE3Z3dIUVlEVlIwbEJCWXdGQVlJS3dZQkJRVUhBd0VHQ0NzR0FRVUZCd01DTUIwR0ExVWREZ1FXQkJRQ1FYbWIKc0hpcS9UQlZUZVhoQ0dpNjhrVy9DakFmQmdOVkhTTUVHREFXZ0JSNTRKQ3pMRlg0T0RTQ1J0dWNBUGZOdVhWegpuREFOQmdrcWhraUc5dzBCQVFzRkFBT0NBZ0VBcm9XL2trMllleFN5NEhaQXFLNDVZaGQ5ay9QVTFiaDlFK1BRCk5jZFgzTUdEY2NDRUFkc1k4dll3NVE1cnhuMGFzcSt3VGFCcGxoYS9rMi9VVW9IQ1RqUVp1Mk94dEF3UTdPaWIKVE1tMEorU3NWT3d4YnFQTW9rK1RqVE16NFdXaFFUTzVwRmNoZDZXZXNCVHlJNzJ0aG1jcDd1c2NLU2h3YktIegpQY2h1QTQ4SzhPdi96WkxmZnduQVNZb3VCczJjd1ZiRDI3ZXZOMzdoMGFzR1BrR1VXdm1PSDduTHNVeTh3TTdqCkNGL3NwMmJmTC9OYVdNclJnTHZBMGZMS2pwWTQrVEpPbkVxQmxPcCsrbHlJTEZMcC9qMHNybjRNUnlKK0t6UTEKR1RPakVtQ1QvVEFtOS9XSThSL0FlYjcwTjEzTytYNEtaOUJHaDAxTzN3T1Vqd3BZZ3lxSnNoRnNRUG50VmMrSQpKQmF4M2VQU3NicUcwTFkzcHdHUkpRNmMrd1lxdGk2Y0tNTjliYlRkMDhCNUk1N1RRTHhNcUoycTFnWmw1R1VUCmVFZGNWRXltMnZmd0NPd0lrbGNBbThxTm5kZGZKV1FabE5VaHNOVWFBMkVINnlDeXdaZm9aak9hSDEwTXowV20KeTNpZ2NSZFQ3Mi9NR2VkZk93MlV0MVVvRFZmdEcxcysrditUQ1lpNmpUQU05dkZPckJ4UGlOeGFkUENHR2NZZAowakZIc2FWOGFPV1dQQjZBQ1JteHdDVDdRTnRTczM2MlpIOUlFWWR4Q00yMDUrZmluVHhkOUcwSmVRRTd2Kyt6CldoeWo2ZmJBWUIxM2wvN1hkRnpNSW5BOGxpekdrVHB2RHMxeTBCUzlwV3ppYmhqbVFoZGZIejdCZGpGTHVvc2wKZzlNZE5sND0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo="}, true}, @@ -48,13 +51,14 @@ func Test_PolicyValidate(t *testing.T) { func TestPolicy_String(t *testing.T) { t.Parallel() tests := []struct { - name string - From string - To string - want string + name string + From string + To string + want string + wantFrom string }{ - {"good", "https://pomerium.io", "https://localhost", "https://pomerium.io → https://localhost"}, - {"failed to validate", "https://pomerium.io", "localhost", "https://pomerium.io → localhost"}, + {"good", "https://pomerium.io", "https://localhost", "https://pomerium.io → https://localhost", `"pomerium.io"`}, + {"failed to validate", "https://pomerium.io", "localhost", "https://pomerium.io → localhost", `"pomerium.io"`}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -66,6 +70,13 @@ func TestPolicy_String(t *testing.T) { if got := p.String(); got != tt.want { t.Errorf("Policy.String() = %v, want %v", got, tt.want) } + out, err := json.Marshal(p.Source) + if err != nil { + t.Fatal(err) + } + if diff := cmp.Diff(string(out), tt.wantFrom); diff != "" { + t.Errorf("json diff() = %s", diff) + } }) } } diff --git a/go.mod b/go.mod index 6c218c451..d3e5e0c58 100644 --- a/go.mod +++ b/go.mod @@ -6,18 +6,20 @@ require ( cloud.google.com/go v0.49.0 // indirect contrib.go.opencensus.io/exporter/jaeger v0.2.0 contrib.go.opencensus.io/exporter/prometheus v0.1.0 + github.com/OneOfOne/xxhash v1.2.7 // indirect github.com/cespare/xxhash/v2 v2.1.1 github.com/fsnotify/fsnotify v1.4.7 github.com/go-redis/redis v6.15.6+incompatible github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e - github.com/golang/mock v1.4.0 github.com/golang/protobuf v1.3.2 github.com/google/go-cmp v0.4.0 github.com/gorilla/mux v1.7.3 github.com/mitchellh/hashstructure v1.0.0 github.com/onsi/ginkgo v1.11.0 // indirect github.com/onsi/gomega v1.8.1 // indirect + github.com/open-policy-agent/opa v0.16.2 github.com/pelletier/go-toml v1.6.0 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pomerium/autocache v0.0.0-20200121155820-dc85d6127c4e github.com/pomerium/csrf v1.6.2-0.20190918035251-f3318380bad3 github.com/pomerium/go-oidc v2.0.0+incompatible @@ -25,6 +27,7 @@ require ( github.com/prometheus/client_golang v1.2.1 github.com/prometheus/procfs v0.0.8 // indirect github.com/rakyll/statik v0.1.6 + github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563 // indirect github.com/rs/cors v1.7.0 github.com/rs/zerolog v1.17.2 github.com/spf13/afero v1.2.2 // indirect diff --git a/go.sum b/go.sum index 49336c878..7b3f80301 100644 --- a/go.sum +++ b/go.sum @@ -23,6 +23,10 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/OneOfOne/xxhash v1.2.3 h1:wS8NNaIgtzapuArKIAjsyXtEN/IUjQkbw90xszUdS40= +github.com/OneOfOne/xxhash v1.2.3/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/OneOfOne/xxhash v1.2.7 h1:fzrmmkskv067ZQbd9wERNGuxckWw67dyzoMG62p7LMo= +github.com/OneOfOne/xxhash v1.2.7/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -64,6 +68,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v0.0.0-20180820084758-c7ce16629ff4/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= @@ -73,9 +79,12 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V github.com/go-redis/redis v6.15.6+incompatible h1:H9evprGPLI8+ci7fxQx6WNZHJSb7be8FqJQRhdQZ5Sg= github.com/go-redis/redis v6.15.6+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -88,8 +97,7 @@ github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0 h1:Rd1kQnQu0Hq3qvJppYSG0HtP+f5LPPUiDswTLiEegLg= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/protobuf v0.0.0-20181025225059-d3de96c4c28e/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -115,6 +123,7 @@ github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/mux v0.0.0-20181024020800-521ea7b17d02/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= @@ -152,6 +161,7 @@ github.com/hashicorp/memberlist v0.1.5 h1:AYBsgJOW9gab/toO5tEB8lWetVgDKZycqkebJ8 github.com/hashicorp/memberlist v0.1.5/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -160,8 +170,10 @@ github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -172,6 +184,7 @@ github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzR github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-runewidth v0.0.0-20181025052659-b20a3daf6a39/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= @@ -183,17 +196,21 @@ github.com/mitchellh/hashstructure v1.0.0 h1:ZkRJX1CyOoTkar7p/mLS5TZU4nJ1Rn/F8u9 github.com/mitchellh/hashstructure v1.0.0/go.mod h1:QjSHrPWS+BGUVBYkbTZWEnOh3G1DutKwClXU/ABz6AQ= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mna/pigeon v0.0.0-20180808201053-bb0192cfc2ae/go.mod h1:Iym28+kJVnC1hfQvv5MUtI6AiFFzvQjHcvI4RFTG/04= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.11.0 h1:JAKSXpt1YjtLA7YpPiqO9ss6sNXEsPfSGdwN0UHqzrw= github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.8.1 h1:C5Dqfs/LeauYDX0jJXIe2SWmwCbGzx9yF8C8xy3Lh34= github.com/onsi/gomega v1.8.1/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= +github.com/open-policy-agent/opa v0.16.2 h1:Fdt1ysSA3p7z88HVHmUFiPM6hqqXbLDDZF9cQFYaIP0= +github.com/open-policy-agent/opa v0.16.2/go.mod h1:P0xUE/GQAAgnvV537GzA0Ikw4+icPELRT327QJPkaKY= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= @@ -201,9 +218,13 @@ github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4= github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= +github.com/peterh/liner v0.0.0-20170211195444-bf27d3ba8e1d/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc= +github.com/pkg/errors v0.0.0-20181023235946-059132a15dd0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pomerium/autocache v0.0.0-20200121155820-dc85d6127c4e h1:kD8uNYLnc/MGAGfFzCEDQPVKulBEBY3FlVHvMJkkmQ4= @@ -215,6 +236,7 @@ github.com/pomerium/go-oidc v2.0.0+incompatible/go.mod h1:DRsGVw6MOgxbfq4Y57jKOE github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU= github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= +github.com/prometheus/client_golang v0.0.0-20181025174421-f30f42803563/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= github.com/prometheus/client_golang v0.9.3 h1:9iH4JKXLzFbOAdtqv/a+j8aewx2Y8lAjAydhbaScPF8= @@ -227,6 +249,7 @@ github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181020173914-7e9e6cabbd39/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM= @@ -245,6 +268,10 @@ github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+Gx github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/rakyll/statik v0.1.6 h1:uICcfUXpgqtw2VopbIncslhAmE5hwc4g20TEyEENBNs= github.com/rakyll/statik v0.1.6/go.mod h1:OEi9wJV/fMUAGx1eNjq75DKDsJVuEv1U0oYdX6GX8Zs= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a h1:9ZKAASQSHhDYGoxY8uLVpewe1GDZ2vu2Tr/vTdVAkFQ= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563 h1:dY6ETXrvDG7Sa4vE8ZQG4yqWg6UnOcbqTAahkV813vQ= +github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= @@ -256,6 +283,7 @@ github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFo github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= @@ -271,10 +299,12 @@ github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.0-20181021141114-fe5e611709b0/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v0.0.0-20181024212040-082b515c9490/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -298,6 +328,8 @@ github.com/uber/jaeger-client-go v2.20.1+incompatible/go.mod h1:WVhlPFC8FDjOFMMW github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yashtewari/glob-intersection v0.0.0-20180916065949-5c77d914dd0b h1:vVRagRXf67ESqAb72hG2C/ZwI8NtJF2u2V76EsuOHGY= +github.com/yashtewari/glob-intersection v0.0.0-20180916065949-5c77d914dd0b/go.mod h1:HptNXiXVDcJjXe9SqMd0v2FsL9f8dz4GnXgltU6q/co= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.etcd.io/bbolt v1.3.2 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= @@ -326,6 +358,7 @@ golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm0 golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181023182221-1baf3a9d7d67/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -397,7 +430,6 @@ golang.org/x/sys v0.0.0-20200117145432-59e60aa80a0c h1:gUYreENmqtjZb2brVfUas1sC6 golang.org/x/sys v0.0.0-20200117145432-59e60aa80a0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200121082415-34d275377bf9 h1:N19i1HjUnR7TF7rMt8O4p3dLvqvmYyzB6ifMFmrbY50= golang.org/x/sys v0.0.0-20200121082415-34d275377bf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -407,6 +439,7 @@ golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= @@ -422,6 +455,7 @@ golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2 h1:EtTFh6h4SAKemS+CURDMTDIANuduG5zKEXShyy18bGA= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -444,6 +478,7 @@ google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -485,8 +520,7 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= -gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099 h1:XJP7lxbSxWLOMNdBE4B/STaqVy6L73o0knwj2vIlxnw= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -495,5 +529,3 @@ honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/internal/frontend/statik/statik.go b/internal/frontend/statik/statik.go index 31843d840..f0bd875a5 100644 --- a/internal/frontend/statik/statik.go +++ b/internal/frontend/statik/statik.go @@ -7,6 +7,6 @@ import ( ) func init() { - data := "PK\x03\x04\x14\x00\x08\x00\x08\x00@\xb0\x81O\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x16\x00 \x00html/dashboard.go.htmlUT\x05\x00\x01X8\xe4]\xecYKo\xeb6\x13\xdd\xe7W\xcc'd\xf9Y\x0c\xd2.\x8a\x0b\xd9hp\x1fE6M\x80\xe4.\xee*\xa0\xa5\xb1<\x05\x1f*I9\x0e\x0c\xff\xf7B\x0f;z9\x96\x15%7(\xeaM$r\x0e9s\x0e\xc9aF\x9bM\x84\x0bR\x08^\xc4\xedr\xae\xb9\x89\xfc\xa5\x93\xc2\xdbn\xcf\x82\xff}\xb9\xf9|\xff\xe3\xf6+d-\xb3\xb3 \xfb\x03\x82\xabx\xea\xa1\xf2 \\rc\xd1M\xbd\xd4-&\xbfy\xb33\x80`\x89<\xca\x1e\x00\x02GN\xe0\xecVK4\x94\xca\x80\x15\xefy\xdff\xe3P&\x82;\x04/C\xa0\xd9O\n\x10\xb0b\x90\xecq\xae\xa3\xa7r\xb8\x88V@\xd1\xd4\x93\x9c\x94W\xb4UZI-\xf4d\xae\xd7\xfb\x9e\xb2/\x14\xdc\xda\xa9\x17r\x13U\xba\xda\x9d\x93\xc2\x8d\x9aM\x16\xce\xe5\xecsj\x0c*\x07\xa9E\x13\xb0\xe5e\xddb\xb3\xa1\x05\xf8wh-i\xe5\xdfR\xe8R\x83\x90\xc7Q\x19\x86d\xbc\x9b\x8cB\xad<\xb0&\x9cz\x9bM\x13\xb8\xddz\xc0E\xc6\xa8E\x03$y\x8c\x1e\xb0\xe6\x8c(,v\xccPk\x80\xda|\x8d\xae|v\xe6'\xa54\x8c[\x8b\xce2\x921\xe3a\xa8S\xe5\x1eB2\xa1\xc0\xc9\xe5\xaf\xc9\xda\xb7\xab\xb89\xc2Z\ne\xa7\xde\xd2\xb9\xe4\x13c\x8f\x8f\x8f\xfe\xe3/\xbe61\xbb\xbc\xb8\xb8`-@;\x04\x15\xd5\"\x08XD\xab\\\xf2}\xcbB\x1b \x12\xddRGS\xef\xf6\xe6\xee\xde\x03\x1e:\xd2\xaa\xe6\xba\xa5X=\xe8\xd45\x85\xb3\x98\xdb\xce\x1a~\x07\xc9\x8e\x17\x89\xd6f\xf4\xce~\xe8\xd4@X\x8al\x0b= B\xc7IX?`Ik\x88\x05\xa1\x88,\xbafGs1\xfc\xc9eS\xa6\x1c/\xf8\x1cE\x1b\x9c9\x9dp5\xcb`\x01\xcb\x1f\xbblH%\xa9\xeb\xe8\x00pO N=\x87k\xd7T\xab\xf8\x95\x81\xe7\xeew[\xac\xb8H\xb1\xb6.\x8b\x18\xba\xad\xf3\xfd\xdc\xdb:\"\xcb\xe7\x02\xa3\x8eN\xd6\x0e4`\x07X\xda\xad\xfe\x06\xd7\x7f\xd0\n\xd5@\xc2s,|(\xda+\xe1\xf4\xe5\xfe(d<\x01\xb2\xbd\xdb\xe0\xff\x1b\x97$\x9e\x06\nP\x80?\x96\x02\xd5\x80\xfaJp\x1c3\xba\x06]Z\xdc\xa5\xf3\xbf0t\x03\x84\xf8n\xd1\\\x7f\xf90\x1a\xec\x03\xe9+\xc0\x11\xc0\xe8\xecWY\xff*9\x89\x01\x9c\xe7\xb8WP\x8e\x19~<\xce\xcb0\xfa2\xfe\xa2\xf9\x9b\xf2\x9d\xad\xd5\x01t\x7f\xcfoq\x83\xd9\x1ew\x81\x171\xf4\xe5\xfa%\xeb\xd1\xa96\\\xc5\x08\xe7\xf4\xff\xf3\x87O\xd3J\x9e5:M\xecI\xbc\xe7\xb2\xe1\xdfpNp\xd1\x01\xdcg\xe1l\xe4\xc3\xcat\xdez\xab\xf8\x17\xa1\x8d\xdb\xe6\x1e\xf9>r\x1f\x97\xf8\xddd\xad\x9dX\xeb\x84\xcc\xd3IZ\x96GV\x0e\xfc0\xbb\xa8p\xc7\xbf\xa7Sru\x0f\xd0\x9b\x92\x7fmm\x8a\xd1\xd5\x90,]@?\x0c\xfd\xbbHN\x14\xa0\x17\xec\xed%\x18\x92C\n\xe0\xcf\x16\xa0\x1d\xc8\x11\xea\xfb\x02F'}\x9fK\xa0\x9eL\xae\xd2\x88P\x85\xa7\xfd\xcb\xd03\x9d\xec\xc6\xfe7e\x94\x8a\x92pH\xbc\xca\xf28h3\xba\xc0\xb5]%\x134V+\xeep\xe8\x95\xf8y\x08R1\xbc\xf6\x82\xfc\x1a\xc6;N\xbbVx\xef\xc4\xf1\x81\x0bY\xc5\x9f\xb7\xbb\x9b\xd5\x05\xf9\xef\xa6\xd6\xf1\x1bW\xef\xb3\xa6yw\xcd1`\x9de\xcejm{!p\xed5\xe7\xc9N\x87\xd0\x9a\xc5\xb7l\xd4f\xb1\x1a \x98\xa7\xcei\xb5\x1b\xa2|[\xa4Bx%\xdb6\x9dKr\xde\xec\x8eb\x057\xa9\x0bXa\xd4t//\xeaV\x1b\x16\xda\xc8\xe7\x96\x9aA\xed\xa58S\xae\xedU$Im\xb7\xbb\xc2\xf0;\x94\xfb\xb3\x90&\xa4&\xdc\xb6\x8b\xfd\xa3U\xd9m\x9a\xa0Y\x91\xc5\xe8!\xb5h\xde\xb2\xda>\xb8\xb2N\xcf'\xcb\xf0\xe2zk\x81\xe7\x82\x92u\x86;m,\x84\\\x81C\x99h\xc3\x0d\x89'\xa8L\n\\i\xb7D\x93\x7fx\xf1[;\xe2\x94\xba\xfc\xb1\x843<\xc5(.\xcbl\xdbL\x0c\x07\xb3\xefxe\x9b\xee\xceD\xf0\x10\x97ZDh\x8a\xefH\xbf\xe3\x9a\xcbD\xa0\x1fj\xd9\x059\xe5|:\xc6\xe4\x91\xdcp\x94IhPY\xe4\xb4#\\\x8ep\xf6\xf7\xa0\x12UL\n\xd1\x90j\xedO8\x89\xc5\x9fs\x9a\xb7\xbc\xeb\\\xbaW\xb9\x03\x9d|\x97LY\xec\xe0\xba#S\xb4lj\x99\xa3\xd1\xdb\xe6\xae\xe2S\x8b\xbeA\xb9&\xe7 U\x8d\xa0C\xf9g\xff\x18\xb0\xe2\x03t\xc0\x8a/\xe0\xbb\xf4\xfcO\x00\x00\x00\xff\xffPK\x07\x08\x18\x02.\xfcj\x04\x00\x006\x1f\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00O\xb0\x81O\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x12\x00 \x00html/error.go.htmlUT\x05\x00\x01v8\xe4]\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@\xb0\x81O\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x13\x00 \x00html/header.go.htmlUT\x05\x00\x01X8\xe4]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\x9bN~O\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1b\x00 \x00img/account_circle-24px.svgUT\x05\x00\x01\xf7;\xe2]<\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\xe2\x03zO\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00 \x00img/pomerium.svgUT\x05\x00\x01Hr\xdc]\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@\xb0\x81O\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1a\x00 \x00img/pomerium_circle_96.svgUT\x05\x00\x01X8\xe4]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@\xb0\x81O\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00 \x00style/main.cssUT\x05\x00\x01X8\xe4]\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\x00@\xb0\x81O\x18\x02.\xfcj\x04\x00\x006\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\x01X8\xe4]PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00O\xb0\x81O\xe4\x92\xc0\x7fU\x02\x00\x00\x96\x06\x00\x00\x12\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xb7\x04\x00\x00html/error.go.htmlUT\x05\x00\x01v8\xe4]PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00@\xb0\x81O\x9c\xd5a\xdc\xaa\x00\x00\x00\xe7\x00\x00\x00\x13\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81U\x07\x00\x00html/header.go.htmlUT\x05\x00\x01X8\xe4]PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\x9bN~O\x83\xba\x83\xe4\xf5\x00\x00\x00|\x01\x00\x00\x1b\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81I\x08\x00\x00img/account_circle-24px.svgUT\x05\x00\x01\xf7;\xe2]PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00@\xb0\x81O\xfc\xc6x\x8f\xb7\x00\x00\x00\xf9\x00\x00\x00\x12\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x90 \x00\x00img/error-24px.svgUT\x05\x00\x01X8\xe4]PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\xe2\x03zOK\xfe\x8b#M\x03\x00\x00d\x08\x00\x00\x10\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x90\n\x00\x00img/pomerium.svgUT\x05\x00\x01Hr\xdc]PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00@\xb0\x81O\xf9\xfe\x13#\x13\x0f\x00\x00\xe5\x13\x00\x00\x1a\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81$\x0e\x00\x00img/pomerium_circle_96.svgUT\x05\x00\x01X8\xe4]PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00@\xb0\x81Ouq\x02\xd2d\x01\x00\x00\x9e\x02\x00\x00#\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x88\x1d\x00\x00img/supervised_user_circle-24px.svgUT\x05\x00\x01X8\xe4]PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00@\xb0\x81OL\xbb\xd3^\xd3\x05\x00\x00^\x12\x00\x00\x0e\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81F\x1f\x00\x00style/main.cssUT\x05\x00\x01X8\xe4]PK\x05\x06\x00\x00\x00\x00 \x00 \x00\xb2\x02\x00\x00^%\x00\x00\x00\x00" + data := "PK\x03\x04\x14\x00\x08\x00\x08\x00\x17\x029P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x16\x00 \x00html/dashboard.go.htmlUT\x05\x00\x01\xee\x88+^\xecYKo\xeb6\x13\xdd\xe7W\xcc'd\xf9Y\x0c\xd2.\x8a\x0b\xd9hp\x1fE6M\x80\xe4.\xee*\xa0\xa5\xb1<\x05\x1f*I9\x0e\x0c\xff\xf7B\x0f;z9\x96\x15%7(\xeaM$r\x0e9s\x0e\xc9aF\x9bM\x84\x0bR\x08^\xc4\xedr\xae\xb9\x89\xfc\xa5\x93\xc2\xdbn\xcf\x82\xff}\xb9\xf9|\xff\xe3\xf6+d-\xb3\xb3 \xfb\x03\x82\xabx\xea\xa1\xf2 \\rc\xd1M\xbd\xd4-&\xbfy\xb33\x80`\x89<\xca\x1e\x00\x02GN\xe0\xecVK4\x94\xca\x80\x15\xefy\xdff\xe3P&\x82;\x04/C\xa0\xd9O\n\x10\xb0b\x90\xecq\xae\xa3\xa7r\xb8\x88V@\xd1\xd4\x93\x9c\x94W\xb4UZI-\xf4d\xae\xd7\xfb\x9e\xb2/\x14\xdc\xda\xa9\x17r\x13U\xba\xda\x9d\x93\xc2\x8d\x9aM\x16\xce\xe5\xecsj\x0c*\x07\xa9E\x13\xb0\xe5e\xddb\xb3\xa1\x05\xf8wh-i\xe5\xdfR\xe8R\x83\x90\xc7Q\x19\x86d\xbc\x9b\x8cB\xad<\xb0&\x9cz\x9bM\x13\xb8\xddz\xc0E\xc6\xa8E\x03$y\x8c\x1e\xb0\xe6\x8c(,v\xccPk\x80\xda|\x8d\xae|v\xe6'\xa54\x8c[\x8b\xce2\x921\xe3a\xa8S\xe5\x1eB2\xa1\xc0\xc9\xe5\xaf\xc9\xda\xb7\xab\xb89\xc2Z\ne\xa7\xde\xd2\xb9\xe4\x13c\x8f\x8f\x8f\xfe\xe3/\xbe61\xbb\xbc\xb8\xb8`-@;\x04\x15\xd5\"\x08XD\xab\\\xf2}\xcbB\x1b \x12\xddRGS\xef\xf6\xe6\xee\xde\x03\x1e:\xd2\xaa\xe6\xba\xa5X=\xe8\xd45\x85\xb3\x98\xdb\xce\x1a~\x07\xc9\x8e\x17\x89\xd6f\xf4\xce~\xe8\xd4@X\x8al\x0b= B\xc7IX?`Ik\x88\x05\xa1\x88,\xbafGs1\xfc\xc9eS\xa6\x1c/\xf8\x1cE\x1b\x9c9\x9dp5\xcb`\x01\xcb\x1f\xbblH%\xa9\xeb\xe8\x00pO N=\x87k\xd7T\xab\xf8\x95\x81\xe7\xeew[\xac\xb8H\xb1\xb6.\x8b\x18\xba\xad\xf3\xfd\xdc\xdb:\"\xcb\xe7\x02\xa3\x8eN\xd6\x0e4`\x07X\xda\xad\xfe\x06\xd7\x7f\xd0\n\xd5@\xc2s,|(\xda+\xe1\xf4\xe5\xfe(d<\x01\xb2\xbd\xdb\xe0\xff\x1b\x97$\x9e\x06\nP\x80?\x96\x02\xd5\x80\xfaJp\x1c3\xba\x06]Z\xdc\xa5\xf3\xbf0t\x03\x84\xf8n\xd1\\\x7f\xf90\x1a\xec\x03\xe9+\xc0\x11\xc0\xe8\xecWY\xff*9\x89\x01\x9c\xe7\xb8WP\x8e\x19~<\xce\xcb0\xfa2\xfe\xa2\xf9\x9b\xf2\x9d\xad\xd5\x01t\x7f\xcfoq\x83\xd9\x1ew\x81\x171\xf4\xe5\xfa%\xeb\xd1\xa96\\\xc5\x08\xe7\xf4\xff\xf3\x87O\xd3J\x9e5:M\xecI\xbc\xe7\xb2\xe1\xdfpNp\xd1\x01\xdcg\xe1l\xe4\xc3\xcat\xdez\xab\xf8\x17\xa1\x8d\xdb\xe6\x1e\xf9>r\x1f\x97\xf8\xddd\xad\x9dX\xeb\x84\xcc\xd3IZ\x96GV\x0e\xfc0\xbb\xa8p\xc7\xbf\xa7Sru\x0f\xd0\x9b\x92\x7fmm\x8a\xd1\xd5\x90,]@?\x0c\xfd\xbbHN\x14\xa0\x17\xec\xed%\x18\x92C\n\xe0\xcf\x16\xa0\x1d\xc8\x11\xea\xfb\x02F'}\x9fK\xa0\x9eL\xae\xd2\x88P\x85\xa7\xfd\xcb\xd03\x9d\xec\xc6\xfe7e\x94\x8a\x92pH\xbc\xca\xf28h3\xba\xc0\xb5]%\x134V+\xeep\xe8\x95\xf8y\x08R1\xbc\xf6\x82\xfc\x1a\xc6;N\xbbVx\xef\xc4\xf1\x81\x0bY\xc5\x9f\xb7\xbb\x9b\xd5\x05\xf9\xef\xa6\xd6\xf1\x1bW\xef\xb3\xa6yw\xcd1`\x9de\xcejm{!p\xed5\xe7\xc9N\x87\xd0\x9a\xc5\xb7l\xd4f\xb1\x1a \x98\xa7\xcei\xb5\x1b\xa2|[\xa4Bx%\xdb6\x9dKr\xde\xec\x8eb\x057\xa9\x0bXa\xd4t//\xeaV\x1b\x16\xda\xc8\xe7\x96\x9aA\xed\xa58S\xae\xedU$Im\xb7\xbb\xc2\xf0;\x94\xfb\xb3\x90&\xa4&\xdc\xb6\x8b\xfd\xa3U\xd9m\x9a\xa0Y\x91\xc5\xe8!\xb5h\xde\xb2\xda>\xb8\xb2N\xcf'\xcb\xf0\xe2zk\x81\xe7\x82\x92u\x86;m,\x84\\\x81C\x99h\xc3\x0d\x89'\xa8L\n\\i\xb7D\x93\x7fx\xf1[;\xe2\x94\xba\xfc\xb1\x843<\xc5(.\xcbl\xdbL\x0c\x07\xb3\xefxe\x9b\xee\xceD\xf0\x10\x97ZDh\x8a\xefH\xbf\xe3\x9a\xcbD\xa0\x1fj\xd9\x059\xe5|:\xc6\xe4\x91\xdcp\x94IhPY\xe4\xb4#\\\x8ep\xf6\xf7\xa0\x12UL\n\xd1\x90j\xedO8\x89\xc5\x9fs\x9a\xb7\xbc\xeb\\\xbaW\xb9\x03\x9d|\x97LY\xec\xe0\xba#S\xb4lj\x99\xa3\xd1\xdb\xe6\xae\xe2S\x8b\xbeA\xb9&\xe7 U\x8d\xa0C\xf9g\xff\x18\xb0\xe2\x03t\xc0\x8a/\xe0\xbb\xf4\xfcO\x00\x00\x00\xff\xffPK\x07\x08\x18\x02.\xfcj\x04\x00\x006\x1f\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00\x17\x029P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x12\x00 \x00html/error.go.htmlUT\x05\x00\x01\xee\x88+^\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\x17\x029P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x13\x00 \x00html/header.go.htmlUT\x05\x00\x01\xee\x88+^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\x17\x029P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1a\x00 \x00img/pomerium_circle_96.svgUT\x05\x00\x01\xee\x88+^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\x17\x029P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00 \x00style/main.cssUT\x05\x00\x01\xee\x88+^\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\x00\x17\x029P\x18\x02.\xfcj\x04\x00\x006\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\xee\x88+^PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\x17\x029P\xe4\x92\xc0\x7fU\x02\x00\x00\x96\x06\x00\x00\x12\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xb7\x04\x00\x00html/error.go.htmlUT\x05\x00\x01\xee\x88+^PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\x17\x029P\x9c\xd5a\xdc\xaa\x00\x00\x00\xe7\x00\x00\x00\x13\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81U\x07\x00\x00html/header.go.htmlUT\x05\x00\x01\xee\x88+^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\x81I\x08\x00\x00img/account_circle-24px.svgUT\x05\x00\x01\xee\x88+^PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\x17\x029P\xfc\xc6x\x8f\xb7\x00\x00\x00\xf9\x00\x00\x00\x12\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x90 \x00\x00img/error-24px.svgUT\x05\x00\x01\xee\x88+^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\x90\n\x00\x00img/pomerium.svgUT\x05\x00\x01\xee\x88+^PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\x17\x029P\xf9\xfe\x13#\x13\x0f\x00\x00\xe5\x13\x00\x00\x1a\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81$\x0e\x00\x00img/pomerium_circle_96.svgUT\x05\x00\x01\xee\x88+^PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\x17\x029Puq\x02\xd2d\x01\x00\x00\x9e\x02\x00\x00#\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x88\x1d\x00\x00img/supervised_user_circle-24px.svgUT\x05\x00\x01\xee\x88+^PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\x17\x029PL\xbb\xd3^\xd3\x05\x00\x00^\x12\x00\x00\x0e\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81F\x1f\x00\x00style/main.cssUT\x05\x00\x01\xee\x88+^PK\x05\x06\x00\x00\x00\x00 \x00 \x00\xb2\x02\x00\x00^%\x00\x00\x00\x00" fs.Register(data) } diff --git a/internal/grpc/authorize/authorize.pb.go b/internal/grpc/authorize/authorize.pb.go index 6d927a2ec..d6444b04f 100644 --- a/internal/grpc/authorize/authorize.pb.go +++ b/internal/grpc/authorize/authorize.pb.go @@ -24,129 +24,224 @@ var _ = math.Inf // proto package needs to be updated. const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package -type Identity struct { - // request context - Route string `protobuf:"bytes,1,opt,name=route,proto3" json:"route,omitempty"` - // user context - User string `protobuf:"bytes,2,opt,name=user,proto3" json:"user,omitempty"` - Email string `protobuf:"bytes,3,opt,name=email,proto3" json:"email,omitempty"` - Groups []string `protobuf:"bytes,4,rep,name=groups,proto3" json:"groups,omitempty"` - // user context - ImpersonateEmail string `protobuf:"bytes,5,opt,name=impersonate_email,json=impersonateEmail,proto3" json:"impersonate_email,omitempty"` - ImpersonateGroups []string `protobuf:"bytes,6,rep,name=impersonate_groups,json=impersonateGroups,proto3" json:"impersonate_groups,omitempty"` +type IsAuthorizedRequest struct { + // User Context + // + UserToken string `protobuf:"bytes,1,opt,name=user_token,json=userToken,proto3" json:"user_token,omitempty"` + // Request Context + // + // Method specifies the HTTP method (GET, POST, PUT, etc.). + RequestMethod string `protobuf:"bytes,2,opt,name=request_method,json=requestMethod,proto3" json:"request_method,omitempty"` + // URL specifies either the URI being requested + RequestUrl string `protobuf:"bytes,3,opt,name=request_url,json=requestUrl,proto3" json:"request_url,omitempty"` + // host specifies the host on which the URL per RFC 7230, section 5.4 + RequestHost string `protobuf:"bytes,4,opt,name=request_host,json=requestHost,proto3" json:"request_host,omitempty"` + // request_uri is the unmodified request-target of the + // Request-Line (RFC 7230, Section 3.1.1) as sent by the client + 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:"-"` +} + +func (m *IsAuthorizedRequest) Reset() { *m = IsAuthorizedRequest{} } +func (m *IsAuthorizedRequest) String() string { return proto.CompactTextString(m) } +func (*IsAuthorizedRequest) ProtoMessage() {} +func (*IsAuthorizedRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_ffbc3c71370bee9a, []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 + } + return "" +} + +func (m *IsAuthorizedRequest) GetRequestMethod() string { + if m != nil { + return m.RequestMethod + } + return "" +} + +func (m *IsAuthorizedRequest) GetRequestUrl() string { + if m != nil { + return m.RequestUrl + } + return "" +} + +func (m *IsAuthorizedRequest) GetRequestHost() string { + if m != nil { + return m.RequestHost + } + return "" +} + +func (m *IsAuthorizedRequest) GetRequestRequestUri() string { + if m != nil { + return m.RequestRequestUri + } + return "" +} + +func (m *IsAuthorizedRequest) GetRequestRemoteAddr() string { + if m != nil { + return m.RequestRemoteAddr + } + return "" +} + +func (m *IsAuthorizedRequest) GetRequestHeaders() map[string]*IsAuthorizedRequest_Headers { + if m != nil { + return m.RequestHeaders + } + 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:"-"` } -func (m *Identity) Reset() { *m = Identity{} } -func (m *Identity) String() string { return proto.CompactTextString(m) } -func (*Identity) ProtoMessage() {} -func (*Identity) Descriptor() ([]byte, []int) { - return fileDescriptor_ffbc3c71370bee9a, []int{0} +func (m *IsAuthorizedRequest_Headers) Reset() { *m = IsAuthorizedRequest_Headers{} } +func (m *IsAuthorizedRequest_Headers) String() string { return proto.CompactTextString(m) } +func (*IsAuthorizedRequest_Headers) ProtoMessage() {} +func (*IsAuthorizedRequest_Headers) Descriptor() ([]byte, []int) { + return fileDescriptor_ffbc3c71370bee9a, []int{0, 0} } -func (m *Identity) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_Identity.Unmarshal(m, b) +func (m *IsAuthorizedRequest_Headers) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_IsAuthorizedRequest_Headers.Unmarshal(m, b) } -func (m *Identity) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_Identity.Marshal(b, m, deterministic) +func (m *IsAuthorizedRequest_Headers) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_IsAuthorizedRequest_Headers.Marshal(b, m, deterministic) } -func (m *Identity) XXX_Merge(src proto.Message) { - xxx_messageInfo_Identity.Merge(m, src) +func (m *IsAuthorizedRequest_Headers) XXX_Merge(src proto.Message) { + xxx_messageInfo_IsAuthorizedRequest_Headers.Merge(m, src) } -func (m *Identity) XXX_Size() int { - return xxx_messageInfo_Identity.Size(m) +func (m *IsAuthorizedRequest_Headers) XXX_Size() int { + return xxx_messageInfo_IsAuthorizedRequest_Headers.Size(m) } -func (m *Identity) XXX_DiscardUnknown() { - xxx_messageInfo_Identity.DiscardUnknown(m) +func (m *IsAuthorizedRequest_Headers) XXX_DiscardUnknown() { + xxx_messageInfo_IsAuthorizedRequest_Headers.DiscardUnknown(m) } -var xxx_messageInfo_Identity proto.InternalMessageInfo +var xxx_messageInfo_IsAuthorizedRequest_Headers proto.InternalMessageInfo -func (m *Identity) GetRoute() string { +func (m *IsAuthorizedRequest_Headers) GetValue() []string { if m != nil { - return m.Route - } - return "" -} - -func (m *Identity) GetUser() string { - if m != nil { - return m.User - } - return "" -} - -func (m *Identity) GetEmail() string { - if m != nil { - return m.Email - } - return "" -} - -func (m *Identity) GetGroups() []string { - if m != nil { - return m.Groups + return m.Value } return nil } -func (m *Identity) GetImpersonateEmail() string { - if m != nil { - return m.ImpersonateEmail - } - return "" -} - -func (m *Identity) GetImpersonateGroups() []string { - if m != nil { - return m.ImpersonateGroups - } - return nil -} - -type AuthorizeReply struct { +type IsAuthorizedReply struct { IsValid bool `protobuf:"varint,1,opt,name=is_valid,json=isValid,proto3" json:"is_valid,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } -func (m *AuthorizeReply) Reset() { *m = AuthorizeReply{} } -func (m *AuthorizeReply) String() string { return proto.CompactTextString(m) } -func (*AuthorizeReply) ProtoMessage() {} -func (*AuthorizeReply) Descriptor() ([]byte, []int) { +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} } -func (m *AuthorizeReply) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_AuthorizeReply.Unmarshal(m, b) +func (m *IsAuthorizedReply) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_IsAuthorizedReply.Unmarshal(m, b) } -func (m *AuthorizeReply) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_AuthorizeReply.Marshal(b, m, deterministic) +func (m *IsAuthorizedReply) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_IsAuthorizedReply.Marshal(b, m, deterministic) } -func (m *AuthorizeReply) XXX_Merge(src proto.Message) { - xxx_messageInfo_AuthorizeReply.Merge(m, src) +func (m *IsAuthorizedReply) XXX_Merge(src proto.Message) { + xxx_messageInfo_IsAuthorizedReply.Merge(m, src) } -func (m *AuthorizeReply) XXX_Size() int { - return xxx_messageInfo_AuthorizeReply.Size(m) +func (m *IsAuthorizedReply) XXX_Size() int { + return xxx_messageInfo_IsAuthorizedReply.Size(m) } -func (m *AuthorizeReply) XXX_DiscardUnknown() { - xxx_messageInfo_AuthorizeReply.DiscardUnknown(m) +func (m *IsAuthorizedReply) XXX_DiscardUnknown() { + xxx_messageInfo_IsAuthorizedReply.DiscardUnknown(m) } -var xxx_messageInfo_AuthorizeReply proto.InternalMessageInfo +var xxx_messageInfo_IsAuthorizedReply proto.InternalMessageInfo -func (m *AuthorizeReply) GetIsValid() bool { +func (m *IsAuthorizedReply) GetIsValid() bool { if m != nil { return m.IsValid } return false } +type IsAdminRequest struct { + UserToken string `protobuf:"bytes,1,opt,name=user_token,json=userToken,proto3" json:"user_token,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *IsAdminRequest) Reset() { *m = IsAdminRequest{} } +func (m *IsAdminRequest) String() string { return proto.CompactTextString(m) } +func (*IsAdminRequest) ProtoMessage() {} +func (*IsAdminRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_ffbc3c71370bee9a, []int{2} +} + +func (m *IsAdminRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_IsAdminRequest.Unmarshal(m, b) +} +func (m *IsAdminRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_IsAdminRequest.Marshal(b, m, deterministic) +} +func (m *IsAdminRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_IsAdminRequest.Merge(m, src) +} +func (m *IsAdminRequest) XXX_Size() int { + return xxx_messageInfo_IsAdminRequest.Size(m) +} +func (m *IsAdminRequest) XXX_DiscardUnknown() { + xxx_messageInfo_IsAdminRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_IsAdminRequest proto.InternalMessageInfo + +func (m *IsAdminRequest) GetUserToken() string { + if m != nil { + return m.UserToken + } + return "" +} + type IsAdminReply struct { - IsAdmin bool `protobuf:"varint,1,opt,name=is_admin,json=isAdmin,proto3" json:"is_admin,omitempty"` + IsValid bool `protobuf:"varint,1,opt,name=is_valid,json=isValid,proto3" json:"is_valid,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` @@ -156,7 +251,7 @@ func (m *IsAdminReply) Reset() { *m = IsAdminReply{} } func (m *IsAdminReply) String() string { return proto.CompactTextString(m) } func (*IsAdminReply) ProtoMessage() {} func (*IsAdminReply) Descriptor() ([]byte, []int) { - return fileDescriptor_ffbc3c71370bee9a, []int{2} + return fileDescriptor_ffbc3c71370bee9a, []int{3} } func (m *IsAdminReply) XXX_Unmarshal(b []byte) error { @@ -177,40 +272,51 @@ func (m *IsAdminReply) XXX_DiscardUnknown() { var xxx_messageInfo_IsAdminReply proto.InternalMessageInfo -func (m *IsAdminReply) GetIsAdmin() bool { +func (m *IsAdminReply) GetIsValid() bool { if m != nil { - return m.IsAdmin + return m.IsValid } return false } func init() { - proto.RegisterType((*Identity)(nil), "authorize.Identity") - proto.RegisterType((*AuthorizeReply)(nil), "authorize.AuthorizeReply") + 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") + proto.RegisterType((*IsAdminRequest)(nil), "authorize.IsAdminRequest") proto.RegisterType((*IsAdminReply)(nil), "authorize.IsAdminReply") } func init() { proto.RegisterFile("authorize.proto", fileDescriptor_ffbc3c71370bee9a) } var fileDescriptor_ffbc3c71370bee9a = []byte{ - // 264 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x51, 0xbd, 0x4e, 0xc3, 0x30, - 0x10, 0x6e, 0x68, 0x9b, 0x26, 0x27, 0xc4, 0xcf, 0x81, 0x20, 0x65, 0xaa, 0x3c, 0x15, 0x55, 0x74, - 0x80, 0x89, 0x81, 0xa1, 0x03, 0x42, 0x5d, 0x33, 0xb0, 0x56, 0x46, 0xb1, 0xe0, 0xa4, 0x24, 0x8e, - 0x6c, 0x07, 0xa9, 0x3c, 0x00, 0x8f, 0xc5, 0xb3, 0xa1, 0x5c, 0xd2, 0xc4, 0x48, 0x6c, 0xf9, 0x7e, - 0x73, 0x77, 0x86, 0x53, 0x59, 0xbb, 0x0f, 0x6d, 0xe8, 0x4b, 0xad, 0x2b, 0xa3, 0x9d, 0xc6, 0xb8, - 0x27, 0xc4, 0x4f, 0x00, 0xd1, 0x36, 0x53, 0xa5, 0x23, 0xb7, 0xc7, 0x4b, 0x98, 0x1a, 0x5d, 0x3b, - 0x95, 0x04, 0x8b, 0x60, 0x19, 0xa7, 0x2d, 0x40, 0x84, 0x49, 0x6d, 0x95, 0x49, 0x8e, 0x98, 0xe4, - 0xef, 0xc6, 0xa9, 0x0a, 0x49, 0x79, 0x32, 0x6e, 0x9d, 0x0c, 0xf0, 0x0a, 0xc2, 0x77, 0xa3, 0xeb, - 0xca, 0x26, 0x93, 0xc5, 0x78, 0x19, 0xa7, 0x1d, 0xc2, 0x15, 0x9c, 0x53, 0x51, 0x29, 0x63, 0x75, - 0x29, 0x9d, 0xda, 0xb5, 0xc9, 0x29, 0x27, 0xcf, 0x3c, 0xe1, 0x99, 0x4b, 0xee, 0x00, 0x7d, 0x73, - 0x57, 0x18, 0x72, 0xa1, 0x5f, 0xf3, 0xc2, 0x82, 0x58, 0xc1, 0xc9, 0xe6, 0xb0, 0x4d, 0xaa, 0xaa, - 0x7c, 0x8f, 0x73, 0x88, 0xc8, 0xee, 0x3e, 0x65, 0x4e, 0x19, 0x2f, 0x12, 0xa5, 0x33, 0xb2, 0xaf, - 0x0d, 0x14, 0xb7, 0x70, 0xbc, 0xb5, 0x9b, 0xac, 0xa0, 0xd2, 0xb7, 0xca, 0x86, 0x18, 0xac, 0xac, - 0xdf, 0x7f, 0x07, 0x00, 0x7d, 0xb1, 0xc1, 0x27, 0x88, 0x7b, 0x84, 0x17, 0xeb, 0xe1, 0xa2, 0x87, - 0xe3, 0xdd, 0xcc, 0x3d, 0xf2, 0xef, 0x44, 0x62, 0x84, 0x8f, 0x30, 0xeb, 0x7e, 0xfc, 0x7f, 0xf8, - 0xda, 0x27, 0xbd, 0x09, 0xc5, 0xe8, 0x2d, 0xe4, 0x37, 0x7b, 0xf8, 0x0d, 0x00, 0x00, 0xff, 0xff, - 0x6d, 0x2f, 0xa0, 0x1b, 0xc6, 0x01, 0x00, 0x00, + // 390 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x93, 0xc1, 0x8e, 0xda, 0x30, + 0x10, 0x86, 0x1b, 0x52, 0x08, 0x19, 0x28, 0x14, 0x53, 0xa9, 0x26, 0x6a, 0x0b, 0x8d, 0xd4, 0x8a, + 0x5e, 0x52, 0x29, 0xbd, 0x54, 0x55, 0xa5, 0x8a, 0x43, 0x25, 0x38, 0xb4, 0x87, 0xa8, 0xed, 0xa5, + 0x87, 0x28, 0x2b, 0x5b, 0xc2, 0x22, 0x60, 0xd6, 0x76, 0x90, 0xb2, 0xef, 0xb2, 0xef, 0xb7, 0x8f, + 0xb1, 0x8a, 0xb1, 0xc3, 0xb2, 0x62, 0xd9, 0x3d, 0xe1, 0xf9, 0xe7, 0x9b, 0xdf, 0xe3, 0x5f, 0x04, + 0xfa, 0x59, 0xa1, 0x96, 0x5c, 0xb0, 0x2b, 0x1a, 0x6d, 0x05, 0x57, 0x1c, 0xf9, 0xb5, 0x10, 0xde, + 0xb8, 0x30, 0x5c, 0xc8, 0x99, 0xad, 0x49, 0x42, 0x2f, 0x0b, 0x2a, 0x15, 0x7a, 0x0b, 0x50, 0x48, + 0x2a, 0x52, 0xc5, 0x57, 0x74, 0x83, 0x9d, 0x89, 0x33, 0xf5, 0x13, 0xbf, 0x52, 0xfe, 0x54, 0x02, + 0xfa, 0x00, 0x3d, 0xb1, 0x27, 0xd3, 0x35, 0x55, 0x4b, 0x4e, 0x70, 0x43, 0x23, 0x2f, 0x8c, 0xfa, + 0x4b, 0x8b, 0x68, 0x0c, 0x1d, 0x8b, 0x15, 0x22, 0xc7, 0xae, 0x66, 0xc0, 0x48, 0x7f, 0x45, 0x8e, + 0xde, 0x43, 0xd7, 0x02, 0x4b, 0x2e, 0x15, 0x7e, 0xae, 0x09, 0x3b, 0x34, 0xe7, 0x52, 0xa1, 0x08, + 0x86, 0x16, 0x39, 0x78, 0x31, 0xdc, 0xd4, 0xe4, 0xc0, 0x48, 0x89, 0xb5, 0x64, 0xc7, 0xfc, 0x9a, + 0x2b, 0x9a, 0x66, 0x84, 0x08, 0xdc, 0xba, 0xc7, 0x57, 0x9d, 0x19, 0x21, 0x02, 0xfd, 0x87, 0x7e, + 0xbd, 0x02, 0xcd, 0x08, 0x15, 0x12, 0x7b, 0x13, 0x77, 0xda, 0x89, 0xe3, 0xe8, 0x90, 0xdb, 0x89, + 0x88, 0x22, 0xf3, 0x3b, 0xdf, 0x0f, 0xfd, 0xdc, 0x28, 0x51, 0x26, 0x36, 0x15, 0x23, 0x06, 0x63, + 0xf0, 0xcc, 0x11, 0xbd, 0x82, 0xe6, 0x2e, 0xcb, 0x0b, 0x8a, 0x9d, 0x89, 0x3b, 0xf5, 0x93, 0x7d, + 0x11, 0x30, 0x18, 0x9e, 0xf0, 0x41, 0x2f, 0xc1, 0x5d, 0xd1, 0xd2, 0xe4, 0x5e, 0x1d, 0xd1, 0x77, + 0x3b, 0x5e, 0x05, 0xdd, 0x89, 0x3f, 0x3e, 0xb2, 0x9c, 0x71, 0x33, 0xd7, 0x7c, 0x6b, 0x7c, 0x75, + 0xc2, 0x08, 0x06, 0xc7, 0xe4, 0x36, 0x2f, 0xd1, 0x08, 0xda, 0x4c, 0xa6, 0xbb, 0x2c, 0x67, 0x44, + 0xdf, 0xd6, 0x4e, 0x3c, 0x26, 0xff, 0x55, 0x65, 0xf8, 0x19, 0x7a, 0x0b, 0x39, 0x23, 0x6b, 0xb6, + 0x79, 0xda, 0x9f, 0x22, 0xfc, 0x04, 0xdd, 0x7a, 0xe0, 0xbc, 0x77, 0x7c, 0xed, 0x00, 0xd4, 0xab, + 0x08, 0xf4, 0x5b, 0x4f, 0xd6, 0xab, 0xa1, 0x77, 0xe7, 0x5f, 0x17, 0xbc, 0x79, 0xb0, 0xbf, 0xcd, + 0xcb, 0xf0, 0x19, 0xfa, 0x01, 0x9e, 0xd9, 0x04, 0x8d, 0x8e, 0xd1, 0x3b, 0xcf, 0x09, 0x5e, 0x9f, + 0x6a, 0x69, 0x83, 0x8b, 0x96, 0xfe, 0x50, 0xbe, 0xdc, 0x06, 0x00, 0x00, 0xff, 0xff, 0x72, 0x3a, + 0xa3, 0xe0, 0x3b, 0x03, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -225,8 +331,8 @@ const _ = grpc.SupportPackageIsVersion4 // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. type AuthorizerClient interface { - Authorize(ctx context.Context, in *Identity, opts ...grpc.CallOption) (*AuthorizeReply, error) - IsAdmin(ctx context.Context, in *Identity, opts ...grpc.CallOption) (*IsAdminReply, error) + IsAuthorized(ctx context.Context, in *IsAuthorizedRequest, opts ...grpc.CallOption) (*IsAuthorizedReply, error) + IsAdmin(ctx context.Context, in *IsAdminRequest, opts ...grpc.CallOption) (*IsAdminReply, error) } type authorizerClient struct { @@ -237,16 +343,16 @@ func NewAuthorizerClient(cc *grpc.ClientConn) AuthorizerClient { return &authorizerClient{cc} } -func (c *authorizerClient) Authorize(ctx context.Context, in *Identity, opts ...grpc.CallOption) (*AuthorizeReply, error) { - out := new(AuthorizeReply) - err := c.cc.Invoke(ctx, "/authorize.Authorizer/Authorize", in, out, opts...) +func (c *authorizerClient) IsAuthorized(ctx context.Context, in *IsAuthorizedRequest, opts ...grpc.CallOption) (*IsAuthorizedReply, error) { + out := new(IsAuthorizedReply) + err := c.cc.Invoke(ctx, "/authorize.Authorizer/IsAuthorized", in, out, opts...) if err != nil { return nil, err } return out, nil } -func (c *authorizerClient) IsAdmin(ctx context.Context, in *Identity, opts ...grpc.CallOption) (*IsAdminReply, error) { +func (c *authorizerClient) IsAdmin(ctx context.Context, in *IsAdminRequest, opts ...grpc.CallOption) (*IsAdminReply, error) { out := new(IsAdminReply) err := c.cc.Invoke(ctx, "/authorize.Authorizer/IsAdmin", in, out, opts...) if err != nil { @@ -257,18 +363,18 @@ func (c *authorizerClient) IsAdmin(ctx context.Context, in *Identity, opts ...gr // AuthorizerServer is the server API for Authorizer service. type AuthorizerServer interface { - Authorize(context.Context, *Identity) (*AuthorizeReply, error) - IsAdmin(context.Context, *Identity) (*IsAdminReply, error) + IsAuthorized(context.Context, *IsAuthorizedRequest) (*IsAuthorizedReply, error) + IsAdmin(context.Context, *IsAdminRequest) (*IsAdminReply, error) } // UnimplementedAuthorizerServer can be embedded to have forward compatible implementations. type UnimplementedAuthorizerServer struct { } -func (*UnimplementedAuthorizerServer) Authorize(ctx context.Context, req *Identity) (*AuthorizeReply, error) { - return nil, status.Errorf(codes.Unimplemented, "method Authorize not implemented") +func (*UnimplementedAuthorizerServer) IsAuthorized(ctx context.Context, req *IsAuthorizedRequest) (*IsAuthorizedReply, error) { + return nil, status.Errorf(codes.Unimplemented, "method IsAuthorized not implemented") } -func (*UnimplementedAuthorizerServer) IsAdmin(ctx context.Context, req *Identity) (*IsAdminReply, error) { +func (*UnimplementedAuthorizerServer) IsAdmin(ctx context.Context, req *IsAdminRequest) (*IsAdminReply, error) { return nil, status.Errorf(codes.Unimplemented, "method IsAdmin not implemented") } @@ -276,26 +382,26 @@ func RegisterAuthorizerServer(s *grpc.Server, srv AuthorizerServer) { s.RegisterService(&_Authorizer_serviceDesc, srv) } -func _Authorizer_Authorize_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(Identity) +func _Authorizer_IsAuthorized_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(IsAuthorizedRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { - return srv.(AuthorizerServer).Authorize(ctx, in) + return srv.(AuthorizerServer).IsAuthorized(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/authorize.Authorizer/Authorize", + FullMethod: "/authorize.Authorizer/IsAuthorized", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(AuthorizerServer).Authorize(ctx, req.(*Identity)) + return srv.(AuthorizerServer).IsAuthorized(ctx, req.(*IsAuthorizedRequest)) } return interceptor(ctx, in, info, handler) } func _Authorizer_IsAdmin_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(Identity) + in := new(IsAdminRequest) if err := dec(in); err != nil { return nil, err } @@ -307,7 +413,7 @@ func _Authorizer_IsAdmin_Handler(srv interface{}, ctx context.Context, dec func( FullMethod: "/authorize.Authorizer/IsAdmin", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(AuthorizerServer).IsAdmin(ctx, req.(*Identity)) + return srv.(AuthorizerServer).IsAdmin(ctx, req.(*IsAdminRequest)) } return interceptor(ctx, in, info, handler) } @@ -317,8 +423,8 @@ var _Authorizer_serviceDesc = grpc.ServiceDesc{ HandlerType: (*AuthorizerServer)(nil), Methods: []grpc.MethodDesc{ { - MethodName: "Authorize", - Handler: _Authorizer_Authorize_Handler, + MethodName: "IsAuthorized", + Handler: _Authorizer_IsAuthorized_Handler, }, { MethodName: "IsAdmin", diff --git a/internal/grpc/authorize/authorize.proto b/internal/grpc/authorize/authorize.proto index c372e57bc..41eaab2fd 100644 --- a/internal/grpc/authorize/authorize.proto +++ b/internal/grpc/authorize/authorize.proto @@ -3,23 +3,35 @@ syntax = "proto3"; package authorize; service Authorizer { - rpc Authorize(Identity) returns (AuthorizeReply) {} - rpc IsAdmin(Identity) returns (IsAdminReply) {} - + rpc IsAuthorized(IsAuthorizedRequest) returns (IsAuthorizedReply) {} + rpc IsAdmin(IsAdminRequest) returns (IsAdminReply) {} } -message Identity { - // request context - string route = 1; - // user context - string user = 2; - string email = 3; - repeated string groups = 4; - // user context - string impersonate_email = 5; - repeated string impersonate_groups = 6; +message IsAuthorizedRequest { + // User Context + // + string user_token = 1; + // Request Context + // + // Method specifies the HTTP method (GET, POST, PUT, etc.). + string request_method = 2; + // URL specifies either the URI being requested + string request_url = 3; + // host specifies the host on which the URL per RFC 7230, section 5.4 + string request_host = 4; + // request_uri is the unmodified request-target of the + // Request-Line (RFC 7230, Section 3.1.1) as sent by the client + string request_request_uri = 5; + // RemoteAddr allows HTTP servers and other software to record + // the network address that sent the request, usually for + string request_remote_addr = 6; + // headers represents key-value pairs in an HTTP header; map[string][]string + message Headers { repeated string value = 1; } + map request_headers = 7; } -message AuthorizeReply { bool is_valid = 1; } +message IsAuthorizedReply { bool is_valid = 1; } -message IsAdminReply { bool is_admin = 1; } +message IsAdminRequest { string user_token = 1; } + +message IsAdminReply { bool is_valid = 1; } diff --git a/internal/grpc/authorize/client/authorize_client.go b/internal/grpc/authorize/client/authorize_client.go index 4a4b73c04..05c4fded5 100644 --- a/internal/grpc/authorize/client/authorize_client.go +++ b/internal/grpc/authorize/client/authorize_client.go @@ -3,10 +3,10 @@ package client import ( "context" - "errors" + "net/http" + "github.com/pomerium/pomerium/internal/grpc/authorize" pb "github.com/pomerium/pomerium/internal/grpc/authorize" - "github.com/pomerium/pomerium/internal/sessions" "github.com/pomerium/pomerium/internal/telemetry/trace" "google.golang.org/grpc" @@ -16,9 +16,9 @@ import ( type Authorizer interface { // Authorize takes a route and user session and returns whether the // request is valid per access policy - Authorize(context.Context, string, *sessions.State) (bool, error) + Authorize(ctx context.Context, user string, r *http.Request) (bool, error) // IsAdmin takes a session and returns whether the user is an administrator - IsAdmin(context.Context, *sessions.State) (bool, error) + IsAdmin(ctx context.Context, user string) (bool, error) // Close closes the auth connection if any. Close() error } @@ -36,37 +36,47 @@ func New(conn *grpc.ClientConn) (p *Client, err error) { // Authorize takes a route and user session and returns whether the // request is valid per access policy -func (c *Client) Authorize(ctx context.Context, route string, s *sessions.State) (bool, error) { +func (c *Client) Authorize(ctx context.Context, user string, r *http.Request) (bool, error) { ctx, span := trace.StartSpan(ctx, "grpc.authorize.client.Authorize") defer span.End() - - if s == nil { - return false, errors.New("session cannot be nil") - } - response, err := c.client.Authorize(ctx, &pb.Identity{ - Route: route, - User: s.User, - Email: s.Email, - Groups: s.Groups, - ImpersonateEmail: s.ImpersonateEmail, - ImpersonateGroups: s.ImpersonateGroups, + // var h map[string]&structpb.ListValue{} + response, err := c.client.IsAuthorized(ctx, &pb.IsAuthorizedRequest{ + UserToken: user, + RequestHost: r.Host, + RequestMethod: r.Method, + RequestHeaders: cloneHeaders(r.Header), + RequestRemoteAddr: r.RemoteAddr, + RequestRequestUri: r.RequestURI, + RequestUrl: r.URL.String(), }) return response.GetIsValid(), err } -// IsAdmin takes a session and returns whether the user is an administrator -func (c *Client) IsAdmin(ctx context.Context, s *sessions.State) (bool, error) { +// IsAdmin takes a route and user session and returns whether the +// request is valid per access policy +func (c *Client) IsAdmin(ctx context.Context, user string) (bool, error) { ctx, span := trace.StartSpan(ctx, "grpc.authorize.client.IsAdmin") defer span.End() - if s == nil { - return false, errors.New("session cannot be nil") - } - response, err := c.client.IsAdmin(ctx, &pb.Identity{Email: s.Email, Groups: s.Groups}) - return response.GetIsAdmin(), err + response, err := c.client.IsAdmin(ctx, &pb.IsAdminRequest{ + UserToken: user, + }) + return response.GetIsValid(), err } // Close tears down the ClientConn and all underlying connections. func (c *Client) Close() error { return c.Conn.Close() } + +type protoHeader map[string]*authorize.IsAuthorizedRequest_Headers + +func cloneHeaders(in http.Header) protoHeader { + out := make(protoHeader, len(in)) + for key, values := range in { + newValues := make([]string, len(values)) + copy(newValues, values) + out[key] = &authorize.IsAuthorizedRequest_Headers{Value: newValues} + } + return out +} diff --git a/internal/grpc/authorize/client/authorize_client_test.go b/internal/grpc/authorize/client/authorize_client_test.go deleted file mode 100644 index 437dd21bf..000000000 --- a/internal/grpc/authorize/client/authorize_client_test.go +++ /dev/null @@ -1,78 +0,0 @@ -package client - -import ( - "context" - "testing" - - "github.com/golang/mock/gomock" - "github.com/pomerium/pomerium/internal/grpc/authorize" - "github.com/pomerium/pomerium/internal/grpc/authorize/client/mock_authorize" - "github.com/pomerium/pomerium/internal/sessions" -) - -func TestClient_Authorize(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - client := mock_authorize.NewMockAuthorizerClient(ctrl) - client.EXPECT().Authorize( - gomock.Any(), - gomock.Any(), - ).Return(&authorize.AuthorizeReply{IsValid: true}, nil).AnyTimes() - - tests := []struct { - name string - route string - s *sessions.State - want bool - wantErr bool - }{ - {"good", "hello.pomerium.io", &sessions.State{User: "admin@pomerium.io", Email: "admin@pomerium.io"}, true, false}, - {"impersonate request", "hello.pomerium.io", &sessions.State{User: "admin@pomerium.io", Email: "admin@pomerium.io", ImpersonateEmail: "other@other.example"}, true, false}, - {"session cannot be nil", "hello.pomerium.io", nil, false, true}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - a := &Client{client: client} - got, err := a.Authorize(context.Background(), tt.route, tt.s) - if (err != nil) != tt.wantErr { - t.Errorf("Client.Authorize() error = %v, wantErr %v", err, tt.wantErr) - return - } - if got != tt.want { - t.Errorf("Client.Authorize() = %v, want %v", got, tt.want) - } - }) - } -} -func TestClient_IsAdmin(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - client := mock_authorize.NewMockAuthorizerClient(ctrl) - client.EXPECT().IsAdmin( - gomock.Any(), - gomock.Any(), - ).Return(&authorize.IsAdminReply{IsAdmin: true}, nil).AnyTimes() - - tests := []struct { - name string - s *sessions.State - want bool - wantErr bool - }{ - {"good", &sessions.State{User: "admin@pomerium.io", Email: "admin@pomerium.io"}, true, false}, - {"session cannot be nil", nil, false, true}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - a := &Client{client: client} - got, err := a.IsAdmin(context.Background(), tt.s) - if (err != nil) != tt.wantErr { - t.Errorf("Client.IsAdmin() error = %v, wantErr %v", err, tt.wantErr) - return - } - if got != tt.want { - t.Errorf("Client.IsAdmin() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/internal/grpc/authorize/client/mock.go b/internal/grpc/authorize/client/mock.go index c2ffa242a..41e4285b2 100644 --- a/internal/grpc/authorize/client/mock.go +++ b/internal/grpc/authorize/client/mock.go @@ -2,8 +2,7 @@ package client import ( "context" - - "github.com/pomerium/pomerium/internal/sessions" + "net/http" ) var _ Authorizer = &MockAuthorize{} @@ -21,11 +20,11 @@ type MockAuthorize struct { func (a MockAuthorize) Close() error { return a.CloseError } // Authorize is a mocked authorizer client function. -func (a MockAuthorize) Authorize(ctx context.Context, route string, s *sessions.State) (bool, error) { +func (a MockAuthorize) Authorize(ctx context.Context, user string, s *http.Request) (bool, error) { return a.AuthorizeResponse, a.AuthorizeError } // IsAdmin is a mocked IsAdmin function. -func (a MockAuthorize) IsAdmin(ctx context.Context, s *sessions.State) (bool, error) { +func (a MockAuthorize) IsAdmin(ctx context.Context, user string) (bool, error) { return a.IsAdminResponse, a.IsAdminError } diff --git a/internal/grpc/authorize/client/mock_authorize/mock_authorize.go b/internal/grpc/authorize/client/mock_authorize/mock_authorize.go deleted file mode 100644 index 31eab0edd..000000000 --- a/internal/grpc/authorize/client/mock_authorize/mock_authorize.go +++ /dev/null @@ -1,130 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: proto/authorize/authorize.pb.go - -// Package mock_authorize is a generated GoMock package. -package mock_authorize - -import ( - context "context" - reflect "reflect" - - gomock "github.com/golang/mock/gomock" - authorize "github.com/pomerium/pomerium/internal/grpc/authorize" - grpc "google.golang.org/grpc" -) - -// MockAuthorizerClient is a mock of AuthorizerClient interface -type MockAuthorizerClient struct { - ctrl *gomock.Controller - recorder *MockAuthorizerClientMockRecorder -} - -// MockAuthorizerClientMockRecorder is the mock recorder for MockAuthorizerClient -type MockAuthorizerClientMockRecorder struct { - mock *MockAuthorizerClient -} - -// NewMockAuthorizerClient creates a new mock instance -func NewMockAuthorizerClient(ctrl *gomock.Controller) *MockAuthorizerClient { - mock := &MockAuthorizerClient{ctrl: ctrl} - mock.recorder = &MockAuthorizerClientMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use -func (m *MockAuthorizerClient) EXPECT() *MockAuthorizerClientMockRecorder { - return m.recorder -} - -// Authorize mocks base method -func (m *MockAuthorizerClient) Authorize(ctx context.Context, in *authorize.Identity, opts ...grpc.CallOption) (*authorize.AuthorizeReply, error) { - m.ctrl.T.Helper() - varargs := []interface{}{ctx, in} - for _, a := range opts { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "Authorize", varargs...) - ret0, _ := ret[0].(*authorize.AuthorizeReply) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Authorize indicates an expected call of Authorize -func (mr *MockAuthorizerClientMockRecorder) Authorize(ctx, in interface{}, opts ...interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{ctx, in}, opts...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Authorize", reflect.TypeOf((*MockAuthorizerClient)(nil).Authorize), varargs...) -} - -// IsAdmin mocks base method -func (m *MockAuthorizerClient) IsAdmin(ctx context.Context, in *authorize.Identity, opts ...grpc.CallOption) (*authorize.IsAdminReply, error) { - m.ctrl.T.Helper() - varargs := []interface{}{ctx, in} - for _, a := range opts { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "IsAdmin", varargs...) - ret0, _ := ret[0].(*authorize.IsAdminReply) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// IsAdmin indicates an expected call of IsAdmin -func (mr *MockAuthorizerClientMockRecorder) IsAdmin(ctx, in interface{}, opts ...interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{ctx, in}, opts...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsAdmin", reflect.TypeOf((*MockAuthorizerClient)(nil).IsAdmin), varargs...) -} - -// MockAuthorizerServer is a mock of AuthorizerServer interface -type MockAuthorizerServer struct { - ctrl *gomock.Controller - recorder *MockAuthorizerServerMockRecorder -} - -// MockAuthorizerServerMockRecorder is the mock recorder for MockAuthorizerServer -type MockAuthorizerServerMockRecorder struct { - mock *MockAuthorizerServer -} - -// NewMockAuthorizerServer creates a new mock instance -func NewMockAuthorizerServer(ctrl *gomock.Controller) *MockAuthorizerServer { - mock := &MockAuthorizerServer{ctrl: ctrl} - mock.recorder = &MockAuthorizerServerMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use -func (m *MockAuthorizerServer) EXPECT() *MockAuthorizerServerMockRecorder { - return m.recorder -} - -// Authorize mocks base method -func (m *MockAuthorizerServer) Authorize(arg0 context.Context, arg1 *authorize.Identity) (*authorize.AuthorizeReply, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Authorize", arg0, arg1) - ret0, _ := ret[0].(*authorize.AuthorizeReply) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Authorize indicates an expected call of Authorize -func (mr *MockAuthorizerServerMockRecorder) Authorize(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Authorize", reflect.TypeOf((*MockAuthorizerServer)(nil).Authorize), arg0, arg1) -} - -// IsAdmin mocks base method -func (m *MockAuthorizerServer) IsAdmin(arg0 context.Context, arg1 *authorize.Identity) (*authorize.IsAdminReply, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "IsAdmin", arg0, arg1) - ret0, _ := ret[0].(*authorize.IsAdminReply) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// IsAdmin indicates an expected call of IsAdmin -func (mr *MockAuthorizerServerMockRecorder) IsAdmin(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsAdmin", reflect.TypeOf((*MockAuthorizerServer)(nil).IsAdmin), arg0, arg1) -} diff --git a/internal/sessions/cache/cache_store.go b/internal/sessions/cache/cache_store.go index b0eabe6d8..db117fb15 100644 --- a/internal/sessions/cache/cache_store.go +++ b/internal/sessions/cache/cache_store.go @@ -51,26 +51,26 @@ func NewStore(o *Options) *Store { // LoadSession looks for a preset query parameter in the request body // representing the key to lookup from the cache. -func (s *Store) LoadSession(r *http.Request) (*sessions.State, error) { +func (s *Store) LoadSession(r *http.Request) (*sessions.State, string, error) { // look for our cache's key in the default query param sessionID := r.URL.Query().Get(s.queryParam) if sessionID == "" { - return nil, sessions.ErrNoSessionFound + return nil, "", sessions.ErrNoSessionFound } exists, val, err := s.cache.Get(r.Context(), sessionID) if err != nil { log.FromRequest(r).Debug().Msg("sessions/cache: miss, trying wrapped loader") - return nil, err + return nil, "", err } if !exists { - return nil, sessions.ErrNoSessionFound + return nil, "", sessions.ErrNoSessionFound } var session sessions.State if err := s.encoder.Unmarshal(val, &session); err != nil { log.FromRequest(r).Error().Err(err).Msg("sessions/cache: unmarshal") - return nil, sessions.ErrMalformed + return nil, "", sessions.ErrMalformed } - return &session, nil + return &session, string(val), nil } // ClearSession clears the session from the wrapped store. diff --git a/internal/sessions/cache/cache_store_test.go b/internal/sessions/cache/cache_store_test.go index 436b0784b..6321e26d2 100644 --- a/internal/sessions/cache/cache_store_test.go +++ b/internal/sessions/cache/cache_store_test.go @@ -187,7 +187,7 @@ func TestStore_LoadSession(t *testing.T) { r.URL.RawQuery = q.Encode() r.Header.Set("Accept", "application/json") - _, err := s.LoadSession(r) + _, _, err := s.LoadSession(r) if (err != nil) != tt.wantErr { t.Errorf("Store.LoadSession() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/internal/sessions/cookie/cookie_store.go b/internal/sessions/cookie/cookie_store.go index 57cd177b4..38143dc3b 100644 --- a/internal/sessions/cookie/cookie_store.go +++ b/internal/sessions/cookie/cookie_store.go @@ -125,21 +125,21 @@ func getCookies(r *http.Request, name string) []*http.Cookie { } // LoadSession returns a State from the cookie in the request. -func (cs *Store) LoadSession(r *http.Request) (*sessions.State, error) { +func (cs *Store) LoadSession(r *http.Request) (*sessions.State, string, error) { cookies := getCookies(r, cs.Name) if len(cookies) == 0 { - return nil, sessions.ErrNoSessionFound + return nil, "", sessions.ErrNoSessionFound } for _, cookie := range cookies { - data := loadChunkedCookie(r, cookie) + jwt := loadChunkedCookie(r, cookie) session := &sessions.State{} - err := cs.decoder.Unmarshal([]byte(data), session) + err := cs.decoder.Unmarshal([]byte(jwt), session) if err == nil { - return session, nil + return session, jwt, nil } } - return nil, sessions.ErrMalformed + return nil, "", sessions.ErrMalformed } // SaveSession saves a session state to a request's cookie store. diff --git a/internal/sessions/cookie/cookie_store_test.go b/internal/sessions/cookie/cookie_store_test.go index 4a4475868..52cdeaf40 100644 --- a/internal/sessions/cookie/cookie_store_test.go +++ b/internal/sessions/cookie/cookie_store_test.go @@ -138,7 +138,7 @@ func TestStore_SaveSession(t *testing.T) { r.AddCookie(cookie) } - state, err := s.LoadSession(r) + state, _, err := s.LoadSession(r) if (err != nil) != tt.wantLoadErr { t.Errorf("LoadSession() error = %v, wantErr %v", err, tt.wantLoadErr) return diff --git a/internal/sessions/cookie/middleware_test.go b/internal/sessions/cookie/middleware_test.go index a077330a9..f1fa3cf64 100644 --- a/internal/sessions/cookie/middleware_test.go +++ b/internal/sessions/cookie/middleware_test.go @@ -18,7 +18,7 @@ import ( func testAuthorizer(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, err := sessions.FromContext(r.Context()) + _, _, err := sessions.FromContext(r.Context()) if err != nil { http.Error(w, err.Error(), http.StatusUnauthorized) return diff --git a/internal/sessions/header/header_store.go b/internal/sessions/header/header_store.go index 55b9d52fd..2e3a09f1e 100644 --- a/internal/sessions/header/header_store.go +++ b/internal/sessions/header/header_store.go @@ -42,16 +42,16 @@ func NewStore(enc encoding.Unmarshaler, headerType string) *Store { } // LoadSession tries to retrieve the token string from the Authorization header. -func (as *Store) LoadSession(r *http.Request) (*sessions.State, error) { - cipherText := TokenFromHeader(r, as.authHeader, as.authType) - if cipherText == "" { - return nil, sessions.ErrNoSessionFound +func (as *Store) LoadSession(r *http.Request) (*sessions.State, string, error) { + jwt := TokenFromHeader(r, as.authHeader, as.authType) + if jwt == "" { + return nil, "", sessions.ErrNoSessionFound } var session sessions.State - if err := as.encoder.Unmarshal([]byte(cipherText), &session); err != nil { - return nil, sessions.ErrMalformed + if err := as.encoder.Unmarshal([]byte(jwt), &session); err != nil { + return nil, "", sessions.ErrMalformed } - return &session, nil + return &session, jwt, nil } // TokenFromHeader retrieves the value of the authorization header from a given diff --git a/internal/sessions/header/middleware_test.go b/internal/sessions/header/middleware_test.go index e1f45f47a..3859c8d7a 100644 --- a/internal/sessions/header/middleware_test.go +++ b/internal/sessions/header/middleware_test.go @@ -18,7 +18,7 @@ import ( func testAuthorizer(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, err := sessions.FromContext(r.Context()) + _, _, err := sessions.FromContext(r.Context()) if err != nil { http.Error(w, err.Error(), http.StatusUnauthorized) return diff --git a/internal/sessions/middleware.go b/internal/sessions/middleware.go index 738a048e2..9895134b4 100644 --- a/internal/sessions/middleware.go +++ b/internal/sessions/middleware.go @@ -10,8 +10,9 @@ import ( // Context keys var ( - SessionCtxKey = &contextKey{"Session"} - ErrorCtxKey = &contextKey{"Error"} + SessionCtxKey = &contextKey{"Session"} + SessionJWTCtxKey = &contextKey{"SessionJWT"} + ErrorCtxKey = &contextKey{"Error"} ) // RetrieveSession takes a slice of session loaders and tries to find a valid @@ -26,8 +27,8 @@ func retrieve(s ...SessionLoader) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { hfn := func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - state, err := retrieveFromRequest(r, s...) - ctx = NewContext(ctx, state, err) + state, jwt, err := retrieveFromRequest(r, s...) + ctx = NewContext(ctx, state, jwt, err) next.ServeHTTP(w, r.WithContext(ctx)) } return http.HandlerFunc(hfn) @@ -36,33 +37,36 @@ func retrieve(s ...SessionLoader) func(http.Handler) http.Handler { // retrieveFromRequest extracts sessions state from the request by calling // token find functions in the order they where provided. -func retrieveFromRequest(r *http.Request, sessions ...SessionLoader) (*State, error) { +func retrieveFromRequest(r *http.Request, sessions ...SessionLoader) (*State, string, error) { for _, s := range sessions { - state, err := s.LoadSession(r) + state, jwt, err := s.LoadSession(r) if err != nil && !errors.Is(err, ErrNoSessionFound) { - return state, err + return state, jwt, err } if state != nil { + //todo(bdd): have authz verify err := state.Verify(urlutil.StripPort(r.Host)) - return state, err // N.B.: state is _not_ nil + return state, jwt, err // N.B.: state is _not_ nil } } - return nil, ErrNoSessionFound + return nil, "", ErrNoSessionFound } // NewContext sets context values for the user session state and error. -func NewContext(ctx context.Context, t *State, err error) context.Context { +func NewContext(ctx context.Context, t *State, jwt string, err error) context.Context { ctx = context.WithValue(ctx, SessionCtxKey, t) + ctx = context.WithValue(ctx, SessionJWTCtxKey, jwt) ctx = context.WithValue(ctx, ErrorCtxKey, err) return ctx } // FromContext retrieves context values for the user session state and error. -func FromContext(ctx context.Context) (*State, error) { +func FromContext(ctx context.Context) (*State, string, error) { state, _ := ctx.Value(SessionCtxKey).(*State) + jwt, _ := ctx.Value(SessionJWTCtxKey).(string) err, _ := ctx.Value(ErrorCtxKey).(error) - return state, err + return state, jwt, err } // contextKey is a value for use with context.WithValue. It's used as diff --git a/internal/sessions/middleware_test.go b/internal/sessions/middleware_test.go index 0435dbf9c..67167cfda 100644 --- a/internal/sessions/middleware_test.go +++ b/internal/sessions/middleware_test.go @@ -25,8 +25,8 @@ func TestNewContext(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ctxOut := NewContext(tt.ctx, tt.t, tt.err) - stateOut, errOut := FromContext(ctxOut) + ctxOut := NewContext(tt.ctx, tt.t, "", tt.err) + stateOut, _, errOut := FromContext(ctxOut) if diff := cmp.Diff(tt.t.Email, stateOut.Email); diff != "" { t.Errorf("NewContext() = %s", diff) } @@ -59,7 +59,7 @@ func Test_contextKey_String(t *testing.T) { func testAuthorizer(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, err := FromContext(r.Context()) + _, _, err := FromContext(r.Context()) if err != nil { http.Error(w, err.Error(), http.StatusUnauthorized) return @@ -84,8 +84,8 @@ func (ms *store) ClearSession(http.ResponseWriter, *http.Request) { } // LoadSession returns the session and a error -func (ms store) LoadSession(*http.Request) (*State, error) { - return ms.Session, ms.LoadError +func (ms store) LoadSession(*http.Request) (*State, string, error) { + return ms.Session, "", ms.LoadError } // SaveSession returns a save error. diff --git a/internal/sessions/mock/mock_store.go b/internal/sessions/mock/mock_store.go index efc891832..dd13a20b3 100644 --- a/internal/sessions/mock/mock_store.go +++ b/internal/sessions/mock/mock_store.go @@ -13,6 +13,7 @@ var _ sessions.SessionLoader = &Store{} // Store is a mock implementation of the SessionStore interface type Store struct { ResponseSession string + SessionJWT string Session *sessions.State SaveError error LoadError error @@ -24,8 +25,8 @@ func (ms *Store) ClearSession(http.ResponseWriter, *http.Request) { } // LoadSession returns the session and a error -func (ms Store) LoadSession(*http.Request) (*sessions.State, error) { - return ms.Session, ms.LoadError +func (ms Store) LoadSession(*http.Request) (*sessions.State, string, error) { + return ms.Session, ms.SessionJWT, ms.LoadError } // SaveSession returns a save error. diff --git a/internal/sessions/mock/mock_store_test.go b/internal/sessions/mock/mock_store_test.go index 9f2ae4c3a..b58386286 100644 --- a/internal/sessions/mock/mock_store_test.go +++ b/internal/sessions/mock/mock_store_test.go @@ -35,7 +35,7 @@ func TestStore(t *testing.T) { t.Errorf("MockCSRFStore.GetCSRF() error = %v, wantSaveErr %v", err, tt.wantSaveErr) return } - got, err := ms.LoadSession(nil) + got, _, err := ms.LoadSession(nil) if (err != nil) != tt.wantLoadErr { t.Errorf("MockCSRFStore.GetCSRF() error = %v, wantLoadErr %v", err, tt.wantLoadErr) return diff --git a/internal/sessions/queryparam/middleware_test.go b/internal/sessions/queryparam/middleware_test.go index c9aab9c59..32ea9b3e3 100644 --- a/internal/sessions/queryparam/middleware_test.go +++ b/internal/sessions/queryparam/middleware_test.go @@ -18,7 +18,7 @@ import ( func testAuthorizer(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, err := sessions.FromContext(r.Context()) + _, _, err := sessions.FromContext(r.Context()) if err != nil { http.Error(w, err.Error(), http.StatusUnauthorized) return diff --git a/internal/sessions/queryparam/query_store.go b/internal/sessions/queryparam/query_store.go index d9eac72bc..936b9c564 100644 --- a/internal/sessions/queryparam/query_store.go +++ b/internal/sessions/queryparam/query_store.go @@ -41,16 +41,16 @@ func NewStore(enc encoding.MarshalUnmarshaler, qp string) *Store { } // LoadSession tries to retrieve the token string from URL query parameters. -func (qp *Store) LoadSession(r *http.Request) (*sessions.State, error) { - cipherText := r.URL.Query().Get(qp.queryParamKey) - if cipherText == "" { - return nil, sessions.ErrNoSessionFound +func (qp *Store) LoadSession(r *http.Request) (*sessions.State, string, error) { + jwt := r.URL.Query().Get(qp.queryParamKey) + if jwt == "" { + return nil, "", sessions.ErrNoSessionFound } var session sessions.State - if err := qp.decoder.Unmarshal([]byte(cipherText), &session); err != nil { - return nil, sessions.ErrMalformed + if err := qp.decoder.Unmarshal([]byte(jwt), &session); err != nil { + return nil, "", sessions.ErrMalformed } - return &session, nil + return &session, jwt, nil } // ClearSession clears the session cookie from a request's query param key `pomerium_session`. diff --git a/internal/sessions/store.go b/internal/sessions/store.go index 792b73500..d5fa5e3da 100644 --- a/internal/sessions/store.go +++ b/internal/sessions/store.go @@ -15,5 +15,5 @@ type SessionStore interface { // SessionLoader defines an interface for loading a session. type SessionLoader interface { - LoadSession(*http.Request) (*State, error) + LoadSession(*http.Request) (*State, string, error) } diff --git a/proxy/forward_auth.go b/proxy/forward_auth.go index ece0f02b0..7b9137d0b 100644 --- a/proxy/forward_auth.go +++ b/proxy/forward_auth.go @@ -81,7 +81,7 @@ func (p *Proxy) Verify(verifyOnly bool) http.Handler { return httputil.NewError(http.StatusBadRequest, err) } - s, err := sessions.FromContext(r.Context()) + s, _, err := sessions.FromContext(r.Context()) if errors.Is(err, sessions.ErrNoSessionFound) || errors.Is(err, sessions.ErrExpired) { if verifyOnly { return httputil.NewError(http.StatusUnauthorized, err) @@ -104,7 +104,8 @@ func (p *Proxy) Verify(verifyOnly bool) http.Handler { return httputil.NewError(http.StatusUnauthorized, err) } p.addPomeriumHeaders(w, r) - if err := p.authorize(uri.Host, r); err != nil { + r.Host = uri.Host + if err := p.authorize(r); err != nil { return err } diff --git a/proxy/forward_auth_test.go b/proxy/forward_auth_test.go index cb9aa1f85..66ed2bc35 100644 --- a/proxy/forward_auth_test.go +++ b/proxy/forward_auth_test.go @@ -91,10 +91,10 @@ func TestProxy_ForwardAuth(t *testing.T) { uri.RawQuery = queryString.Encode() r := httptest.NewRequest(tt.method, uri.String(), nil) - state, _ := tt.sessionStore.LoadSession(r) + state, _, _ := tt.sessionStore.LoadSession(r) ctx := r.Context() - ctx = sessions.NewContext(ctx, state, tt.ctxError) + ctx = sessions.NewContext(ctx, state, "", tt.ctxError) r = r.WithContext(ctx) r.Header.Set("Accept", "application/json") if len(tt.headers) != 0 { diff --git a/proxy/handlers.go b/proxy/handlers.go index 25bd16f0c..78c9c716e 100644 --- a/proxy/handlers.go +++ b/proxy/handlers.go @@ -87,12 +87,12 @@ func (p *Proxy) SignOut(w http.ResponseWriter, r *http.Request) { // It also contains certain administrative actions like user impersonation. // Nota bene: This endpoint does authentication, not authorization. func (p *Proxy) UserDashboard(w http.ResponseWriter, r *http.Request) error { - session, err := sessions.FromContext(r.Context()) + session, jwt, err := sessions.FromContext(r.Context()) if err != nil { return err } - isAdmin, err := p.AuthorizeClient.IsAdmin(r.Context(), session) + isAdmin, err := p.AuthorizeClient.IsAdmin(r.Context(), jwt) if err != nil { return err } @@ -112,11 +112,11 @@ func (p *Proxy) UserDashboard(w http.ResponseWriter, r *http.Request) error { // to the user's current user sessions state if the user is currently an // administrative user. Requests are redirected back to the user dashboard. func (p *Proxy) Impersonate(w http.ResponseWriter, r *http.Request) error { - session, err := sessions.FromContext(r.Context()) + session, jwt, err := sessions.FromContext(r.Context()) if err != nil { return err } - isAdmin, err := p.AuthorizeClient.IsAdmin(r.Context(), session) + isAdmin, err := p.AuthorizeClient.IsAdmin(r.Context(), jwt) if err != nil { return err } diff --git a/proxy/handlers_test.go b/proxy/handlers_test.go index c8f301758..3d4048c21 100644 --- a/proxy/handlers_test.go +++ b/proxy/handlers_test.go @@ -96,9 +96,9 @@ func TestProxy_UserDashboard(t *testing.T) { p.AuthorizeClient = tt.authorizer r := httptest.NewRequest(tt.method, "/", nil) - state, _ := tt.session.LoadSession(r) + state, _, _ := tt.session.LoadSession(r) ctx := r.Context() - ctx = sessions.NewContext(ctx, state, tt.ctxError) + ctx = sessions.NewContext(ctx, state, "", tt.ctxError) r = r.WithContext(ctx) r.Header.Set("Accept", "application/json") @@ -159,9 +159,9 @@ func TestProxy_Impersonate(t *testing.T) { uri := &url.URL{Path: "/"} r := httptest.NewRequest(tt.method, uri.String(), bytes.NewBufferString(postForm.Encode())) - state, _ := tt.sessionStore.LoadSession(r) + state, _, _ := tt.sessionStore.LoadSession(r) ctx := r.Context() - ctx = sessions.NewContext(ctx, state, tt.ctxError) + ctx = sessions.NewContext(ctx, state, "", tt.ctxError) r = r.WithContext(ctx) r.Header.Set("Content-Type", "application/x-www-form-urlencoded; param=value") diff --git a/proxy/middleware.go b/proxy/middleware.go index 9e995d859..56c3a0f2a 100644 --- a/proxy/middleware.go +++ b/proxy/middleware.go @@ -34,7 +34,7 @@ func (p *Proxy) AuthenticateSession(next http.Handler) http.Handler { ctx, span := trace.StartSpan(r.Context(), "proxy.AuthenticateSession") defer span.End() - _, err := sessions.FromContext(ctx) + _, _, err := sessions.FromContext(ctx) if errors.Is(err, sessions.ErrExpired) { ctx, err = p.refresh(ctx, w, r) if err != nil { @@ -55,7 +55,7 @@ func (p *Proxy) AuthenticateSession(next http.Handler) http.Handler { func (p *Proxy) refresh(ctx context.Context, w http.ResponseWriter, r *http.Request) (context.Context, error) { ctx, span := trace.StartSpan(ctx, "proxy.AuthenticateSession/refresh") defer span.End() - s, err := sessions.FromContext(ctx) + s, _, err := sessions.FromContext(ctx) if !errors.Is(err, sessions.ErrExpired) || s == nil { return nil, errors.New("proxy: unexpected session state for refresh") } @@ -101,11 +101,11 @@ func (p *Proxy) refresh(ctx context.Context, w http.ResponseWriter, r *http.Requ if err := state.Verify(urlutil.StripPort(r.Host)); err != nil { return nil, err } - return sessions.NewContext(r.Context(), &state, err), nil + return sessions.NewContext(r.Context(), &state, string(jwtBytes), err), nil } func (p *Proxy) redirectToSignin(w http.ResponseWriter, r *http.Request) error { - s, err := sessions.FromContext(r.Context()) + s, _, err := sessions.FromContext(r.Context()) p.sessionStore.ClearSession(w, r) if s != nil && err != nil && s.Programmatic { return httputil.NewError(http.StatusUnauthorized, err) @@ -120,7 +120,7 @@ func (p *Proxy) redirectToSignin(w http.ResponseWriter, r *http.Request) error { } func (p *Proxy) addPomeriumHeaders(w http.ResponseWriter, r *http.Request) { - s, err := sessions.FromContext(r.Context()) + s, _, err := sessions.FromContext(r.Context()) if err == nil && s != nil { r.Header.Set(HeaderUserID, s.Subject) r.Header.Set(HeaderEmail, s.RequestEmail()) @@ -137,7 +137,7 @@ func (p *Proxy) AuthorizeSession(next http.Handler) http.Handler { return httputil.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { ctx, span := trace.StartSpan(r.Context(), "proxy.AuthorizeSession") defer span.End() - if err := p.authorize(r.Host, r.WithContext(ctx)); err != nil { + if err := p.authorize(r.WithContext(ctx)); err != nil { log.FromRequest(r).Debug().Err(err).Msg("proxy: AuthorizeSession") return err } @@ -146,16 +146,16 @@ func (p *Proxy) AuthorizeSession(next http.Handler) http.Handler { }) } -func (p *Proxy) authorize(host string, r *http.Request) error { - s, err := sessions.FromContext(r.Context()) +func (p *Proxy) authorize(r *http.Request) error { + s, jwt, err := sessions.FromContext(r.Context()) if err != nil { return httputil.NewError(http.StatusInternalServerError, err) } - authorized, err := p.AuthorizeClient.Authorize(r.Context(), host, s) + authorized, err := p.AuthorizeClient.Authorize(r.Context(), jwt, r) if err != nil { return err } else if !authorized { - return httputil.NewError(http.StatusForbidden, fmt.Errorf("%s is not authorized for %s", s.RequestEmail(), host)) + return httputil.NewError(http.StatusForbidden, fmt.Errorf("%s is not authorized for %s", s.RequestEmail(), r.Host)) } return nil } @@ -167,7 +167,7 @@ func (p *Proxy) SignRequest(signer encoding.Marshaler) func(next http.Handler) h return httputil.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { ctx, span := trace.StartSpan(r.Context(), "proxy.SignRequest") defer span.End() - s, err := sessions.FromContext(r.Context()) + s, _, err := sessions.FromContext(r.Context()) if err != nil { return httputil.NewError(http.StatusForbidden, err) } diff --git a/proxy/middleware_test.go b/proxy/middleware_test.go index 74f05aab9..229592f75 100644 --- a/proxy/middleware_test.go +++ b/proxy/middleware_test.go @@ -73,9 +73,9 @@ func TestProxy_AuthenticateSession(t *testing.T) { encoder: tt.encoder, } r := httptest.NewRequest(http.MethodGet, "/", nil) - state, _ := tt.session.LoadSession(r) + state, _, _ := tt.session.LoadSession(r) ctx := r.Context() - ctx = sessions.NewContext(ctx, state, tt.ctxError) + ctx = sessions.NewContext(ctx, state, "", tt.ctxError) r = r.WithContext(ctx) r.Header.Set("Accept", "application/json") w := httptest.NewRecorder() @@ -122,9 +122,9 @@ func TestProxy_AuthorizeSession(t *testing.T) { AuthorizeClient: tt.authzClient, } r := httptest.NewRequest(http.MethodGet, "/", nil) - state, _ := tt.session.LoadSession(r) + state, _, _ := tt.session.LoadSession(r) ctx := r.Context() - ctx = sessions.NewContext(ctx, state, tt.ctxError) + ctx = sessions.NewContext(ctx, state, "", tt.ctxError) r = r.WithContext(ctx) r.Header.Set("Accept", "application/json") w := httptest.NewRecorder() @@ -181,9 +181,9 @@ func TestProxy_SignRequest(t *testing.T) { sessionStore: tt.session, } r := httptest.NewRequest(http.MethodGet, "/", nil) - state, _ := tt.session.LoadSession(r) + state, _, _ := tt.session.LoadSession(r) ctx := r.Context() - ctx = sessions.NewContext(ctx, state, tt.ctxError) + ctx = sessions.NewContext(ctx, state, "", tt.ctxError) r = r.WithContext(ctx) r.Header.Set("Accept", "application/json") w := httptest.NewRecorder() diff --git a/proxy/proxy.go b/proxy/proxy.go index a56b14bf6..e797228a8 100755 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -176,7 +176,8 @@ func New(opts config.Options) (*Proxy, error) { return p, err } -// UpdateOptions updates internal structures based on config.Options +// UpdateOptions implements the OptionsUpdater interface and updates internal +// structures based on config.Options func (p *Proxy) UpdateOptions(o config.Options) error { if p == nil { return nil @@ -273,12 +274,13 @@ func (p *Proxy) reverseProxyHandler(r *mux.Router, policy config.Policy) (*mux.R // 4. Retrieve the user session and add it to the request context rp.Use(sessions.RetrieveSession(p.sessionLoaders...)) - // 5. Strip the user session cookie from the downstream request - rp.Use(middleware.StripCookie(p.cookieOptions.Name)) - // 6. AuthN - Verify the user is authenticated. Set email, group, & id headers + // 5. AuthN - Verify the user is authenticated. Set email, group, & id headers rp.Use(p.AuthenticateSession) - // 7. AuthZ - Verify the user is authorized for route + // 6. AuthZ - Verify the user is authorized for route rp.Use(p.AuthorizeSession) + // 7. Strip the user session cookie from the downstream request + rp.Use(middleware.StripCookie(p.cookieOptions.Name)) + // Optional: Add a signed JWT attesting to the user's id, email, and group if len(p.signingKey) != 0 { signer, err := jws.NewES256Signer(p.signingKey, policy.Destination.Host)