mirror of
https://github.com/pomerium/pomerium.git
synced 2025-08-04 01:09:36 +02:00
feature/databroker: user data and session refactor project (#926)
* databroker: add databroker, identity manager, update cache (#864) * databroker: add databroker, identity manager, update cache * fix cache tests * directory service (#885) * directory: add google and okta * add onelogin * add directory provider * initialize before sync, upate google provider, remove dead code * add azure provider * fix azure provider * fix gitlab * add gitlab test, fix azure test * hook up okta * remove dead code * fix tests * fix flaky test * authorize: use databroker data for rego policy (#904) * wip * add directory provider * initialize before sync, upate google provider, remove dead code * fix flaky test * update authorize to use databroker data * implement signed jwt * wait for session and user to appear * fix test * directory service (#885) * directory: add google and okta * add onelogin * add directory provider * initialize before sync, upate google provider, remove dead code * add azure provider * fix azure provider * fix gitlab * add gitlab test, fix azure test * hook up okta * remove dead code * fix tests * fix flaky test * remove log line * only redirect when no session id exists * prepare rego query as part of create * return on ctx done * retry on disconnect for sync * move jwt signing * use != * use parent ctx for wait * remove session state, remove logs * rename function * add log message * pre-allocate slice * use errgroup * return nil on eof for sync * move check * disable timeout on gRPC requests in envoy * fix gitlab test * use v4 backoff * authenticate: databroker changes (#914) * wip * add directory provider * initialize before sync, upate google provider, remove dead code * fix flaky test * update authorize to use databroker data * implement signed jwt * wait for session and user to appear * fix test * directory service (#885) * directory: add google and okta * add onelogin * add directory provider * initialize before sync, upate google provider, remove dead code * add azure provider * fix azure provider * fix gitlab * add gitlab test, fix azure test * hook up okta * remove dead code * fix tests * fix flaky test * remove log line * only redirect when no session id exists * prepare rego query as part of create * return on ctx done * retry on disconnect for sync * move jwt signing * use != * use parent ctx for wait * remove session state, remove logs * rename function * add log message * pre-allocate slice * use errgroup * return nil on eof for sync * move check * disable timeout on gRPC requests in envoy * fix dashboard * delete session on logout * permanently delete sessions once they are marked as deleted * remove permanent delete * fix tests * remove groups and refresh test * databroker: remove dead code, rename cache url, move dashboard (#925) * wip * add directory provider * initialize before sync, upate google provider, remove dead code * fix flaky test * update authorize to use databroker data * implement signed jwt * wait for session and user to appear * fix test * directory service (#885) * directory: add google and okta * add onelogin * add directory provider * initialize before sync, upate google provider, remove dead code * add azure provider * fix azure provider * fix gitlab * add gitlab test, fix azure test * hook up okta * remove dead code * fix tests * fix flaky test * remove log line * only redirect when no session id exists * prepare rego query as part of create * return on ctx done * retry on disconnect for sync * move jwt signing * use != * use parent ctx for wait * remove session state, remove logs * rename function * add log message * pre-allocate slice * use errgroup * return nil on eof for sync * move check * disable timeout on gRPC requests in envoy * fix dashboard * delete session on logout * permanently delete sessions once they are marked as deleted * remove permanent delete * fix tests * remove cache service * remove kv * remove refresh docs * remove obsolete cache docs * add databroker url option * cache: use memberlist to detect multiple instances * add databroker service url * remove cache service * remove kv * remove refresh docs * remove obsolete cache docs * add databroker url option * cache: use memberlist to detect multiple instances * add databroker service url * wip * remove groups and refresh test * fix redirect, signout * remove databroker client from proxy * remove unused method * remove user dashboard test * handle missing session ids * session: reject sessions with no id * sessions: invalidate old sessions via databroker server version (#930) * session: add a version field tied to the databroker server version that can be used to invalidate sessions * fix tests * add log * authenticate: create user record immediately, call "get" directly in authorize (#931)
This commit is contained in:
parent
39cdb31170
commit
dbd7f55b20
115 changed files with 8479 additions and 3584 deletions
|
@ -4,48 +4,321 @@ package evaluator
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
pb "github.com/pomerium/pomerium/internal/grpc/authorize"
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/golang/protobuf/ptypes"
|
||||
"github.com/open-policy-agent/opa/rego"
|
||||
"github.com/open-policy-agent/opa/storage/inmem"
|
||||
"google.golang.org/protobuf/reflect/protoregistry"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
|
||||
"github.com/pomerium/pomerium/config"
|
||||
"github.com/pomerium/pomerium/internal/cryptutil"
|
||||
"github.com/pomerium/pomerium/internal/directory"
|
||||
"github.com/pomerium/pomerium/internal/grpc/databroker"
|
||||
"github.com/pomerium/pomerium/internal/grpc/session"
|
||||
"github.com/pomerium/pomerium/internal/grpc/user"
|
||||
"github.com/pomerium/pomerium/internal/log"
|
||||
)
|
||||
|
||||
// Evaluator specifies the interface for a policy engine.
|
||||
type Evaluator interface {
|
||||
IsAuthorized(ctx context.Context, req *Request) (*pb.IsAuthorizedReply, error)
|
||||
PutData(ctx context.Context, data map[string]interface{}) error
|
||||
type Evaluator struct {
|
||||
rego *rego.Rego
|
||||
query rego.PreparedEvalQuery
|
||||
|
||||
clientCA string
|
||||
authenticateHost string
|
||||
jwk interface{}
|
||||
}
|
||||
|
||||
// A Request represents an evaluable request with an associated user, device,
|
||||
// and request context.
|
||||
type Request struct {
|
||||
// User context
|
||||
//
|
||||
// User contains the associated user's JWT created by the authenticate
|
||||
// service
|
||||
User string `json:"user,omitempty"`
|
||||
// New creates a new Evaluator.
|
||||
func New(options *config.Options) (*Evaluator, error) {
|
||||
e := &Evaluator{
|
||||
authenticateHost: options.AuthenticateURL.Host,
|
||||
}
|
||||
if options.ClientCA != "" {
|
||||
e.clientCA = options.ClientCA
|
||||
} else if options.ClientCAFile != "" {
|
||||
bs, err := ioutil.ReadFile(options.ClientCAFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
e.clientCA = string(bs)
|
||||
}
|
||||
|
||||
// Request context
|
||||
//
|
||||
// Method specifies the HTTP method (GET, POST, PUT, etc.).
|
||||
Method string `json:"method,omitempty"`
|
||||
// URL specifies either the URI being requested.
|
||||
URL string `json:"url,omitempty"`
|
||||
// Header contains the request header fields either received
|
||||
// by the server or to be sent by the client.
|
||||
Header map[string][]string `json:"headers,omitempty"`
|
||||
// Host specifies the host on which the URL is sought.
|
||||
Host string `json:"host,omitempty"`
|
||||
// RequestURI is the unmodified request-target of the
|
||||
// Request-Line (RFC 7230, Section 3.1.1) as sent by the client
|
||||
// to a server. Usually the URL field should be used instead.
|
||||
// It is an error to set this field in an HTTP client request.
|
||||
RequestURI string `json:"request_uri,omitempty"`
|
||||
if options.SigningKey == "" {
|
||||
key, err := cryptutil.NewSigningKey()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("authorize: couldn't generate signing key: %w", err)
|
||||
}
|
||||
e.jwk = key
|
||||
pubKeyBytes, err := cryptutil.EncodePublicKey(&key.PublicKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("authorize: encode public key: %w", err)
|
||||
}
|
||||
log.Info().Interface("PublicKey", pubKeyBytes).Msg("authorize: ecdsa public key")
|
||||
} else {
|
||||
decodedCert, err := base64.StdEncoding.DecodeString(options.SigningKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("authorize: failed to decode certificate cert %v: %w", decodedCert, err)
|
||||
}
|
||||
keyBytes, err := cryptutil.DecodePrivateKey((decodedCert))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("authorize: couldn't generate signing key: %w", err)
|
||||
}
|
||||
e.jwk = keyBytes
|
||||
}
|
||||
|
||||
// Connection context
|
||||
//
|
||||
// ClientCertificate is the PEM-encoded public certificate used for the user's TLS connection.
|
||||
ClientCertificate string `json:"client_certificate"`
|
||||
authzPolicy, err := readPolicy("/authz.rego")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error loading rego policy: %w", err)
|
||||
}
|
||||
|
||||
// Device context
|
||||
//
|
||||
// todo(bdd): Use the peer TLS certificate to bind device state with a request
|
||||
e.rego = rego.New(
|
||||
rego.Store(inmem.NewFromObject(map[string]interface{}{
|
||||
"admins": options.Administrators,
|
||||
"route_policies": options.Policies,
|
||||
})),
|
||||
rego.Module("pomerium.authz", string(authzPolicy)),
|
||||
rego.Query("result = data.pomerium.authz"),
|
||||
)
|
||||
|
||||
e.query, err = e.rego.PrepareForEval(context.Background())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error preparing rego query: %w", err)
|
||||
}
|
||||
|
||||
return e, nil
|
||||
}
|
||||
|
||||
// Evaluate evaluates the policy against the request.
|
||||
func (e *Evaluator) Evaluate(ctx context.Context, req *Request) (*Result, error) {
|
||||
isValid, err := isValidClientCertificate(e.clientCA, req.HTTP.ClientCertificate)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error validating client certificate: %w", err)
|
||||
}
|
||||
|
||||
res, err := e.query.Eval(ctx, rego.EvalInput(e.newInput(req, isValid)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error evaluating rego policy: %w", err)
|
||||
}
|
||||
|
||||
deny := getDenyVar(res[0].Bindings.WithoutWildcards())
|
||||
if len(deny) > 0 {
|
||||
return &deny[0], nil
|
||||
}
|
||||
|
||||
signedJWT, err := e.getSignedJWT(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error signing JWT: %w", err)
|
||||
}
|
||||
|
||||
allow := allowed(res[0].Bindings.WithoutWildcards())
|
||||
if allow {
|
||||
return &Result{
|
||||
Status: http.StatusOK,
|
||||
Message: "OK",
|
||||
SignedJWT: signedJWT,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if req.Session.ID == "" {
|
||||
return &Result{
|
||||
Status: http.StatusUnauthorized,
|
||||
Message: "login required",
|
||||
SignedJWT: signedJWT,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &Result{
|
||||
Status: http.StatusForbidden,
|
||||
Message: "forbidden",
|
||||
SignedJWT: signedJWT,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (e *Evaluator) getSignedJWT(req *Request) (string, error) {
|
||||
signer, err := jose.NewSigner(jose.SigningKey{
|
||||
Algorithm: jose.ES256,
|
||||
Key: e.jwk,
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"iss": e.authenticateHost,
|
||||
}
|
||||
if u, err := url.Parse(req.HTTP.URL); err == nil {
|
||||
payload["aud"] = u.Hostname()
|
||||
}
|
||||
if s, ok := req.DataBrokerData.Get("type.googleapis.com/session.Session", req.Session.ID).(*session.Session); ok {
|
||||
if tm, err := ptypes.Timestamp(s.GetIdToken().GetExpiresAt()); err == nil {
|
||||
payload["exp"] = tm.Unix()
|
||||
}
|
||||
if tm, err := ptypes.Timestamp(s.GetIdToken().GetIssuedAt()); err == nil {
|
||||
payload["iat"] = tm.Unix()
|
||||
}
|
||||
if u, ok := req.DataBrokerData.Get("type.googleapis.com/user.User", s.GetUserId()).(*user.User); ok {
|
||||
payload["sub"] = u.GetId()
|
||||
payload["email"] = u.GetEmail()
|
||||
}
|
||||
if du, ok := req.DataBrokerData.Get("type.googleapis.com/directory.User", s.GetUserId()).(*directory.User); ok {
|
||||
payload["groups"] = du.GetGroups()
|
||||
}
|
||||
}
|
||||
|
||||
bs, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
jws, err := signer.Sign(bs)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return jws.CompactSerialize()
|
||||
}
|
||||
|
||||
type input struct {
|
||||
DataBrokerData DataBrokerData `json:"databroker_data"`
|
||||
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.DataBrokerData = req.DataBrokerData
|
||||
i.HTTP = req.HTTP
|
||||
i.Session = req.Session
|
||||
i.IsValidClientCertificate = isValidClientCertificate
|
||||
return i
|
||||
}
|
||||
|
||||
type (
|
||||
// Request is the request data used for the evaluator.
|
||||
Request struct {
|
||||
DataBrokerData DataBrokerData `json:"databroker_data"`
|
||||
HTTP RequestHTTP `json:"http"`
|
||||
Session RequestSession `json:"session"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
ImpersonateEmail string `json:"impersonate_email"`
|
||||
ImpersonateGroups []string `json:"impersonate_groups"`
|
||||
}
|
||||
)
|
||||
|
||||
// Result is the result of evaluation.
|
||||
type Result struct {
|
||||
Status int
|
||||
Message string
|
||||
SignedJWT string
|
||||
}
|
||||
|
||||
func allowed(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().Err(err).Msg("invalid type in deny")
|
||||
continue
|
||||
}
|
||||
msg := fmt.Sprint(denial[1])
|
||||
|
||||
results = append(results, Result{
|
||||
Status: status,
|
||||
Message: msg,
|
||||
})
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// DataBrokerData stores the data broker data by type => id => record
|
||||
type DataBrokerData map[string]map[string]interface{}
|
||||
|
||||
// Get gets a record from the DataBrokerData.
|
||||
func (dbd DataBrokerData) Get(typeURL, id string) interface{} {
|
||||
m, ok := dbd[typeURL]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return m[id]
|
||||
}
|
||||
|
||||
// Update updates a record in the DataBrokerData.
|
||||
func (dbd DataBrokerData) Update(record *databroker.Record) {
|
||||
db, ok := dbd[record.GetType()]
|
||||
if !ok {
|
||||
db = make(map[string]interface{})
|
||||
dbd[record.GetType()] = db
|
||||
}
|
||||
|
||||
if record.GetDeletedAt() != nil {
|
||||
delete(db, record.GetId())
|
||||
} else {
|
||||
if obj, err := unmarshalAny(record.GetData()); err == nil {
|
||||
db[record.GetId()] = obj
|
||||
} else {
|
||||
log.Warn().Err(err).Msg("failed to unmarshal unknown any type")
|
||||
delete(db, record.GetId())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func unmarshalAny(any *anypb.Any) (proto.Message, error) {
|
||||
messageType, err := protoregistry.GlobalTypes.FindMessageByURL(any.GetTypeUrl())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
msg := proto.MessageV1(messageType.New())
|
||||
return msg, ptypes.UnmarshalAny(any, msg)
|
||||
}
|
||||
|
|
67
authorize/evaluator/evaluator_test.go
Normal file
67
authorize/evaluator/evaluator_test.go
Normal file
|
@ -0,0 +1,67 @@
|
|||
package evaluator
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/grpc/directory"
|
||||
)
|
||||
|
||||
func TestJSONMarshal(t *testing.T) {
|
||||
dbd := DataBrokerData{
|
||||
"type.googleapis.com/directory.User": map[string]interface{}{
|
||||
"user1": &directory.User{
|
||||
Id: "user1",
|
||||
Groups: []string{"group1", "group2"},
|
||||
},
|
||||
},
|
||||
"type.googleapis.com/session.Session": map[string]interface{}{},
|
||||
"type.googleapis.com/user.User": map[string]interface{}{},
|
||||
}
|
||||
|
||||
bs, _ := json.Marshal(input{
|
||||
DataBrokerData: dbd,
|
||||
HTTP: RequestHTTP{
|
||||
Method: "GET",
|
||||
URL: "https://example.com",
|
||||
Headers: map[string]string{
|
||||
"Accept": "application/json",
|
||||
},
|
||||
ClientCertificate: "CLIENT_CERTIFICATE",
|
||||
},
|
||||
Session: RequestSession{
|
||||
ID: "SESSION_ID",
|
||||
ImpersonateEmail: "y@example.com",
|
||||
ImpersonateGroups: []string{"group1"},
|
||||
},
|
||||
IsValidClientCertificate: true,
|
||||
})
|
||||
assert.JSONEq(t, `{
|
||||
"databroker_data": {
|
||||
"type.googleapis.com/directory.User": {
|
||||
"user1": {
|
||||
"id": "user1",
|
||||
"groups": ["group1", "group2"]
|
||||
}
|
||||
},
|
||||
"type.googleapis.com/session.Session": {},
|
||||
"type.googleapis.com/user.User": {}
|
||||
},
|
||||
"http": {
|
||||
"client_certificate": "CLIENT_CERTIFICATE",
|
||||
"headers": {
|
||||
"Accept": "application/json"
|
||||
},
|
||||
"method": "GET",
|
||||
"url": "https://example.com"
|
||||
},
|
||||
"session": {
|
||||
"id": "SESSION_ID",
|
||||
"impersonate_email": "y@example.com",
|
||||
"impersonate_groups": ["group1"]
|
||||
},
|
||||
"is_valid_client_certificate": true
|
||||
}`, string(bs))
|
||||
}
|
|
@ -1,11 +1,15 @@
|
|||
package opa
|
||||
package evaluator
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
|
||||
lru "github.com/hashicorp/golang-lru"
|
||||
"github.com/rakyll/statik/fs"
|
||||
|
||||
_ "github.com/pomerium/pomerium/authorize/evaluator/opa/policy" // load static assets
|
||||
)
|
||||
|
||||
var isValidClientCertificateCache, _ = lru.New2Q(100)
|
||||
|
@ -56,3 +60,18 @@ func parseCertificate(pemStr string) (*x509.Certificate, error) {
|
|||
}
|
||||
return x509.ParseCertificate(block.Bytes)
|
||||
}
|
||||
|
||||
const statikNamespace = "rego"
|
||||
|
||||
func readPolicy(fn string) ([]byte, error) {
|
||||
statikFS, err := fs.NewWithNamespace(statikNamespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r, err := statikFS.Open(fn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer r.Close()
|
||||
return ioutil.ReadAll(r)
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package opa
|
||||
package evaluator
|
||||
|
||||
import (
|
||||
"testing"
|
|
@ -1,278 +1,6 @@
|
|||
//go:generate go run github.com/rakyll/statik -src=./policy -include=*.rego -ns rego -p policy
|
||||
//go:generate go fmt ./policy/statik.go
|
||||
|
||||
// Package opa implements the policy evaluator interface to make authorization
|
||||
// decisions.
|
||||
package opa
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/open-policy-agent/opa/rego"
|
||||
"github.com/open-policy-agent/opa/storage"
|
||||
"github.com/open-policy-agent/opa/storage/inmem"
|
||||
"github.com/rakyll/statik/fs"
|
||||
|
||||
"github.com/pomerium/pomerium/authorize/evaluator"
|
||||
_ "github.com/pomerium/pomerium/authorize/evaluator/opa/policy" // load static assets
|
||||
pb "github.com/pomerium/pomerium/internal/grpc/authorize"
|
||||
"github.com/pomerium/pomerium/internal/telemetry/trace"
|
||||
)
|
||||
|
||||
const statikNamespace = "rego"
|
||||
|
||||
var _ evaluator.Evaluator = &PolicyEvaluator{}
|
||||
|
||||
// PolicyEvaluator implements the evaluator interface using the open policy
|
||||
// agent framework. The Open Policy Agent (OPA, pronounced “oh-pa”) is an open
|
||||
// source, general-purpose policy engine that unifies policy enforcement across
|
||||
// the stack.
|
||||
// https://www.openpolicyagent.org/docs/latest/
|
||||
type PolicyEvaluator struct {
|
||||
// The in-memory store supports multi-reader/single-writer concurrency with
|
||||
// rollback so we leverage a RWMutex.
|
||||
mu sync.RWMutex
|
||||
store storage.Store
|
||||
isAuthorized rego.PreparedEvalQuery
|
||||
clientCA string
|
||||
}
|
||||
|
||||
// Options represent OPA's evaluator configurations.
|
||||
type Options struct {
|
||||
// AuthorizationPolicy accepts custom rego code which can be used to
|
||||
// apply custom authorization policy.
|
||||
// Defaults to authorization policy defined in config.yaml's policy.
|
||||
AuthorizationPolicy string
|
||||
// Data maps data that will be bound and
|
||||
Data map[string]interface{}
|
||||
}
|
||||
|
||||
// New creates a new OPA policy evaluator.
|
||||
func New(ctx context.Context, opts *Options) (*PolicyEvaluator, error) {
|
||||
var pe PolicyEvaluator
|
||||
pe.store = inmem.New()
|
||||
|
||||
if opts.Data == nil {
|
||||
return nil, errors.New("opa: cannot create new evaluator without data")
|
||||
}
|
||||
if opts.AuthorizationPolicy == "" {
|
||||
b, err := readPolicy("/authz.rego")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
opts.AuthorizationPolicy = string(b)
|
||||
}
|
||||
if err := pe.PutData(ctx, opts.Data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := pe.UpdatePolicy(ctx, opts.AuthorizationPolicy); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pe, nil
|
||||
}
|
||||
|
||||
// UpdatePolicy takes authorization and privilege access management rego code
|
||||
// as an input and updates the prepared policy evaluator.
|
||||
func (pe *PolicyEvaluator) UpdatePolicy(ctx context.Context, authz string) error {
|
||||
ctx, span := trace.StartSpan(ctx, "authorize.evaluator.opa.UpdatePolicy")
|
||||
defer span.End()
|
||||
|
||||
var err error
|
||||
pe.mu.Lock()
|
||||
defer pe.mu.Unlock()
|
||||
|
||||
r := rego.New(
|
||||
rego.Store(pe.store),
|
||||
rego.Module("pomerium.authz", authz),
|
||||
rego.Query("result = data.pomerium.authz"),
|
||||
)
|
||||
pe.isAuthorized, err = r.PrepareForEval(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("opa: prepare policy: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsAuthorized determines if a given request input is authorized.
|
||||
func (pe *PolicyEvaluator) IsAuthorized(ctx context.Context, req *evaluator.Request) (*pb.IsAuthorizedReply, error) {
|
||||
ctx, span := trace.StartSpan(ctx, "authorize.evaluator.opa.IsAuthorized")
|
||||
defer span.End()
|
||||
return pe.runBoolQuery(ctx, req, pe.isAuthorized)
|
||||
}
|
||||
|
||||
// PutData adds (or replaces if the mapping key is the same) contextual data
|
||||
// for making policy decisions.
|
||||
func (pe *PolicyEvaluator) PutData(ctx context.Context, data map[string]interface{}) error {
|
||||
ctx, span := trace.StartSpan(ctx, "authorize.evaluator.opa.PutData")
|
||||
defer span.End()
|
||||
|
||||
pe.mu.Lock()
|
||||
defer pe.mu.Unlock()
|
||||
|
||||
if ca, ok := data["client_ca"].(string); ok {
|
||||
pe.clientCA = ca
|
||||
}
|
||||
|
||||
txn, err := pe.store.NewTransaction(ctx, storage.WriteParams)
|
||||
if err != nil {
|
||||
return fmt.Errorf("opa: bad transaction: %w", err)
|
||||
}
|
||||
if err := pe.store.Write(ctx, txn, storage.ReplaceOp, storage.Path{}, data); err != nil {
|
||||
pe.store.Abort(ctx, txn)
|
||||
return fmt.Errorf("opa: write failed %v : %w", data, err)
|
||||
}
|
||||
if err := pe.store.Commit(ctx, txn); err != nil {
|
||||
return fmt.Errorf("opa: commit failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func decisionFromInterface(i interface{}) (*pb.IsAuthorizedReply, error) {
|
||||
var d pb.IsAuthorizedReply
|
||||
var ok bool
|
||||
m, ok := i.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, errors.New("interface must be a map")
|
||||
}
|
||||
if d.Allow, ok = m["allow"].(bool); !ok {
|
||||
return nil, errors.New("allow should be bool")
|
||||
}
|
||||
if d.SessionExpired, ok = m["expired"].(bool); !ok {
|
||||
return nil, errors.New("expired should be bool")
|
||||
}
|
||||
|
||||
switch v := m["deny"].(type) {
|
||||
case []interface{}:
|
||||
for _, cause := range v {
|
||||
if c, ok := cause.(string); ok {
|
||||
d.DenyReasons = append(d.DenyReasons, c)
|
||||
}
|
||||
}
|
||||
case string:
|
||||
d.DenyReasons = []string{v}
|
||||
}
|
||||
|
||||
if v, ok := m["user"].(string); ok {
|
||||
d.User = v
|
||||
}
|
||||
|
||||
if v, ok := m["email"].(string); ok {
|
||||
d.Email = v
|
||||
}
|
||||
|
||||
switch v := m["groups"].(type) {
|
||||
case []interface{}:
|
||||
for _, cause := range v {
|
||||
if c, ok := cause.(string); ok {
|
||||
d.Groups = append(d.Groups, c)
|
||||
}
|
||||
}
|
||||
case string:
|
||||
d.Groups = []string{v}
|
||||
}
|
||||
|
||||
if v, ok := m["signed_jwt"].(string); ok {
|
||||
d.SignedJwt = v
|
||||
}
|
||||
|
||||
// http_status = [200, "OK", { "HEADER": "VALUE" }]
|
||||
if v, ok := m["http_status"].([]interface{}); ok {
|
||||
d.HttpStatus = new(pb.HTTPStatus)
|
||||
if len(v) > 0 {
|
||||
d.HttpStatus.Code = int32(anyToInt(v[0]))
|
||||
}
|
||||
if len(v) > 1 {
|
||||
if msg, ok := v[1].(string); ok {
|
||||
d.HttpStatus.Message = msg
|
||||
}
|
||||
}
|
||||
if len(v) > 2 {
|
||||
if headers, ok := v[2].(map[string]interface{}); ok {
|
||||
d.HttpStatus.Headers = make(map[string]string)
|
||||
for hk, hv := range headers {
|
||||
d.HttpStatus.Headers[hk] = fmt.Sprint(hv)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &d, nil
|
||||
}
|
||||
|
||||
func (pe *PolicyEvaluator) runBoolQuery(ctx context.Context, req *evaluator.Request, q rego.PreparedEvalQuery) (*pb.IsAuthorizedReply, error) {
|
||||
pe.mu.RLock()
|
||||
defer pe.mu.RUnlock()
|
||||
|
||||
// `opa test` doesn't support custom function, so we'll pre-compute is_valid_client_certificate
|
||||
isValid, err := isValidClientCertificate(pe.clientCA, req.ClientCertificate)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("certificate error: %w", err)
|
||||
}
|
||||
input := struct {
|
||||
*evaluator.Request
|
||||
IsValidClientCertificate bool `json:"is_valid_client_certificate"`
|
||||
}{
|
||||
Request: req,
|
||||
IsValidClientCertificate: isValid,
|
||||
}
|
||||
|
||||
rs, err := q.Eval(ctx, rego.EvalInput(input))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("eval query: %w", err)
|
||||
} else if len(rs) == 0 {
|
||||
return nil, fmt.Errorf("empty eval result set %v", rs)
|
||||
}
|
||||
bindings := rs[0].Bindings.WithoutWildcards()["result"]
|
||||
return decisionFromInterface(bindings)
|
||||
}
|
||||
|
||||
func readPolicy(fn string) ([]byte, error) {
|
||||
statikFS, err := fs.NewWithNamespace(statikNamespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r, err := statikFS.Open(fn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer r.Close()
|
||||
return ioutil.ReadAll(r)
|
||||
}
|
||||
|
||||
func anyToInt(obj interface{}) int {
|
||||
switch v := obj.(type) {
|
||||
case int:
|
||||
return v
|
||||
case int64:
|
||||
return int(v)
|
||||
case int32:
|
||||
return int(v)
|
||||
case int16:
|
||||
return int(v)
|
||||
case int8:
|
||||
return int(v)
|
||||
case uint64:
|
||||
return int(v)
|
||||
case uint32:
|
||||
return int(v)
|
||||
case uint16:
|
||||
return int(v)
|
||||
case uint8:
|
||||
return int(v)
|
||||
case json.Number:
|
||||
i, _ := v.Int64()
|
||||
return int(i)
|
||||
case string:
|
||||
i, _ := strconv.Atoi(v)
|
||||
return i
|
||||
default:
|
||||
i, _ := strconv.Atoi(fmt.Sprint(v))
|
||||
return i
|
||||
}
|
||||
}
|
||||
//go:generate go run github.com/rakyll/statik -src=./policy -include=*.rego -ns rego -p policy
|
||||
//go:generate go fmt ./policy/statik.go
|
||||
|
|
|
@ -1,123 +0,0 @@
|
|||
package opa
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
"gopkg.in/square/go-jose.v2/jwt"
|
||||
|
||||
"github.com/pomerium/pomerium/authorize/evaluator"
|
||||
"github.com/pomerium/pomerium/config"
|
||||
)
|
||||
|
||||
func Test_Eval(t *testing.T) {
|
||||
t.Parallel()
|
||||
type Identity struct {
|
||||
User string `json:"user,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
Groups []string `json:"groups,omitempty"`
|
||||
ImpersonateEmail string `json:"impersonate_email,omitempty"`
|
||||
ImpersonateGroups []string `json:"impersonate_groups,omitempty"`
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
policies []config.Policy
|
||||
route string
|
||||
Identity *Identity
|
||||
admins []string
|
||||
secret string
|
||||
want bool
|
||||
}{
|
||||
{"valid domain", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedDomains: []string{"example.com"}}}, "from.example", &Identity{Email: "user@example.com"}, nil, "secret", true},
|
||||
{"valid domain with admins", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedDomains: []string{"example.com"}}}, "from.example", &Identity{Email: "user@example.com"}, []string{"admin@example.com"}, "secret", true},
|
||||
{"invalid domain prepend", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedDomains: []string{"example.com"}}}, "from.example", &Identity{Email: "a@1example.com"}, nil, "secret", false},
|
||||
{"invalid domain postpend", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedDomains: []string{"example.com"}}}, "from.example", &Identity{Email: "user@example.com2"}, nil, "secret", false},
|
||||
{"valid group", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedGroups: []string{"admin"}}}, "from.example", &Identity{Email: "user@example.com", Groups: []string{"admin"}}, nil, "secret", true},
|
||||
{"invalid group", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedGroups: []string{"admin"}}}, "from.example", &Identity{Email: "user@example.com", Groups: []string{"everyone"}}, nil, "secret", false},
|
||||
{"invalid empty", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedGroups: []string{"admin"}}}, "from.example", &Identity{Email: "user@example.com", Groups: []string{""}}, nil, "secret", false},
|
||||
{"valid group multiple", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedGroups: []string{"admin"}}}, "from.example", &Identity{Email: "user@example.com", Groups: []string{"everyone", "admin"}}, nil, "secret", true},
|
||||
{"invalid group multiple", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedGroups: []string{"admin"}}}, "from.example", &Identity{Email: "user@example.com", Groups: []string{"everyones", "sadmin"}}, nil, "secret", false},
|
||||
{"valid user email", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedUsers: []string{"user@example.com"}}}, "from.example", &Identity{Email: "user@example.com"}, nil, "secret", true},
|
||||
{"invalid user email", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedUsers: []string{"user@example.com"}}}, "from.example", &Identity{Email: "user2@example.com"}, nil, "secret", false},
|
||||
{"empty everything", []config.Policy{{From: "https://from.example", To: "https://to.example"}}, "from.example", &Identity{Email: "user2@example.com"}, nil, "secret", false},
|
||||
{"empty policy", []config.Policy{}, "from.example", &Identity{Email: "user2@example.com"}, nil, "secret", false},
|
||||
// impersonation related
|
||||
{"admin not impersonating allowed", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedDomains: []string{"example.com"}}}, "from.example", &Identity{Email: "admin@example.com"}, []string{"admin@example.com"}, "secret", true},
|
||||
{"admin not impersonating denied", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedDomains: []string{"example.com"}}}, "from.example", &Identity{Email: "admin@admin-domain.com"}, []string{"admin@admin-domain.com"}, "secret", false},
|
||||
{"impersonating match domain", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedDomains: []string{"example.com"}}}, "from.example", &Identity{Email: "admin@admin-domain.com", ImpersonateEmail: "user@example.com"}, []string{"admin@admin-domain.com"}, "secret", true},
|
||||
{"impersonating does not match domain", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedDomains: []string{"example.com"}}}, "from.example", &Identity{Email: "admin@admin-domain.com", ImpersonateEmail: "user@not-example.com"}, []string{"admin@admin-domain.com"}, "secret", false},
|
||||
{"impersonating match email", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedUsers: []string{"user@example.com"}}}, "from.example", &Identity{Email: "admin@admin-domain.com", ImpersonateEmail: "user@example.com"}, []string{"admin@admin-domain.com"}, "secret", true},
|
||||
{"impersonating does not match email", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedUsers: []string{"user@example.com"}}}, "from.example", &Identity{Email: "admin@admin-domain.com", ImpersonateEmail: "user@not-example.com"}, []string{"admin@admin-domain.com"}, "secret", false},
|
||||
{"impersonating match groups", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedGroups: []string{"support"}}}, "from.example", &Identity{Email: "admin@admin-domain.com", ImpersonateGroups: []string{"support"}}, []string{"admin@admin-domain.com"}, "secret", true},
|
||||
{"impersonating match many groups", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedGroups: []string{"support"}}}, "from.example", &Identity{Email: "admin@admin-domain.com", ImpersonateGroups: []string{"a", "b", "c", "support"}}, []string{"admin@admin-domain.com"}, "secret", true},
|
||||
{"impersonating does not match groups", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedGroups: []string{"support"}}}, "from.example", &Identity{Email: "admin@admin-domain.com", ImpersonateGroups: []string{"not support"}}, []string{"admin@admin-domain.com"}, "secret", false},
|
||||
{"impersonating does not match many groups", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedGroups: []string{"support"}}}, "from.example", &Identity{Email: "admin@admin-domain.com", ImpersonateGroups: []string{"not support", "b", "c"}}, []string{"admin@admin-domain.com"}, "secret", false},
|
||||
{"impersonating does not match empty groups", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedGroups: []string{"support"}}}, "from.example", &Identity{Email: "admin@admin-domain.com", ImpersonateGroups: []string{""}}, []string{"admin@admin-domain.com"}, "secret", false},
|
||||
// jwt validation
|
||||
{"bad jwt shared secret", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedDomains: []string{"example.com"}}}, "from.example", &Identity{Email: "user@example.com"}, nil, "bad-secret", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
for i := range tt.policies {
|
||||
if err := (&tt.policies[i]).Validate(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
key := []byte("secret")
|
||||
sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.HS256, Key: key},
|
||||
(&jose.SignerOptions{}).WithType("JWT"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cl := jwt.Claims{
|
||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||
Expiry: jwt.NewNumericDate(time.Now().Add(time.Hour)),
|
||||
Audience: jwt.Audience{tt.route},
|
||||
}
|
||||
rawJWT, err := jwt.Signed(sig).Claims(cl).Claims(tt.Identity).CompactSerialize()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"route_policies": tt.policies,
|
||||
"admins": tt.admins,
|
||||
"shared_key": tt.secret,
|
||||
}
|
||||
pe, err := New(context.Background(), &Options{Data: data})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req := &evaluator.Request{
|
||||
Host: tt.route,
|
||||
URL: "https://" + tt.route,
|
||||
User: rawJWT,
|
||||
}
|
||||
got, err := pe.IsAuthorized(context.TODO(), req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.GetAllow() != tt.want {
|
||||
t.Errorf("pe.Eval() = %v, want %v", got.GetAllow(), tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_anyToInt(t *testing.T) {
|
||||
assert.Equal(t, 5, anyToInt("5"))
|
||||
assert.Equal(t, 7, anyToInt(7))
|
||||
assert.Equal(t, 9, anyToInt(int8(9)))
|
||||
assert.Equal(t, 9, anyToInt(int16(9)))
|
||||
assert.Equal(t, 9, anyToInt(int32(9)))
|
||||
assert.Equal(t, 9, anyToInt(int64(9)))
|
||||
assert.Equal(t, 11, anyToInt(uint8(11)))
|
||||
assert.Equal(t, 11, anyToInt(uint16(11)))
|
||||
assert.Equal(t, 11, anyToInt(uint32(11)))
|
||||
assert.Equal(t, 11, anyToInt(uint64(11)))
|
||||
assert.Equal(t, 13, anyToInt(13.0))
|
||||
}
|
|
@ -1,84 +1,91 @@
|
|||
package pomerium.authz
|
||||
|
||||
import data.route_policies
|
||||
import data.shared_key
|
||||
|
||||
default allow = false
|
||||
|
||||
route := first_allowed_route(input.url)
|
||||
|
||||
http_status = [495, "invalid client certificate"]{
|
||||
not input.is_valid_client_certificate
|
||||
}
|
||||
route := first_allowed_route(input.http.url)
|
||||
session := input.databroker_data["type.googleapis.com/session.Session"][input.session.id]
|
||||
user := input.databroker_data["type.googleapis.com/user.User"][session.user_id]
|
||||
directory_user := input.databroker_data["type.googleapis.com/directory.User"][session.user_id]
|
||||
|
||||
|
||||
# allow public
|
||||
allow {
|
||||
route_policies[route].AllowPublicUnauthenticatedAccess == true
|
||||
data.route_policies[route].AllowPublicUnauthenticatedAccess == true
|
||||
}
|
||||
|
||||
# allow cors preflight
|
||||
allow {
|
||||
route_policies[route].CORSAllowPreflight == true
|
||||
input.method == "OPTIONS"
|
||||
count(object.get(input.headers, "Access-Control-Request-Method", [])) > 0
|
||||
count(object.get(input.headers, "Origin", [])) > 0
|
||||
data.route_policies[route].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 by email
|
||||
allow {
|
||||
token.payload.email = route_policies[route].allowed_users[_]
|
||||
token.valid
|
||||
count(deny)==0
|
||||
user.email == data.route_policies[route].allowed_users[_]
|
||||
}
|
||||
|
||||
# allow group
|
||||
allow {
|
||||
some group
|
||||
token.payload.groups[group] == route_policies[route].allowed_groups[_]
|
||||
token.valid
|
||||
count(deny)==0
|
||||
directory_user.groups[_] = group
|
||||
data.route_policies[route].allowed_groups[_] = group
|
||||
}
|
||||
|
||||
# allow by impersonate email
|
||||
allow {
|
||||
token.payload.impersonate_email = route_policies[route].allowed_users[_]
|
||||
token.valid
|
||||
count(deny)==0
|
||||
data.route_policies[route].allowed_users[_] = input.session.impersonate_email
|
||||
}
|
||||
|
||||
# allow by impersonate group
|
||||
allow {
|
||||
some group
|
||||
token.payload.impersonate_groups[group] == route_policies[route].allowed_groups[_]
|
||||
token.valid
|
||||
count(deny)==0
|
||||
input.session.impersonate_groups[_] = group
|
||||
data.route_policies[route].allowed_groups[_] = group
|
||||
}
|
||||
|
||||
# allow by domain
|
||||
allow {
|
||||
some domain
|
||||
email_in_domain(token.payload.email, route_policies[route].allowed_domains[domain])
|
||||
token.valid
|
||||
count(deny)==0
|
||||
email_in_domain(user.email, data.route_policies[route].allowed_domains[domain])
|
||||
}
|
||||
|
||||
# allow by impersonate domain
|
||||
allow {
|
||||
some domain
|
||||
email_in_domain(token.payload.impersonate_email, route_policies[route].allowed_domains[domain])
|
||||
token.valid
|
||||
count(deny)==0
|
||||
email_in_domain(input.session.impersonate_email, data.route_policies[route].allowed_domains[domain])
|
||||
}
|
||||
|
||||
# allow pomerium urls
|
||||
allow {
|
||||
contains(input.url, "/.pomerium/")
|
||||
not contains(input.url,"/.pomerium/admin")
|
||||
contains(input.http.url, "/.pomerium/")
|
||||
not contains(input.http.url, "/.pomerium/admin")
|
||||
}
|
||||
|
||||
# allow user is admin
|
||||
allow {
|
||||
element_in_list(data.admins, input.user.email)
|
||||
contains(input.http.url, ".pomerium/admin")
|
||||
}
|
||||
|
||||
# deny non-admin users from accesing admin routes
|
||||
deny[reason] {
|
||||
reason = [403, "user is not admin"]
|
||||
not element_in_list(data.admins, user.email)
|
||||
contains(input.http.url,".pomerium/admin")
|
||||
}
|
||||
|
||||
deny[reason] {
|
||||
reason = [495, "invalid client certificate"]
|
||||
is_boolean(input.is_valid_client_certificate)
|
||||
not input.is_valid_client_certificate
|
||||
}
|
||||
|
||||
# returns the first matching route
|
||||
first_allowed_route(input_url) = route {
|
||||
route := [route | some route ; allowed_route(input.url, route_policies[route])][0]
|
||||
route := [route | some route ; allowed_route(input.http.url, data.route_policies[route])][0]
|
||||
}
|
||||
|
||||
allowed_route(input_url, policy){
|
||||
|
@ -142,53 +149,6 @@ email_in_domain(email, domain) {
|
|||
x[1] == domain
|
||||
}
|
||||
|
||||
default expired = false
|
||||
|
||||
expired {
|
||||
now_seconds:=time.now_ns()/1e9
|
||||
expiry < now_seconds
|
||||
}
|
||||
|
||||
deny["token is expired (exp)"]{
|
||||
expired
|
||||
}
|
||||
|
||||
deny[sprintf("token has bad audience (aud): %s not in %+v",[input.host,audiences])]{
|
||||
not element_in_list(audiences,input.host)
|
||||
}
|
||||
|
||||
# allow user is admin
|
||||
allow {
|
||||
element_in_list(data.admins, token.payload.email)
|
||||
token.valid
|
||||
count(deny)==0
|
||||
contains(input.url,".pomerium/admin")
|
||||
}
|
||||
|
||||
|
||||
# deny non-admin users from accesing admin routes
|
||||
deny["user is not admin"]{
|
||||
not element_in_list(data.admins, token.payload.email)
|
||||
contains(input.url,".pomerium/admin")
|
||||
}
|
||||
|
||||
token = {"payload": payload, "valid": valid} {
|
||||
[valid, header, payload] := io.jwt.decode_verify(
|
||||
input.user, {
|
||||
"secret": shared_key,
|
||||
"aud": input.host,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
user:=token.payload.user
|
||||
email:=token.payload.email
|
||||
groups:=token.payload.groups
|
||||
audiences:=token.payload.aud
|
||||
expiry:=token.payload.exp
|
||||
signed_jwt:=io.jwt.encode_sign({"alg": "ES256"}, token.payload, data.signing_key)
|
||||
|
||||
|
||||
element_in_list(list, elem) {
|
||||
list[_] = elem
|
||||
}
|
||||
|
|
|
@ -1,143 +1,131 @@
|
|||
package pomerium.authz
|
||||
|
||||
jwt_header := {
|
||||
"typ": "JWT",
|
||||
"alg": "HS256"
|
||||
}
|
||||
signing_key := {
|
||||
"kty": "oct",
|
||||
"k": "OkFmqMK9U0dmPhMCW0VYy6D_raJKwEJsMdxqdnukThzko3D_XrsihwYE0pxrUSpm0JTrW2QpIz4rT1vdEvZw67WP4xrqjiwyd7PgpPTD5xvQBM7TIKiSW0X2R0pfq_OItszPQRtb7VirrSbGJiLNS-NJMMrYVKWWtUbVSTXEjL7VcFqML5PiSe7XDmyCZjpgEpfE5Q82zIeXM2sLrz6HW2A9IwGk7mWS0c57R_2JGyFO2tCA4zEIYhWvLE62Os2tZ6YrrwdB8n35jlPpgUE6poEvIU20lPLaocozXYMqAku-KJnloJlAzKg2Xa_0iSiSgSAumx44B3n7DQjg3jPhRg"
|
||||
}
|
||||
shared_key := base64url.decode(signing_key.k)
|
||||
|
||||
test_email_allowed {
|
||||
user := io.jwt.encode_sign(jwt_header, {
|
||||
"aud": ["example.com"],
|
||||
"email": "joe@example.com"
|
||||
}, signing_key)
|
||||
|
||||
allow with data.route_policies as [{
|
||||
"source": "example.com",
|
||||
"allowed_users": ["joe@example.com"]
|
||||
}] with data.signing_key as signing_key with data.shared_key as shared_key with input as {
|
||||
"url": "http://example.com",
|
||||
"host": "example.com",
|
||||
"user": user
|
||||
}
|
||||
allow with
|
||||
data.route_policies as [{
|
||||
"source": "example.com",
|
||||
"allowed_users": ["x@example.com"]
|
||||
}] with
|
||||
input.databroker_data as {
|
||||
"type.googleapis.com/session.Session": {
|
||||
"session1": {
|
||||
"user_id": "user1"
|
||||
}
|
||||
},
|
||||
"type.googleapis.com/user.User": {
|
||||
"user1": {
|
||||
"email": "x@example.com"
|
||||
}
|
||||
}
|
||||
} with
|
||||
input.http as { "url": "http://example.com" } with
|
||||
input.session as { "id": "session1" }
|
||||
}
|
||||
|
||||
test_example {
|
||||
user := io.jwt.encode_sign(jwt_header, {
|
||||
"aud": ["example.com"],
|
||||
"email": "joe@example.com"
|
||||
}, signing_key)
|
||||
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 data.signing_key as signing_key with data.shared_key as shared_key with input as {
|
||||
"url": "http://example.com/b",
|
||||
"host": "example.com",
|
||||
"user": user
|
||||
}
|
||||
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 {
|
||||
user := io.jwt.encode_sign(jwt_header, {
|
||||
"aud": ["example.com"],
|
||||
"email": "joe@example.com"
|
||||
}, signing_key)
|
||||
|
||||
not allow with data.route_policies as [{
|
||||
"source": "example.com",
|
||||
"allowed_users": ["bob@example.com"]
|
||||
}] with data.signing_key as signing_key with data.shared_key as shared_key with input as {
|
||||
"url": "http://example.com",
|
||||
"host": "example.com",
|
||||
"user": user
|
||||
}
|
||||
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 as {
|
||||
"url": "http://example.com",
|
||||
"host": "example.com"
|
||||
}
|
||||
}
|
||||
test_public_denied {
|
||||
not allow with data.route_policies as [
|
||||
{
|
||||
"source": "example.com",
|
||||
"prefix": "/by-user",
|
||||
"allowed_users": ["bob@example.com"]
|
||||
},
|
||||
{
|
||||
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"
|
||||
}
|
||||
] with input as {
|
||||
"url": "http://example.com/by-user",
|
||||
"host": "example.com"
|
||||
}
|
||||
}
|
||||
|
||||
test_pomerium_allowed {
|
||||
allow with data.route_policies as [{
|
||||
"source": "example.com",
|
||||
"allowed_users": ["bob@example.com"]
|
||||
}] with input as {
|
||||
"url": "http://example.com/.pomerium/",
|
||||
"host": "example.com"
|
||||
}
|
||||
allow with
|
||||
data.route_policies as [{
|
||||
"source": "example.com",
|
||||
"allowed_users": ["bob@example.com"]
|
||||
}] with
|
||||
input.http as { "url": "http://example.com/.pomerium/" }
|
||||
}
|
||||
test_pomerium_denied {
|
||||
not allow with data.route_policies as [{
|
||||
"source": "example.com",
|
||||
"allowed_users": ["bob@example.com"]
|
||||
}] with input as {
|
||||
"url": "http://example.com/.pomerium/admin",
|
||||
"host": "example.com"
|
||||
}
|
||||
not allow with
|
||||
data.route_policies as [{
|
||||
"source": "example.com",
|
||||
"allowed_users": ["bob@example.com"]
|
||||
}] with
|
||||
input.http as {
|
||||
"url": "http://example.com/.pomerium/admin",
|
||||
"host": "example.com"
|
||||
}
|
||||
}
|
||||
|
||||
test_cors_preflight_allowed {
|
||||
allow with data.route_policies as [{
|
||||
"source": "example.com",
|
||||
"allowed_users": ["bob@example.com"],
|
||||
"CORSAllowPreflight": true
|
||||
}] with input as {
|
||||
"url": "http://example.com/",
|
||||
"host": "example.com",
|
||||
"method": "OPTIONS",
|
||||
"headers": {
|
||||
"Origin": ["someorigin"],
|
||||
"Access-Control-Request-Method": ["GET"]
|
||||
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 as {
|
||||
"url": "http://example.com/",
|
||||
"host": "example.com",
|
||||
"method": "OPTIONS",
|
||||
"headers": {
|
||||
"Origin": ["someorigin"],
|
||||
"Access-Control-Request-Method": ["GET"]
|
||||
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 {
|
||||
|
|
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue