Files

316 lines
8.5 KiB
Go
Raw Permalink Normal View History

package policy
import (
"bytes"
"context"
"errors"
"log/slog"
"strings"
"testing"
"time"
"github.com/open-policy-agent/opa/v1/rego"
)
func TestLockedCapabilitiesRejectHTTP(t *testing.T) {
source := `package silo_custom.scope
import rego.v1
override(base, _) := base if {
http.send({"method": "get", "url": "https://example.test"})
}`
_, err := rego.New(
rego.Query("data.silo_custom.scope.override({}, {})"),
rego.Module("bad.rego", source),
rego.Capabilities(LockedCapabilities()),
).PrepareForEval(context.Background())
if err == nil {
t.Fatal("expected http.send compile failure")
}
}
func TestCompileCheckRejectsForbiddenBuiltins(t *testing.T) {
tests := []struct {
name string
source string
}{
{
name: "http_send",
source: `package silo_custom.scope
import rego.v1
override(base, _) := base if {
resp := http.send({"method": "get", "url": "https://example.test"})
resp.status_code >= 100
}`,
},
{
name: "net_lookup",
source: `package silo_custom.scope
import rego.v1
override(base, _) := base if {
ips := net.lookup_ip_addr("localhost")
count(ips) >= 0
}`,
},
{
name: "opa_runtime",
source: `package silo_custom.scope
import rego.v1
override(base, _) := base if {
runtime := opa.runtime()
object.get(runtime, "version", "") == ""
}`,
},
{
name: "time_now",
source: `package silo_custom.scope
import rego.v1
override(base, _) := base if {
time.now_ns() > 0
}`,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := CompileCheck(context.Background(), "scope", test.source)
if !errors.Is(err, ErrCompileFailed) {
t.Fatalf("CompileCheck() error = %v, want ErrCompileFailed", err)
}
})
}
}
func TestCompileCheckRejectsWrongPackagePath(t *testing.T) {
err := CompileCheck(context.Background(), "scope", `package silo_custom.permission
import rego.v1
override(base, _) := base`)
if !errors.Is(err, ErrCompileFailed) {
t.Fatalf("CompileCheck() error = %v, want ErrCompileFailed", err)
}
if !strings.Contains(err.Error(), "policy package must be silo_custom.scope") {
t.Fatalf("CompileCheck() error = %v, want package mismatch", err)
}
}
func TestCompileCheckAllowsPureNetHelpers(t *testing.T) {
// net.cidr_contains is deterministic; only impure builtins are locked.
err := CompileCheck(context.Background(), "scope", `package silo_custom.scope
import rego.v1
override(base, i) := base if {
net.cidr_contains("10.0.0.0/8", object.get(i, "client_ip", "10.1.2.3"))
}`)
if err != nil {
t.Fatalf("CompileCheck() error = %v, want pure net helper allowed", err)
}
}
func TestCompileCheckRejectsOversizedSource(t *testing.T) {
source := "package silo_custom.scope\n\nimport rego.v1\n\n# " +
strings.Repeat("x", maxPolicySourceBytes)
err := CompileCheck(context.Background(), "scope", source)
if !errors.Is(err, ErrCompileFailed) {
t.Fatalf("CompileCheck() error = %v, want ErrCompileFailed", err)
}
if !strings.Contains(err.Error(), "byte limit") {
t.Fatalf("CompileCheck() error = %v, want size limit message", err)
}
}
func TestCompileCheckAcceptsValidScopeOverride(t *testing.T) {
if err := CompileCheck(context.Background(), "scope", tighteningScopeOverrideSource()); err != nil {
t.Fatalf("CompileCheck() error: %v", err)
}
}
func TestCustomStubAndScopeOverrideCoexist(t *testing.T) {
engine, err := NewEngineWithCustom(context.Background(), map[string]ActiveSource{
"scope": {DocumentID: 1, VersionID: 1, Source: tighteningScopeOverrideSource()},
})
if err != nil {
t.Fatalf("NewEngineWithCustom() error: %v", err)
}
pdp := NewPDP(engine)
decision, _, err := pdp.ResolveViewerScope(context.Background(), ScopeInput{
SchemaVersion: 1,
UserID: 42,
SessionID: "sess-1",
AccountRestricted: false,
AccessPolicyRevision: 9,
ProfileVerified: true,
RequestTime: "2026-07-02T12:00:00Z",
})
if err != nil {
t.Fatalf("ResolveViewerScope() error: %v", err)
}
if decision.Unrestricted {
t.Fatalf("Unrestricted = true, want tightened restricted decision: %#v", decision)
}
if got, want := decision.AllowedLibraryIDs, []int{2}; len(got) != 1 || got[0] != want[0] {
t.Fatalf("AllowedLibraryIDs = %#v, want %#v", got, want)
}
}
func TestNewEngineWithCustomSkipsInvalidSource(t *testing.T) {
var logs bytes.Buffer
logger := slog.New(slog.NewTextHandler(&logs, &slog.HandlerOptions{Level: slog.LevelWarn}))
engine, err := NewEngineWithCustom(
context.Background(),
map[string]ActiveSource{
"scope": {
DocumentID: 123,
VersionID: 456,
Source: `package silo_custom.scope
import rego.v1
override(base, _) := base if {`,
},
},
WithLogger(logger),
)
if err != nil {
t.Fatalf("NewEngineWithCustom() error: %v", err)
}
pdp := NewPDP(engine)
decision, _, err := pdp.ResolveViewerScope(context.Background(), ScopeInput{
SchemaVersion: 1,
UserID: 42,
SessionID: "sess-1",
AccountRestricted: false,
AccessPolicyRevision: 9,
ProfileVerified: true,
RequestTime: "2026-07-02T12:00:00Z",
})
if err != nil {
t.Fatalf("ResolveViewerScope() error: %v", err)
}
if !decision.Unrestricted {
t.Fatalf("Unrestricted = false, want vendor-only unrestricted decision: %#v", decision)
}
logOutput := logs.String()
if !strings.Contains(logOutput, "skipping invalid custom policy source") ||
!strings.Contains(logOutput, "domain=scope") ||
!strings.Contains(logOutput, "document_id=123") {
t.Fatalf("error log did not include skip fields: %s", logOutput)
}
skipped := engine.SkippedSources()
if len(skipped) != 1 || skipped[0].Domain != "scope" || skipped[0].DocumentID != 123 || skipped[0].Err == nil {
t.Fatalf("SkippedSources() = %#v, want the dropped scope source recorded", skipped)
}
}
func TestEngineReloadRejectsInvalidCustomSource(t *testing.T) {
ctx := context.Background()
engine, err := NewEngineWithCustom(ctx, map[string]ActiveSource{
"scope": {DocumentID: 1, VersionID: 1, Source: tighteningScopeOverrideSource()},
}, WithRevision(7))
if err != nil {
t.Fatalf("NewEngineWithCustom() error: %v", err)
}
err = engine.Reload(ctx, map[string]ActiveSource{
"scope": {
DocumentID: 1,
VersionID: 2,
Source: `package silo_custom.scope
import rego.v1
override(base, _) := base if {`,
},
}, 8)
if err == nil {
t.Fatal("Reload() error = nil, want compile failure for the invalid custom source")
}
if !strings.Contains(err.Error(), `domain "scope"`) || !strings.Contains(err.Error(), "document 1") {
t.Fatalf("Reload() error = %v, want domain and document identified", err)
}
// The last known-good bundle must keep serving: revision unchanged and the
// previous tightening override still applied.
if got := engine.Revision(); got != 7 {
t.Fatalf("Revision() after failed reload = %d, want 7", got)
}
pdp := NewPDP(engine)
decision, _, err := pdp.ResolveViewerScope(ctx, ScopeInput{
SchemaVersion: 1,
UserID: 42,
SessionID: "sess-1",
AccountRestricted: false,
AccessPolicyRevision: 9,
ProfileVerified: true,
RequestTime: "2026-07-02T12:00:00Z",
})
if err != nil {
t.Fatalf("ResolveViewerScope() error: %v", err)
}
if decision.Unrestricted {
t.Fatalf("Unrestricted = true after failed reload, want last known-good tightened decision: %#v", decision)
}
}
func TestEvaluateTimeoutFailsClosed(t *testing.T) {
engine, err := newEngineFromModules(context.Background(), time.Nanosecond, []ModuleSource{{
Path: "slow.rego",
Source: `package silo.slow
import rego.v1
decision := count([x |
some i in numbers.range(1, 10000)
some j in numbers.range(1, 10000)
x := i + j
])`,
}}, map[DecisionName]string{
"slow.decision": "data.silo.slow.decision",
})
if err != nil {
t.Fatalf("compile slow policy: %v", err)
}
var out int
_, err = engine.Evaluate(context.Background(), "slow.decision", map[string]any{}, &out)
if !errors.Is(err, ErrPolicyEvalFailed) {
t.Fatalf("Evaluate() error = %v, want ErrPolicyEvalFailed", err)
}
// Timeouts stay fail-closed but carry the distinct sentinel and counter so
// a slow policy is attributable, not just a generic eval failure.
if !errors.Is(err, ErrPolicyEvalTimeout) {
t.Fatalf("Evaluate() error = %v, want ErrPolicyEvalTimeout to match", err)
}
if got := engine.EvalTimeouts(); got != 1 {
t.Fatalf("EvalTimeouts() = %d, want 1", got)
}
}
func tighteningScopeOverrideSource() string {
return `package silo_custom.scope
import rego.v1
override(_, _) := result if {
result := {
"unrestricted": false,
"allowed_library_ids": [2],
}
}`
}