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
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue