mirror of
https://github.com/pomerium/pomerium.git
synced 2025-04-29 10:26:29 +02:00
ppl: refactor authorize to evaluate PPL (#2224)
* ppl: refactor authorize to evaluate PPL * remove opa test step * add log statement * simplify assignment * deny with forbidden if logged in * add safeEval function * create evaluator-specific config and options * embed the headers rego file directly
This commit is contained in:
parent
8c56d64f31
commit
dad35bcfb0
26 changed files with 1451 additions and 2211 deletions
3
Makefile
3
Makefile
|
@ -34,7 +34,6 @@ GOOS = $(shell $(GO) env GOOS)
|
||||||
GOARCH= $(shell $(GO) env GOARCH)
|
GOARCH= $(shell $(GO) env GOARCH)
|
||||||
MISSPELL_VERSION = v0.3.4
|
MISSPELL_VERSION = v0.3.4
|
||||||
GOLANGCI_VERSION = v1.34.1
|
GOLANGCI_VERSION = v1.34.1
|
||||||
OPA_VERSION = v0.26.0
|
|
||||||
GETENVOY_VERSION = v0.2.0
|
GETENVOY_VERSION = v0.2.0
|
||||||
GORELEASER_VERSION = v0.157.0
|
GORELEASER_VERSION = v0.157.0
|
||||||
|
|
||||||
|
@ -56,7 +55,6 @@ deps-lint: ## Install lint dependencies
|
||||||
.PHONY: deps-build
|
.PHONY: deps-build
|
||||||
deps-build: ## Install build dependencies
|
deps-build: ## Install build dependencies
|
||||||
@echo "==> $@"
|
@echo "==> $@"
|
||||||
@$(GO) install github.com/open-policy-agent/opa@${OPA_VERSION}
|
|
||||||
@$(GO) install github.com/tetratelabs/getenvoy/cmd/getenvoy@${GETENVOY_VERSION}
|
@$(GO) install github.com/tetratelabs/getenvoy/cmd/getenvoy@${GETENVOY_VERSION}
|
||||||
|
|
||||||
.PHONY: deps-release
|
.PHONY: deps-release
|
||||||
|
@ -107,7 +105,6 @@ lint: ## Verifies `golint` passes.
|
||||||
test: ## Runs the go tests.
|
test: ## Runs the go tests.
|
||||||
@echo "==> $@"
|
@echo "==> $@"
|
||||||
@$(GO) test -tags "$(BUILDTAGS)" $(shell $(GO) list ./... | grep -v vendor | grep -v github.com/pomerium/pomerium/integration)
|
@$(GO) test -tags "$(BUILDTAGS)" $(shell $(GO) list ./... | grep -v vendor | grep -v github.com/pomerium/pomerium/integration)
|
||||||
@opa test ./authorize/evaluator/opa/policy
|
|
||||||
|
|
||||||
.PHONY: spellcheck
|
.PHONY: spellcheck
|
||||||
spellcheck: # Spellcheck docs
|
spellcheck: # Spellcheck docs
|
||||||
|
|
|
@ -88,7 +88,25 @@ func newPolicyEvaluator(opts *config.Options, store *evaluator.Store) (*evaluato
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
_, span := trace.StartSpan(ctx, "authorize.newPolicyEvaluator")
|
_, span := trace.StartSpan(ctx, "authorize.newPolicyEvaluator")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
return evaluator.New(opts, store)
|
|
||||||
|
clientCA, err := opts.GetClientCA()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("authorize: invalid client CA: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
authenticateURL, err := opts.GetAuthenticateURL()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("authorize: invalid authenticate url: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return evaluator.New(ctx, store,
|
||||||
|
evaluator.WithPolicies(opts.GetAllPolicies()),
|
||||||
|
evaluator.WithClientCA(clientCA),
|
||||||
|
evaluator.WithSigningKey(opts.SigningKeyAlgorithm, opts.SigningKey),
|
||||||
|
evaluator.WithAuthenticateURL(authenticateURL.String()),
|
||||||
|
evaluator.WithGoogleCloudServerlessAuthenticationServiceAccount(opts.GetGoogleCloudServerlessAuthenticationServiceAccount()),
|
||||||
|
evaluator.WithJWTClaimsHeaders(opts.JWTClaimsHeaders),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnConfigChange updates internal structures based on config.Options
|
// OnConfigChange updates internal structures based on config.Options
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
envoy_config_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
|
envoy_config_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
|
||||||
envoy_service_auth_v3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3"
|
envoy_service_auth_v3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3"
|
||||||
|
@ -25,15 +26,15 @@ import (
|
||||||
|
|
||||||
func (a *Authorize) okResponse(reply *evaluator.Result) *envoy_service_auth_v3.CheckResponse {
|
func (a *Authorize) okResponse(reply *evaluator.Result) *envoy_service_auth_v3.CheckResponse {
|
||||||
var requestHeaders []*envoy_config_core_v3.HeaderValueOption
|
var requestHeaders []*envoy_config_core_v3.HeaderValueOption
|
||||||
for k, v := range reply.Headers {
|
for k, vs := range reply.Headers {
|
||||||
requestHeaders = append(requestHeaders, mkHeader(k, v, false))
|
requestHeaders = append(requestHeaders, mkHeader(k, strings.Join(vs, ","), false))
|
||||||
}
|
}
|
||||||
// ensure request headers are sorted by key for deterministic output
|
// ensure request headers are sorted by key for deterministic output
|
||||||
sort.Slice(requestHeaders, func(i, j int) bool {
|
sort.Slice(requestHeaders, func(i, j int) bool {
|
||||||
return requestHeaders[i].Header.Key < requestHeaders[j].Header.Value
|
return requestHeaders[i].Header.Key < requestHeaders[j].Header.Value
|
||||||
})
|
})
|
||||||
return &envoy_service_auth_v3.CheckResponse{
|
return &envoy_service_auth_v3.CheckResponse{
|
||||||
Status: &status.Status{Code: int32(codes.OK), Message: reply.Message},
|
Status: &status.Status{Code: int32(codes.OK), Message: "OK"},
|
||||||
HttpResponse: &envoy_service_auth_v3.CheckResponse_OkResponse{
|
HttpResponse: &envoy_service_auth_v3.CheckResponse_OkResponse{
|
||||||
OkResponse: &envoy_service_auth_v3.OkHttpResponse{
|
OkResponse: &envoy_service_auth_v3.OkHttpResponse{
|
||||||
Headers: requestHeaders,
|
Headers: requestHeaders,
|
||||||
|
@ -47,7 +48,6 @@ func (a *Authorize) deniedResponse(
|
||||||
in *envoy_service_auth_v3.CheckRequest,
|
in *envoy_service_auth_v3.CheckRequest,
|
||||||
code int32, reason string, headers map[string]string,
|
code int32, reason string, headers map[string]string,
|
||||||
) (*envoy_service_auth_v3.CheckResponse, error) {
|
) (*envoy_service_auth_v3.CheckResponse, error) {
|
||||||
|
|
||||||
var details string
|
var details string
|
||||||
switch code {
|
switch code {
|
||||||
case httputil.StatusInvalidClientCertificate:
|
case httputil.StatusInvalidClientCertificate:
|
||||||
|
|
|
@ -30,6 +30,7 @@ func TestAuthorize_okResponse(t *testing.T) {
|
||||||
AuthenticateURLString: "https://authenticate.example.com",
|
AuthenticateURLString: "https://authenticate.example.com",
|
||||||
Policies: []config.Policy{{
|
Policies: []config.Policy{{
|
||||||
Source: &config.StringURL{URL: &url.URL{Host: "example.com"}},
|
Source: &config.StringURL{URL: &url.URL{Host: "example.com"}},
|
||||||
|
To: mustParseWeightedURLs(t, "https://to.example.com"),
|
||||||
SubPolicies: []config.SubPolicy{{
|
SubPolicies: []config.SubPolicy{{
|
||||||
Rego: []string{"allow = true"},
|
Rego: []string{"allow = true"},
|
||||||
}},
|
}},
|
||||||
|
@ -62,45 +63,30 @@ func TestAuthorize_okResponse(t *testing.T) {
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
"ok reply",
|
"ok reply",
|
||||||
&evaluator.Result{Status: 0, Message: "ok"},
|
&evaluator.Result{Allow: true},
|
||||||
&envoy_service_auth_v3.CheckResponse{
|
&envoy_service_auth_v3.CheckResponse{
|
||||||
Status: &status.Status{Code: 0, Message: "ok"},
|
Status: &status.Status{Code: 0, Message: "OK"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ok reply with k8s svc",
|
"ok reply with k8s svc",
|
||||||
&evaluator.Result{
|
&evaluator.Result{Allow: true},
|
||||||
Status: 0,
|
|
||||||
Message: "ok",
|
|
||||||
MatchingPolicy: &config.Policy{
|
|
||||||
KubernetesServiceAccountToken: "k8s-svc-account",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
&envoy_service_auth_v3.CheckResponse{
|
&envoy_service_auth_v3.CheckResponse{
|
||||||
Status: &status.Status{Code: 0, Message: "ok"},
|
Status: &status.Status{Code: 0, Message: "OK"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ok reply with k8s svc impersonate",
|
"ok reply with k8s svc impersonate",
|
||||||
&evaluator.Result{
|
&evaluator.Result{Allow: true},
|
||||||
Status: 0,
|
|
||||||
Message: "ok",
|
|
||||||
MatchingPolicy: &config.Policy{
|
|
||||||
KubernetesServiceAccountToken: "k8s-svc-account",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
&envoy_service_auth_v3.CheckResponse{
|
&envoy_service_auth_v3.CheckResponse{
|
||||||
Status: &status.Status{Code: 0, Message: "ok"},
|
Status: &status.Status{Code: 0, Message: "OK"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ok reply with jwt claims header",
|
"ok reply with jwt claims header",
|
||||||
&evaluator.Result{
|
&evaluator.Result{Allow: true},
|
||||||
Status: 0,
|
|
||||||
Message: "ok",
|
|
||||||
},
|
|
||||||
&envoy_service_auth_v3.CheckResponse{
|
&envoy_service_auth_v3.CheckResponse{
|
||||||
Status: &status.Status{Code: 0, Message: "ok"},
|
Status: &status.Status{Code: 0, Message: "OK"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
70
authorize/evaluator/config.go
Normal file
70
authorize/evaluator/config.go
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
package evaluator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/pomerium/pomerium/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
type evaluatorConfig struct {
|
||||||
|
policies []config.Policy
|
||||||
|
clientCA []byte
|
||||||
|
signingKey string
|
||||||
|
signingKeyAlgorithm string
|
||||||
|
authenticateURL string
|
||||||
|
googleCloudServerlessAuthenticationServiceAccount string
|
||||||
|
jwtClaimsHeaders config.JWTClaimHeaders
|
||||||
|
}
|
||||||
|
|
||||||
|
// An Option customizes the evaluator config.
|
||||||
|
type Option func(*evaluatorConfig)
|
||||||
|
|
||||||
|
func getConfig(options ...Option) *evaluatorConfig {
|
||||||
|
cfg := new(evaluatorConfig)
|
||||||
|
for _, o := range options {
|
||||||
|
o(cfg)
|
||||||
|
}
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithPolicies sets the policies in the config.
|
||||||
|
func WithPolicies(policies []config.Policy) Option {
|
||||||
|
return func(cfg *evaluatorConfig) {
|
||||||
|
cfg.policies = policies
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithClientCA sets the client CA in the config.
|
||||||
|
func WithClientCA(clientCA []byte) Option {
|
||||||
|
return func(cfg *evaluatorConfig) {
|
||||||
|
cfg.clientCA = clientCA
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithSigningKey sets the signing key and algorithm in the config.
|
||||||
|
func WithSigningKey(signingKeyAlgorithm, signingKey string) Option {
|
||||||
|
return func(cfg *evaluatorConfig) {
|
||||||
|
cfg.signingKeyAlgorithm = signingKeyAlgorithm
|
||||||
|
cfg.signingKey = signingKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithAuthenticateURL sets the authenticate URL in the config.
|
||||||
|
func WithAuthenticateURL(authenticateURL string) Option {
|
||||||
|
return func(cfg *evaluatorConfig) {
|
||||||
|
cfg.authenticateURL = authenticateURL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithGoogleCloudServerlessAuthenticationServiceAccount sets the google cloud serverless authentication service
|
||||||
|
// account in the config.
|
||||||
|
func WithGoogleCloudServerlessAuthenticationServiceAccount(serviceAccount string) Option {
|
||||||
|
return func(cfg *evaluatorConfig) {
|
||||||
|
cfg.googleCloudServerlessAuthenticationServiceAccount = serviceAccount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithJWTClaimsHeaders sets the JWT claims headers in the config.
|
||||||
|
func WithJWTClaimsHeaders(headers config.JWTClaimHeaders) Option {
|
||||||
|
return func(cfg *evaluatorConfig) {
|
||||||
|
cfg.jwtClaimsHeaders = headers
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,124 +0,0 @@
|
||||||
package evaluator
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/open-policy-agent/opa/rego"
|
|
||||||
"github.com/open-policy-agent/opa/storage"
|
|
||||||
|
|
||||||
"github.com/pomerium/pomerium/internal/telemetry/trace"
|
|
||||||
)
|
|
||||||
|
|
||||||
// A CustomEvaluatorRequest is the data needed to evaluate a custom rego policy.
|
|
||||||
type CustomEvaluatorRequest struct {
|
|
||||||
RegoPolicy string
|
|
||||||
HTTP RequestHTTP `json:"http"`
|
|
||||||
Session RequestSession `json:"session"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// A CustomEvaluatorResponse is the response from the evaluation of a custom rego policy.
|
|
||||||
type CustomEvaluatorResponse struct {
|
|
||||||
Allowed bool
|
|
||||||
Denied bool
|
|
||||||
Reason string
|
|
||||||
Headers map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
// A CustomEvaluator evaluates custom rego policies.
|
|
||||||
type CustomEvaluator struct {
|
|
||||||
store storage.Store
|
|
||||||
mu sync.Mutex
|
|
||||||
queries map[string]rego.PreparedEvalQuery
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewCustomEvaluator creates a new CustomEvaluator.
|
|
||||||
func NewCustomEvaluator(store storage.Store) *CustomEvaluator {
|
|
||||||
ce := &CustomEvaluator{
|
|
||||||
store: store,
|
|
||||||
queries: map[string]rego.PreparedEvalQuery{},
|
|
||||||
}
|
|
||||||
return ce
|
|
||||||
}
|
|
||||||
|
|
||||||
// Evaluate evaluates the custom rego policy.
|
|
||||||
func (ce *CustomEvaluator) Evaluate(ctx context.Context, req *CustomEvaluatorRequest) (*CustomEvaluatorResponse, error) {
|
|
||||||
_, span := trace.StartSpan(ctx, "authorize.evaluator.custom.Evaluate")
|
|
||||||
defer span.End()
|
|
||||||
|
|
||||||
q, err := ce.getPreparedEvalQuery(ctx, req.RegoPolicy)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
resultSet, err := q.Eval(ctx, rego.EvalInput(struct {
|
|
||||||
HTTP RequestHTTP `json:"http"`
|
|
||||||
Session RequestSession `json:"session"`
|
|
||||||
}{HTTP: req.HTTP, Session: req.Session}))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
vars, ok := resultSet[0].Bindings.WithoutWildcards()["result"].(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
vars = make(map[string]interface{})
|
|
||||||
}
|
|
||||||
|
|
||||||
res := &CustomEvaluatorResponse{
|
|
||||||
Headers: getHeadersVar(resultSet[0].Bindings.WithoutWildcards()),
|
|
||||||
}
|
|
||||||
res.Allowed, _ = vars["allow"].(bool)
|
|
||||||
if v, ok := vars["deny"]; ok {
|
|
||||||
// support `deny = true`
|
|
||||||
if b, ok := v.(bool); ok {
|
|
||||||
res.Denied = b
|
|
||||||
}
|
|
||||||
|
|
||||||
// support `deny[reason] = true`
|
|
||||||
if m, ok := v.(map[string]interface{}); ok {
|
|
||||||
for mk, mv := range m {
|
|
||||||
if b, ok := mv.(bool); ok {
|
|
||||||
res.Denied = b
|
|
||||||
res.Reason = mk
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ce *CustomEvaluator) getPreparedEvalQuery(ctx context.Context, src string) (rego.PreparedEvalQuery, error) {
|
|
||||||
ce.mu.Lock()
|
|
||||||
defer ce.mu.Unlock()
|
|
||||||
|
|
||||||
q, ok := ce.queries[src]
|
|
||||||
if ok {
|
|
||||||
return q, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
r := rego.New(
|
|
||||||
rego.Store(ce.store),
|
|
||||||
rego.Module("pomerium.custom_policy", src),
|
|
||||||
rego.Query("result = data.pomerium.custom_policy"),
|
|
||||||
)
|
|
||||||
q, err := r.PrepareForEval(ctx)
|
|
||||||
if err != nil {
|
|
||||||
// if no package is in the src, add it
|
|
||||||
if strings.Contains(err.Error(), "package expected") {
|
|
||||||
r = rego.New(
|
|
||||||
rego.Store(ce.store),
|
|
||||||
rego.Module("pomerium.custom_policy", "package pomerium.custom_policy\n\n"+src),
|
|
||||||
rego.Query("result = data.pomerium.custom_policy"),
|
|
||||||
)
|
|
||||||
q, err = r.PrepareForEval(ctx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return q, fmt.Errorf("invalid rego policy: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ce.queries[src] = q
|
|
||||||
return q, nil
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
package evaluator
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestCustomEvaluator(t *testing.T) {
|
|
||||||
ctx, clearTimeout := context.WithTimeout(context.Background(), time.Second*10)
|
|
||||||
defer clearTimeout()
|
|
||||||
|
|
||||||
store := NewStore()
|
|
||||||
t.Run("bool deny", func(t *testing.T) {
|
|
||||||
ce := NewCustomEvaluator(store)
|
|
||||||
res, err := ce.Evaluate(ctx, &CustomEvaluatorRequest{
|
|
||||||
RegoPolicy: `
|
|
||||||
package pomerium.custom_policy
|
|
||||||
|
|
||||||
deny = true
|
|
||||||
`,
|
|
||||||
})
|
|
||||||
if !assert.NoError(t, err) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
assert.Equal(t, true, res.Denied)
|
|
||||||
assert.Empty(t, res.Reason)
|
|
||||||
})
|
|
||||||
t.Run("set deny", func(t *testing.T) {
|
|
||||||
ce := NewCustomEvaluator(store)
|
|
||||||
res, err := ce.Evaluate(ctx, &CustomEvaluatorRequest{
|
|
||||||
RegoPolicy: `
|
|
||||||
package pomerium.custom_policy
|
|
||||||
|
|
||||||
deny["test"] = true
|
|
||||||
`,
|
|
||||||
})
|
|
||||||
if !assert.NoError(t, err) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
assert.Equal(t, true, res.Denied)
|
|
||||||
assert.Equal(t, "test", res.Reason)
|
|
||||||
})
|
|
||||||
t.Run("missing package", func(t *testing.T) {
|
|
||||||
ce := NewCustomEvaluator(store)
|
|
||||||
res, err := ce.Evaluate(ctx, &CustomEvaluatorRequest{
|
|
||||||
RegoPolicy: `allow = true`,
|
|
||||||
})
|
|
||||||
if !assert.NoError(t, err) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
assert.NotNil(t, res)
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,5 +1,4 @@
|
||||||
// Package evaluator defines a Evaluator interfaces that can be implemented by
|
// Package evaluator contains rego evaluators for evaluating authorize policy.
|
||||||
// a policy evaluator framework.
|
|
||||||
package evaluator
|
package evaluator
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -13,133 +12,180 @@ import (
|
||||||
|
|
||||||
"github.com/pomerium/pomerium/config"
|
"github.com/pomerium/pomerium/config"
|
||||||
"github.com/pomerium/pomerium/internal/log"
|
"github.com/pomerium/pomerium/internal/log"
|
||||||
|
"github.com/pomerium/pomerium/internal/urlutil"
|
||||||
"github.com/pomerium/pomerium/pkg/cryptutil"
|
"github.com/pomerium/pomerium/pkg/cryptutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Evaluator specifies the interface for a policy engine.
|
// notFoundOutput is what's returned if a route isn't found for a policy.
|
||||||
|
var notFoundOutput = &Result{
|
||||||
|
Allow: false,
|
||||||
|
Deny: &Denial{
|
||||||
|
Status: http.StatusNotFound,
|
||||||
|
Message: "route not found",
|
||||||
|
},
|
||||||
|
Headers: make(http.Header),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request contains the inputs needed for evaluation.
|
||||||
|
type Request struct {
|
||||||
|
Policy *config.Policy
|
||||||
|
HTTP RequestHTTP
|
||||||
|
Session RequestSession
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestHTTP is the HTTP field in the request.
|
||||||
|
type RequestHTTP struct {
|
||||||
|
Method string `json:"method"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
Headers map[string]string `json:"headers"`
|
||||||
|
ClientCertificate string `json:"client_certificate"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestSession is the session field in the request.
|
||||||
|
type RequestSession struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Result is the result of evaluation.
|
||||||
|
type Result struct {
|
||||||
|
Allow bool
|
||||||
|
Deny *Denial
|
||||||
|
Headers http.Header
|
||||||
|
|
||||||
|
DataBrokerServerVersion, DataBrokerRecordVersion uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// An Evaluator evaluates policies.
|
||||||
type Evaluator struct {
|
type Evaluator struct {
|
||||||
custom *CustomEvaluator
|
store *Store
|
||||||
rego *rego.Rego
|
policyEvaluators map[uint64]*PolicyEvaluator
|
||||||
query rego.PreparedEvalQuery
|
headersEvaluators *HeadersEvaluator
|
||||||
policies []config.Policy
|
clientCA []byte
|
||||||
store *Store
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new Evaluator.
|
// New creates a new Evaluator.
|
||||||
func New(options *config.Options, store *Store) (*Evaluator, error) {
|
func New(ctx context.Context, store *Store, options ...Option) (*Evaluator, error) {
|
||||||
e := &Evaluator{
|
e := &Evaluator{store: store}
|
||||||
custom: NewCustomEvaluator(store),
|
|
||||||
policies: options.GetAllPolicies(),
|
cfg := getConfig(options...)
|
||||||
store: store,
|
|
||||||
}
|
err := e.updateStore(cfg)
|
||||||
jwk, err := getJWK(options)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("authorize: couldn't create signer: %w", err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
authzPolicy, err := readPolicy()
|
e.headersEvaluators, err = NewHeadersEvaluator(ctx, store)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error loading rego policy: %w", err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
authenticateURL, err := options.GetAuthenticateURL()
|
e.policyEvaluators = make(map[uint64]*PolicyEvaluator)
|
||||||
if err != nil {
|
for _, configPolicy := range cfg.policies {
|
||||||
return nil, fmt.Errorf("authorize: invalid authenticate URL: %w", err)
|
id, err := configPolicy.RouteID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("authorize: error computing policy route id: %w", err)
|
||||||
|
}
|
||||||
|
policyEvaluator, err := NewPolicyEvaluator(ctx, store, &configPolicy) //nolint
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
e.policyEvaluators[id] = policyEvaluator
|
||||||
}
|
}
|
||||||
|
|
||||||
store.UpdateIssuer(authenticateURL.Host)
|
e.clientCA = cfg.clientCA
|
||||||
store.UpdateGoogleCloudServerlessAuthenticationServiceAccount(
|
|
||||||
options.GetGoogleCloudServerlessAuthenticationServiceAccount(),
|
|
||||||
)
|
|
||||||
store.UpdateJWTClaimHeaders(options.JWTClaimsHeaders)
|
|
||||||
store.UpdateRoutePolicies(options.GetAllPolicies())
|
|
||||||
store.UpdateSigningKey(jwk)
|
|
||||||
|
|
||||||
e.rego = rego.New(
|
|
||||||
rego.Store(store),
|
|
||||||
rego.Module("pomerium.authz", string(authzPolicy)),
|
|
||||||
rego.Query("result = data.pomerium.authz"),
|
|
||||||
getGoogleCloudServerlessHeadersRegoOption,
|
|
||||||
store.GetDataBrokerRecordOption(),
|
|
||||||
)
|
|
||||||
|
|
||||||
e.query, err = e.rego.PrepareForEval(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error preparing rego query: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return e, nil
|
return e, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Evaluate evaluates the policy against the request.
|
// Evaluate evaluates the rego for the given policy and generates the identity headers.
|
||||||
func (e *Evaluator) Evaluate(ctx context.Context, req *Request) (*Result, error) {
|
func (e *Evaluator) Evaluate(ctx context.Context, req *Request) (*Result, error) {
|
||||||
isValid, err := isValidClientCertificate(req.ClientCA, req.HTTP.ClientCertificate)
|
if req.Policy == nil {
|
||||||
|
return notFoundOutput, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := req.Policy.RouteID()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error validating client certificate: %w", err)
|
return nil, fmt.Errorf("authorize: error computing policy route id: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := e.query.Eval(ctx, rego.EvalInput(e.newInput(req, isValid)))
|
policyEvaluator, ok := e.policyEvaluators[id]
|
||||||
|
if !ok {
|
||||||
|
return notFoundOutput, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
clientCA, err := e.getClientCA(req.Policy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error evaluating rego policy: %w", err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
deny := getDenyVar(res[0].Bindings.WithoutWildcards())
|
isValidClientCertificate, err := isValidClientCertificate(clientCA, req.HTTP.ClientCertificate)
|
||||||
if len(deny) > 0 {
|
if err != nil {
|
||||||
return &deny[0], nil
|
return nil, fmt.Errorf("authorize: error validating client certificate: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
evalResult := &Result{
|
policyOutput, err := policyEvaluator.Evaluate(ctx, &PolicyRequest{
|
||||||
MatchingPolicy: getMatchingPolicy(res[0].Bindings.WithoutWildcards(), e.policies),
|
HTTP: req.HTTP,
|
||||||
Headers: getHeadersVar(res[0].Bindings.WithoutWildcards()),
|
Session: req.Session,
|
||||||
}
|
IsValidClientCertificate: isValidClientCertificate,
|
||||||
evalResult.DataBrokerServerVersion, evalResult.DataBrokerRecordVersion = getDataBrokerVersions(
|
})
|
||||||
res[0].Bindings,
|
if err != nil {
|
||||||
)
|
return nil, err
|
||||||
|
|
||||||
allow := getAllowVar(res[0].Bindings.WithoutWildcards())
|
|
||||||
// evaluate any custom policies
|
|
||||||
if allow {
|
|
||||||
for _, src := range req.CustomPolicies {
|
|
||||||
cres, err := e.custom.Evaluate(ctx, &CustomEvaluatorRequest{
|
|
||||||
RegoPolicy: src,
|
|
||||||
HTTP: req.HTTP,
|
|
||||||
Session: req.Session,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
allow = allow && (cres.Allowed && !cres.Denied)
|
|
||||||
if cres.Reason != "" {
|
|
||||||
evalResult.Message = cres.Reason
|
|
||||||
}
|
|
||||||
for k, v := range cres.Headers {
|
|
||||||
evalResult.Headers[k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if allow {
|
|
||||||
evalResult.Status = http.StatusOK
|
|
||||||
evalResult.Message = http.StatusText(http.StatusOK)
|
|
||||||
return evalResult, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Session.ID == "" {
|
headersReq := NewHeadersRequestFromPolicy(req.Policy)
|
||||||
evalResult.Status = http.StatusUnauthorized
|
headersReq.Session = req.Session
|
||||||
evalResult.Message = "login required"
|
headersOutput, err := e.headersEvaluators.Evaluate(ctx, headersReq)
|
||||||
return evalResult, nil
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
evalResult.Status = http.StatusForbidden
|
res := &Result{
|
||||||
if evalResult.Message == "" {
|
Allow: policyOutput.Allow,
|
||||||
evalResult.Message = http.StatusText(http.StatusForbidden)
|
Deny: policyOutput.Deny,
|
||||||
|
Headers: headersOutput.Headers,
|
||||||
}
|
}
|
||||||
return evalResult, nil
|
res.DataBrokerServerVersion, res.DataBrokerRecordVersion = e.store.GetDataBrokerVersions()
|
||||||
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getJWK(options *config.Options) (*jose.JSONWebKey, error) {
|
func (e *Evaluator) getClientCA(policy *config.Policy) (string, error) {
|
||||||
|
if policy != nil && policy.TLSDownstreamClientCA != "" {
|
||||||
|
bs, err := base64.StdEncoding.DecodeString(policy.TLSDownstreamClientCA)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(bs), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(e.clientCA), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Evaluator) updateStore(cfg *evaluatorConfig) error {
|
||||||
|
jwk, err := getJWK(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("authorize: couldn't create signer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
authenticateURL, err := urlutil.ParseAndValidateURL(cfg.authenticateURL)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("authorize: invalid authenticate URL: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
e.store.UpdateIssuer(authenticateURL.Host)
|
||||||
|
e.store.UpdateGoogleCloudServerlessAuthenticationServiceAccount(
|
||||||
|
cfg.googleCloudServerlessAuthenticationServiceAccount,
|
||||||
|
)
|
||||||
|
e.store.UpdateJWTClaimHeaders(cfg.jwtClaimsHeaders)
|
||||||
|
e.store.UpdateRoutePolicies(cfg.policies)
|
||||||
|
e.store.UpdateSigningKey(jwk)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getJWK(cfg *evaluatorConfig) (*jose.JSONWebKey, error) {
|
||||||
var decodedCert []byte
|
var decodedCert []byte
|
||||||
// if we don't have a signing key, generate one
|
// if we don't have a signing key, generate one
|
||||||
if options.SigningKey == "" {
|
if cfg.signingKey == "" {
|
||||||
key, err := cryptutil.NewSigningKey()
|
key, err := cryptutil.NewSigningKey()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("couldn't generate signing key: %w", err)
|
return nil, fmt.Errorf("couldn't generate signing key: %w", err)
|
||||||
|
@ -150,12 +196,12 @@ func getJWK(options *config.Options) (*jose.JSONWebKey, error) {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var err error
|
var err error
|
||||||
decodedCert, err = base64.StdEncoding.DecodeString(options.SigningKey)
|
decodedCert, err = base64.StdEncoding.DecodeString(cfg.signingKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("bad signing key: %w", err)
|
return nil, fmt.Errorf("bad signing key: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
signingKeyAlgorithm := options.SigningKeyAlgorithm
|
signingKeyAlgorithm := cfg.signingKeyAlgorithm
|
||||||
if signingKeyAlgorithm == "" {
|
if signingKeyAlgorithm == "" {
|
||||||
signingKeyAlgorithm = string(jose.ES256)
|
signingKeyAlgorithm = string(jose.ES256)
|
||||||
}
|
}
|
||||||
|
@ -172,16 +218,12 @@ func getJWK(options *config.Options) (*jose.JSONWebKey, error) {
|
||||||
return jwk, nil
|
return jwk, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type input struct {
|
func safeEval(ctx context.Context, q rego.PreparedEvalQuery, options ...rego.EvalOption) (resultSet rego.ResultSet, err error) {
|
||||||
HTTP RequestHTTP `json:"http"`
|
defer func() {
|
||||||
Session RequestSession `json:"session"`
|
if e := recover(); e != nil {
|
||||||
IsValidClientCertificate bool `json:"is_valid_client_certificate"`
|
err = fmt.Errorf("%v", e)
|
||||||
}
|
}
|
||||||
|
}()
|
||||||
func (e *Evaluator) newInput(req *Request, isValidClientCertificate bool) *input {
|
resultSet, err = q.Eval(ctx, options...)
|
||||||
i := new(input)
|
return resultSet, err
|
||||||
i.HTTP = req.HTTP
|
|
||||||
i.Session = req.Session
|
|
||||||
i.IsValidClientCertificate = isValidClientCertificate
|
|
||||||
return i
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,153 +2,457 @@ package evaluator
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"math"
|
||||||
"net/url"
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/golang/protobuf/ptypes"
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
"google.golang.org/protobuf/types/known/anypb"
|
"google.golang.org/protobuf/types/known/anypb"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
|
||||||
"github.com/pomerium/pomerium/config"
|
"github.com/pomerium/pomerium/config"
|
||||||
|
"github.com/pomerium/pomerium/pkg/cryptutil"
|
||||||
"github.com/pomerium/pomerium/pkg/grpc/databroker"
|
"github.com/pomerium/pomerium/pkg/grpc/databroker"
|
||||||
"github.com/pomerium/pomerium/pkg/grpc/directory"
|
"github.com/pomerium/pomerium/pkg/grpc/directory"
|
||||||
"github.com/pomerium/pomerium/pkg/grpc/session"
|
"github.com/pomerium/pomerium/pkg/grpc/session"
|
||||||
"github.com/pomerium/pomerium/pkg/grpc/user"
|
"github.com/pomerium/pomerium/pkg/grpc/user"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestJSONMarshal(t *testing.T) {
|
func TestEvaluator(t *testing.T) {
|
||||||
opt := config.NewDefaultOptions()
|
signingKey, err := cryptutil.NewSigningKey()
|
||||||
opt.AuthenticateURLString = "https://authenticate.example.com"
|
require.NoError(t, err)
|
||||||
e, err := New(opt, NewStoreFromProtos(0,
|
encodedSigningKey, err := cryptutil.EncodePrivateKey(signingKey)
|
||||||
&session.Session{
|
require.NoError(t, err)
|
||||||
UserId: "user1",
|
privateJWK, err := cryptutil.PrivateJWKFromBytes(encodedSigningKey, jose.ES256)
|
||||||
},
|
|
||||||
&directory.User{
|
|
||||||
Id: "user1",
|
|
||||||
GroupIds: []string{"group1", "group2"},
|
|
||||||
},
|
|
||||||
&directory.Group{
|
|
||||||
Id: "group1",
|
|
||||||
Name: "admin",
|
|
||||||
Email: "admin@example.com",
|
|
||||||
},
|
|
||||||
&directory.Group{
|
|
||||||
Id: "group2",
|
|
||||||
Name: "test",
|
|
||||||
},
|
|
||||||
))
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
bs, _ := json.Marshal(e.newInput(&Request{
|
|
||||||
HTTP: RequestHTTP{
|
|
||||||
Method: "GET",
|
|
||||||
URL: "https://example.com",
|
|
||||||
Headers: map[string]string{
|
|
||||||
"Accept": "application/json",
|
|
||||||
},
|
|
||||||
ClientCertificate: "CLIENT_CERTIFICATE",
|
|
||||||
},
|
|
||||||
Session: RequestSession{
|
|
||||||
ID: "SESSION_ID",
|
|
||||||
},
|
|
||||||
}, true))
|
|
||||||
assert.JSONEq(t, `{
|
|
||||||
"http": {
|
|
||||||
"client_certificate": "CLIENT_CERTIFICATE",
|
|
||||||
"headers": {
|
|
||||||
"Accept": "application/json"
|
|
||||||
},
|
|
||||||
"method": "GET",
|
|
||||||
"url": "https://example.com"
|
|
||||||
},
|
|
||||||
"session": {
|
|
||||||
"id": "SESSION_ID"
|
|
||||||
},
|
|
||||||
"is_valid_client_certificate": true
|
|
||||||
}`, string(bs))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEvaluator_Evaluate(t *testing.T) {
|
eval := func(t *testing.T, options []Option, data []proto.Message, req *Request) (*Result, error) {
|
||||||
sessionID := uuid.New().String()
|
store := NewStoreFromProtos(math.MaxUint64, data...)
|
||||||
userID := uuid.New().String()
|
store.UpdateIssuer("authenticate.example.com")
|
||||||
|
store.UpdateJWTClaimHeaders(config.NewJWTClaimHeaders("email", "groups", "user", "CUSTOM_KEY"))
|
||||||
ctx := context.Background()
|
store.UpdateSigningKey(privateJWK)
|
||||||
allowedPolicy := []config.Policy{{From: "https://foo.com", AllowedUsers: []string{"foo@example.com"}}}
|
e, err := New(context.Background(), store, options...)
|
||||||
forbiddenPolicy := []config.Policy{{From: "https://bar.com", AllowedUsers: []string{"bar@example.com"}}}
|
require.NoError(t, err)
|
||||||
|
return e.Evaluate(context.Background(), req)
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
reqURL string
|
|
||||||
policies []config.Policy
|
|
||||||
customPolicies []string
|
|
||||||
sessionID string
|
|
||||||
expectedStatus int
|
|
||||||
}{
|
|
||||||
{"allowed", "https://foo.com/path", allowedPolicy, nil, sessionID, http.StatusOK},
|
|
||||||
{"forbidden", "https://bar.com/path", forbiddenPolicy, nil, sessionID, http.StatusForbidden},
|
|
||||||
{"unauthorized", "https://foo.com/path", allowedPolicy, nil, "", http.StatusUnauthorized},
|
|
||||||
{"custom policy overwrite main policy", "https://foo.com/path", allowedPolicy, []string{"deny = true"}, sessionID, http.StatusForbidden},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tests {
|
policies := []config.Policy{
|
||||||
tc := tc
|
{
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
To: config.WeightedURLs{{URL: *mustParseURL("https://to1.example.com")}},
|
||||||
t.Parallel()
|
AllowPublicUnauthenticatedAccess: true,
|
||||||
|
},
|
||||||
store := NewStoreFromProtos(0)
|
{
|
||||||
data, _ := ptypes.MarshalAny(&session.Session{
|
To: config.WeightedURLs{{URL: *mustParseURL("https://to2.example.com")}},
|
||||||
Version: "1",
|
AllowPublicUnauthenticatedAccess: true,
|
||||||
Id: sessionID,
|
KubernetesServiceAccountToken: "KUBERNETES",
|
||||||
UserId: userID,
|
},
|
||||||
IdToken: &session.IDToken{
|
{
|
||||||
Issuer: "TestEvaluatorEvaluate",
|
To: config.WeightedURLs{{URL: *mustParseURL("https://to3.example.com")}},
|
||||||
Subject: userID,
|
AllowPublicUnauthenticatedAccess: true,
|
||||||
IssuedAt: ptypes.TimestampNow(),
|
EnableGoogleCloudServerlessAuthentication: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
To: config.WeightedURLs{{URL: *mustParseURL("https://to4.example.com")}},
|
||||||
|
AllowedUsers: []string{"a@example.com"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
To: config.WeightedURLs{{URL: *mustParseURL("https://to5.example.com")}},
|
||||||
|
SubPolicies: []config.SubPolicy{
|
||||||
|
{
|
||||||
|
AllowedUsers: []string{"a@example.com"},
|
||||||
},
|
},
|
||||||
OauthToken: &session.OAuthToken{
|
},
|
||||||
AccessToken: "ACCESS TOKEN",
|
},
|
||||||
TokenType: "Bearer",
|
{
|
||||||
RefreshToken: "REFRESH TOKEN",
|
To: config.WeightedURLs{{URL: *mustParseURL("https://to6.example.com")}},
|
||||||
},
|
AllowedUsers: []string{"example/1234"},
|
||||||
})
|
},
|
||||||
store.UpdateRecord(0, &databroker.Record{
|
{
|
||||||
Version: 1,
|
To: config.WeightedURLs{{URL: *mustParseURL("https://to7.example.com")}},
|
||||||
Type: "type.googleapis.com/session.Session",
|
AllowedDomains: []string{"example.com"},
|
||||||
Id: sessionID,
|
},
|
||||||
Data: data,
|
{
|
||||||
})
|
To: config.WeightedURLs{{URL: *mustParseURL("https://to8.example.com")}},
|
||||||
data, _ = ptypes.MarshalAny(&user.User{
|
AllowedGroups: []string{"group1@example.com"},
|
||||||
Version: "1",
|
},
|
||||||
Id: userID,
|
{
|
||||||
Email: "foo@example.com",
|
To: config.WeightedURLs{{URL: *mustParseURL("https://to9.example.com")}},
|
||||||
})
|
AllowAnyAuthenticatedUser: true,
|
||||||
store.UpdateRecord(0, &databroker.Record{
|
},
|
||||||
Version: 1,
|
}
|
||||||
Type: "type.googleapis.com/user.User",
|
options := []Option{
|
||||||
Id: userID,
|
WithAuthenticateURL("https://authn.example.com"),
|
||||||
Data: data,
|
WithClientCA([]byte(testCA)),
|
||||||
})
|
WithPolicies(policies),
|
||||||
|
}
|
||||||
|
|
||||||
e, err := New(&config.Options{
|
t.Run("client certificate", func(t *testing.T) {
|
||||||
AuthenticateURLString: "https://authn.example.com",
|
t.Run("invalid", func(t *testing.T) {
|
||||||
Policies: tc.policies,
|
res, err := eval(t, options, nil, &Request{
|
||||||
}, store)
|
Policy: &policies[0],
|
||||||
require.NoError(t, err)
|
|
||||||
res, err := e.Evaluate(ctx, &Request{
|
|
||||||
HTTP: RequestHTTP{Method: "GET", URL: tc.reqURL},
|
|
||||||
Session: RequestSession{ID: tc.sessionID},
|
|
||||||
CustomPolicies: tc.customPolicies,
|
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.NotNil(t, res)
|
assert.Equal(t, &Denial{Status: 495, Message: "invalid client certificate"}, res.Deny)
|
||||||
assert.Equal(t, tc.expectedStatus, res.Status)
|
|
||||||
})
|
})
|
||||||
}
|
t.Run("valid", func(t *testing.T) {
|
||||||
|
res, err := eval(t, options, nil, &Request{
|
||||||
|
Policy: &policies[0],
|
||||||
|
HTTP: RequestHTTP{
|
||||||
|
ClientCertificate: testValidCert,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Nil(t, res.Deny)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
t.Run("identity_headers", func(t *testing.T) {
|
||||||
|
t.Run("kubernetes", func(t *testing.T) {
|
||||||
|
res, err := eval(t, options, []proto.Message{
|
||||||
|
&session.Session{
|
||||||
|
Id: "session1",
|
||||||
|
UserId: "user1",
|
||||||
|
ImpersonateGroups: []string{"i1", "i2"},
|
||||||
|
},
|
||||||
|
&user.User{
|
||||||
|
Id: "user1",
|
||||||
|
Email: "a@example.com",
|
||||||
|
},
|
||||||
|
}, &Request{
|
||||||
|
Policy: &policies[1],
|
||||||
|
Session: RequestSession{
|
||||||
|
ID: "session1",
|
||||||
|
},
|
||||||
|
HTTP: RequestHTTP{
|
||||||
|
Method: "GET",
|
||||||
|
URL: "https://from.example.com",
|
||||||
|
ClientCertificate: testValidCert,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "a@example.com", res.Headers.Get("Impersonate-User"))
|
||||||
|
assert.Equal(t, "i1,i2", res.Headers.Get("Impersonate-Group"))
|
||||||
|
})
|
||||||
|
t.Run("google_cloud_serverless", func(t *testing.T) {
|
||||||
|
withMockGCP(t, func() {
|
||||||
|
res, err := eval(t, options, []proto.Message{
|
||||||
|
&session.Session{
|
||||||
|
Id: "session1",
|
||||||
|
UserId: "user1",
|
||||||
|
ImpersonateGroups: []string{"i1", "i2"},
|
||||||
|
},
|
||||||
|
&user.User{
|
||||||
|
Id: "user1",
|
||||||
|
Email: "a@example.com",
|
||||||
|
},
|
||||||
|
}, &Request{
|
||||||
|
Policy: &policies[2],
|
||||||
|
Session: RequestSession{
|
||||||
|
ID: "session1",
|
||||||
|
},
|
||||||
|
HTTP: RequestHTTP{
|
||||||
|
Method: "GET",
|
||||||
|
URL: "https://from.example.com",
|
||||||
|
ClientCertificate: testValidCert,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, res.Headers.Get("Authorization"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
t.Run("email", func(t *testing.T) {
|
||||||
|
t.Run("allowed", func(t *testing.T) {
|
||||||
|
res, err := eval(t, options, []proto.Message{
|
||||||
|
&session.Session{
|
||||||
|
Id: "session1",
|
||||||
|
UserId: "user1",
|
||||||
|
},
|
||||||
|
&user.User{
|
||||||
|
Id: "user1",
|
||||||
|
Email: "a@example.com",
|
||||||
|
},
|
||||||
|
}, &Request{
|
||||||
|
Policy: &policies[3],
|
||||||
|
Session: RequestSession{
|
||||||
|
ID: "session1",
|
||||||
|
},
|
||||||
|
HTTP: RequestHTTP{
|
||||||
|
Method: "GET",
|
||||||
|
URL: "https://from.example.com",
|
||||||
|
ClientCertificate: testValidCert,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, res.Allow)
|
||||||
|
})
|
||||||
|
t.Run("allowed sub", func(t *testing.T) {
|
||||||
|
res, err := eval(t, options, []proto.Message{
|
||||||
|
&session.Session{
|
||||||
|
Id: "session1",
|
||||||
|
UserId: "user1",
|
||||||
|
},
|
||||||
|
&user.User{
|
||||||
|
Id: "user1",
|
||||||
|
Email: "a@example.com",
|
||||||
|
},
|
||||||
|
}, &Request{
|
||||||
|
Policy: &policies[4],
|
||||||
|
Session: RequestSession{
|
||||||
|
ID: "session1",
|
||||||
|
},
|
||||||
|
HTTP: RequestHTTP{
|
||||||
|
Method: "GET",
|
||||||
|
URL: "https://from.example.com",
|
||||||
|
ClientCertificate: testValidCert,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, res.Allow)
|
||||||
|
})
|
||||||
|
t.Run("denied", func(t *testing.T) {
|
||||||
|
res, err := eval(t, options, []proto.Message{
|
||||||
|
&session.Session{
|
||||||
|
Id: "session1",
|
||||||
|
UserId: "user1",
|
||||||
|
},
|
||||||
|
&user.User{
|
||||||
|
Id: "user1",
|
||||||
|
Email: "b@example.com",
|
||||||
|
},
|
||||||
|
}, &Request{
|
||||||
|
Policy: &policies[3],
|
||||||
|
Session: RequestSession{
|
||||||
|
ID: "session1",
|
||||||
|
},
|
||||||
|
HTTP: RequestHTTP{
|
||||||
|
Method: "GET",
|
||||||
|
URL: "https://from.example.com",
|
||||||
|
ClientCertificate: testValidCert,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.False(t, res.Allow)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
t.Run("impersonate email", func(t *testing.T) {
|
||||||
|
t.Run("allowed", func(t *testing.T) {
|
||||||
|
res, err := eval(t, options, []proto.Message{
|
||||||
|
&user.ServiceAccount{
|
||||||
|
Id: "session1",
|
||||||
|
UserId: "user1",
|
||||||
|
ImpersonateEmail: proto.String("a@example.com"),
|
||||||
|
},
|
||||||
|
&user.User{
|
||||||
|
Id: "user1",
|
||||||
|
Email: "b@example.com",
|
||||||
|
},
|
||||||
|
}, &Request{
|
||||||
|
Policy: &policies[3],
|
||||||
|
Session: RequestSession{
|
||||||
|
ID: "session1",
|
||||||
|
},
|
||||||
|
HTTP: RequestHTTP{
|
||||||
|
Method: "GET",
|
||||||
|
URL: "https://from.example.com",
|
||||||
|
ClientCertificate: testValidCert,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, res.Allow)
|
||||||
|
})
|
||||||
|
t.Run("denied", func(t *testing.T) {
|
||||||
|
res, err := eval(t, options, []proto.Message{
|
||||||
|
&session.Session{
|
||||||
|
Id: "session1",
|
||||||
|
UserId: "user1",
|
||||||
|
ImpersonateEmail: proto.String("b@example.com"),
|
||||||
|
},
|
||||||
|
&user.User{
|
||||||
|
Id: "user1",
|
||||||
|
Email: "a@example.com",
|
||||||
|
},
|
||||||
|
}, &Request{
|
||||||
|
Policy: &policies[3],
|
||||||
|
Session: RequestSession{
|
||||||
|
ID: "session1",
|
||||||
|
},
|
||||||
|
HTTP: RequestHTTP{
|
||||||
|
Method: "GET",
|
||||||
|
URL: "https://from.example.com",
|
||||||
|
ClientCertificate: testValidCert,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.False(t, res.Allow)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
t.Run("user_id", func(t *testing.T) {
|
||||||
|
res, err := eval(t, options, []proto.Message{
|
||||||
|
&session.Session{
|
||||||
|
Id: "session1",
|
||||||
|
UserId: "example/1234",
|
||||||
|
},
|
||||||
|
&user.User{
|
||||||
|
Id: "example/1234",
|
||||||
|
Email: "a@example.com",
|
||||||
|
},
|
||||||
|
}, &Request{
|
||||||
|
Policy: &policies[5],
|
||||||
|
Session: RequestSession{
|
||||||
|
ID: "session1",
|
||||||
|
},
|
||||||
|
HTTP: RequestHTTP{
|
||||||
|
Method: "GET",
|
||||||
|
URL: "https://from.example.com",
|
||||||
|
ClientCertificate: testValidCert,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, res.Allow)
|
||||||
|
})
|
||||||
|
t.Run("domain", func(t *testing.T) {
|
||||||
|
res, err := eval(t, options, []proto.Message{
|
||||||
|
&session.Session{
|
||||||
|
Id: "session1",
|
||||||
|
UserId: "user1",
|
||||||
|
},
|
||||||
|
&user.User{
|
||||||
|
Id: "user1",
|
||||||
|
Email: "a@example.com",
|
||||||
|
},
|
||||||
|
}, &Request{
|
||||||
|
Policy: &policies[6],
|
||||||
|
Session: RequestSession{
|
||||||
|
ID: "session1",
|
||||||
|
},
|
||||||
|
HTTP: RequestHTTP{
|
||||||
|
Method: "GET",
|
||||||
|
URL: "https://from.example.com",
|
||||||
|
ClientCertificate: testValidCert,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, res.Allow)
|
||||||
|
})
|
||||||
|
t.Run("impersonate domain", func(t *testing.T) {
|
||||||
|
res, err := eval(t, options, []proto.Message{
|
||||||
|
&session.Session{
|
||||||
|
Id: "session1",
|
||||||
|
UserId: "user1",
|
||||||
|
ImpersonateEmail: proto.String("a@example.com"),
|
||||||
|
},
|
||||||
|
&user.User{
|
||||||
|
Id: "user1",
|
||||||
|
Email: "a@notexample.com",
|
||||||
|
},
|
||||||
|
}, &Request{
|
||||||
|
Policy: &policies[6],
|
||||||
|
Session: RequestSession{
|
||||||
|
ID: "session1",
|
||||||
|
},
|
||||||
|
HTTP: RequestHTTP{
|
||||||
|
Method: "GET",
|
||||||
|
URL: "https://from.example.com",
|
||||||
|
ClientCertificate: testValidCert,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, res.Allow)
|
||||||
|
})
|
||||||
|
t.Run("groups", func(t *testing.T) {
|
||||||
|
res, err := eval(t, options, []proto.Message{
|
||||||
|
&session.Session{
|
||||||
|
Id: "session1",
|
||||||
|
UserId: "user1",
|
||||||
|
},
|
||||||
|
&user.User{
|
||||||
|
Id: "user1",
|
||||||
|
Email: "a@example.com",
|
||||||
|
},
|
||||||
|
&directory.User{
|
||||||
|
Id: "user1",
|
||||||
|
GroupIds: []string{"group1"},
|
||||||
|
},
|
||||||
|
&directory.Group{
|
||||||
|
Id: "group1",
|
||||||
|
Name: "group1name",
|
||||||
|
Email: "group1@example.com",
|
||||||
|
},
|
||||||
|
}, &Request{
|
||||||
|
Policy: &policies[7],
|
||||||
|
Session: RequestSession{
|
||||||
|
ID: "session1",
|
||||||
|
},
|
||||||
|
HTTP: RequestHTTP{
|
||||||
|
Method: "GET",
|
||||||
|
URL: "https://from.example.com",
|
||||||
|
ClientCertificate: testValidCert,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, res.Allow)
|
||||||
|
})
|
||||||
|
t.Run("impersonate groups", func(t *testing.T) {
|
||||||
|
res, err := eval(t, options, []proto.Message{
|
||||||
|
&session.Session{
|
||||||
|
Id: "session1",
|
||||||
|
UserId: "user1",
|
||||||
|
ImpersonateGroups: []string{"group1"},
|
||||||
|
},
|
||||||
|
&user.User{
|
||||||
|
Id: "user1",
|
||||||
|
Email: "a@example.com",
|
||||||
|
},
|
||||||
|
&directory.User{
|
||||||
|
Id: "user1",
|
||||||
|
},
|
||||||
|
&directory.Group{
|
||||||
|
Id: "group1",
|
||||||
|
Name: "group1name",
|
||||||
|
Email: "group1@example.com",
|
||||||
|
},
|
||||||
|
}, &Request{
|
||||||
|
Policy: &policies[7],
|
||||||
|
Session: RequestSession{
|
||||||
|
ID: "session1",
|
||||||
|
},
|
||||||
|
HTTP: RequestHTTP{
|
||||||
|
Method: "GET",
|
||||||
|
URL: "https://from.example.com",
|
||||||
|
ClientCertificate: testValidCert,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, res.Allow)
|
||||||
|
})
|
||||||
|
t.Run("any authenticated user", func(t *testing.T) {
|
||||||
|
res, err := eval(t, options, []proto.Message{
|
||||||
|
&session.Session{
|
||||||
|
Id: "session1",
|
||||||
|
UserId: "user1",
|
||||||
|
},
|
||||||
|
&user.User{
|
||||||
|
Id: "user1",
|
||||||
|
},
|
||||||
|
}, &Request{
|
||||||
|
Policy: &policies[8],
|
||||||
|
Session: RequestSession{
|
||||||
|
ID: "session1",
|
||||||
|
},
|
||||||
|
HTTP: RequestHTTP{
|
||||||
|
Method: "GET",
|
||||||
|
URL: "https://from.example.com",
|
||||||
|
ClientCertificate: testValidCert,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, res.Allow)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func mustParseURL(str string) *url.URL {
|
func mustParseURL(str string) *url.URL {
|
||||||
|
@ -161,9 +465,22 @@ func mustParseURL(str string) *url.URL {
|
||||||
|
|
||||||
func BenchmarkEvaluator_Evaluate(b *testing.B) {
|
func BenchmarkEvaluator_Evaluate(b *testing.B) {
|
||||||
store := NewStore()
|
store := NewStore()
|
||||||
e, err := New(&config.Options{
|
|
||||||
AuthenticateURLString: "https://authn.example.com",
|
policies := []config.Policy{
|
||||||
}, store)
|
{
|
||||||
|
From: "https://from.example.com",
|
||||||
|
To: config.WeightedURLs{
|
||||||
|
{URL: *mustParseURL("https://to.example.com")},
|
||||||
|
},
|
||||||
|
AllowedUsers: []string{"SOME_USER"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
options := []Option{
|
||||||
|
WithAuthenticateURL("https://authn.example.com"),
|
||||||
|
WithPolicies(policies),
|
||||||
|
}
|
||||||
|
|
||||||
|
e, err := New(context.Background(), store, options...)
|
||||||
if !assert.NoError(b, err) {
|
if !assert.NoError(b, err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -233,7 +550,8 @@ func BenchmarkEvaluator_Evaluate(b *testing.B) {
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
e.Evaluate(ctx, &Request{
|
_, _ = e.Evaluate(ctx, &Request{
|
||||||
|
Policy: &policies[0],
|
||||||
HTTP: RequestHTTP{
|
HTTP: RequestHTTP{
|
||||||
Method: "GET",
|
Method: "GET",
|
||||||
URL: "https://example.com/path",
|
URL: "https://example.com/path",
|
||||||
|
|
|
@ -8,7 +8,6 @@ import (
|
||||||
|
|
||||||
lru "github.com/hashicorp/golang-lru"
|
lru "github.com/hashicorp/golang-lru"
|
||||||
|
|
||||||
"github.com/pomerium/pomerium/authorize/evaluator/opa"
|
|
||||||
"github.com/pomerium/pomerium/internal/log"
|
"github.com/pomerium/pomerium/internal/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -65,7 +64,3 @@ func parseCertificate(pemStr string) (*x509.Certificate, error) {
|
||||||
}
|
}
|
||||||
return x509.ParseCertificate(block.Bytes)
|
return x509.ParseCertificate(block.Bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
func readPolicy() ([]byte, error) {
|
|
||||||
return opa.FS.ReadFile("policy/authz.rego")
|
|
||||||
}
|
|
||||||
|
|
107
authorize/evaluator/headers_evaluator.go
Normal file
107
authorize/evaluator/headers_evaluator.go
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
package evaluator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/open-policy-agent/opa/rego"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/authorize/evaluator/opa"
|
||||||
|
"github.com/pomerium/pomerium/config"
|
||||||
|
"github.com/pomerium/pomerium/internal/urlutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HeadersRequest is the input to the headers.rego script.
|
||||||
|
type HeadersRequest struct {
|
||||||
|
EnableGoogleCloudServerlessAuthentication bool `json:"enable_google_cloud_serverless_authentication"`
|
||||||
|
FromAudience string `json:"from_audience"`
|
||||||
|
KubernetesServiceAccountToken string `json:"kubernetes_service_account_token"`
|
||||||
|
ToAudience string `json:"to_audience"`
|
||||||
|
Session RequestSession `json:"session"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHeadersRequestFromPolicy creates a new HeadersRequest from a policy.
|
||||||
|
func NewHeadersRequestFromPolicy(policy *config.Policy) *HeadersRequest {
|
||||||
|
input := new(HeadersRequest)
|
||||||
|
input.EnableGoogleCloudServerlessAuthentication = policy.EnableGoogleCloudServerlessAuthentication
|
||||||
|
if u, err := urlutil.ParseAndValidateURL(policy.From); err == nil {
|
||||||
|
input.FromAudience = u.Hostname()
|
||||||
|
}
|
||||||
|
input.KubernetesServiceAccountToken = policy.KubernetesServiceAccountToken
|
||||||
|
for _, wu := range policy.To {
|
||||||
|
input.ToAudience = wu.URL.Hostname()
|
||||||
|
}
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
|
||||||
|
// HeadersResponse is the output from the headers.rego script.
|
||||||
|
type HeadersResponse struct {
|
||||||
|
Headers http.Header
|
||||||
|
}
|
||||||
|
|
||||||
|
// A HeadersEvaluator evaluates the headers.rego script.
|
||||||
|
type HeadersEvaluator struct {
|
||||||
|
q rego.PreparedEvalQuery
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHeadersEvaluator creates a new HeadersEvaluator.
|
||||||
|
func NewHeadersEvaluator(ctx context.Context, store *Store) (*HeadersEvaluator, error) {
|
||||||
|
r := rego.New(
|
||||||
|
rego.Store(store),
|
||||||
|
rego.Module("pomerium.headers", opa.HeadersRego),
|
||||||
|
rego.Query("result = data.pomerium.headers"),
|
||||||
|
getGoogleCloudServerlessHeadersRegoOption,
|
||||||
|
store.GetDataBrokerRecordOption(),
|
||||||
|
)
|
||||||
|
|
||||||
|
q, err := r.PrepareForEval(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &HeadersEvaluator{
|
||||||
|
q: q,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evaluate evaluates the headers.rego script.
|
||||||
|
func (e *HeadersEvaluator) Evaluate(ctx context.Context, req *HeadersRequest) (*HeadersResponse, error) {
|
||||||
|
rs, err := safeEval(ctx, e.q, rego.EvalInput(req))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("authorize: error evaluating headers.rego: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rs) == 0 {
|
||||||
|
return nil, fmt.Errorf("authorize: unexpected empty result from evaluating headers.rego")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &HeadersResponse{
|
||||||
|
Headers: e.getHeader(rs[0].Bindings),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *HeadersEvaluator) getHeader(vars rego.Vars) http.Header {
|
||||||
|
h := make(http.Header)
|
||||||
|
|
||||||
|
m, ok := vars["result"].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
m, ok = m["identity_headers"].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
for k := range m {
|
||||||
|
vs, ok := m[k].([]interface{})
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, v := range vs {
|
||||||
|
h.Add(k, fmt.Sprintf("%v", v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return h
|
||||||
|
}
|
62
authorize/evaluator/headers_evaluator_test.go
Normal file
62
authorize/evaluator/headers_evaluator_test.go
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
package evaluator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"math"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
"gopkg.in/square/go-jose.v2/jwt"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/config"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/pkg/cryptutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHeadersEvaluator(t *testing.T) {
|
||||||
|
type A = []interface{}
|
||||||
|
type M = map[string]interface{}
|
||||||
|
|
||||||
|
signingKey, err := cryptutil.NewSigningKey()
|
||||||
|
require.NoError(t, err)
|
||||||
|
encodedSigningKey, err := cryptutil.EncodePrivateKey(signingKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
privateJWK, err := cryptutil.PrivateJWKFromBytes(encodedSigningKey, jose.ES256)
|
||||||
|
require.NoError(t, err)
|
||||||
|
publicJWK, err := cryptutil.PublicJWKFromBytes(encodedSigningKey, jose.ES256)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
eval := func(t *testing.T, data []proto.Message, input *HeadersRequest) (*HeadersResponse, error) {
|
||||||
|
store := NewStoreFromProtos(math.MaxUint64, data...)
|
||||||
|
store.UpdateIssuer("authenticate.example.com")
|
||||||
|
store.UpdateJWTClaimHeaders(config.NewJWTClaimHeaders("email", "groups", "user", "CUSTOM_KEY"))
|
||||||
|
store.UpdateSigningKey(privateJWK)
|
||||||
|
e, err := NewHeadersEvaluator(context.Background(), store)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return e.Evaluate(context.Background(), input)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("jwt", func(t *testing.T) {
|
||||||
|
output, err := eval(t,
|
||||||
|
[]proto.Message{},
|
||||||
|
&HeadersRequest{
|
||||||
|
FromAudience: "from.example.com",
|
||||||
|
ToAudience: "to.example.com",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
rawJWT, err := jwt.ParseSigned(output.Headers.Get("X-Pomerium-Jwt-Assertion"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var claims M
|
||||||
|
err = rawJWT.Claims(publicJWK, &claims)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.LessOrEqual(t, claims["exp"], float64(time.Now().Add(time.Minute*6).Unix()),
|
||||||
|
"JWT should expire within 5 minutes, but got: %v", claims["exp"])
|
||||||
|
})
|
||||||
|
}
|
|
@ -2,8 +2,8 @@
|
||||||
// decisions.
|
// decisions.
|
||||||
package opa
|
package opa
|
||||||
|
|
||||||
import "embed"
|
import _ "embed" // to embed files
|
||||||
|
|
||||||
// FS is the filesystem for OPA files.
|
// HeadersRego is the headers.rego script.
|
||||||
//go:embed policy
|
//go:embed policy/headers.rego
|
||||||
var FS embed.FS
|
var HeadersRego string
|
||||||
|
|
|
@ -1,474 +0,0 @@
|
||||||
package pomerium.authz
|
|
||||||
|
|
||||||
default allow = false
|
|
||||||
|
|
||||||
# 5 minutes from now in seconds
|
|
||||||
five_minutes := (time.now_ns() / 1e9) + (60 * 5)
|
|
||||||
|
|
||||||
# databroker versions to know which version of the data was evaluated
|
|
||||||
databroker_server_version := data.databroker_server_version
|
|
||||||
|
|
||||||
databroker_record_version := data.databroker_record_version
|
|
||||||
|
|
||||||
route_policy_idx := first_allowed_route_policy_idx(input.http.url)
|
|
||||||
|
|
||||||
route_policy := data.route_policies[route_policy_idx]
|
|
||||||
|
|
||||||
session = s {
|
|
||||||
s = get_databroker_record("type.googleapis.com/user.ServiceAccount", input.session.id)
|
|
||||||
s != null
|
|
||||||
} else = s {
|
|
||||||
s = get_databroker_record("type.googleapis.com/session.Session", input.session.id)
|
|
||||||
s != null
|
|
||||||
} else = {} {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
user = u {
|
|
||||||
u = get_databroker_record("type.googleapis.com/user.User", session.impersonate_user_id)
|
|
||||||
u != null
|
|
||||||
} else = u {
|
|
||||||
u = get_databroker_record("type.googleapis.com/user.User", session.user_id)
|
|
||||||
u != null
|
|
||||||
} else = {} {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
directory_user = du {
|
|
||||||
du = get_databroker_record("type.googleapis.com/directory.User", session.impersonate_user_id)
|
|
||||||
du != null
|
|
||||||
} else = du {
|
|
||||||
du = get_databroker_record("type.googleapis.com/directory.User", session.user_id)
|
|
||||||
du != null
|
|
||||||
} else = {} {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
group_ids = gs {
|
|
||||||
gs = session.impersonate_groups
|
|
||||||
gs != null
|
|
||||||
} else = gs {
|
|
||||||
gs = directory_user.group_ids
|
|
||||||
gs != null
|
|
||||||
} else = [] {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
groups := array.concat(group_ids, array.concat(get_databroker_group_names(group_ids), get_databroker_group_emails(group_ids)))
|
|
||||||
|
|
||||||
all_allowed_domains := get_allowed_domains(route_policy)
|
|
||||||
|
|
||||||
all_allowed_groups := get_allowed_groups(route_policy)
|
|
||||||
|
|
||||||
all_allowed_users := get_allowed_users(route_policy)
|
|
||||||
|
|
||||||
all_allowed_idp_claims := get_allowed_idp_claims(route_policy)
|
|
||||||
|
|
||||||
is_impersonating := count(session.impersonate_email) > 0
|
|
||||||
|
|
||||||
# allow public
|
|
||||||
allow {
|
|
||||||
route_policy.AllowPublicUnauthenticatedAccess == true
|
|
||||||
}
|
|
||||||
|
|
||||||
# allow cors preflight
|
|
||||||
allow {
|
|
||||||
route_policy.CORSAllowPreflight == true
|
|
||||||
input.http.method == "OPTIONS"
|
|
||||||
count(object.get(input.http.headers, "Access-Control-Request-Method", [])) > 0
|
|
||||||
count(object.get(input.http.headers, "Origin", [])) > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# allow any authenticated user
|
|
||||||
allow {
|
|
||||||
route_policy.AllowAnyAuthenticatedUser == true
|
|
||||||
session.user_id != ""
|
|
||||||
}
|
|
||||||
|
|
||||||
# allow by user email
|
|
||||||
allow {
|
|
||||||
not is_impersonating
|
|
||||||
user.email == all_allowed_users[_]
|
|
||||||
}
|
|
||||||
|
|
||||||
# allow by user id
|
|
||||||
allow {
|
|
||||||
not is_impersonating
|
|
||||||
user.id == all_allowed_users[_]
|
|
||||||
}
|
|
||||||
|
|
||||||
# allow group
|
|
||||||
allow {
|
|
||||||
not is_impersonating
|
|
||||||
some group
|
|
||||||
groups[_] = group
|
|
||||||
all_allowed_groups[_] = group
|
|
||||||
}
|
|
||||||
|
|
||||||
# allow by impersonate email
|
|
||||||
allow {
|
|
||||||
is_impersonating
|
|
||||||
all_allowed_users[_] = session.impersonate_email
|
|
||||||
}
|
|
||||||
|
|
||||||
# allow by impersonate group
|
|
||||||
allow {
|
|
||||||
is_impersonating
|
|
||||||
some group
|
|
||||||
session.impersonate_groups[_] = group
|
|
||||||
all_allowed_groups[_] = group
|
|
||||||
}
|
|
||||||
|
|
||||||
# allow by domain
|
|
||||||
allow {
|
|
||||||
not is_impersonating
|
|
||||||
some domain
|
|
||||||
email_in_domain(user.email, all_allowed_domains[domain])
|
|
||||||
}
|
|
||||||
|
|
||||||
# allow by impersonate domain
|
|
||||||
allow {
|
|
||||||
is_impersonating
|
|
||||||
some domain
|
|
||||||
email_in_domain(session.impersonate_email, all_allowed_domains[domain])
|
|
||||||
}
|
|
||||||
|
|
||||||
# allow by arbitrary idp claims
|
|
||||||
allow {
|
|
||||||
are_claims_allowed(all_allowed_idp_claims[_], session.claims)
|
|
||||||
}
|
|
||||||
|
|
||||||
allow {
|
|
||||||
are_claims_allowed(all_allowed_idp_claims[_], user.claims)
|
|
||||||
}
|
|
||||||
|
|
||||||
# allow pomerium urls
|
|
||||||
allow {
|
|
||||||
contains(input.http.url, "/.pomerium/")
|
|
||||||
}
|
|
||||||
|
|
||||||
deny[reason] {
|
|
||||||
reason = [495, "invalid client certificate"]
|
|
||||||
is_boolean(input.is_valid_client_certificate)
|
|
||||||
not input.is_valid_client_certificate
|
|
||||||
}
|
|
||||||
|
|
||||||
jwt_headers = {
|
|
||||||
"typ": "JWT",
|
|
||||||
"alg": data.signing_key.alg,
|
|
||||||
"kid": data.signing_key.kid,
|
|
||||||
}
|
|
||||||
|
|
||||||
jwt_payload_aud = v {
|
|
||||||
v = parse_url(input.http.url).hostname
|
|
||||||
} else = "" {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
jwt_payload_iss = data.issuer
|
|
||||||
|
|
||||||
jwt_payload_jti = v {
|
|
||||||
v = session.id
|
|
||||||
} else = "" {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
jwt_payload_exp = v {
|
|
||||||
v = min([five_minutes, session.expires_at.seconds])
|
|
||||||
} else = v {
|
|
||||||
v = five_minutes
|
|
||||||
} else = null {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
jwt_payload_iat = v {
|
|
||||||
# sessions store the issued_at on the id_token
|
|
||||||
v = session.id_token.issued_at.seconds
|
|
||||||
} else = v {
|
|
||||||
# service accounts store the issued at directly
|
|
||||||
v = session.issued_at.seconds
|
|
||||||
} else = null {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
jwt_payload_sub = v {
|
|
||||||
v = user.id
|
|
||||||
} else = "" {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
jwt_payload_user = v {
|
|
||||||
v = user.id
|
|
||||||
} else = "" {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
jwt_payload_email = v {
|
|
||||||
v = session.impersonate_email
|
|
||||||
} else = v {
|
|
||||||
v = directory_user.email
|
|
||||||
} else = v {
|
|
||||||
v = user.email
|
|
||||||
} else = "" {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
jwt_payload_groups = v {
|
|
||||||
v = array.concat(group_ids, get_databroker_group_names(group_ids))
|
|
||||||
v != []
|
|
||||||
} else = v {
|
|
||||||
v = session.claims.groups
|
|
||||||
v != null
|
|
||||||
} else = [] {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
base_jwt_claims := [
|
|
||||||
["iss", jwt_payload_iss],
|
|
||||||
["aud", jwt_payload_aud],
|
|
||||||
["jti", jwt_payload_jti],
|
|
||||||
["exp", jwt_payload_exp],
|
|
||||||
["iat", jwt_payload_iat],
|
|
||||||
["sub", jwt_payload_sub],
|
|
||||||
["user", jwt_payload_user],
|
|
||||||
["email", jwt_payload_email],
|
|
||||||
["groups", jwt_payload_groups],
|
|
||||||
]
|
|
||||||
|
|
||||||
additional_jwt_claims := [[k, v] |
|
|
||||||
some header_name
|
|
||||||
claim_key := data.jwt_claim_headers[header_name]
|
|
||||||
|
|
||||||
# exclude base_jwt_claims
|
|
||||||
count([1 |
|
|
||||||
[xk, xv] := base_jwt_claims[_]
|
|
||||||
xk == claim_key
|
|
||||||
]) == 0
|
|
||||||
|
|
||||||
# the claim value can come from session claims or user claims
|
|
||||||
claim_value := object.get(session.claims, claim_key, object.get(user.claims, claim_key, null))
|
|
||||||
|
|
||||||
k := claim_key
|
|
||||||
v := get_header_string_value(claim_value)
|
|
||||||
]
|
|
||||||
|
|
||||||
jwt_claims := array.concat(base_jwt_claims, additional_jwt_claims)
|
|
||||||
|
|
||||||
jwt_payload = {key: value |
|
|
||||||
# use a comprehension over an array to remove nil values
|
|
||||||
[key, value] := jwt_claims[_]
|
|
||||||
value != null
|
|
||||||
}
|
|
||||||
|
|
||||||
signed_jwt = io.jwt.encode_sign(jwt_headers, jwt_payload, data.signing_key)
|
|
||||||
|
|
||||||
kubernetes_headers = h {
|
|
||||||
route_policy.KubernetesServiceAccountToken != ""
|
|
||||||
h := [
|
|
||||||
["Authorization", concat(" ", ["Bearer", route_policy.KubernetesServiceAccountToken])],
|
|
||||||
["Impersonate-User", jwt_payload_email],
|
|
||||||
["Impersonate-Group", get_header_string_value(jwt_payload_groups)],
|
|
||||||
]
|
|
||||||
} else = [] {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
google_cloud_serverless_authentication_service_account = s {
|
|
||||||
s := data.google_cloud_serverless_authentication_service_account
|
|
||||||
} else = "" {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
google_cloud_serverless_headers = h {
|
|
||||||
route_policy.EnableGoogleCloudServerlessAuthentication
|
|
||||||
[hostname, _] := parse_host_port(route_policy.To[0].URL.Host)
|
|
||||||
audience := concat("", ["https://", hostname])
|
|
||||||
h := get_google_cloud_serverless_headers(google_cloud_serverless_authentication_service_account, audience)
|
|
||||||
} else = {} {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
identity_headers := {key: value |
|
|
||||||
h1 := [["x-pomerium-jwt-assertion", signed_jwt]]
|
|
||||||
h2 := [[k, v] |
|
|
||||||
[claim_key, claim_value] := jwt_claims[_]
|
|
||||||
claim_value != null
|
|
||||||
|
|
||||||
# only include those headers requested by the user
|
|
||||||
some header_name
|
|
||||||
available := data.jwt_claim_headers[header_name]
|
|
||||||
available == claim_key
|
|
||||||
|
|
||||||
# create the header key and value
|
|
||||||
k := header_name
|
|
||||||
v := get_header_string_value(claim_value)
|
|
||||||
]
|
|
||||||
|
|
||||||
h3 := kubernetes_headers
|
|
||||||
h4 := [[k, v] | v := google_cloud_serverless_headers[k]]
|
|
||||||
|
|
||||||
h := array.concat(array.concat(array.concat(h1, h2), h3), h4)
|
|
||||||
[key, value] := h[_]
|
|
||||||
}
|
|
||||||
|
|
||||||
# returns the first matching route
|
|
||||||
first_allowed_route_policy_idx(input_url) = first_policy_idx {
|
|
||||||
first_policy_idx := [idx | some idx, policy; policy = data.route_policies[idx]; allowed_route(input.http.url, policy)][0]
|
|
||||||
}
|
|
||||||
|
|
||||||
allowed_route(input_url, policy) {
|
|
||||||
input_url_obj := parse_url(input_url)
|
|
||||||
allowed_route_source(input_url_obj, policy)
|
|
||||||
allowed_route_prefix(input_url_obj, policy)
|
|
||||||
allowed_route_path(input_url_obj, policy)
|
|
||||||
allowed_route_regex(input_url_obj, policy)
|
|
||||||
}
|
|
||||||
|
|
||||||
allowed_route_source(input_url_obj, policy) {
|
|
||||||
object.get(policy, "source", "") == ""
|
|
||||||
}
|
|
||||||
|
|
||||||
allowed_route_source(input_url_obj, policy) {
|
|
||||||
object.get(policy, "source", "") != ""
|
|
||||||
source_url_obj := parse_url(policy.source)
|
|
||||||
input_url_obj.host == source_url_obj.host
|
|
||||||
}
|
|
||||||
|
|
||||||
allowed_route_prefix(input_url_obj, policy) {
|
|
||||||
object.get(policy, "prefix", "") == ""
|
|
||||||
}
|
|
||||||
|
|
||||||
allowed_route_prefix(input_url_obj, policy) {
|
|
||||||
object.get(policy, "prefix", "") != ""
|
|
||||||
startswith(input_url_obj.path, policy.prefix)
|
|
||||||
}
|
|
||||||
|
|
||||||
allowed_route_path(input_url_obj, policy) {
|
|
||||||
object.get(policy, "path", "") == ""
|
|
||||||
}
|
|
||||||
|
|
||||||
allowed_route_path(input_url_obj, policy) {
|
|
||||||
object.get(policy, "path", "") != ""
|
|
||||||
policy.path == input_url_obj.path
|
|
||||||
}
|
|
||||||
|
|
||||||
allowed_route_regex(input_url_obj, policy) {
|
|
||||||
object.get(policy, "regex", "") == ""
|
|
||||||
}
|
|
||||||
|
|
||||||
allowed_route_regex(input_url_obj, policy) {
|
|
||||||
object.get(policy, "regex", "") != ""
|
|
||||||
re_match(policy.regex, input_url_obj.path)
|
|
||||||
}
|
|
||||||
|
|
||||||
parse_url(str) = {"scheme": scheme, "host": host, "hostname": hostname, "path": path} {
|
|
||||||
[_, scheme, host, rawpath] = regex.find_all_string_submatch_n(`(?:((?:tcp[+])?http[s]?)://)?([^/]+)([^?#]*)`, str, 1)[0]
|
|
||||||
[hostname, _] = parse_host_port(host)
|
|
||||||
path = normalize_url_path(rawpath)
|
|
||||||
}
|
|
||||||
|
|
||||||
parse_host_port(str) = [host, port] {
|
|
||||||
contains(str, ":")
|
|
||||||
[host, port] = split(str, ":")
|
|
||||||
} else = [host, port] {
|
|
||||||
host = str
|
|
||||||
port = "443"
|
|
||||||
}
|
|
||||||
|
|
||||||
normalize_url_path(str) = "/" {
|
|
||||||
str == ""
|
|
||||||
}
|
|
||||||
|
|
||||||
normalize_url_path(str) = str {
|
|
||||||
str != ""
|
|
||||||
}
|
|
||||||
|
|
||||||
email_in_domain(email, domain) {
|
|
||||||
x := split(email, "@")
|
|
||||||
count(x) == 2
|
|
||||||
x[1] == domain
|
|
||||||
}
|
|
||||||
|
|
||||||
element_in_list(list, elem) {
|
|
||||||
list[_] = elem
|
|
||||||
}
|
|
||||||
|
|
||||||
get_allowed_users(policy) = v {
|
|
||||||
sub_array := [x | x = policy.sub_policies[_].allowed_users[_]]
|
|
||||||
main_array := [x | x = policy.allowed_users[_]]
|
|
||||||
v := {x | x = array.concat(main_array, sub_array)[_]}
|
|
||||||
}
|
|
||||||
|
|
||||||
get_allowed_domains(policy) = v {
|
|
||||||
sub_array := [x | x = policy.sub_policies[_].allowed_domains[_]]
|
|
||||||
main_array := [x | x = policy.allowed_domains[_]]
|
|
||||||
v := {x | x = array.concat(main_array, sub_array)[_]}
|
|
||||||
}
|
|
||||||
|
|
||||||
get_allowed_groups(policy) = v {
|
|
||||||
sub_array := [x | x = policy.sub_policies[_].allowed_groups[_]]
|
|
||||||
main_array := [x | x = policy.allowed_groups[_]]
|
|
||||||
v := {x | x = array.concat(main_array, sub_array)[_]}
|
|
||||||
}
|
|
||||||
|
|
||||||
get_allowed_idp_claims(policy) = v {
|
|
||||||
v := array.concat([policy.allowed_idp_claims], [u | u := policy.sub_policies[_].allowed_idp_claims])
|
|
||||||
}
|
|
||||||
|
|
||||||
are_claims_allowed(a, b) {
|
|
||||||
is_object(a)
|
|
||||||
is_object(b)
|
|
||||||
avs := a[ak]
|
|
||||||
bvs := object.get(b, ak, null)
|
|
||||||
|
|
||||||
is_array(avs)
|
|
||||||
is_array(bvs)
|
|
||||||
avs[_] == bvs[_]
|
|
||||||
}
|
|
||||||
|
|
||||||
get_databroker_group_names(ids) = gs {
|
|
||||||
gs := [name | id := ids[i]; group := get_databroker_record("type.googleapis.com/directory.Group", id); name := group.name]
|
|
||||||
}
|
|
||||||
|
|
||||||
get_databroker_group_emails(ids) = gs {
|
|
||||||
gs := [email | id := ids[i]; group := get_databroker_record("type.googleapis.com/directory.Group", id); email := group.email]
|
|
||||||
}
|
|
||||||
|
|
||||||
get_header_string_value(obj) = s {
|
|
||||||
is_array(obj)
|
|
||||||
s := concat(",", obj)
|
|
||||||
} else = s {
|
|
||||||
s := concat(",", [obj])
|
|
||||||
}
|
|
||||||
|
|
||||||
# object_get is like object.get, but supports converting "/" in keys to separate lookups
|
|
||||||
# rego doesn't support recursion, so we hard code a limited number of /'s
|
|
||||||
object_get(obj, key, def) = value {
|
|
||||||
segments := split(key, "/")
|
|
||||||
count(segments) == 2
|
|
||||||
o1 := object.get(obj, segments[0], {})
|
|
||||||
value = object.get(o1, segments[1], def)
|
|
||||||
} else = value {
|
|
||||||
segments := split(key, "/")
|
|
||||||
count(segments) == 3
|
|
||||||
o1 := object.get(obj, segments[0], {})
|
|
||||||
o2 := object.get(o1, segments[1], {})
|
|
||||||
value = object.get(o2, segments[2], def)
|
|
||||||
} else = value {
|
|
||||||
segments := split(key, "/")
|
|
||||||
count(segments) == 4
|
|
||||||
o1 := object.get(obj, segments[0], {})
|
|
||||||
o2 := object.get(o1, segments[1], {})
|
|
||||||
o3 := object.get(o2, segments[2], {})
|
|
||||||
value = object.get(o3, segments[3], def)
|
|
||||||
} else = value {
|
|
||||||
segments := split(key, "/")
|
|
||||||
count(segments) == 5
|
|
||||||
o1 := object.get(obj, segments[0], {})
|
|
||||||
o2 := object.get(o1, segments[1], {})
|
|
||||||
o3 := object.get(o2, segments[2], {})
|
|
||||||
o4 := object.get(o3, segments[3], {})
|
|
||||||
value = object.get(o4, segments[4], def)
|
|
||||||
} else = value {
|
|
||||||
value = object.get(obj, key, def)
|
|
||||||
}
|
|
|
@ -1,341 +0,0 @@
|
||||||
package pomerium.authz
|
|
||||||
|
|
||||||
get_google_cloud_serverless_headers(serviceAccount, audience) = h {
|
|
||||||
h := {"Authorization": "Bearer xxx"}
|
|
||||||
}
|
|
||||||
|
|
||||||
get_databroker_record(typeURL, id) = v {
|
|
||||||
v := object_get(data.databroker_data, typeURL, null)[id]
|
|
||||||
}
|
|
||||||
|
|
||||||
test_email_allowed {
|
|
||||||
allow with data.route_policies as [{
|
|
||||||
"source": "example.com",
|
|
||||||
"allowed_users": ["x@example.com"],
|
|
||||||
}]
|
|
||||||
with data.databroker_data as {"type.googleapis.com": {
|
|
||||||
"session.Session": {"session1": {"id": "session1", "user_id": "user1"}},
|
|
||||||
"user.User": {"user1": {"id": "user1", "email": "x@example.com"}},
|
|
||||||
}}
|
|
||||||
with input.http as {"url": "http://example.com"}
|
|
||||||
with input.session as {"id": "session1"}
|
|
||||||
}
|
|
||||||
|
|
||||||
test_impersonate_email_not_allowed {
|
|
||||||
not allow with data.route_policies as [{
|
|
||||||
"source": "example.com",
|
|
||||||
"allowed_users": ["x@example.com"],
|
|
||||||
}]
|
|
||||||
with data.databroker_data as {"type.googleapis.com": {
|
|
||||||
"session.Session": {"session1": {"id": "session1", "user_id": "user1", "impersonate_email": "y@example.com"}},
|
|
||||||
"user.User": {"user1": {"id": "user1", "email": "x@example.com"}},
|
|
||||||
}}
|
|
||||||
with input.http as {"url": "http://example.com"}
|
|
||||||
with input.session as {"id": "session1"}
|
|
||||||
}
|
|
||||||
|
|
||||||
test_impersonate_email_allowed {
|
|
||||||
allow with data.route_policies as [{
|
|
||||||
"source": "example.com",
|
|
||||||
"allowed_users": ["y@example.com"],
|
|
||||||
}]
|
|
||||||
with data.databroker_data as {"type.googleapis.com": {
|
|
||||||
"session.Session": {"session1": {"id": "session1", "user_id": "user1", "impersonate_email": "y@example.com"}},
|
|
||||||
"user.User": {"user1": {"id": "user1", "email": "x@example.com"}},
|
|
||||||
}}
|
|
||||||
with input.http as {"url": "http://example.com"}
|
|
||||||
with input.session as {"id": "session1"}
|
|
||||||
}
|
|
||||||
|
|
||||||
test_group_allowed {
|
|
||||||
allow with data.route_policies as [{
|
|
||||||
"source": "example.com",
|
|
||||||
"allowed_groups": ["group1"],
|
|
||||||
}]
|
|
||||||
with data.databroker_data as {"type.googleapis.com": {
|
|
||||||
"session.Session": {"session1": {"id": "session1", "user_id": "user1"}},
|
|
||||||
"user.User": {"user1": {"id": "user1", "email": "x@example.com"}},
|
|
||||||
"directory.User": {"user1": {"id": "user1", "group_ids": ["group1"]}},
|
|
||||||
"directory.Group": {"group1": {"id": "group1"}},
|
|
||||||
}}
|
|
||||||
with input.http as {"url": "http://example.com"}
|
|
||||||
with input.session as {"id": "session1"}
|
|
||||||
}
|
|
||||||
|
|
||||||
test_impersonate_groups_not_allowed {
|
|
||||||
not allow with data.route_policies as [{
|
|
||||||
"source": "example.com",
|
|
||||||
"allowed_groups": ["group1"],
|
|
||||||
}]
|
|
||||||
with data.databroker_data as {"type.googleapis.com": {
|
|
||||||
"session.Session": {"session1": {"id": "session1", "user_id": "user1", "impersonate_email": "y@example.com", "impersonate_groups": ["group2"]}},
|
|
||||||
"user.User": {"user1": {"id": "user1", "email": "x@example.com"}},
|
|
||||||
"directory.User": {"user1": {"id": "user1", "group_ids": ["group1"]}},
|
|
||||||
"directory.Group": {"group1": {"id": "group1"}},
|
|
||||||
}}
|
|
||||||
with input.http as {"url": "http://example.com"}
|
|
||||||
with input.session as {"id": "session1"}
|
|
||||||
}
|
|
||||||
|
|
||||||
test_impersonate_groups_allowed {
|
|
||||||
allow with data.route_policies as [{
|
|
||||||
"source": "example.com",
|
|
||||||
"allowed_groups": ["group2"],
|
|
||||||
}]
|
|
||||||
with data.databroker_data as {"type.googleapis.com": {
|
|
||||||
"session.Session": {"session1": {"id": "session1", "user_id": "user1", "impersonate_email": "y@example.com", "impersonate_groups": ["group2"]}},
|
|
||||||
"user.User": {"user1": {"id": "user1", "email": "x@example.com"}},
|
|
||||||
"directory.User": {"user1": {"id": "user1", "group_ids": ["group1"]}},
|
|
||||||
"directory.Group": {"group1": {"id": "group1"}},
|
|
||||||
}}
|
|
||||||
with input.http as {"url": "http://example.com"}
|
|
||||||
with input.session as {"id": "session1"}
|
|
||||||
}
|
|
||||||
|
|
||||||
test_domain_allowed {
|
|
||||||
allow with data.route_policies as [{
|
|
||||||
"source": "example.com",
|
|
||||||
"allowed_domains": ["example.com"],
|
|
||||||
}]
|
|
||||||
with data.databroker_data as {"type.googleapis.com": {
|
|
||||||
"session.Session": {"session1": {"id": "session1", "user_id": "user1"}},
|
|
||||||
"user.User": {"user1": {"id": "user1", "email": "x@example.com"}},
|
|
||||||
}}
|
|
||||||
with input.http as {"url": "http://example.com"}
|
|
||||||
with input.session as {"id": "session1"}
|
|
||||||
}
|
|
||||||
|
|
||||||
test_impersonate_domain_not_allowed {
|
|
||||||
not allow with data.route_policies as [{
|
|
||||||
"source": "example.com",
|
|
||||||
"allowed_domains": ["example.com"],
|
|
||||||
}]
|
|
||||||
with data.databroker_data as {"type.googleapis.com": {
|
|
||||||
"session.Session": {"session1": {"id": "session1", "user_id": "user1", "impersonate_email": "y@notexample.com"}},
|
|
||||||
"user.User": {"user1": {"id": "user1", "email": "x@example.com"}},
|
|
||||||
}}
|
|
||||||
with input.http as {"url": "http://example.com"}
|
|
||||||
with input.session as {"id": "session1"}
|
|
||||||
}
|
|
||||||
|
|
||||||
test_impersonate_domain_allowed {
|
|
||||||
allow with data.route_policies as [{
|
|
||||||
"source": "example.com",
|
|
||||||
"allowed_domains": ["notexample.com"],
|
|
||||||
}]
|
|
||||||
with data.databroker_data as {"type.googleapis.com": {
|
|
||||||
"session.Session": {"session1": {"id": "session1", "user_id": "user1", "impersonate_email": "y@notexample.com"}},
|
|
||||||
"user.User": {"user1": {"id": "user1", "email": "x@example.com"}},
|
|
||||||
}}
|
|
||||||
with input.http as {"url": "http://example.com"}
|
|
||||||
with input.session as {"id": "session1"}
|
|
||||||
}
|
|
||||||
|
|
||||||
test_idp_claims_allowed {
|
|
||||||
allow with data.route_policies as [{
|
|
||||||
"source": "example.com",
|
|
||||||
"allowed_idp_claims": {"some.claim": ["a", "b"]},
|
|
||||||
}]
|
|
||||||
with data.databroker_data as {"type.googleapis.com": {"session.Session": {"session1": {"id": "session1", "claims": {"some.claim": ["b"]}}}}}
|
|
||||||
with input.http as {"url": "http://example.com"}
|
|
||||||
with input.session as {"id": "session1"}
|
|
||||||
}
|
|
||||||
|
|
||||||
test_example {
|
|
||||||
not allow with data.route_policies as [
|
|
||||||
{
|
|
||||||
"source": "http://example.com",
|
|
||||||
"path": "/a",
|
|
||||||
"allowed_domains": ["example.com"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"source": "http://example.com",
|
|
||||||
"path": "/b",
|
|
||||||
"allowed_users": ["noone@pomerium.com"],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
with input.http as {"url": "http://example.com/b"}
|
|
||||||
with input.user as {"id": "1", "email": "joe@example.com"}
|
|
||||||
}
|
|
||||||
|
|
||||||
test_email_denied {
|
|
||||||
not allow with data.route_policies as [{
|
|
||||||
"source": "example.com",
|
|
||||||
"allowed_users": ["bob@example.com"],
|
|
||||||
}]
|
|
||||||
with input.http as {"url": "http://example.com"}
|
|
||||||
with input.user as {"id": "1", "email": "joe@example.com"}
|
|
||||||
}
|
|
||||||
|
|
||||||
test_public_allowed {
|
|
||||||
allow with data.route_policies as [{
|
|
||||||
"source": "example.com",
|
|
||||||
"AllowPublicUnauthenticatedAccess": true,
|
|
||||||
}]
|
|
||||||
with input.http as {"url": "http://example.com"}
|
|
||||||
}
|
|
||||||
|
|
||||||
test_public_denied {
|
|
||||||
not allow with data.route_policies as [
|
|
||||||
{
|
|
||||||
"source": "example.com",
|
|
||||||
"prefix": "/by-user",
|
|
||||||
"allowed_users": ["bob@example.com"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"source": "example.com",
|
|
||||||
"AllowPublicUnauthenticatedAccess": true,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
with input.http as {"url": "http://example.com/by-user"}
|
|
||||||
}
|
|
||||||
|
|
||||||
test_pomerium_allowed {
|
|
||||||
allow with data.route_policies as [{
|
|
||||||
"source": "example.com",
|
|
||||||
"allowed_users": ["bob@example.com"],
|
|
||||||
}]
|
|
||||||
with input.http as {"url": "http://example.com/.pomerium/"}
|
|
||||||
}
|
|
||||||
|
|
||||||
test_cors_preflight_allowed {
|
|
||||||
allow with data.route_policies as [{
|
|
||||||
"source": "example.com",
|
|
||||||
"allowed_users": ["bob@example.com"],
|
|
||||||
"CORSAllowPreflight": true,
|
|
||||||
}]
|
|
||||||
with input.http as {
|
|
||||||
"method": "OPTIONS",
|
|
||||||
"url": "http://example.com/",
|
|
||||||
"headers": {
|
|
||||||
"Origin": ["someorigin"],
|
|
||||||
"Access-Control-Request-Method": ["GET"],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test_cors_preflight_denied {
|
|
||||||
not allow with data.route_policies as [{
|
|
||||||
"source": "example.com",
|
|
||||||
"allowed_users": ["bob@example.com"],
|
|
||||||
}]
|
|
||||||
with input.http as {
|
|
||||||
"method": "OPTIONS",
|
|
||||||
"url": "http://example.com/",
|
|
||||||
"headers": {
|
|
||||||
"Origin": ["someorigin"],
|
|
||||||
"Access-Control-Request-Method": ["GET"],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test_parse_url {
|
|
||||||
url := parse_url("http://example.com/some/path?qs")
|
|
||||||
url.scheme == "http"
|
|
||||||
url.host == "example.com"
|
|
||||||
url.path == "/some/path"
|
|
||||||
}
|
|
||||||
|
|
||||||
test_parse_tcp_url {
|
|
||||||
url := parse_url("tcp+http://example.com/some/path?qs")
|
|
||||||
url.scheme == "tcp+http"
|
|
||||||
url.host == "example.com"
|
|
||||||
url.path == "/some/path"
|
|
||||||
}
|
|
||||||
|
|
||||||
test_allowed_route_source {
|
|
||||||
allowed_route("http://example.com", {"source": "example.com"})
|
|
||||||
allowed_route("http://example.com", {"source": "http://example.com"})
|
|
||||||
allowed_route("http://example.com", {"source": "https://example.com"})
|
|
||||||
allowed_route("http://example.com/", {"source": "https://example.com"})
|
|
||||||
allowed_route("http://example.com", {"source": "https://example.com/"})
|
|
||||||
allowed_route("http://example.com/", {"source": "https://example.com/"})
|
|
||||||
not allowed_route("http://example.org", {"source": "example.com"})
|
|
||||||
}
|
|
||||||
|
|
||||||
test_allowed_route_prefix {
|
|
||||||
allowed_route("http://example.com", {"prefix": "/"})
|
|
||||||
allowed_route("http://example.com/admin/somepath", {"prefix": "/admin"})
|
|
||||||
not allowed_route("http://example.com", {"prefix": "/admin"})
|
|
||||||
}
|
|
||||||
|
|
||||||
test_allowed_route_path {
|
|
||||||
allowed_route("http://example.com", {"path": "/"})
|
|
||||||
allowed_route("http://example.com/", {"path": "/"})
|
|
||||||
not allowed_route("http://example.com/admin/somepath", {"path": "/admin"})
|
|
||||||
not allowed_route("http://example.com", {"path": "/admin"})
|
|
||||||
}
|
|
||||||
|
|
||||||
test_allowed_route_regex {
|
|
||||||
allowed_route("http://example.com", {"regex": ".*"})
|
|
||||||
allowed_route("http://example.com/admin/somepath", {"regex": "/admin/.*"})
|
|
||||||
not allowed_route("http://example.com", {"regex": "[xyz]"})
|
|
||||||
}
|
|
||||||
|
|
||||||
test_sub_policy {
|
|
||||||
x := get_allowed_users({
|
|
||||||
"source": "example.com",
|
|
||||||
"allowed_users": ["u1", "u2"],
|
|
||||||
"sub_policies": [
|
|
||||||
{"allowed_users": ["u1", "u3"]},
|
|
||||||
{"allowed_users": ["u2", "u4"]},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
x == {"u1", "u2", "u3", "u4"}
|
|
||||||
|
|
||||||
y := get_allowed_domains({
|
|
||||||
"source": "example.com",
|
|
||||||
"allowed_domains": ["d1", "d2"],
|
|
||||||
"sub_policies": [
|
|
||||||
{"allowed_domains": ["d1", "d3"]},
|
|
||||||
{"allowed_domains": ["d2", "d4"]},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
y == {"d1", "d2", "d3", "d4"}
|
|
||||||
|
|
||||||
z := get_allowed_groups({
|
|
||||||
"source": "example.com",
|
|
||||||
"allowed_groups": ["g1", "g2"],
|
|
||||||
"sub_policies": [
|
|
||||||
{"allowed_groups": ["g1", "g3"]},
|
|
||||||
{"allowed_groups": ["g2", "g4"]},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
z == {"g1", "g2", "g3", "g4"}
|
|
||||||
}
|
|
||||||
|
|
||||||
test_are_claims_allowed {
|
|
||||||
are_claims_allowed({"a": ["1"]}, {"a": ["1"]})
|
|
||||||
not are_claims_allowed({"a": ["2"]}, {"a": ["1"]})
|
|
||||||
|
|
||||||
are_claims_allowed({"a": ["1", "2", "3"]}, {"a": ["1"]})
|
|
||||||
are_claims_allowed({"a": ["1"]}, {"a": ["1", "2", "3"]})
|
|
||||||
not are_claims_allowed({"a": ["4", "5", "6"]}, {"a": ["1"]})
|
|
||||||
|
|
||||||
are_claims_allowed({"a.b.c": ["1"], "d.e.f": ["2"]}, {"d.e.f": ["2"]})
|
|
||||||
}
|
|
||||||
|
|
||||||
test_any_authenticated_user_allowed {
|
|
||||||
allow with data.route_policies as [{
|
|
||||||
"source": "example.com",
|
|
||||||
"AllowAnyAuthenticatedUser": true,
|
|
||||||
}]
|
|
||||||
with data.databroker_data as {"type.googleapis.com": {
|
|
||||||
"session.Session": {"session1": {"id": "session1", "user_id": "user1"}},
|
|
||||||
"user.User": {"user1": {"id": "user1", "email": "x@example.com"}},
|
|
||||||
}}
|
|
||||||
with input.http as {"url": "http://example.com"}
|
|
||||||
with input.session as {"id": "session1"}
|
|
||||||
}
|
|
||||||
|
|
||||||
test_any_authenticated_user_denied {
|
|
||||||
not allow with data.route_policies as [{
|
|
||||||
"source": "example.com",
|
|
||||||
"AllowAnyAuthenticatedUser": true,
|
|
||||||
}]
|
|
||||||
with input.http as {"url": "http://example.com"}
|
|
||||||
with input.session as {"id": "session1"}
|
|
||||||
}
|
|
247
authorize/evaluator/opa/policy/headers.rego
Normal file
247
authorize/evaluator/opa/policy/headers.rego
Normal file
|
@ -0,0 +1,247 @@
|
||||||
|
package pomerium.headers
|
||||||
|
|
||||||
|
# input:
|
||||||
|
# enable_google_cloud_serverless_authentication: boolean
|
||||||
|
# from_audience: string
|
||||||
|
# kubernetes_service_account_token: string
|
||||||
|
# session:
|
||||||
|
# id: string
|
||||||
|
# to_audience: string
|
||||||
|
#
|
||||||
|
# data:
|
||||||
|
# issuer: string
|
||||||
|
# jwt_claim_headers: map[string]string
|
||||||
|
# signing_key:
|
||||||
|
# alg: string
|
||||||
|
# kid: string
|
||||||
|
#
|
||||||
|
# functions:
|
||||||
|
# get_databroker_record
|
||||||
|
# get_google_cloud_serverless_headers
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# output:
|
||||||
|
# identity_headers: map[string][]string
|
||||||
|
|
||||||
|
# 5 minutes from now in seconds
|
||||||
|
five_minutes := (time.now_ns() / 1e9) + (60 * 5)
|
||||||
|
|
||||||
|
session = s {
|
||||||
|
s = get_databroker_record("type.googleapis.com/user.ServiceAccount", input.session.id)
|
||||||
|
s != null
|
||||||
|
} else = s {
|
||||||
|
s = get_databroker_record("type.googleapis.com/session.Session", input.session.id)
|
||||||
|
s != null
|
||||||
|
} else = {} {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
user = u {
|
||||||
|
u = get_databroker_record("type.googleapis.com/user.User", session.impersonate_user_id)
|
||||||
|
u != null
|
||||||
|
} else = u {
|
||||||
|
u = get_databroker_record("type.googleapis.com/user.User", session.user_id)
|
||||||
|
u != null
|
||||||
|
} else = {} {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
directory_user = du {
|
||||||
|
du = get_databroker_record("type.googleapis.com/directory.User", session.impersonate_user_id)
|
||||||
|
du != null
|
||||||
|
} else = du {
|
||||||
|
du = get_databroker_record("type.googleapis.com/directory.User", session.user_id)
|
||||||
|
du != null
|
||||||
|
} else = {} {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
group_ids = gs {
|
||||||
|
gs = session.impersonate_groups
|
||||||
|
gs != null
|
||||||
|
} else = gs {
|
||||||
|
gs = directory_user.group_ids
|
||||||
|
gs != null
|
||||||
|
} else = [] {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
groups := array.concat(group_ids, array.concat(get_databroker_group_names(group_ids), get_databroker_group_emails(group_ids)))
|
||||||
|
|
||||||
|
jwt_headers = {
|
||||||
|
"typ": "JWT",
|
||||||
|
"alg": data.signing_key.alg,
|
||||||
|
"kid": data.signing_key.kid,
|
||||||
|
}
|
||||||
|
|
||||||
|
jwt_payload_aud = v {
|
||||||
|
v := input.from_audience
|
||||||
|
} else = "" {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
jwt_payload_iss = data.issuer
|
||||||
|
|
||||||
|
jwt_payload_jti = v {
|
||||||
|
v = session.id
|
||||||
|
} else = "" {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
jwt_payload_exp = v {
|
||||||
|
v = min([five_minutes, session.expires_at.seconds])
|
||||||
|
} else = v {
|
||||||
|
v = five_minutes
|
||||||
|
} else = null {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
jwt_payload_iat = v {
|
||||||
|
# sessions store the issued_at on the id_token
|
||||||
|
v = session.id_token.issued_at.seconds
|
||||||
|
} else = v {
|
||||||
|
# service accounts store the issued at directly
|
||||||
|
v = session.issued_at.seconds
|
||||||
|
} else = null {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
jwt_payload_sub = v {
|
||||||
|
v = user.id
|
||||||
|
} else = "" {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
jwt_payload_user = v {
|
||||||
|
v = user.id
|
||||||
|
} else = "" {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
jwt_payload_email = v {
|
||||||
|
v = session.impersonate_email
|
||||||
|
} else = v {
|
||||||
|
v = directory_user.email
|
||||||
|
} else = v {
|
||||||
|
v = user.email
|
||||||
|
} else = "" {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
jwt_payload_groups = v {
|
||||||
|
v = array.concat(group_ids, get_databroker_group_names(group_ids))
|
||||||
|
v != []
|
||||||
|
} else = v {
|
||||||
|
v = session.claims.groups
|
||||||
|
v != null
|
||||||
|
} else = [] {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
base_jwt_claims := [
|
||||||
|
["iss", jwt_payload_iss],
|
||||||
|
["aud", jwt_payload_aud],
|
||||||
|
["jti", jwt_payload_jti],
|
||||||
|
["exp", jwt_payload_exp],
|
||||||
|
["iat", jwt_payload_iat],
|
||||||
|
["sub", jwt_payload_sub],
|
||||||
|
["user", jwt_payload_user],
|
||||||
|
["email", jwt_payload_email],
|
||||||
|
["groups", jwt_payload_groups],
|
||||||
|
]
|
||||||
|
|
||||||
|
additional_jwt_claims := [[k, v] |
|
||||||
|
some header_name
|
||||||
|
claim_key := data.jwt_claim_headers[header_name]
|
||||||
|
|
||||||
|
# exclude base_jwt_claims
|
||||||
|
count([1 |
|
||||||
|
[xk, xv] := base_jwt_claims[_]
|
||||||
|
xk == claim_key
|
||||||
|
]) == 0
|
||||||
|
|
||||||
|
# the claim value can come from session claims or user claims
|
||||||
|
claim_value := object.get(session.claims, claim_key, object.get(user.claims, claim_key, null))
|
||||||
|
|
||||||
|
k := claim_key
|
||||||
|
v := get_header_string_value(claim_value)
|
||||||
|
]
|
||||||
|
|
||||||
|
jwt_claims := array.concat(base_jwt_claims, additional_jwt_claims)
|
||||||
|
|
||||||
|
jwt_payload = {key: value |
|
||||||
|
# use a comprehension over an array to remove nil values
|
||||||
|
[key, value] := jwt_claims[_]
|
||||||
|
value != null
|
||||||
|
}
|
||||||
|
|
||||||
|
signed_jwt = io.jwt.encode_sign(jwt_headers, jwt_payload, data.signing_key)
|
||||||
|
|
||||||
|
kubernetes_headers = h {
|
||||||
|
input.kubernetes_service_account_token != ""
|
||||||
|
h := [
|
||||||
|
["Authorization", concat(" ", ["Bearer", input.kubernetes_service_account_token])],
|
||||||
|
["Impersonate-User", jwt_payload_email],
|
||||||
|
["Impersonate-Group", get_header_string_value(jwt_payload_groups)],
|
||||||
|
]
|
||||||
|
} else = [] {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
google_cloud_serverless_authentication_service_account = s {
|
||||||
|
s := data.google_cloud_serverless_authentication_service_account
|
||||||
|
} else = "" {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
google_cloud_serverless_headers = h {
|
||||||
|
input.enable_google_cloud_serverless_authentication
|
||||||
|
h := get_google_cloud_serverless_headers(google_cloud_serverless_authentication_service_account, input.to_audience)
|
||||||
|
} else = {} {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
identity_headers := {key: values |
|
||||||
|
h1 := [["x-pomerium-jwt-assertion", signed_jwt]]
|
||||||
|
h2 := [[k, v] |
|
||||||
|
[claim_key, claim_value] := jwt_claims[_]
|
||||||
|
claim_value != null
|
||||||
|
|
||||||
|
# only include those headers requested by the user
|
||||||
|
some header_name
|
||||||
|
available := data.jwt_claim_headers[header_name]
|
||||||
|
available == claim_key
|
||||||
|
|
||||||
|
# create the header key and value
|
||||||
|
k := header_name
|
||||||
|
v := get_header_string_value(claim_value)
|
||||||
|
]
|
||||||
|
|
||||||
|
h3 := kubernetes_headers
|
||||||
|
h4 := [[k, v] | v := google_cloud_serverless_headers[k]]
|
||||||
|
|
||||||
|
h := array.concat(array.concat(array.concat(h1, h2), h3), h4)
|
||||||
|
|
||||||
|
some i
|
||||||
|
[key, v1] := h[i]
|
||||||
|
values := [ v2 |
|
||||||
|
some j
|
||||||
|
[k2, v2] := h[j]
|
||||||
|
key == k2
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
get_databroker_group_names(ids) = gs {
|
||||||
|
gs := [name | id := ids[i]; group := get_databroker_record("type.googleapis.com/directory.Group", id); name := group.name]
|
||||||
|
}
|
||||||
|
|
||||||
|
get_databroker_group_emails(ids) = gs {
|
||||||
|
gs := [email | id := ids[i]; group := get_databroker_record("type.googleapis.com/directory.Group", id); email := group.email]
|
||||||
|
}
|
||||||
|
|
||||||
|
get_header_string_value(obj) = s {
|
||||||
|
is_array(obj)
|
||||||
|
s := concat(",", obj)
|
||||||
|
} else = s {
|
||||||
|
s := concat(",", [obj])
|
||||||
|
}
|
|
@ -1,747 +0,0 @@
|
||||||
package evaluator
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"math"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/open-policy-agent/opa/rego"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"google.golang.org/protobuf/proto"
|
|
||||||
"google.golang.org/protobuf/types/known/structpb"
|
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
|
||||||
"google.golang.org/protobuf/types/known/wrapperspb"
|
|
||||||
"gopkg.in/square/go-jose.v2"
|
|
||||||
"gopkg.in/square/go-jose.v2/jwt"
|
|
||||||
|
|
||||||
"github.com/pomerium/pomerium/config"
|
|
||||||
"github.com/pomerium/pomerium/pkg/cryptutil"
|
|
||||||
"github.com/pomerium/pomerium/pkg/grpc/directory"
|
|
||||||
"github.com/pomerium/pomerium/pkg/grpc/session"
|
|
||||||
"github.com/pomerium/pomerium/pkg/grpc/user"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestOPA(t *testing.T) {
|
|
||||||
type A = []interface{}
|
|
||||||
type M = map[string]interface{}
|
|
||||||
|
|
||||||
signingKey, err := cryptutil.NewSigningKey()
|
|
||||||
require.NoError(t, err)
|
|
||||||
encodedSigningKey, err := cryptutil.EncodePrivateKey(signingKey)
|
|
||||||
require.NoError(t, err)
|
|
||||||
privateJWK, err := cryptutil.PrivateJWKFromBytes(encodedSigningKey, jose.ES256)
|
|
||||||
require.NoError(t, err)
|
|
||||||
publicJWK, err := cryptutil.PublicJWKFromBytes(encodedSigningKey, jose.ES256)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
eval := func(t *testing.T, policies []config.Policy, data []proto.Message, req *Request, isValidClientCertificate bool) rego.Result {
|
|
||||||
authzPolicy, err := readPolicy()
|
|
||||||
require.NoError(t, err)
|
|
||||||
store := NewStoreFromProtos(math.MaxUint64, data...)
|
|
||||||
store.UpdateIssuer("authenticate.example.com")
|
|
||||||
store.UpdateJWTClaimHeaders(config.NewJWTClaimHeaders("email", "groups", "user", "CUSTOM_KEY"))
|
|
||||||
store.UpdateRoutePolicies(policies)
|
|
||||||
store.UpdateSigningKey(privateJWK)
|
|
||||||
r := rego.New(
|
|
||||||
rego.Store(store),
|
|
||||||
rego.Module("pomerium.authz", string(authzPolicy)),
|
|
||||||
rego.Query("result = data.pomerium.authz"),
|
|
||||||
getGoogleCloudServerlessHeadersRegoOption,
|
|
||||||
store.GetDataBrokerRecordOption(),
|
|
||||||
)
|
|
||||||
q, err := r.PrepareForEval(context.Background())
|
|
||||||
require.NoError(t, err)
|
|
||||||
rs, err := q.Eval(context.Background(),
|
|
||||||
rego.EvalInput((&Evaluator{store: store}).newInput(req, isValidClientCertificate)),
|
|
||||||
)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Len(t, rs, 1)
|
|
||||||
return rs[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("client certificate", func(t *testing.T) {
|
|
||||||
res := eval(t, nil, nil, &Request{}, false)
|
|
||||||
assert.Equal(t,
|
|
||||||
A{A{json.Number("495"), "invalid client certificate"}},
|
|
||||||
res.Bindings["result"].(M)["deny"])
|
|
||||||
})
|
|
||||||
t.Run("identity_headers", func(t *testing.T) {
|
|
||||||
t.Run("kubernetes", func(t *testing.T) {
|
|
||||||
res := eval(t, []config.Policy{{
|
|
||||||
Source: &config.StringURL{URL: mustParseURL("https://from.example.com")},
|
|
||||||
To: config.WeightedURLs{
|
|
||||||
{URL: *mustParseURL("https://to.example.com")},
|
|
||||||
},
|
|
||||||
KubernetesServiceAccountToken: "KUBERNETES",
|
|
||||||
}}, []proto.Message{
|
|
||||||
&session.Session{
|
|
||||||
Id: "session1",
|
|
||||||
UserId: "user1",
|
|
||||||
ImpersonateGroups: []string{"i1", "i2"},
|
|
||||||
},
|
|
||||||
&user.User{
|
|
||||||
Id: "user1",
|
|
||||||
Email: "a@example.com",
|
|
||||||
},
|
|
||||||
}, &Request{
|
|
||||||
Session: RequestSession{
|
|
||||||
ID: "session1",
|
|
||||||
},
|
|
||||||
HTTP: RequestHTTP{
|
|
||||||
Method: "GET",
|
|
||||||
URL: "https://from.example.com",
|
|
||||||
},
|
|
||||||
}, true)
|
|
||||||
headers := res.Bindings["result"].(M)["identity_headers"].(M)
|
|
||||||
assert.NotEmpty(t, headers["Authorization"])
|
|
||||||
assert.Equal(t, "a@example.com", headers["Impersonate-User"])
|
|
||||||
assert.Equal(t, "i1,i2", headers["Impersonate-Group"])
|
|
||||||
})
|
|
||||||
t.Run("google_cloud_serverless", func(t *testing.T) {
|
|
||||||
withMockGCP(t, func() {
|
|
||||||
res := eval(t, []config.Policy{{
|
|
||||||
Source: &config.StringURL{URL: mustParseURL("https://from.example.com")},
|
|
||||||
To: config.WeightedURLs{
|
|
||||||
{URL: *mustParseURL("https://to.example.com")},
|
|
||||||
},
|
|
||||||
EnableGoogleCloudServerlessAuthentication: true,
|
|
||||||
}}, []proto.Message{
|
|
||||||
&session.Session{
|
|
||||||
Id: "session1",
|
|
||||||
UserId: "user1",
|
|
||||||
ImpersonateGroups: []string{"i1", "i2"},
|
|
||||||
},
|
|
||||||
&user.User{
|
|
||||||
Id: "user1",
|
|
||||||
Email: "a@example.com",
|
|
||||||
},
|
|
||||||
}, &Request{
|
|
||||||
Session: RequestSession{
|
|
||||||
ID: "session1",
|
|
||||||
},
|
|
||||||
HTTP: RequestHTTP{
|
|
||||||
Method: "GET",
|
|
||||||
URL: "https://from.example.com",
|
|
||||||
},
|
|
||||||
}, true)
|
|
||||||
headers := res.Bindings["result"].(M)["identity_headers"].(M)
|
|
||||||
assert.NotEmpty(t, headers["Authorization"])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
t.Run("jwt", func(t *testing.T) {
|
|
||||||
evalJWT := func(msgs ...proto.Message) M {
|
|
||||||
res := eval(t, []config.Policy{{
|
|
||||||
Source: &config.StringURL{URL: mustParseURL("https://from.example.com:8000")},
|
|
||||||
To: config.WeightedURLs{
|
|
||||||
{URL: *mustParseURL("https://to.example.com")},
|
|
||||||
},
|
|
||||||
}}, msgs, &Request{
|
|
||||||
Session: RequestSession{
|
|
||||||
ID: "session1",
|
|
||||||
},
|
|
||||||
HTTP: RequestHTTP{
|
|
||||||
Method: "GET",
|
|
||||||
URL: "https://from.example.com:8000",
|
|
||||||
},
|
|
||||||
}, true)
|
|
||||||
signedCompactJWTStr := res.Bindings["result"].(M)["signed_jwt"].(string)
|
|
||||||
authJWT, err := jwt.ParseSigned(signedCompactJWTStr)
|
|
||||||
require.NoError(t, err)
|
|
||||||
var claims M
|
|
||||||
err = authJWT.Claims(publicJWK, &claims)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.LessOrEqual(t, claims["exp"], float64(time.Now().Add(time.Minute*6).Unix()),
|
|
||||||
"JWT should expire within 5 minutes, but got: %v", claims["exp"])
|
|
||||||
return claims
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("impersonate groups", func(t *testing.T) {
|
|
||||||
payload := evalJWT(
|
|
||||||
&session.Session{
|
|
||||||
Id: "session1",
|
|
||||||
UserId: "user1",
|
|
||||||
ImpersonateGroups: []string{"i1", "i2"},
|
|
||||||
},
|
|
||||||
&user.User{
|
|
||||||
Id: "user1",
|
|
||||||
Email: "a@example.com",
|
|
||||||
},
|
|
||||||
&directory.User{
|
|
||||||
Id: "user1",
|
|
||||||
GroupIds: []string{"group1"},
|
|
||||||
},
|
|
||||||
&directory.Group{
|
|
||||||
Id: "group1",
|
|
||||||
Name: "group1name",
|
|
||||||
Email: "group1@example.com",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
delete(payload, "exp")
|
|
||||||
assert.Equal(t, M{
|
|
||||||
"aud": "from.example.com",
|
|
||||||
"iss": "authenticate.example.com",
|
|
||||||
"jti": "session1",
|
|
||||||
"sub": "user1",
|
|
||||||
"user": "user1",
|
|
||||||
"email": "a@example.com",
|
|
||||||
"groups": []interface{}{"i1", "i2"},
|
|
||||||
}, payload)
|
|
||||||
})
|
|
||||||
t.Run("directory", func(t *testing.T) {
|
|
||||||
payload := evalJWT(
|
|
||||||
&session.Session{
|
|
||||||
Id: "session1",
|
|
||||||
UserId: "user1",
|
|
||||||
ExpiresAt: timestamppb.New(time.Date(2021, 1, 1, 1, 1, 1, 1, time.UTC)),
|
|
||||||
IdToken: &session.IDToken{
|
|
||||||
IssuedAt: timestamppb.New(time.Date(2021, 2, 1, 1, 1, 1, 1, time.UTC)),
|
|
||||||
},
|
|
||||||
Claims: map[string]*structpb.ListValue{
|
|
||||||
"CUSTOM_KEY": {
|
|
||||||
Values: []*structpb.Value{
|
|
||||||
structpb.NewStringValue("FROM_SESSION"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"email": {
|
|
||||||
Values: []*structpb.Value{
|
|
||||||
structpb.NewStringValue("value"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
&user.User{
|
|
||||||
Id: "user1",
|
|
||||||
Email: "a@example.com",
|
|
||||||
Claims: map[string]*structpb.ListValue{
|
|
||||||
"CUSTOM_KEY": {
|
|
||||||
Values: []*structpb.Value{
|
|
||||||
structpb.NewStringValue("FROM_USER"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
&directory.User{
|
|
||||||
Id: "user1",
|
|
||||||
GroupIds: []string{"group1"},
|
|
||||||
},
|
|
||||||
&directory.Group{
|
|
||||||
Id: "group1",
|
|
||||||
Name: "group1name",
|
|
||||||
Email: "group1@example.com",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
assert.Equal(t, M{
|
|
||||||
"aud": "from.example.com",
|
|
||||||
"iss": "authenticate.example.com",
|
|
||||||
"jti": "session1",
|
|
||||||
"iat": 1612141261.0,
|
|
||||||
"exp": 1609462861.0,
|
|
||||||
"sub": "user1",
|
|
||||||
"user": "user1",
|
|
||||||
"email": "a@example.com",
|
|
||||||
"groups": A{"group1", "group1name"},
|
|
||||||
"CUSTOM_KEY": "FROM_SESSION",
|
|
||||||
}, payload)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
t.Run("email", func(t *testing.T) {
|
|
||||||
t.Run("allowed", func(t *testing.T) {
|
|
||||||
res := eval(t, []config.Policy{
|
|
||||||
{
|
|
||||||
Source: &config.StringURL{URL: mustParseURL("https://from.example.com:8000")},
|
|
||||||
To: config.WeightedURLs{
|
|
||||||
{URL: *mustParseURL("https://to.example.com")},
|
|
||||||
},
|
|
||||||
AllowedUsers: []string{"a@example.com"},
|
|
||||||
},
|
|
||||||
}, []proto.Message{
|
|
||||||
&session.Session{
|
|
||||||
Id: "session1",
|
|
||||||
UserId: "user1",
|
|
||||||
},
|
|
||||||
&user.User{
|
|
||||||
Id: "user1",
|
|
||||||
Email: "a@example.com",
|
|
||||||
},
|
|
||||||
}, &Request{
|
|
||||||
Session: RequestSession{
|
|
||||||
ID: "session1",
|
|
||||||
},
|
|
||||||
HTTP: RequestHTTP{
|
|
||||||
Method: "GET",
|
|
||||||
URL: "https://from.example.com:8000",
|
|
||||||
},
|
|
||||||
}, true)
|
|
||||||
assert.True(t, res.Bindings["result"].(M)["allow"].(bool))
|
|
||||||
})
|
|
||||||
t.Run("allowed sub", func(t *testing.T) {
|
|
||||||
res := eval(t, []config.Policy{
|
|
||||||
{
|
|
||||||
Source: &config.StringURL{URL: mustParseURL("https://from.example.com:8000")},
|
|
||||||
To: config.WeightedURLs{
|
|
||||||
{URL: *mustParseURL("https://to.example.com")},
|
|
||||||
},
|
|
||||||
SubPolicies: []config.SubPolicy{
|
|
||||||
{
|
|
||||||
AllowedUsers: []string{"a@example.com"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, []proto.Message{
|
|
||||||
&session.Session{
|
|
||||||
Id: "session1",
|
|
||||||
UserId: "user1",
|
|
||||||
},
|
|
||||||
&user.User{
|
|
||||||
Id: "user1",
|
|
||||||
Email: "a@example.com",
|
|
||||||
},
|
|
||||||
}, &Request{
|
|
||||||
Session: RequestSession{
|
|
||||||
ID: "session1",
|
|
||||||
},
|
|
||||||
HTTP: RequestHTTP{
|
|
||||||
Method: "GET",
|
|
||||||
URL: "https://from.example.com:8000",
|
|
||||||
},
|
|
||||||
}, true)
|
|
||||||
assert.True(t, res.Bindings["result"].(M)["allow"].(bool))
|
|
||||||
})
|
|
||||||
t.Run("denied", func(t *testing.T) {
|
|
||||||
res := eval(t, []config.Policy{
|
|
||||||
{
|
|
||||||
Source: &config.StringURL{URL: mustParseURL("https://from.example.com")},
|
|
||||||
To: config.WeightedURLs{
|
|
||||||
{URL: *mustParseURL("https://to.example.com")},
|
|
||||||
},
|
|
||||||
AllowedUsers: []string{"a@example.com"},
|
|
||||||
},
|
|
||||||
}, []proto.Message{
|
|
||||||
&session.Session{
|
|
||||||
Id: "session1",
|
|
||||||
UserId: "user1",
|
|
||||||
},
|
|
||||||
&user.User{
|
|
||||||
Id: "user1",
|
|
||||||
Email: "b@example.com",
|
|
||||||
},
|
|
||||||
}, &Request{
|
|
||||||
Session: RequestSession{
|
|
||||||
ID: "session1",
|
|
||||||
},
|
|
||||||
HTTP: RequestHTTP{
|
|
||||||
Method: "GET",
|
|
||||||
URL: "https://from.example.com",
|
|
||||||
},
|
|
||||||
}, true)
|
|
||||||
assert.False(t, res.Bindings["result"].(M)["allow"].(bool))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
t.Run("impersonate email", func(t *testing.T) {
|
|
||||||
t.Run("allowed", func(t *testing.T) {
|
|
||||||
res := eval(t, []config.Policy{
|
|
||||||
{
|
|
||||||
Source: &config.StringURL{URL: mustParseURL("https://from.example.com")},
|
|
||||||
To: config.WeightedURLs{
|
|
||||||
{URL: *mustParseURL("https://to.example.com")},
|
|
||||||
},
|
|
||||||
AllowedUsers: []string{"b@example.com"},
|
|
||||||
},
|
|
||||||
}, []proto.Message{
|
|
||||||
&user.ServiceAccount{
|
|
||||||
Id: "session1",
|
|
||||||
UserId: "user1",
|
|
||||||
ImpersonateEmail: proto.String("b@example.com"),
|
|
||||||
},
|
|
||||||
&user.User{
|
|
||||||
Id: "user1",
|
|
||||||
Email: "a@example.com",
|
|
||||||
},
|
|
||||||
}, &Request{
|
|
||||||
Session: RequestSession{
|
|
||||||
ID: "session1",
|
|
||||||
},
|
|
||||||
HTTP: RequestHTTP{
|
|
||||||
Method: "GET",
|
|
||||||
URL: "https://from.example.com",
|
|
||||||
},
|
|
||||||
}, true)
|
|
||||||
assert.True(t, res.Bindings["result"].(M)["allow"].(bool))
|
|
||||||
})
|
|
||||||
t.Run("denied", func(t *testing.T) {
|
|
||||||
res := eval(t, []config.Policy{
|
|
||||||
{
|
|
||||||
Source: &config.StringURL{URL: mustParseURL("https://from.example.com")},
|
|
||||||
To: config.WeightedURLs{
|
|
||||||
{URL: *mustParseURL("https://to.example.com")},
|
|
||||||
},
|
|
||||||
AllowedUsers: []string{"a@example.com"},
|
|
||||||
},
|
|
||||||
}, []proto.Message{
|
|
||||||
&session.Session{
|
|
||||||
Id: "session1",
|
|
||||||
UserId: "user1",
|
|
||||||
ImpersonateEmail: proto.String("b@example.com"),
|
|
||||||
},
|
|
||||||
&user.User{
|
|
||||||
Id: "user1",
|
|
||||||
Email: "a@example.com",
|
|
||||||
},
|
|
||||||
}, &Request{
|
|
||||||
Session: RequestSession{
|
|
||||||
ID: "session1",
|
|
||||||
},
|
|
||||||
HTTP: RequestHTTP{
|
|
||||||
Method: "GET",
|
|
||||||
URL: "https://from.example.com",
|
|
||||||
},
|
|
||||||
}, true)
|
|
||||||
assert.False(t, res.Bindings["result"].(M)["allow"].(bool))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
t.Run("user_id", func(t *testing.T) {
|
|
||||||
res := eval(t, []config.Policy{
|
|
||||||
{
|
|
||||||
Source: &config.StringURL{URL: mustParseURL("https://from.example.com")},
|
|
||||||
To: config.WeightedURLs{
|
|
||||||
{URL: *mustParseURL("https://to.example.com")},
|
|
||||||
},
|
|
||||||
AllowedUsers: []string{"example/1234"},
|
|
||||||
},
|
|
||||||
}, []proto.Message{
|
|
||||||
&session.Session{
|
|
||||||
Id: "session1",
|
|
||||||
UserId: "example/1234",
|
|
||||||
},
|
|
||||||
&user.User{
|
|
||||||
Id: "example/1234",
|
|
||||||
Email: "a@example.com",
|
|
||||||
},
|
|
||||||
}, &Request{
|
|
||||||
Session: RequestSession{
|
|
||||||
ID: "session1",
|
|
||||||
},
|
|
||||||
HTTP: RequestHTTP{
|
|
||||||
Method: "GET",
|
|
||||||
URL: "https://from.example.com",
|
|
||||||
},
|
|
||||||
}, true)
|
|
||||||
assert.True(t, res.Bindings["result"].(M)["allow"].(bool))
|
|
||||||
})
|
|
||||||
t.Run("domain", func(t *testing.T) {
|
|
||||||
t.Run("allowed", func(t *testing.T) {
|
|
||||||
res := eval(t, []config.Policy{
|
|
||||||
{
|
|
||||||
Source: &config.StringURL{URL: mustParseURL("https://from.example.com")},
|
|
||||||
To: config.WeightedURLs{
|
|
||||||
{URL: *mustParseURL("https://to.example.com")},
|
|
||||||
},
|
|
||||||
AllowedDomains: []string{"example.com"},
|
|
||||||
},
|
|
||||||
}, []proto.Message{
|
|
||||||
&user.ServiceAccount{Id: "serviceaccount1"},
|
|
||||||
&session.Session{
|
|
||||||
Id: "session1",
|
|
||||||
UserId: "example/user1",
|
|
||||||
},
|
|
||||||
&user.User{
|
|
||||||
Id: "example/user1",
|
|
||||||
Email: "a@example.com",
|
|
||||||
},
|
|
||||||
}, &Request{
|
|
||||||
Session: RequestSession{
|
|
||||||
ID: "session1",
|
|
||||||
},
|
|
||||||
HTTP: RequestHTTP{
|
|
||||||
Method: "GET",
|
|
||||||
URL: "https://from.example.com",
|
|
||||||
},
|
|
||||||
}, true)
|
|
||||||
assert.True(t, res.Bindings["result"].(M)["allow"].(bool))
|
|
||||||
})
|
|
||||||
t.Run("allowed sub", func(t *testing.T) {
|
|
||||||
res := eval(t, []config.Policy{
|
|
||||||
{
|
|
||||||
Source: &config.StringURL{URL: mustParseURL("https://from.example.com")},
|
|
||||||
To: config.WeightedURLs{
|
|
||||||
{URL: *mustParseURL("https://to.example.com")},
|
|
||||||
},
|
|
||||||
SubPolicies: []config.SubPolicy{
|
|
||||||
{
|
|
||||||
AllowedDomains: []string{"example.com"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, []proto.Message{
|
|
||||||
&user.ServiceAccount{Id: "serviceaccount1"},
|
|
||||||
&session.Session{
|
|
||||||
Id: "session1",
|
|
||||||
UserId: "example/user1",
|
|
||||||
},
|
|
||||||
&user.User{
|
|
||||||
Id: "example/user1",
|
|
||||||
Email: "a@example.com",
|
|
||||||
},
|
|
||||||
}, &Request{
|
|
||||||
Session: RequestSession{
|
|
||||||
ID: "session1",
|
|
||||||
},
|
|
||||||
HTTP: RequestHTTP{
|
|
||||||
Method: "GET",
|
|
||||||
URL: "https://from.example.com",
|
|
||||||
},
|
|
||||||
}, true)
|
|
||||||
assert.True(t, res.Bindings["result"].(M)["allow"].(bool))
|
|
||||||
})
|
|
||||||
t.Run("denied", func(t *testing.T) {
|
|
||||||
res := eval(t, []config.Policy{
|
|
||||||
{
|
|
||||||
Source: &config.StringURL{URL: mustParseURL("https://from.example.com")},
|
|
||||||
To: config.WeightedURLs{
|
|
||||||
{URL: *mustParseURL("https://to.example.com")},
|
|
||||||
},
|
|
||||||
AllowedDomains: []string{"notexample.com"},
|
|
||||||
},
|
|
||||||
}, []proto.Message{
|
|
||||||
&session.Session{
|
|
||||||
Id: "session1",
|
|
||||||
UserId: "user1",
|
|
||||||
},
|
|
||||||
&user.User{
|
|
||||||
Id: "user1",
|
|
||||||
Email: "a@example.com",
|
|
||||||
},
|
|
||||||
}, &Request{
|
|
||||||
Session: RequestSession{
|
|
||||||
ID: "session1",
|
|
||||||
},
|
|
||||||
HTTP: RequestHTTP{
|
|
||||||
Method: "GET",
|
|
||||||
URL: "https://from.example.com",
|
|
||||||
},
|
|
||||||
}, true)
|
|
||||||
assert.False(t, res.Bindings["result"].(M)["allow"].(bool))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
t.Run("impersonate domain", func(t *testing.T) {
|
|
||||||
t.Run("allowed", func(t *testing.T) {
|
|
||||||
res := eval(t, []config.Policy{
|
|
||||||
{
|
|
||||||
Source: &config.StringURL{URL: mustParseURL("https://from.example.com")},
|
|
||||||
To: config.WeightedURLs{
|
|
||||||
{URL: *mustParseURL("https://to.example.com")},
|
|
||||||
},
|
|
||||||
AllowedDomains: []string{"example.com"},
|
|
||||||
},
|
|
||||||
}, []proto.Message{
|
|
||||||
&session.Session{
|
|
||||||
Id: "session1",
|
|
||||||
UserId: "user1",
|
|
||||||
ImpersonateEmail: proto.String("a@example.com"),
|
|
||||||
},
|
|
||||||
&user.User{
|
|
||||||
Id: "user1",
|
|
||||||
Email: "a@notexample.com",
|
|
||||||
},
|
|
||||||
}, &Request{
|
|
||||||
Session: RequestSession{
|
|
||||||
ID: "session1",
|
|
||||||
},
|
|
||||||
HTTP: RequestHTTP{
|
|
||||||
Method: "GET",
|
|
||||||
URL: "https://from.example.com",
|
|
||||||
},
|
|
||||||
}, true)
|
|
||||||
assert.True(t, res.Bindings["result"].(M)["allow"].(bool))
|
|
||||||
})
|
|
||||||
t.Run("denied", func(t *testing.T) {
|
|
||||||
res := eval(t, []config.Policy{
|
|
||||||
{
|
|
||||||
Source: &config.StringURL{URL: mustParseURL("https://from.example.com")},
|
|
||||||
To: config.WeightedURLs{
|
|
||||||
{URL: *mustParseURL("https://to.example.com")},
|
|
||||||
},
|
|
||||||
AllowedDomains: []string{"example.com"},
|
|
||||||
},
|
|
||||||
}, []proto.Message{
|
|
||||||
&session.Session{
|
|
||||||
Id: "session1",
|
|
||||||
UserId: "user1",
|
|
||||||
ImpersonateEmail: proto.String("a@notexample.com"),
|
|
||||||
},
|
|
||||||
&user.User{
|
|
||||||
Id: "user1",
|
|
||||||
Email: "a@example.com",
|
|
||||||
},
|
|
||||||
}, &Request{
|
|
||||||
Session: RequestSession{
|
|
||||||
ID: "session1",
|
|
||||||
},
|
|
||||||
HTTP: RequestHTTP{
|
|
||||||
Method: "GET",
|
|
||||||
URL: "https://from.example.com",
|
|
||||||
},
|
|
||||||
}, true)
|
|
||||||
assert.False(t, res.Bindings["result"].(M)["allow"].(bool))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
t.Run("groups", func(t *testing.T) {
|
|
||||||
t.Run("allowed", func(t *testing.T) {
|
|
||||||
for _, nm := range []string{"group1", "group1name", "group1@example.com"} {
|
|
||||||
t.Run(nm, func(t *testing.T) {
|
|
||||||
res := eval(t, []config.Policy{
|
|
||||||
{
|
|
||||||
Source: &config.StringURL{URL: mustParseURL("https://from.example.com")},
|
|
||||||
To: config.WeightedURLs{
|
|
||||||
{URL: *mustParseURL("https://to.example.com")},
|
|
||||||
},
|
|
||||||
AllowedGroups: []string{nm},
|
|
||||||
},
|
|
||||||
}, []proto.Message{
|
|
||||||
&session.Session{
|
|
||||||
Id: "session1",
|
|
||||||
UserId: "user1",
|
|
||||||
},
|
|
||||||
&user.User{
|
|
||||||
Id: "user1",
|
|
||||||
Email: "a@example.com",
|
|
||||||
},
|
|
||||||
&directory.User{
|
|
||||||
Id: "user1",
|
|
||||||
GroupIds: []string{"group1"},
|
|
||||||
},
|
|
||||||
&directory.Group{
|
|
||||||
Id: "group1",
|
|
||||||
Name: "group1name",
|
|
||||||
Email: "group1@example.com",
|
|
||||||
},
|
|
||||||
}, &Request{
|
|
||||||
Session: RequestSession{
|
|
||||||
ID: "session1",
|
|
||||||
},
|
|
||||||
HTTP: RequestHTTP{
|
|
||||||
Method: "GET",
|
|
||||||
URL: "https://from.example.com",
|
|
||||||
},
|
|
||||||
}, true)
|
|
||||||
assert.True(t, res.Bindings["result"].(M)["allow"].(bool))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
t.Run("denied", func(t *testing.T) {
|
|
||||||
res := eval(t, []config.Policy{
|
|
||||||
{
|
|
||||||
Source: &config.StringURL{URL: mustParseURL("https://from.example.com")},
|
|
||||||
To: config.WeightedURLs{
|
|
||||||
{URL: *mustParseURL("https://to.example.com")},
|
|
||||||
},
|
|
||||||
AllowedGroups: []string{"group1"},
|
|
||||||
},
|
|
||||||
}, []proto.Message{
|
|
||||||
&session.Session{
|
|
||||||
Id: "session1",
|
|
||||||
UserId: "user1",
|
|
||||||
},
|
|
||||||
&user.User{
|
|
||||||
Id: "user1",
|
|
||||||
Email: "a@example.com",
|
|
||||||
},
|
|
||||||
&directory.User{
|
|
||||||
Id: "user1",
|
|
||||||
GroupIds: []string{"group2"},
|
|
||||||
},
|
|
||||||
&directory.Group{
|
|
||||||
Id: "group1",
|
|
||||||
Name: "group-1",
|
|
||||||
Email: "group1@example.com",
|
|
||||||
},
|
|
||||||
}, &Request{
|
|
||||||
Session: RequestSession{
|
|
||||||
ID: "session1",
|
|
||||||
},
|
|
||||||
HTTP: RequestHTTP{
|
|
||||||
Method: "GET",
|
|
||||||
URL: "https://from.example.com",
|
|
||||||
},
|
|
||||||
}, true)
|
|
||||||
assert.False(t, res.Bindings["result"].(M)["allow"].(bool))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
t.Run("impersonate groups", func(t *testing.T) {
|
|
||||||
res := eval(t, []config.Policy{
|
|
||||||
{
|
|
||||||
Source: &config.StringURL{URL: mustParseURL("https://from.example.com")},
|
|
||||||
To: config.WeightedURLs{
|
|
||||||
{URL: *mustParseURL("https://to.example.com")},
|
|
||||||
},
|
|
||||||
AllowedGroups: []string{"group1"},
|
|
||||||
},
|
|
||||||
}, []proto.Message{
|
|
||||||
&session.Session{
|
|
||||||
Id: "session1",
|
|
||||||
UserId: "user1",
|
|
||||||
ImpersonateEmail: proto.String("a@example.com"),
|
|
||||||
ImpersonateGroups: []string{"group1"},
|
|
||||||
},
|
|
||||||
&user.User{
|
|
||||||
Id: "user1",
|
|
||||||
Email: "a@example.com",
|
|
||||||
},
|
|
||||||
&directory.Group{
|
|
||||||
Id: "group1",
|
|
||||||
Name: "group-1",
|
|
||||||
Email: "group1@example.com",
|
|
||||||
},
|
|
||||||
}, &Request{
|
|
||||||
Session: RequestSession{
|
|
||||||
ID: "session1",
|
|
||||||
},
|
|
||||||
HTTP: RequestHTTP{
|
|
||||||
Method: "GET",
|
|
||||||
URL: "https://from.example.com",
|
|
||||||
},
|
|
||||||
}, true)
|
|
||||||
assert.True(t, res.Bindings["result"].(M)["allow"].(bool))
|
|
||||||
})
|
|
||||||
t.Run("any authenticated user", func(t *testing.T) {
|
|
||||||
res := eval(t, []config.Policy{
|
|
||||||
{
|
|
||||||
Source: &config.StringURL{URL: mustParseURL("https://from.example.com")},
|
|
||||||
To: config.WeightedURLs{
|
|
||||||
{URL: *mustParseURL("https://to.example.com")},
|
|
||||||
},
|
|
||||||
AllowAnyAuthenticatedUser: true,
|
|
||||||
},
|
|
||||||
}, []proto.Message{
|
|
||||||
&session.Session{
|
|
||||||
Id: "session1",
|
|
||||||
UserId: "user1",
|
|
||||||
},
|
|
||||||
&user.User{
|
|
||||||
Id: "user1",
|
|
||||||
Email: "a@example.com",
|
|
||||||
},
|
|
||||||
}, &Request{
|
|
||||||
Session: RequestSession{
|
|
||||||
ID: "session1",
|
|
||||||
},
|
|
||||||
HTTP: RequestHTTP{
|
|
||||||
Method: "GET",
|
|
||||||
URL: "https://from.example.com",
|
|
||||||
},
|
|
||||||
}, true)
|
|
||||||
assert.True(t, res.Bindings["result"].(M)["allow"].(bool))
|
|
||||||
})
|
|
||||||
t.Run("databroker versions", func(t *testing.T) {
|
|
||||||
res := eval(t, nil, []proto.Message{
|
|
||||||
wrapperspb.String("test"),
|
|
||||||
}, &Request{}, false)
|
|
||||||
serverVersion, recordVersion := getDataBrokerVersions(res.Bindings)
|
|
||||||
assert.Equal(t, uint64(math.MaxUint64), serverVersion)
|
|
||||||
assert.NotEqual(t, uint64(0), recordVersion) // random
|
|
||||||
})
|
|
||||||
}
|
|
183
authorize/evaluator/policy_evaluator.go
Normal file
183
authorize/evaluator/policy_evaluator.go
Normal file
|
@ -0,0 +1,183 @@
|
||||||
|
package evaluator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/open-policy-agent/opa/rego"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/config"
|
||||||
|
"github.com/pomerium/pomerium/internal/log"
|
||||||
|
"github.com/pomerium/pomerium/pkg/policy"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PolicyRequest is the input to policy evaluation.
|
||||||
|
type PolicyRequest struct {
|
||||||
|
HTTP RequestHTTP `json:"http"`
|
||||||
|
Session RequestSession `json:"session"`
|
||||||
|
IsValidClientCertificate bool `json:"is_valid_client_certificate"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PolicyResponse is the result of evaluating a policy.
|
||||||
|
type PolicyResponse struct {
|
||||||
|
Allow bool
|
||||||
|
Deny *Denial
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge merges another PolicyResponse into this PolicyResponse. Access is allowed if either is allowed. Access is denied if
|
||||||
|
// either is denied. (and denials take precedence)
|
||||||
|
func (res *PolicyResponse) Merge(other *PolicyResponse) *PolicyResponse {
|
||||||
|
merged := &PolicyResponse{
|
||||||
|
Allow: res.Allow || other.Allow,
|
||||||
|
Deny: res.Deny,
|
||||||
|
}
|
||||||
|
if other.Deny != nil {
|
||||||
|
merged.Deny = other.Deny
|
||||||
|
}
|
||||||
|
return merged
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Denial indicates the request should be denied (even if otherwise allowed).
|
||||||
|
type Denial struct {
|
||||||
|
Status int
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
// A PolicyEvaluator evaluates policies.
|
||||||
|
type PolicyEvaluator struct {
|
||||||
|
queries []rego.PreparedEvalQuery
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPolicyEvaluator creates a new PolicyEvaluator.
|
||||||
|
func NewPolicyEvaluator(ctx context.Context, store *Store, configPolicy *config.Policy) (*PolicyEvaluator, error) {
|
||||||
|
e := new(PolicyEvaluator)
|
||||||
|
|
||||||
|
// generate the base rego script for the policy
|
||||||
|
ppl := configPolicy.ToPPL()
|
||||||
|
base, err := policy.GenerateRegoFromPolicy(ppl)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
scripts := []string{base}
|
||||||
|
|
||||||
|
// add any custom rego
|
||||||
|
for _, sp := range configPolicy.SubPolicies {
|
||||||
|
for _, src := range sp.Rego {
|
||||||
|
if src == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
scripts = append(scripts, src)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// for each script, create a rego and prepare a query.
|
||||||
|
for _, script := range scripts {
|
||||||
|
log.Debug(ctx).
|
||||||
|
Str("script", script).
|
||||||
|
Str("from", configPolicy.From).
|
||||||
|
Interface("to", configPolicy.To).
|
||||||
|
Msg("authorize: rego script for policy evaluation")
|
||||||
|
|
||||||
|
r := rego.New(
|
||||||
|
rego.Store(store),
|
||||||
|
rego.Module("pomerium.policy", script),
|
||||||
|
rego.Query("result = data.pomerium.policy"),
|
||||||
|
getGoogleCloudServerlessHeadersRegoOption,
|
||||||
|
store.GetDataBrokerRecordOption(),
|
||||||
|
)
|
||||||
|
|
||||||
|
q, err := r.PrepareForEval(ctx)
|
||||||
|
// if no package is in the src, add it
|
||||||
|
if err != nil && strings.Contains(err.Error(), "package expected") {
|
||||||
|
r := rego.New(
|
||||||
|
rego.Store(store),
|
||||||
|
rego.Module("pomerium.policy", "package pomerium.policy\n\n"+script),
|
||||||
|
rego.Query("result = data.pomerium.policy"),
|
||||||
|
getGoogleCloudServerlessHeadersRegoOption,
|
||||||
|
store.GetDataBrokerRecordOption(),
|
||||||
|
)
|
||||||
|
q, err = r.PrepareForEval(ctx)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
e.queries = append(e.queries, q)
|
||||||
|
}
|
||||||
|
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evaluate evaluates the policy rego scripts.
|
||||||
|
func (e *PolicyEvaluator) Evaluate(ctx context.Context, req *PolicyRequest) (*PolicyResponse, error) {
|
||||||
|
res := new(PolicyResponse)
|
||||||
|
// run each query and merge the results
|
||||||
|
for _, query := range e.queries {
|
||||||
|
o, err := e.evaluateQuery(ctx, req, query)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res = res.Merge(o)
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *PolicyEvaluator) evaluateQuery(ctx context.Context, req *PolicyRequest, query rego.PreparedEvalQuery) (*PolicyResponse, error) {
|
||||||
|
rs, err := safeEval(ctx, query, rego.EvalInput(req))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("authorize: error evaluating policy.rego: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rs) == 0 {
|
||||||
|
return nil, fmt.Errorf("authorize: unexpected empty result from evaluating policy.rego")
|
||||||
|
}
|
||||||
|
|
||||||
|
res := &PolicyResponse{
|
||||||
|
Allow: e.getAllow(rs[0].Bindings),
|
||||||
|
Deny: e.getDeny(ctx, rs[0].Bindings),
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getAllow gets the allow var. It expects a boolean.
|
||||||
|
func (e *PolicyEvaluator) getAllow(vars rego.Vars) bool {
|
||||||
|
m, ok := vars["result"].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
allow, ok := m["allow"].(bool)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return allow
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDeny gets the deny var. It expects an (http status code, message) pair.
|
||||||
|
func (e *PolicyEvaluator) getDeny(ctx context.Context, vars rego.Vars) *Denial {
|
||||||
|
m, ok := vars["result"].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pair, ok := m["deny"].([]interface{})
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
status, err := strconv.Atoi(fmt.Sprint(pair[0]))
|
||||||
|
if err != nil {
|
||||||
|
log.Error(ctx).Err(err).Msg("invalid type in deny")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
msg := fmt.Sprint(pair[1])
|
||||||
|
|
||||||
|
return &Denial{
|
||||||
|
Status: status,
|
||||||
|
Message: msg,
|
||||||
|
}
|
||||||
|
}
|
112
authorize/evaluator/policy_evaluator_test.go
Normal file
112
authorize/evaluator/policy_evaluator_test.go
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
package evaluator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"math"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/pkg/grpc/session"
|
||||||
|
"github.com/pomerium/pomerium/pkg/grpc/user"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/config"
|
||||||
|
"github.com/pomerium/pomerium/pkg/cryptutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPolicyEvaluator(t *testing.T) {
|
||||||
|
type A = []interface{}
|
||||||
|
type M = map[string]interface{}
|
||||||
|
|
||||||
|
signingKey, err := cryptutil.NewSigningKey()
|
||||||
|
require.NoError(t, err)
|
||||||
|
encodedSigningKey, err := cryptutil.EncodePrivateKey(signingKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
privateJWK, err := cryptutil.PrivateJWKFromBytes(encodedSigningKey, jose.ES256)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
eval := func(t *testing.T, policy *config.Policy, data []proto.Message, input *PolicyRequest) (*PolicyResponse, error) {
|
||||||
|
store := NewStoreFromProtos(math.MaxUint64, data...)
|
||||||
|
store.UpdateIssuer("authenticate.example.com")
|
||||||
|
store.UpdateJWTClaimHeaders(config.NewJWTClaimHeaders("email", "groups", "user", "CUSTOM_KEY"))
|
||||||
|
store.UpdateSigningKey(privateJWK)
|
||||||
|
e, err := NewPolicyEvaluator(context.Background(), store, policy)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return e.Evaluate(context.Background(), input)
|
||||||
|
}
|
||||||
|
|
||||||
|
p1 := &config.Policy{
|
||||||
|
From: "https://from.example.com",
|
||||||
|
To: config.WeightedURLs{{URL: *mustParseURL("https://to.example.com")}},
|
||||||
|
AllowedUsers: []string{"u1@example.com"},
|
||||||
|
}
|
||||||
|
s1 := &session.Session{
|
||||||
|
Id: "s1",
|
||||||
|
UserId: "u1",
|
||||||
|
}
|
||||||
|
s2 := &session.Session{
|
||||||
|
Id: "s2",
|
||||||
|
UserId: "u2",
|
||||||
|
}
|
||||||
|
u1 := &user.User{
|
||||||
|
Id: "u1",
|
||||||
|
Email: "u1@example.com",
|
||||||
|
}
|
||||||
|
u2 := &user.User{
|
||||||
|
Id: "u2",
|
||||||
|
Email: "u2@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("allowed", func(t *testing.T) {
|
||||||
|
output, err := eval(t,
|
||||||
|
p1,
|
||||||
|
[]proto.Message{s1, u1, s2, u2},
|
||||||
|
&PolicyRequest{
|
||||||
|
HTTP: RequestHTTP{Method: "GET", URL: "https://from.example.com/path"},
|
||||||
|
Session: RequestSession{ID: "s1"},
|
||||||
|
|
||||||
|
IsValidClientCertificate: true,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, &PolicyResponse{
|
||||||
|
Allow: true,
|
||||||
|
}, output)
|
||||||
|
})
|
||||||
|
t.Run("invalid cert", func(t *testing.T) {
|
||||||
|
output, err := eval(t,
|
||||||
|
p1,
|
||||||
|
[]proto.Message{s1, u1, s2, u2},
|
||||||
|
&PolicyRequest{
|
||||||
|
HTTP: RequestHTTP{Method: "GET", URL: "https://from.example.com/path"},
|
||||||
|
Session: RequestSession{ID: "s1"},
|
||||||
|
|
||||||
|
IsValidClientCertificate: false,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, &PolicyResponse{
|
||||||
|
Allow: true,
|
||||||
|
Deny: &Denial{
|
||||||
|
Status: 495,
|
||||||
|
Message: "invalid client certificate",
|
||||||
|
},
|
||||||
|
}, output)
|
||||||
|
})
|
||||||
|
t.Run("forbidden", func(t *testing.T) {
|
||||||
|
output, err := eval(t,
|
||||||
|
p1,
|
||||||
|
[]proto.Message{s1, u1, s2, u2},
|
||||||
|
&PolicyRequest{
|
||||||
|
HTTP: RequestHTTP{Method: "GET", URL: "https://from.example.com/path"},
|
||||||
|
Session: RequestSession{ID: "s2"},
|
||||||
|
|
||||||
|
IsValidClientCertificate: true,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, &PolicyResponse{
|
||||||
|
Allow: false,
|
||||||
|
}, output)
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,24 +0,0 @@
|
||||||
package evaluator
|
|
||||||
|
|
||||||
type (
|
|
||||||
// Request is the request data used for the evaluator.
|
|
||||||
Request struct {
|
|
||||||
HTTP RequestHTTP `json:"http"`
|
|
||||||
Session RequestSession `json:"session"`
|
|
||||||
CustomPolicies []string
|
|
||||||
ClientCA string // pem-encoded certificate authority
|
|
||||||
}
|
|
||||||
|
|
||||||
// RequestHTTP is the HTTP field in the request.
|
|
||||||
RequestHTTP struct {
|
|
||||||
Method string `json:"method"`
|
|
||||||
URL string `json:"url"`
|
|
||||||
Headers map[string]string `json:"headers"`
|
|
||||||
ClientCertificate string `json:"client_certificate"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// RequestSession is the session field in the request.
|
|
||||||
RequestSession struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
}
|
|
||||||
)
|
|
|
@ -1,116 +0,0 @@
|
||||||
package evaluator
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/open-policy-agent/opa/rego"
|
|
||||||
|
|
||||||
"github.com/pomerium/pomerium/config"
|
|
||||||
"github.com/pomerium/pomerium/internal/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Result is the result of evaluation.
|
|
||||||
type Result struct {
|
|
||||||
Status int
|
|
||||||
Message string
|
|
||||||
Headers map[string]string
|
|
||||||
MatchingPolicy *config.Policy
|
|
||||||
|
|
||||||
DataBrokerServerVersion, DataBrokerRecordVersion uint64
|
|
||||||
}
|
|
||||||
|
|
||||||
func getMatchingPolicy(vars rego.Vars, policies []config.Policy) *config.Policy {
|
|
||||||
result, ok := vars["result"].(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
idx, err := strconv.Atoi(fmt.Sprint(result["route_policy_idx"]))
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if idx >= len(policies) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return &policies[idx]
|
|
||||||
}
|
|
||||||
|
|
||||||
func getAllowVar(vars rego.Vars) bool {
|
|
||||||
result, ok := vars["result"].(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
allow, ok := result["allow"].(bool)
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return allow
|
|
||||||
}
|
|
||||||
|
|
||||||
func getDenyVar(vars rego.Vars) []Result {
|
|
||||||
result, ok := vars["result"].(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
denials, ok := result["deny"].([]interface{})
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
results := make([]Result, 0, len(denials))
|
|
||||||
for _, denial := range denials {
|
|
||||||
denial, ok := denial.([]interface{})
|
|
||||||
if !ok || len(denial) != 2 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
status, err := strconv.Atoi(fmt.Sprint(denial[0]))
|
|
||||||
if err != nil {
|
|
||||||
log.Error(context.TODO()).Err(err).Msg("invalid type in deny")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
msg := fmt.Sprint(denial[1])
|
|
||||||
|
|
||||||
results = append(results, Result{
|
|
||||||
Status: status,
|
|
||||||
Message: msg,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
func getHeadersVar(vars rego.Vars) map[string]string {
|
|
||||||
headers := make(map[string]string)
|
|
||||||
|
|
||||||
result, ok := vars["result"].(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
return headers
|
|
||||||
}
|
|
||||||
|
|
||||||
m, ok := result["identity_headers"].(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
return headers
|
|
||||||
}
|
|
||||||
|
|
||||||
for k, v := range m {
|
|
||||||
headers[k] = fmt.Sprint(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
return headers
|
|
||||||
}
|
|
||||||
|
|
||||||
func getDataBrokerVersions(vars rego.Vars) (serverVersion, recordVersion uint64) {
|
|
||||||
result, ok := vars["result"].(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
return 0, 0
|
|
||||||
}
|
|
||||||
serverVersion, _ = strconv.ParseUint(fmt.Sprint(result["databroker_server_version"]), 10, 64)
|
|
||||||
recordVersion, _ = strconv.ParseUint(fmt.Sprint(result["databroker_record_version"]), 10, 64)
|
|
||||||
return serverVersion, recordVersion
|
|
||||||
}
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/open-policy-agent/opa/ast"
|
"github.com/open-policy-agent/opa/ast"
|
||||||
|
@ -85,6 +86,8 @@ type Store struct {
|
||||||
storage.Store
|
storage.Store
|
||||||
|
|
||||||
dataBrokerData *dataBrokerData
|
dataBrokerData *dataBrokerData
|
||||||
|
|
||||||
|
dataBrokerServerVersion, dataBrokerRecordVersion uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewStore creates a new Store.
|
// NewStore creates a new Store.
|
||||||
|
@ -124,6 +127,12 @@ func (s *Store) ClearRecords() {
|
||||||
s.dataBrokerData.clear()
|
s.dataBrokerData.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetDataBrokerVersions gets the databroker versions.
|
||||||
|
func (s *Store) GetDataBrokerVersions() (serverVersion, recordVersion uint64) {
|
||||||
|
return atomic.LoadUint64(&s.dataBrokerServerVersion),
|
||||||
|
atomic.LoadUint64(&s.dataBrokerRecordVersion)
|
||||||
|
}
|
||||||
|
|
||||||
// GetRecordData gets a record's data from the store. `nil` is returned
|
// GetRecordData gets a record's data from the store. `nil` is returned
|
||||||
// if no record exists for the given type and id.
|
// if no record exists for the given type and id.
|
||||||
func (s *Store) GetRecordData(typeURL, id string) proto.Message {
|
func (s *Store) GetRecordData(typeURL, id string) proto.Message {
|
||||||
|
@ -161,6 +170,8 @@ func (s *Store) UpdateRecord(serverVersion uint64, record *databroker.Record) {
|
||||||
}
|
}
|
||||||
s.write("/databroker_server_version", fmt.Sprint(serverVersion))
|
s.write("/databroker_server_version", fmt.Sprint(serverVersion))
|
||||||
s.write("/databroker_record_version", fmt.Sprint(record.GetVersion()))
|
s.write("/databroker_record_version", fmt.Sprint(record.GetVersion()))
|
||||||
|
atomic.StoreUint64(&s.dataBrokerServerVersion, serverVersion)
|
||||||
|
atomic.StoreUint64(&s.dataBrokerRecordVersion, record.GetVersion())
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateSigningKey updates the signing key stored in the database. Signing operations
|
// UpdateSigningKey updates the signing key stored in the database. Signing operations
|
||||||
|
|
|
@ -2,7 +2,6 @@ package authorize
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
@ -58,26 +57,34 @@ func (a *Authorize) Check(ctx context.Context, in *envoy_service_auth_v3.CheckRe
|
||||||
|
|
||||||
// take the state lock here so we don't update while evaluating
|
// take the state lock here so we don't update while evaluating
|
||||||
a.stateLock.RLock()
|
a.stateLock.RLock()
|
||||||
reply, err := state.evaluator.Evaluate(ctx, req)
|
res, err := state.evaluator.Evaluate(ctx, req)
|
||||||
a.stateLock.RUnlock()
|
a.stateLock.RUnlock()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(ctx).Err(err).Msg("error during OPA evaluation")
|
log.Error(ctx).Err(err).Msg("error during OPA evaluation")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
a.logAuthorizeCheck(ctx, in, out, reply, u)
|
a.logAuthorizeCheck(ctx, in, out, res, u)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
switch {
|
if res.Deny != nil {
|
||||||
case reply.Status == http.StatusOK:
|
return a.deniedResponse(ctx, in, int32(res.Deny.Status), res.Deny.Message, nil)
|
||||||
return a.okResponse(reply), nil
|
|
||||||
case reply.Status == http.StatusUnauthorized:
|
|
||||||
if isForwardAuth && hreq.URL.Path == "/verify" {
|
|
||||||
return a.deniedResponse(ctx, in, http.StatusUnauthorized, "Unauthenticated", nil)
|
|
||||||
}
|
|
||||||
return a.redirectResponse(ctx, in)
|
|
||||||
}
|
}
|
||||||
return a.deniedResponse(ctx, in, int32(reply.Status), reply.Message, nil)
|
|
||||||
|
if res.Allow {
|
||||||
|
return a.okResponse(res), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if isForwardAuth && hreq.URL.Path == "/verify" {
|
||||||
|
return a.deniedResponse(ctx, in, http.StatusUnauthorized, "Unauthenticated", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we're logged in, don't redirect, deny with forbidden
|
||||||
|
if req.Session.ID != "" {
|
||||||
|
return a.deniedResponse(ctx, in, http.StatusForbidden, http.StatusText(http.StatusForbidden), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.redirectResponse(ctx, in)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getForwardAuthURL(r *http.Request) *url.URL {
|
func getForwardAuthURL(r *http.Request) *url.URL {
|
||||||
|
@ -132,40 +139,10 @@ func (a *Authorize) getEvaluatorRequestFromCheckRequest(
|
||||||
ID: sessionState.ID,
|
ID: sessionState.ID,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
p := a.getMatchingPolicy(requestURL)
|
req.Policy = a.getMatchingPolicy(requestURL)
|
||||||
if p != nil {
|
|
||||||
for _, sp := range p.SubPolicies {
|
|
||||||
req.CustomPolicies = append(req.CustomPolicies, sp.Rego...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ca, err := a.getDownstreamClientCA(p)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
req.ClientCA = ca
|
|
||||||
|
|
||||||
return req, nil
|
return req, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Authorize) getDownstreamClientCA(policy *config.Policy) (string, error) {
|
|
||||||
options := a.currentOptions.Load()
|
|
||||||
|
|
||||||
if policy != nil && policy.TLSDownstreamClientCA != "" {
|
|
||||||
bs, err := base64.StdEncoding.DecodeString(policy.TLSDownstreamClientCA)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return string(bs), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ca, err := options.GetClientCA()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return string(ca), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Authorize) getMatchingPolicy(requestURL url.URL) *config.Policy {
|
func (a *Authorize) getMatchingPolicy(requestURL url.URL) *config.Policy {
|
||||||
options := a.currentOptions.Load()
|
options := a.currentOptions.Load()
|
||||||
|
|
||||||
|
|
|
@ -86,6 +86,7 @@ func Test_getEvaluatorRequest(t *testing.T) {
|
||||||
)
|
)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
expect := &evaluator.Request{
|
expect := &evaluator.Request{
|
||||||
|
Policy: &a.currentOptions.Load().Policies[0],
|
||||||
Session: evaluator.RequestSession{
|
Session: evaluator.RequestSession{
|
||||||
ID: "SESSION_ID",
|
ID: "SESSION_ID",
|
||||||
},
|
},
|
||||||
|
@ -98,7 +99,6 @@ func Test_getEvaluatorRequest(t *testing.T) {
|
||||||
},
|
},
|
||||||
ClientCertificate: certPEM,
|
ClientCertificate: certPEM,
|
||||||
},
|
},
|
||||||
CustomPolicies: []string{"allow = true"},
|
|
||||||
}
|
}
|
||||||
assert.Equal(t, expect, actual)
|
assert.Equal(t, expect, actual)
|
||||||
}
|
}
|
||||||
|
@ -294,6 +294,7 @@ func Test_getEvaluatorRequestWithPortInHostHeader(t *testing.T) {
|
||||||
}, nil)
|
}, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
expect := &evaluator.Request{
|
expect := &evaluator.Request{
|
||||||
|
Policy: &a.currentOptions.Load().Policies[0],
|
||||||
Session: evaluator.RequestSession{},
|
Session: evaluator.RequestSession{},
|
||||||
HTTP: evaluator.RequestHTTP{
|
HTTP: evaluator.RequestHTTP{
|
||||||
Method: "GET",
|
Method: "GET",
|
||||||
|
@ -304,7 +305,6 @@ func Test_getEvaluatorRequestWithPortInHostHeader(t *testing.T) {
|
||||||
},
|
},
|
||||||
ClientCertificate: certPEM,
|
ClientCertificate: certPEM,
|
||||||
},
|
},
|
||||||
CustomPolicies: []string{"allow = true"},
|
|
||||||
}
|
}
|
||||||
assert.Equal(t, expect, actual)
|
assert.Equal(t, expect, actual)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ package authorize
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
envoy_service_auth_v3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3"
|
envoy_service_auth_v3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3"
|
||||||
|
@ -19,7 +18,7 @@ import (
|
||||||
func (a *Authorize) logAuthorizeCheck(
|
func (a *Authorize) logAuthorizeCheck(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
in *envoy_service_auth_v3.CheckRequest, out *envoy_service_auth_v3.CheckResponse,
|
in *envoy_service_auth_v3.CheckRequest, out *envoy_service_auth_v3.CheckResponse,
|
||||||
reply *evaluator.Result, u *user.User,
|
res *evaluator.Result, u *user.User,
|
||||||
) {
|
) {
|
||||||
ctx, span := trace.StartSpan(ctx, "authorize.grpc.LogAuthorizeCheck")
|
ctx, span := trace.StartSpan(ctx, "authorize.grpc.LogAuthorizeCheck")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
@ -34,15 +33,14 @@ func (a *Authorize) logAuthorizeCheck(
|
||||||
evt = evt.Str("path", stripQueryString(hattrs.GetPath()))
|
evt = evt.Str("path", stripQueryString(hattrs.GetPath()))
|
||||||
evt = evt.Str("host", hattrs.GetHost())
|
evt = evt.Str("host", hattrs.GetHost())
|
||||||
evt = evt.Str("query", hattrs.GetQuery())
|
evt = evt.Str("query", hattrs.GetQuery())
|
||||||
// reply
|
// result
|
||||||
if reply != nil {
|
if res != nil {
|
||||||
evt = evt.Bool("allow", reply.Status == http.StatusOK)
|
evt = evt.Bool("allow", res.Allow)
|
||||||
evt = evt.Int("reply-status", reply.Status)
|
evt = evt.Interface("deny", res.Deny)
|
||||||
evt = evt.Str("reply-message", reply.Message)
|
|
||||||
evt = evt.Str("user", u.GetId())
|
evt = evt.Str("user", u.GetId())
|
||||||
evt = evt.Str("email", u.GetEmail())
|
evt = evt.Str("email", u.GetEmail())
|
||||||
evt = evt.Uint64("databroker_server_version", reply.DataBrokerServerVersion)
|
evt = evt.Uint64("databroker_server_version", res.DataBrokerServerVersion)
|
||||||
evt = evt.Uint64("databroker_record_version", reply.DataBrokerRecordVersion)
|
evt = evt.Uint64("databroker_record_version", res.DataBrokerRecordVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
// potentially sensitive, only log if debug mode
|
// potentially sensitive, only log if debug mode
|
||||||
|
@ -60,9 +58,9 @@ func (a *Authorize) logAuthorizeCheck(
|
||||||
Request: in,
|
Request: in,
|
||||||
Response: out,
|
Response: out,
|
||||||
}
|
}
|
||||||
if reply != nil {
|
if res != nil {
|
||||||
record.DatabrokerServerVersion = reply.DataBrokerServerVersion
|
record.DatabrokerServerVersion = res.DataBrokerServerVersion
|
||||||
record.DatabrokerRecordVersion = reply.DataBrokerRecordVersion
|
record.DatabrokerRecordVersion = res.DataBrokerRecordVersion
|
||||||
}
|
}
|
||||||
sealed, err := enc.Encrypt(record)
|
sealed, err := enc.Encrypt(record)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
1
go.sum
1
go.sum
|
@ -774,7 +774,6 @@ golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v
|
||||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
|
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
|
||||||
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420 h1:a8jGStKg0XqKDlKqjLrXn0ioF5MH36pT7Z0BRTqLhbk=
|
|
||||||
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023 h1:ADo5wSpq2gqaCGQWzk7S5vd//0iyyLeAratkEoG5dLE=
|
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023 h1:ADo5wSpq2gqaCGQWzk7S5vd//0iyyLeAratkEoG5dLE=
|
||||||
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
|
Loading…
Add table
Reference in a new issue