Files
flex-auth/internal/adapters/rule/adapter_test.go
tegwick ad4895187b
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
Add rule PDP adapter boundary
2026-05-17 07:13:27 +02:00

270 lines
8.8 KiB
Go

package rule_test
import (
"context"
"errors"
"path/filepath"
"testing"
"github.com/netkingdom/flex-auth/internal/adapters/rule"
"github.com/netkingdom/flex-auth/internal/policy"
"github.com/netkingdom/flex-auth/pkg/api"
)
func TestCanonicalInputFromCheck(t *testing.T) {
adapter := newAdapter(t, &fakeBackend{})
decision, err := adapter.Check(context.Background(), api.CheckRequest{
ID: "check:input",
Subject: api.SubjectRef{ID: "user:alice"},
Action: "read",
Resource: api.ResourceRef{ID: "document:internal-note", Type: "document", System: "markitect-tool"},
Context: map[string]any{"purpose": "support"},
CaringContext: caringDescriptor(),
})
if err != nil {
t.Fatalf("Check: %v", err)
}
if decision.Diagnostics["input_seen"] != true {
t.Fatalf("backend did not receive canonical input: %+v", decision.Diagnostics)
}
}
func TestPolicyArtifactFromPackagePreservesRegoAndFixtures(t *testing.T) {
pkg := loadPolicy(t)
artifact := rule.PolicyArtifactFromPackage(pkg)
if artifact.ID != "markitect.documents.internal-read" || artifact.Version != "v1" {
t.Fatalf("artifact metadata = %+v", artifact)
}
if artifact.Language != rule.LanguageRego {
t.Fatalf("Language = %q", artifact.Language)
}
if artifact.Module != pkg.RegoModule {
t.Fatal("Rego module changed during artifact creation")
}
if len(artifact.Fixtures) != len(pkg.Fixtures) {
t.Fatalf("fixtures = %d; want %d", len(artifact.Fixtures), len(pkg.Fixtures))
}
if artifact.Caring.Profile != api.CaringProfileCaring040RC2 {
t.Fatalf("Caring profile = %q", artifact.Caring.Profile)
}
}
func TestAdapterCheckWrapsRuleResult(t *testing.T) {
backend := &fakeBackend{
result: rule.EvaluationResult{
Effect: api.DecisionEffectRedact,
Reason: "masked_internal_document",
MatchedRule: "rule.mask_internal",
PolicyVersion: "v2",
Obligations: []api.Obligation{
{Type: "mask_fields", Parameters: map[string]any{"fields": []string{"email"}}},
},
Diagnostics: map[string]any{"backend_trace": "trace-1"},
CaringDescriptor: caringDescriptor(),
ConformanceFindings: []api.CaringConformanceFinding{
{Code: "RULE-MASKED", Severity: "info", Message: "masked internal document"},
},
},
}
adapter := newAdapter(t, backend)
got, err := adapter.Check(context.Background(), api.CheckRequest{
ID: "check:redact",
Subject: api.SubjectRef{ID: "user:alice"},
Action: "read",
Resource: api.ResourceRef{ID: "document:internal-note", Type: "document", System: "markitect-tool"},
})
if err != nil {
t.Fatalf("Check: %v", err)
}
if got.Effect != api.DecisionEffectRedact || got.Reason != "masked_internal_document" {
t.Fatalf("decision = %s/%s", got.Effect, got.Reason)
}
if got.Provenance.Evaluator != "rule-pdp/opa" || got.MatchedPolicyVersion != "v2" {
t.Fatalf("provenance = %+v matched=%s", got.Provenance, got.MatchedPolicyVersion)
}
if len(got.Obligations) != 1 || got.Obligations[0].Type != "mask_fields" {
t.Fatalf("obligations = %+v", got.Obligations)
}
if got.Caring == nil || got.Caring.Descriptor == nil || got.Caring.ExposureEvent == nil {
t.Fatalf("CARING metadata = %+v", got.Caring)
}
}
func TestAdapterFailsClosedOnStalePolicy(t *testing.T) {
backend := &fakeBackend{
err: rule.NewBackendError(rule.FailureStalePolicy, "evaluate", errors.New("policy revision too old")),
}
adapter := newAdapter(t, backend)
got, err := adapter.Check(context.Background(), api.CheckRequest{
ID: "check:stale",
Subject: api.SubjectRef{ID: "user:alice"},
Action: "read",
Resource: api.ResourceRef{ID: "document:internal-note", Type: "document", System: "markitect-tool"},
CaringContext: caringDescriptor(),
})
if err != nil {
t.Fatalf("Check: %v", err)
}
if got.Effect != api.DecisionEffectDeny || got.Reason != "rule_policy_stale" {
t.Fatalf("decision = %s/%s; want stale deny", got.Effect, got.Reason)
}
if got.Diagnostics["rule_failure"] != "stale_policy" {
t.Fatalf("diagnostics = %+v", got.Diagnostics)
}
if got.Caring.ConformanceFindings[0].Code != "RULE-POLICY-STALE" {
t.Fatalf("finding = %+v", got.Caring.ConformanceFindings[0])
}
}
func TestBatchCheckPreservesOrder(t *testing.T) {
backend := &fakeBackend{
batch: []rule.EvaluationResult{
{Effect: api.DecisionEffectAllow, Reason: "first"},
{Effect: api.DecisionEffectDeny, Reason: "second"},
},
}
adapter := newAdapter(t, backend)
got, err := adapter.BatchCheck(context.Background(), api.BatchCheckRequest{
ID: "batch:rule",
Subject: api.SubjectRef{ID: "user:alice"},
Action: "read",
Resources: []api.ResourceRef{
{ID: "document:internal-note", Type: "document", System: "markitect-tool"},
{ID: "document:missing", Type: "document", System: "markitect-tool"},
},
})
if err != nil {
t.Fatalf("BatchCheck: %v", err)
}
if len(got) != 2 || got[0].Reason != "first" || got[1].Reason != "second" {
t.Fatalf("batch = %+v", got)
}
}
func TestEvaluateFixturesComparesExpectations(t *testing.T) {
backend := &fakeBackend{
result: rule.EvaluationResult{Effect: api.DecisionEffectDeny, Reason: "no_matching_rule"},
}
adapter := newAdapter(t, backend)
results := adapter.EvaluateFixtures(context.Background(), []api.PolicyFixture{
{
ID: "fixture:deny",
Request: api.CheckRequest{
Subject: api.SubjectRef{ID: "user:bob"},
Action: "read",
Resource: api.ResourceRef{ID: "document:internal-note", Type: "document", System: "markitect-tool"},
},
Expect: api.DecisionExpectation{Effect: api.DecisionEffectDeny, Reason: "no_matching_rule"},
},
})
if len(results) != 1 || !results[0].Passed {
t.Fatalf("fixture results = %+v", results)
}
}
func TestImportPolicyDelegatesArtifact(t *testing.T) {
backend := &fakeBackend{}
adapter := newAdapter(t, backend)
pkg := loadPolicy(t)
report, err := adapter.ImportPolicy(context.Background(), pkg)
if err != nil {
t.Fatalf("ImportPolicy: %v", err)
}
if report.ArtifactID != pkg.Metadata.ID || backend.artifact.Module != pkg.RegoModule {
t.Fatalf("report = %+v artifact = %+v", report, backend.artifact)
}
}
func newAdapter(t *testing.T, backend *fakeBackend) *rule.Adapter {
t.Helper()
adapter, err := rule.New(backend, rule.Options{
BackendName: "opa",
PolicyPackage: "markitect.documents.internal-read",
PolicyVersion: "v1",
Language: rule.LanguageRego,
Caring: api.CaringPolicyMetadata{
Profile: api.CaringProfileCaring040RC2,
CanonicalRoles: []api.CanonicalRole{api.CanonicalRoleDoer},
Planes: []api.Plane{api.PlaneData},
Capabilities: []api.Capability{api.CapabilityView},
ExposureModes: []api.ExposureMode{api.ExposureModeMasked},
},
})
if err != nil {
t.Fatalf("New: %v", err)
}
return adapter
}
func loadPolicy(t *testing.T) *policy.Package {
t.Helper()
pkg, err := policy.LoadAndValidateFile(context.Background(), filepath.Join("..", "..", "..", "examples", "caring", "policy_package.md"))
if err != nil {
t.Fatalf("LoadAndValidateFile: %v", err)
}
return pkg
}
func caringDescriptor() *api.CaringAccessDescriptor {
return &api.CaringAccessDescriptor{
ID: "descriptor:rule-reader",
Profile: api.CaringProfileCaring040RC2,
SubjectType: api.SubjectTypeHuman,
OrganizationRelation: api.OrganizationRelationCustomer,
CanonicalRole: api.CanonicalRoleDoer,
Scope: api.CaringScope{Level: api.ScopeLevelResource, ID: "document:internal-note"},
Planes: []api.Plane{api.PlaneData},
Capabilities: []api.Capability{api.CapabilityView},
ExposureModes: []api.ExposureMode{api.ExposureModeMasked},
Restrictions: []api.Restriction{api.RestrictionExportBlocked},
ExposureEvent: api.ExposureEventSupport,
}
}
type fakeBackend struct {
result rule.EvaluationResult
err error
batch []rule.EvaluationResult
artifact rule.PolicyArtifact
}
func (b *fakeBackend) Evaluate(_ context.Context, request rule.EvaluationRequest) (rule.EvaluationResult, error) {
if request.Input["subject"] != nil && b.result.Diagnostics == nil {
b.result.Diagnostics = map[string]any{"input_seen": true}
}
return b.result, b.err
}
func (b *fakeBackend) BatchEvaluate(_ context.Context, requests []rule.EvaluationRequest) ([]rule.EvaluationResult, error) {
if b.batch != nil {
return b.batch, nil
}
results := make([]rule.EvaluationResult, len(requests))
for i := range results {
results[i] = b.result
}
return results, b.err
}
func (b *fakeBackend) ImportPolicy(_ context.Context, artifact rule.PolicyArtifact) (rule.PolicyImportReport, error) {
b.artifact = artifact
return rule.PolicyImportReport{
ArtifactID: artifact.ID,
Version: artifact.Version,
Language: artifact.Language,
BackendRef: "opa:" + artifact.ID,
}, nil
}
func (b *fakeBackend) Health(context.Context) error {
return nil
}