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:
Caleb Doxsey 2020-06-19 07:52:44 -06:00 committed by GitHub
parent 39cdb31170
commit dbd7f55b20
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
115 changed files with 8479 additions and 3584 deletions

View file

@ -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