8000 enhancement: Make `now` fully deterministic by haines · Pull Request #2353 · cerbos/cerbos · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

enhancement: Make now fully deterministic #2353

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Oct 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions cmd/cerbos/repl/internal/repl.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import (
"sort"
"strconv"
"strings"
"time"
"unicode"

"github.com/alecthomas/participle/v2"
Expand Down Expand Up @@ -411,7 +410,7 @@ func (r *REPL) evalExpr(expr string) (ref.Val, *exprpb.Type, error) {
return nil, nil, errSilent
}

val, _, err := conditions.Eval(env, ast, r.vars, time.Now)
val, _, err := conditions.Eval(env, ast, r.vars, conditions.Now())
if err != nil {
return nil, nil, err
}
Expand Down
1 change: 1 addition & 0 deletions cmd/cerbosctl/put/put_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ func testPutCmd(clientCtx *cmdclient.Context, globals *flagset.Globals) func(*te
"resource.leave_request.vdefault/acme.hr.uk",
"resource.leave_request.vstaging",
"resource.missing_attr.vdefault",
"resource.output_now.vdefault",
"resource.products.vdefault",
"resource.purchase_order.vdefault",
"resource.runtime_effective_derived_roles.vdefault",
Expand Down
22 changes: 16 additions & 6 deletions internal/conditions/cerbos_lib.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,21 @@ func (clib cerbosLib) ProgramOptions() []cel.ProgramOption {
return nil
}

type NowFunc = func() time.Time

// Now returns a NowFunc that always returns the time at which Now was called.
func Now() NowFunc {
now := time.Now()
return func() time.Time { return now }
}

// Eval returns the result of an evaluation of the ast and environment against the input vars,
// providing time-based functions with a static definition of the current time.
//
// The given nowFunc must return the same timestamp each time it is called.
//
// See https://pkg.go.dev/github.com/google/cel-go/cel#Program.Eval.
func Eval(env *cel.Env, ast *cel.Ast, vars any, nowFunc func() time.Time, opts ...cel.ProgramOption) (ref.Val, *cel.EvalDetails, error) {
func Eval(env *cel.Env, ast *cel.Ast, vars any, nowFunc NowFunc, opts ...cel.ProgramOption) (ref.Val, *cel.EvalDetails, error) {
programOpts := append([]cel.ProgramOption{cel.CustomDecorator(newTimeDecorator(nowFunc))}, opts...)
prg, err := env.Program(ast, programOpts...)
if err != nil {
Expand All @@ -141,13 +151,13 @@ func Eval(env *cel.Env, ast *cel.Ast, vars any, nowFunc func() time.Time, opts .
return prg.Eval(vars)
}

func newTimeDecorator(nowFunc func() time.Time) interpreter.InterpretableDecorator {
td := timeDecorator{now: nowFunc()}
func newTimeDecorator(nowFunc NowFunc) interpreter.InterpretableDecorator {
td := timeDecorator{nowFunc: nowFunc}
return td.decorate
}

type timeDecorator struct {
now time.Time
nowFunc NowFunc
}

func (t *timeDecorator) decorate(in interpreter.Interpretable) (interpreter.Interpretable, error) {
Expand All @@ -159,7 +169,7 @@ func (t *timeDecorator) decorate(in interpreter.Interpretable) (interpreter.Inte
funcName := call.Function()
switch funcName {
case nowFn:
return interpreter.NewConstValue(call.ID(), types.DefaultTypeAdapter.NativeToValue(t.now)), nil
return interpreter.NewConstValue(call.ID(), types.DefaultTypeAdapter.NativeToValue(t.nowFunc())), nil
case timeSinceFn:
return interpreter.NewCall(call.ID(), funcName, call.OverloadID(), call.Args(), func(values ...ref.Val) ref.Val {
if len(values) != 1 {
Expand All @@ -172,7 +182,7 @@ func (t *timeDecorator) decorate(in interpreter.Interpretable) (interpreter.Inte
return types.NoSuchOverloadErr()
}

return types.DefaultTypeAdapter.NativeToValue(t.now.Sub(ts))
return types.DefaultTypeAdapter.NativeToValue(t.nowFunc().Sub(ts))
}), nil
default:
return in, nil
Expand Down
2 changes: 1 addition & 1 deletion internal/conditions/cerbos_lib_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ func TestCerbosLib(t *testing.T) {
ast, issues := env.Compile(tc.expr)
is.NoError(issues.Err())

have, _, err := conditions.Eval(env, ast, cel.NoVars(), time.Now)
have, _, err := conditions.Eval(env, ast, cel.NoVars(), conditions.Now())
if tc.wantErr {
is.Error(err)
} else {
Expand Down
12 changes: 8 additions & 4 deletions internal/engine/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
runtimev1 "github.com/cerbos/cerbos/api/genpb/cerbos/runtime/v1"
schemav1 "github.com/cerbos/cerbos/api/genpb/cerbos/schema/v1"
"github.com/cerbos/cerbos/internal/audit"
"github.com/cerbos/cerbos/internal/conditions"
"github.com/cerbos/cerbos/internal/engine/planner"
"github.com/cerbos/cerbos/internal/engine/tracer"
"github.com/cerbos/cerbos/internal/namer"
Expand Down Expand Up @@ -87,6 +88,10 @@ func newCheckOptions(ctx context.Context, conf *Conf, opts ...CheckOpt) *CheckOp
opt(co)
}

if co.evalParams.nowFunc == nil {
co.evalParams.nowFunc = conditions.Now()
}

return co
}

Expand All @@ -105,6 +110,7 @@ func WithZapTraceSink(log *zap.Logger) CheckOpt {
}

// WithNowFunc sets the function for determining `now` during condition evaluation.
// The function should return the same timestamp every time it is invoked.
func WithNowFunc(nowFunc func() time.Time) CheckOpt {
return func(co *CheckOptions) {
co.evalParams.nowFunc = nowFunc
Expand Down Expand Up @@ -262,10 +268,8 @@ func (engine *Engine) doPlanResources(ctx context.Context, input *enginev1.PlanR

result := new(planner.PolicyPlanResult)
auditTrail := &auditv1.AuditTrail{EffectivePolicies: make(map[string]*policyv1.SourceAttributes, 2)} //nolint:mnd
now := opts.NowFunc()()
nowFn := func() time.Time { return now }
if policy := policySet.GetPrincipalPolicy(); policy != nil {
policyEvaluator := planner.PrincipalPolicyEvaluator{Policy: policy, Globals: opts.Globals(), NowFn: nowFn}
policyEvaluator := planner.PrincipalPolicyEvaluator{Policy: policy, Globals: opts.Globals(), NowFn: opts.NowFunc()}
result, err = policyEvaluator.EvaluateResourcesQueryPlan(ctx, input)
if err != nil {
return nil, nil, err
Expand All @@ -282,7 +286,7 @@ func (engine *Engine) doPlanResources(ctx context.Context, input *enginev1.PlanR
}

if policy := policySet.GetResourcePolicy(); policy != nil {
policyEvaluator := planner.ResourcePolicyEvaluator{Policy: policy, Globals: opts.Globals(), SchemaMgr: engine.schemaMgr, NowFn: nowFn}
policyEvaluator := planner.ResourcePolicyEvaluator{Policy: policy, Globals: opts.Globals(), SchemaMgr: engine.schemaMgr, NowFn: opts.NowFunc()}
plan, err := policyEvaluator.EvaluateResourcesQueryPlan(ctx, input)
if err != nil {
return nil, nil, err
Expand Down
49 changes: 42 additions & 7 deletions internal/engine/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,47 @@ func TestCheck(t *testing.T) {
))
})
}

t.Run("deterministic_now", func(t *testing.T) {
roles := []string{"user"}
actions := []string{"a", "b", "c"}

inputs := []*enginev1.CheckInput{
{
Principal: &enginev1.Principal{Id: "1", Roles: roles},
Resource: &enginev1.Resource{Kind: "output_now", Id: "1"},
Actions: actions,
},
{
Principal: &enginev1.Principal{Id: "2", Roles: roles},
Resource: &enginev1.Resource{Kind: "output_now", Id: "1"},
Actions: actions,
},
{
Principal: &enginev1.Principal{Id: "1", Roles: roles},
Resource: &enginev1.Resource{Kind: "output_now", Id: "2"},
Actions: actions,
},
{
Principal: &enginev1.Principal{Id: "2", Roles: roles},
Resource: &enginev1.Resource{Kind: "output_now", Id: "2"},
Actions: actions,
},
}

outputs, err := eng.Check(context.Background(), inputs)
require.NoError(t, err)
require.Len(t, outputs, len(inputs))

uniqueNows := make(map[string]struct{})
for _, output := range outputs {
require.Len(t, output.Outputs, 3)
for _, entry := range output.Outputs {
uniqueNows[entry.Val.GetStringValue()] = struct{}{}
}
}
require.Len(t, uniqueNows, 1)
})
}

func TestCheckWithLenientScopeSearch(t *testing.T) {
Expand Down Expand Up @@ -306,19 +347,13 @@ func TestQueryPlan(t *testing.T) {
IncludeMeta: true,
AuxData: auxData,
}
nowFnCallsCounter := 0
nowFn := func() time.Time {
nowFnCallsCounter++
return timestamp
}
response, err := eng.PlanResources(context.Background(), request, WithNowFunc(nowFn))
response, err := eng.PlanResources(context.Background(), request, WithNowFunc(func() time.Time { return timestamp }))
if tt.WantErr {
is.Error(err)
} else {
is.NoError(err)
is.NotNil(response)
is.Empty(cmp.Diff(tt.Want, response.Filter, protocmp.Transform()), "AST: %s\n%s\n", response.FilterDebug, protojson.Format(response.Filter))
is.Equal(1, nowFnCallsCounter, "time function should be called once")
}
})
}
Expand Down
4 changes: 1 addition & 3 deletions internal/engine/evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"fmt"
"reflect"
"sort"
"time"

"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
Expand Down Expand Up @@ -41,15 +40,14 @@ var ErrPolicyNotExecutable = errors.New("policy not executable")

type evalParams struct {
globals map[string]any
nowFunc func() time.Time
nowFunc conditions.NowFunc
defaultPolicyVersion string
lenientScopeSearch bool
}

func defaultEvalParams(conf *Conf) evalParams {
return evalParams{
globals: conf.Globals,
nowFunc: time.Now,
defaultPolicyVersion: conf.DefaultPolicyVersion,
lenientScopeSearch: conf.LenientScopeSearch,
}
Expand Down
2 changes: 1 addition & 1 deletion internal/storage/index/builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func TestBuildIndexWithDisk(t *testing.T) {

t.Run("check_contents", func(t *testing.T) {
data := idxImpl.Inspect()
require.Len(t, data, 38)
require.Len(t, data, 39)

rp1 := filepath.Join("resource_policies", "policy_01.yaml")
rp2 := filepath.Join("resource_policies", "policy_02.yaml")
Expand Down
36 changes: 36 additions & 0 deletions internal/test/testdata/store/resource_policies/policy_15.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# yaml-language-server: $schema=../../../../../schema/jsonschema/cerbos/policy/v1/Policy.schema.json
---
apiVersion: api.cerbos.dev/v1
resourcePolicy:
version: default
resource: output_now
rules:
- name: a
actions:
- a
effect: EFFECT_ALLOW
roles:
- user
output:
when:
ruleActivated: now()

- name: b
actions:
- b
effect: EFFECT_ALLOW
roles:
- user
output:
when:
ruleActivated: now()

- name: c
actions:
- c
effect: EFFECT_ALLOW
roles:
- user
output:
when:
ruleActivated: now()
1 change: 1 addition & 0 deletions private/verify/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ func Bundle(ctx context.Context, params BundleParams) (*policyv1.TestResults, er
type CheckOptions interface {
Globals() map[string]any
NowFunc() func() time.Time
DefaultPolicyVersion() string
LenientScopeSearch() bool
}

Expand Down
0