mirror of
https://github.com/pomerium/pomerium.git
synced 2025-04-29 02:16:28 +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)
|
||||
MISSPELL_VERSION = v0.3.4
|
||||
GOLANGCI_VERSION = v1.34.1
|
||||
OPA_VERSION = v0.26.0
|
||||
GETENVOY_VERSION = v0.2.0
|
||||
GORELEASER_VERSION = v0.157.0
|
||||
|
||||
|
@ -56,7 +55,6 @@ deps-lint: ## Install lint dependencies
|
|||
.PHONY: deps-build
|
||||
deps-build: ## Install build dependencies
|
||||
@echo "==> $@"
|
||||
@$(GO) install github.com/open-policy-agent/opa@${OPA_VERSION}
|
||||
@$(GO) install github.com/tetratelabs/getenvoy/cmd/getenvoy@${GETENVOY_VERSION}
|
||||
|
||||
.PHONY: deps-release
|
||||
|
@ -107,7 +105,6 @@ lint: ## Verifies `golint` passes.
|
|||
test: ## Runs the go tests.
|
||||
@echo "==> $@"
|
||||
@$(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
|
||||
spellcheck: # Spellcheck docs
|
||||
|
|
|
@ -88,7 +88,25 @@ func newPolicyEvaluator(opts *config.Options, store *evaluator.Store) (*evaluato
|
|||
ctx := context.Background()
|
||||
_, span := trace.StartSpan(ctx, "authorize.newPolicyEvaluator")
|
||||
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
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"net/http/httptest"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
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"
|
||||
|
@ -25,15 +26,15 @@ import (
|
|||
|
||||
func (a *Authorize) okResponse(reply *evaluator.Result) *envoy_service_auth_v3.CheckResponse {
|
||||
var requestHeaders []*envoy_config_core_v3.HeaderValueOption
|
||||
for k, v := range reply.Headers {
|
||||
requestHeaders = append(requestHeaders, mkHeader(k, v, false))
|
||||
for k, vs := range reply.Headers {
|
||||
requestHeaders = append(requestHeaders, mkHeader(k, strings.Join(vs, ","), false))
|
||||
}
|
||||
// ensure request headers are sorted by key for deterministic output
|
||||
sort.Slice(requestHeaders, func(i, j int) bool {
|
||||
return requestHeaders[i].Header.Key < requestHeaders[j].Header.Value
|
||||
})
|
||||
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{
|
||||
OkResponse: &envoy_service_auth_v3.OkHttpResponse{
|
||||
Headers: requestHeaders,
|
||||
|
@ -47,7 +48,6 @@ func (a *Authorize) deniedResponse(
|
|||
in *envoy_service_auth_v3.CheckRequest,
|
||||
code int32, reason string, headers map[string]string,
|
||||
) (*envoy_service_auth_v3.CheckResponse, error) {
|
||||
|
||||
var details string
|
||||
switch code {
|
||||
case httputil.StatusInvalidClientCertificate:
|
||||
|
|
|
@ -30,6 +30,7 @@ func TestAuthorize_okResponse(t *testing.T) {
|
|||
AuthenticateURLString: "https://authenticate.example.com",
|
||||
Policies: []config.Policy{{
|
||||
Source: &config.StringURL{URL: &url.URL{Host: "example.com"}},
|
||||
To: mustParseWeightedURLs(t, "https://to.example.com"),
|
||||
SubPolicies: []config.SubPolicy{{
|
||||
Rego: []string{"allow = true"},
|
||||
}},
|
||||
|
@ -62,45 +63,30 @@ func TestAuthorize_okResponse(t *testing.T) {
|
|||
}{
|
||||
{
|
||||
"ok reply",
|
||||
&evaluator.Result{Status: 0, Message: "ok"},
|
||||
&evaluator.Result{Allow: true},
|
||||
&envoy_service_auth_v3.CheckResponse{
|
||||
Status: &status.Status{Code: 0, Message: "ok"},
|
||||
Status: &status.Status{Code: 0, Message: "OK"},
|
||||
},
|
||||
},
|
||||
{
|
||||
"ok reply with k8s svc",
|
||||
&evaluator.Result{
|
||||
Status: 0,
|
||||
Message: "ok",
|
||||
MatchingPolicy: &config.Policy{
|
||||
KubernetesServiceAccountToken: "k8s-svc-account",
|
||||
},
|
||||
},
|
||||
&evaluator.Result{Allow: true},
|
||||
&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",
|
||||
&evaluator.Result{
|
||||
Status: 0,
|
||||
Message: "ok",
|
||||
MatchingPolicy: &config.Policy{
|
||||
KubernetesServiceAccountToken: "k8s-svc-account",
|
||||
},
|
||||
},
|
||||
&evaluator.Result{Allow: true},
|
||||
&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",
|
||||
&evaluator.Result{
|
||||
Status: 0,
|
||||
Message: "ok",
|
||||
},
|
||||
&evaluator.Result{Allow: true},
|
||||
&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
|
||||
// a policy evaluator framework.
|
||||
// Package evaluator contains rego evaluators for evaluating authorize policy.
|
||||
package evaluator
|
||||
|
||||
import (
|
||||
|
@ -13,133 +12,180 @@ import (
|
|||
|
||||
"github.com/pomerium/pomerium/config"
|
||||
"github.com/pomerium/pomerium/internal/log"
|
||||
"github.com/pomerium/pomerium/internal/urlutil"
|
||||
"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 {
|
||||
custom *CustomEvaluator
|
||||
rego *rego.Rego
|
||||
query rego.PreparedEvalQuery
|
||||
policies []config.Policy
|
||||
store *Store
|
||||
store *Store
|
||||
policyEvaluators map[uint64]*PolicyEvaluator
|
||||
headersEvaluators *HeadersEvaluator
|
||||
clientCA []byte
|
||||
}
|
||||
|
||||
// New creates a new Evaluator.
|
||||
func New(options *config.Options, store *Store) (*Evaluator, error) {
|
||||
e := &Evaluator{
|
||||
custom: NewCustomEvaluator(store),
|
||||
policies: options.GetAllPolicies(),
|
||||
store: store,
|
||||
}
|
||||
jwk, err := getJWK(options)
|
||||
func New(ctx context.Context, store *Store, options ...Option) (*Evaluator, error) {
|
||||
e := &Evaluator{store: store}
|
||||
|
||||
cfg := getConfig(options...)
|
||||
|
||||
err := e.updateStore(cfg)
|
||||
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 {
|
||||
return nil, fmt.Errorf("error loading rego policy: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
authenticateURL, err := options.GetAuthenticateURL()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("authorize: invalid authenticate URL: %w", err)
|
||||
e.policyEvaluators = make(map[uint64]*PolicyEvaluator)
|
||||
for _, configPolicy := range cfg.policies {
|
||||
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)
|
||||
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)
|
||||
}
|
||||
e.clientCA = cfg.clientCA
|
||||
|
||||
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) {
|
||||
isValid, err := isValidClientCertificate(req.ClientCA, req.HTTP.ClientCertificate)
|
||||
if req.Policy == nil {
|
||||
return notFoundOutput, nil
|
||||
}
|
||||
|
||||
id, err := req.Policy.RouteID()
|
||||
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 {
|
||||
return nil, fmt.Errorf("error evaluating rego policy: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
deny := getDenyVar(res[0].Bindings.WithoutWildcards())
|
||||
if len(deny) > 0 {
|
||||
return &deny[0], nil
|
||||
isValidClientCertificate, err := isValidClientCertificate(clientCA, req.HTTP.ClientCertificate)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("authorize: error validating client certificate: %w", err)
|
||||
}
|
||||
|
||||
evalResult := &Result{
|
||||
MatchingPolicy: getMatchingPolicy(res[0].Bindings.WithoutWildcards(), e.policies),
|
||||
Headers: getHeadersVar(res[0].Bindings.WithoutWildcards()),
|
||||
}
|
||||
evalResult.DataBrokerServerVersion, evalResult.DataBrokerRecordVersion = getDataBrokerVersions(
|
||||
res[0].Bindings,
|
||||
)
|
||||
|
||||
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
|
||||
policyOutput, err := policyEvaluator.Evaluate(ctx, &PolicyRequest{
|
||||
HTTP: req.HTTP,
|
||||
Session: req.Session,
|
||||
IsValidClientCertificate: isValidClientCertificate,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if req.Session.ID == "" {
|
||||
evalResult.Status = http.StatusUnauthorized
|
||||
evalResult.Message = "login required"
|
||||
return evalResult, nil
|
||||
headersReq := NewHeadersRequestFromPolicy(req.Policy)
|
||||
headersReq.Session = req.Session
|
||||
headersOutput, err := e.headersEvaluators.Evaluate(ctx, headersReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
evalResult.Status = http.StatusForbidden
|
||||
if evalResult.Message == "" {
|
||||
evalResult.Message = http.StatusText(http.StatusForbidden)
|
||||
res := &Result{
|
||||
Allow: policyOutput.Allow,
|
||||
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
|
||||
// if we don't have a signing key, generate one
|
||||
if options.SigningKey == "" {
|
||||
if cfg.signingKey == "" {
|
||||
key, err := cryptutil.NewSigningKey()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("couldn't generate signing key: %w", err)
|
||||
|
@ -150,12 +196,12 @@ func getJWK(options *config.Options) (*jose.JSONWebKey, error) {
|
|||
}
|
||||
} else {
|
||||
var err error
|
||||
decodedCert, err = base64.StdEncoding.DecodeString(options.SigningKey)
|
||||
decodedCert, err = base64.StdEncoding.DecodeString(cfg.signingKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("bad signing key: %w", err)
|
||||
}
|
||||
}
|
||||
signingKeyAlgorithm := options.SigningKeyAlgorithm
|
||||
signingKeyAlgorithm := cfg.signingKeyAlgorithm
|
||||
if signingKeyAlgorithm == "" {
|
||||
signingKeyAlgorithm = string(jose.ES256)
|
||||
}
|
||||
|
@ -172,16 +218,12 @@ func getJWK(options *config.Options) (*jose.JSONWebKey, error) {
|
|||
return jwk, nil
|
||||
}
|
||||
|
||||
type input struct {
|
||||
HTTP RequestHTTP `json:"http"`
|
||||
Session RequestSession `json:"session"`
|
||||
IsValidClientCertificate bool `json:"is_valid_client_certificate"`
|
||||
}
|
||||
|
||||
func (e *Evaluator) newInput(req *Request, isValidClientCertificate bool) *input {
|
||||
i := new(input)
|
||||
i.HTTP = req.HTTP
|
||||
i.Session = req.Session
|
||||
i.IsValidClientCertificate = isValidClientCertificate
|
||||
return i
|
||||
func safeEval(ctx context.Context, q rego.PreparedEvalQuery, options ...rego.EvalOption) (resultSet rego.ResultSet, err error) {
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
err = fmt.Errorf("%v", e)
|
||||
}
|
||||
}()
|
||||
resultSet, err = q.Eval(ctx, options...)
|
||||
return resultSet, err
|
||||
}
|
||||
|
|
|
@ -2,153 +2,457 @@ package evaluator
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"math"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/protobuf/ptypes"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
|
||||
"github.com/pomerium/pomerium/config"
|
||||
"github.com/pomerium/pomerium/pkg/cryptutil"
|
||||
"github.com/pomerium/pomerium/pkg/grpc/databroker"
|
||||
"github.com/pomerium/pomerium/pkg/grpc/directory"
|
||||
"github.com/pomerium/pomerium/pkg/grpc/session"
|
||||
"github.com/pomerium/pomerium/pkg/grpc/user"
|
||||
)
|
||||
|
||||
func TestJSONMarshal(t *testing.T) {
|
||||
opt := config.NewDefaultOptions()
|
||||
opt.AuthenticateURLString = "https://authenticate.example.com"
|
||||
e, err := New(opt, NewStoreFromProtos(0,
|
||||
&session.Session{
|
||||
UserId: "user1",
|
||||
},
|
||||
&directory.User{
|
||||
Id: "user1",
|
||||
GroupIds: []string{"group1", "group2"},
|
||||
},
|
||||
&directory.Group{
|
||||
Id: "group1",
|
||||
Name: "admin",
|
||||
Email: "admin@example.com",
|
||||
},
|
||||
&directory.Group{
|
||||
Id: "group2",
|
||||
Name: "test",
|
||||
},
|
||||
))
|
||||
func TestEvaluator(t *testing.T) {
|
||||
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)
|
||||
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) {
|
||||
sessionID := uuid.New().String()
|
||||
userID := uuid.New().String()
|
||||
|
||||
ctx := context.Background()
|
||||
allowedPolicy := []config.Policy{{From: "https://foo.com", AllowedUsers: []string{"foo@example.com"}}}
|
||||
forbiddenPolicy := []config.Policy{{From: "https://bar.com", AllowedUsers: []string{"bar@example.com"}}}
|
||||
|
||||
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},
|
||||
eval := func(t *testing.T, options []Option, data []proto.Message, req *Request) (*Result, 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 := New(context.Background(), store, options...)
|
||||
require.NoError(t, err)
|
||||
return e.Evaluate(context.Background(), req)
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
store := NewStoreFromProtos(0)
|
||||
data, _ := ptypes.MarshalAny(&session.Session{
|
||||
Version: "1",
|
||||
Id: sessionID,
|
||||
UserId: userID,
|
||||
IdToken: &session.IDToken{
|
||||
Issuer: "TestEvaluatorEvaluate",
|
||||
Subject: userID,
|
||||
IssuedAt: ptypes.TimestampNow(),
|
||||
policies := []config.Policy{
|
||||
{
|
||||
To: config.WeightedURLs{{URL: *mustParseURL("https://to1.example.com")}},
|
||||
AllowPublicUnauthenticatedAccess: true,
|
||||
},
|
||||
{
|
||||
To: config.WeightedURLs{{URL: *mustParseURL("https://to2.example.com")}},
|
||||
AllowPublicUnauthenticatedAccess: true,
|
||||
KubernetesServiceAccountToken: "KUBERNETES",
|
||||
},
|
||||
{
|
||||
To: config.WeightedURLs{{URL: *mustParseURL("https://to3.example.com")}},
|
||||
AllowPublicUnauthenticatedAccess: true,
|
||||
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",
|
||||
},
|
||||
})
|
||||
store.UpdateRecord(0, &databroker.Record{
|
||||
Version: 1,
|
||||
Type: "type.googleapis.com/session.Session",
|
||||
Id: sessionID,
|
||||
Data: data,
|
||||
})
|
||||
data, _ = ptypes.MarshalAny(&user.User{
|
||||
Version: "1",
|
||||
Id: userID,
|
||||
Email: "foo@example.com",
|
||||
})
|
||||
store.UpdateRecord(0, &databroker.Record{
|
||||
Version: 1,
|
||||
Type: "type.googleapis.com/user.User",
|
||||
Id: userID,
|
||||
Data: data,
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
To: config.WeightedURLs{{URL: *mustParseURL("https://to6.example.com")}},
|
||||
AllowedUsers: []string{"example/1234"},
|
||||
},
|
||||
{
|
||||
To: config.WeightedURLs{{URL: *mustParseURL("https://to7.example.com")}},
|
||||
AllowedDomains: []string{"example.com"},
|
||||
},
|
||||
{
|
||||
To: config.WeightedURLs{{URL: *mustParseURL("https://to8.example.com")}},
|
||||
AllowedGroups: []string{"group1@example.com"},
|
||||
},
|
||||
{
|
||||
To: config.WeightedURLs{{URL: *mustParseURL("https://to9.example.com")}},
|
||||
AllowAnyAuthenticatedUser: true,
|
||||
},
|
||||
}
|
||||
options := []Option{
|
||||
WithAuthenticateURL("https://authn.example.com"),
|
||||
WithClientCA([]byte(testCA)),
|
||||
WithPolicies(policies),
|
||||
}
|
||||
|
||||
e, err := New(&config.Options{
|
||||
AuthenticateURLString: "https://authn.example.com",
|
||||
Policies: tc.policies,
|
||||
}, store)
|
||||
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,
|
||||
t.Run("client certificate", func(t *testing.T) {
|
||||
t.Run("invalid", func(t *testing.T) {
|
||||
res, err := eval(t, options, nil, &Request{
|
||||
Policy: &policies[0],
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, res)
|
||||
assert.Equal(t, tc.expectedStatus, res.Status)
|
||||
assert.Equal(t, &Denial{Status: 495, Message: "invalid client certificate"}, res.Deny)
|
||||
})
|
||||
}
|
||||
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 {
|
||||
|
@ -161,9 +465,22 @@ func mustParseURL(str string) *url.URL {
|
|||
|
||||
func BenchmarkEvaluator_Evaluate(b *testing.B) {
|
||||
store := NewStore()
|
||||
e, err := New(&config.Options{
|
||||
AuthenticateURLString: "https://authn.example.com",
|
||||
}, store)
|
||||
|
||||
policies := []config.Policy{
|
||||
{
|
||||
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) {
|
||||
return
|
||||
}
|
||||
|
@ -233,7 +550,8 @@ func BenchmarkEvaluator_Evaluate(b *testing.B) {
|
|||
b.ResetTimer()
|
||||
ctx := context.Background()
|
||||
for i := 0; i < b.N; i++ {
|
||||
e.Evaluate(ctx, &Request{
|
||||
_, _ = e.Evaluate(ctx, &Request{
|
||||
Policy: &policies[0],
|
||||
HTTP: RequestHTTP{
|
||||
Method: "GET",
|
||||
URL: "https://example.com/path",
|
||||
|
|
|
@ -8,7 +8,6 @@ import (
|
|||
|
||||
lru "github.com/hashicorp/golang-lru"
|
||||
|
||||
"github.com/pomerium/pomerium/authorize/evaluator/opa"
|
||||
"github.com/pomerium/pomerium/internal/log"
|
||||
)
|
||||
|
||||
|
@ -65,7 +64,3 @@ func parseCertificate(pemStr string) (*x509.Certificate, error) {
|
|||
}
|
||||
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.
|
||||
package opa
|
||||
|
||||
import "embed"
|
||||
import _ "embed" // to embed files
|
||||
|
||||
// FS is the filesystem for OPA files.
|
||||
//go:embed policy
|
||||
var FS embed.FS
|
||||
// HeadersRego is the headers.rego script.
|
||||
//go:embed policy/headers.rego
|
||||
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"
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/open-policy-agent/opa/ast"
|
||||
|
@ -85,6 +86,8 @@ type Store struct {
|
|||
storage.Store
|
||||
|
||||
dataBrokerData *dataBrokerData
|
||||
|
||||
dataBrokerServerVersion, dataBrokerRecordVersion uint64
|
||||
}
|
||||
|
||||
// NewStore creates a new Store.
|
||||
|
@ -124,6 +127,12 @@ func (s *Store) ClearRecords() {
|
|||
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
|
||||
// if no record exists for the given type and id.
|
||||
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_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
|
||||
|
|
|
@ -2,7 +2,6 @@ package authorize
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"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
|
||||
a.stateLock.RLock()
|
||||
reply, err := state.evaluator.Evaluate(ctx, req)
|
||||
res, err := state.evaluator.Evaluate(ctx, req)
|
||||
a.stateLock.RUnlock()
|
||||
if err != nil {
|
||||
log.Error(ctx).Err(err).Msg("error during OPA evaluation")
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
a.logAuthorizeCheck(ctx, in, out, reply, u)
|
||||
a.logAuthorizeCheck(ctx, in, out, res, u)
|
||||
}()
|
||||
|
||||
switch {
|
||||
case reply.Status == http.StatusOK:
|
||||
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)
|
||||
if res.Deny != nil {
|
||||
return a.deniedResponse(ctx, in, int32(res.Deny.Status), res.Deny.Message, nil)
|
||||
}
|
||||
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 {
|
||||
|
@ -132,40 +139,10 @@ func (a *Authorize) getEvaluatorRequestFromCheckRequest(
|
|||
ID: sessionState.ID,
|
||||
}
|
||||
}
|
||||
p := 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
|
||||
|
||||
req.Policy = a.getMatchingPolicy(requestURL)
|
||||
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 {
|
||||
options := a.currentOptions.Load()
|
||||
|
||||
|
|
|
@ -86,6 +86,7 @@ func Test_getEvaluatorRequest(t *testing.T) {
|
|||
)
|
||||
require.NoError(t, err)
|
||||
expect := &evaluator.Request{
|
||||
Policy: &a.currentOptions.Load().Policies[0],
|
||||
Session: evaluator.RequestSession{
|
||||
ID: "SESSION_ID",
|
||||
},
|
||||
|
@ -98,7 +99,6 @@ func Test_getEvaluatorRequest(t *testing.T) {
|
|||
},
|
||||
ClientCertificate: certPEM,
|
||||
},
|
||||
CustomPolicies: []string{"allow = true"},
|
||||
}
|
||||
assert.Equal(t, expect, actual)
|
||||
}
|
||||
|
@ -294,6 +294,7 @@ func Test_getEvaluatorRequestWithPortInHostHeader(t *testing.T) {
|
|||
}, nil)
|
||||
require.NoError(t, err)
|
||||
expect := &evaluator.Request{
|
||||
Policy: &a.currentOptions.Load().Policies[0],
|
||||
Session: evaluator.RequestSession{},
|
||||
HTTP: evaluator.RequestHTTP{
|
||||
Method: "GET",
|
||||
|
@ -304,7 +305,6 @@ func Test_getEvaluatorRequestWithPortInHostHeader(t *testing.T) {
|
|||
},
|
||||
ClientCertificate: certPEM,
|
||||
},
|
||||
CustomPolicies: []string{"allow = true"},
|
||||
}
|
||||
assert.Equal(t, expect, actual)
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ package authorize
|
|||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
envoy_service_auth_v3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3"
|
||||
|
@ -19,7 +18,7 @@ import (
|
|||
func (a *Authorize) logAuthorizeCheck(
|
||||
ctx context.Context,
|
||||
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")
|
||||
defer span.End()
|
||||
|
@ -34,15 +33,14 @@ func (a *Authorize) logAuthorizeCheck(
|
|||
evt = evt.Str("path", stripQueryString(hattrs.GetPath()))
|
||||
evt = evt.Str("host", hattrs.GetHost())
|
||||
evt = evt.Str("query", hattrs.GetQuery())
|
||||
// reply
|
||||
if reply != nil {
|
||||
evt = evt.Bool("allow", reply.Status == http.StatusOK)
|
||||
evt = evt.Int("reply-status", reply.Status)
|
||||
evt = evt.Str("reply-message", reply.Message)
|
||||
// result
|
||||
if res != nil {
|
||||
evt = evt.Bool("allow", res.Allow)
|
||||
evt = evt.Interface("deny", res.Deny)
|
||||
evt = evt.Str("user", u.GetId())
|
||||
evt = evt.Str("email", u.GetEmail())
|
||||
evt = evt.Uint64("databroker_server_version", reply.DataBrokerServerVersion)
|
||||
evt = evt.Uint64("databroker_record_version", reply.DataBrokerRecordVersion)
|
||||
evt = evt.Uint64("databroker_server_version", res.DataBrokerServerVersion)
|
||||
evt = evt.Uint64("databroker_record_version", res.DataBrokerRecordVersion)
|
||||
}
|
||||
|
||||
// potentially sensitive, only log if debug mode
|
||||
|
@ -60,9 +58,9 @@ func (a *Authorize) logAuthorizeCheck(
|
|||
Request: in,
|
||||
Response: out,
|
||||
}
|
||||
if reply != nil {
|
||||
record.DatabrokerServerVersion = reply.DataBrokerServerVersion
|
||||
record.DatabrokerRecordVersion = reply.DataBrokerRecordVersion
|
||||
if res != nil {
|
||||
record.DatabrokerServerVersion = res.DataBrokerServerVersion
|
||||
record.DatabrokerRecordVersion = res.DataBrokerRecordVersion
|
||||
}
|
||||
sealed, err := enc.Encrypt(record)
|
||||
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-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-20210503060351-7fd8e65b6420 h1:a8jGStKg0XqKDlKqjLrXn0ioF5MH36pT7Z0BRTqLhbk=
|
||||
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/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
|
|
Loading…
Add table
Reference in a new issue