generated from coulomb/repo-seed
305 lines
9.9 KiB
Go
305 lines
9.9 KiB
Go
package relationship_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/netkingdom/flex-auth/internal/adapters/relationship"
|
|
"github.com/netkingdom/flex-auth/internal/registry"
|
|
"github.com/netkingdom/flex-auth/pkg/api"
|
|
)
|
|
|
|
func TestSnapshotToTuplesMapsCanonicalRegistry(t *testing.T) {
|
|
tuples := relationship.SnapshotToTuples(loadRegistry(t).Snapshot())
|
|
|
|
want := relationship.Tuple{
|
|
ObjectType: "document",
|
|
ObjectID: "document:internal-note",
|
|
Relation: "reader",
|
|
SubjectType: "group",
|
|
SubjectID: "group:platform-architecture",
|
|
SubjectRelation: "member",
|
|
}
|
|
got := findTuple(tuples, want)
|
|
if got.Caring == nil || got.Caring.CanonicalRole != api.CanonicalRoleDoer {
|
|
t.Fatalf("reader tuple CARING = %+v; want Doer descriptor", got.Caring)
|
|
}
|
|
if len(got.Conditions) != 1 || got.Conditions[0] != api.ConditionLogged {
|
|
t.Fatalf("conditions = %+v; want Logged", got.Conditions)
|
|
}
|
|
}
|
|
|
|
func TestAdapterCheckPreservesCARINGFromMatchedTuple(t *testing.T) {
|
|
descriptor := caringDescriptor()
|
|
backend := &fakeBackend{
|
|
checkResult: relationship.TupleCheckResult{
|
|
Allowed: true,
|
|
Reason: "direct_tuple",
|
|
ConsistencyToken: "zed:42",
|
|
MatchedTuples: []relationship.Tuple{
|
|
{
|
|
ObjectType: "document",
|
|
ObjectID: "document:internal-note",
|
|
Relation: "reader",
|
|
SubjectType: "group",
|
|
SubjectID: "group:platform-architecture",
|
|
Caring: descriptor,
|
|
},
|
|
},
|
|
InheritedFrom: []relationship.Tuple{
|
|
{ObjectType: "knowledge_base", ObjectID: "knowledge-base:markitect-example", Relation: "reader"},
|
|
},
|
|
Diagnostics: map[string]any{"backend_trace": "trace-1"},
|
|
},
|
|
}
|
|
adapter := newAdapter(t, backend)
|
|
|
|
got, err := adapter.Check(context.Background(), api.CheckRequest{
|
|
ID: "check:relationship-allow",
|
|
Subject: api.SubjectRef{ID: "user:alice", Type: api.SubjectTypeHuman},
|
|
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.DecisionEffectAllow || got.Reason != "direct_tuple" {
|
|
t.Fatalf("decision = %s/%s; want allow/direct_tuple", got.Effect, got.Reason)
|
|
}
|
|
if got.Provenance.Evaluator != "relationship-pdp/openfga" || got.Provenance.DirectoryETag != "zed:42" {
|
|
t.Fatalf("provenance = %+v", got.Provenance)
|
|
}
|
|
if got.Diagnostics["matched_tuples"] != 1 || got.Diagnostics["inherited_tuples"] != 1 {
|
|
t.Fatalf("diagnostics = %+v; want matched and inherited counts", got.Diagnostics)
|
|
}
|
|
if got.Caring == nil || got.Caring.Descriptor == nil {
|
|
t.Fatal("missing CARING metadata")
|
|
}
|
|
if len(got.Caring.DerivedCapabilities) != 1 || got.Caring.DerivedCapabilities[0].Capability != api.CapabilityViewCollection {
|
|
t.Fatalf("derived capabilities = %+v", got.Caring.DerivedCapabilities)
|
|
}
|
|
if got.Caring.ExposureEvent == nil || got.Caring.ExposureEvent.Type != api.ExposureEventSupport {
|
|
t.Fatalf("exposure event = %+v", got.Caring.ExposureEvent)
|
|
}
|
|
}
|
|
|
|
func TestAdapterCheckFailsClosedOnStaleBackend(t *testing.T) {
|
|
backend := &fakeBackend{
|
|
checkErr: relationship.NewBackendError(relationship.FailureStaleData, "check", errors.New("zed token 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 != "relationship_data_stale" {
|
|
t.Fatalf("decision = %s/%s; want stale deny", got.Effect, got.Reason)
|
|
}
|
|
if got.Diagnostics["relationship_failure"] != "stale_data" {
|
|
t.Fatalf("diagnostics = %+v", got.Diagnostics)
|
|
}
|
|
if got.Caring.ConformanceFindings[0].Code != "RELATIONSHIP-DATA-STALE" {
|
|
t.Fatalf("finding = %+v", got.Caring.ConformanceFindings[0])
|
|
}
|
|
}
|
|
|
|
func TestBatchCheckPreservesBackendOrder(t *testing.T) {
|
|
backend := &fakeBackend{
|
|
batchResults: []relationship.TupleCheckResult{
|
|
{Allowed: true, ConsistencyToken: "zed:1"},
|
|
{Allowed: false, Reason: "no_tuple", ConsistencyToken: "zed:2"},
|
|
},
|
|
}
|
|
adapter := newAdapter(t, backend)
|
|
|
|
got, err := adapter.BatchCheck(context.Background(), api.BatchCheckRequest{
|
|
ID: "batch:documents",
|
|
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 {
|
|
t.Fatalf("len(got) = %d; want 2", len(got))
|
|
}
|
|
if got[0].Effect != api.DecisionEffectAllow || got[1].Effect != api.DecisionEffectDeny {
|
|
t.Fatalf("effects = %s/%s; want allow/deny", got[0].Effect, got[1].Effect)
|
|
}
|
|
if got[0].Resource.ID != "document:internal-note" || got[1].Resource.ID != "document:missing" {
|
|
t.Fatalf("resource order = %s/%s", got[0].Resource.ID, got[1].Resource.ID)
|
|
}
|
|
}
|
|
|
|
func TestListAllowedReturnsEnvelopeResults(t *testing.T) {
|
|
backend := &fakeBackend{
|
|
listResult: relationship.TupleListResult{
|
|
ConsistencyToken: "zed:list",
|
|
Objects: []relationship.ListedObject{
|
|
{
|
|
Resource: api.ResourceRef{
|
|
ID: "document:internal-note",
|
|
Type: "document",
|
|
System: "markitect-tool",
|
|
},
|
|
CaringDescriptor: caringDescriptor(),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
adapter := newAdapter(t, backend)
|
|
|
|
got, err := adapter.ListAllowed(context.Background(), relationship.TupleListRequest{
|
|
Subject: api.SubjectRef{ID: "user:alice"},
|
|
Relation: "read",
|
|
ResourceType: "document",
|
|
System: "markitect-tool",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ListAllowed: %v", err)
|
|
}
|
|
if got.ConsistencyToken != "zed:list" || len(got.Decisions) != 1 {
|
|
t.Fatalf("list result = %+v", got)
|
|
}
|
|
if got.Decisions[0].Effect != api.DecisionEffectAllow || got.Decisions[0].Reason != "relationship_list_allowed" {
|
|
t.Fatalf("decision = %+v", got.Decisions[0])
|
|
}
|
|
}
|
|
|
|
func TestImportTuplesDelegatesSnapshotTuples(t *testing.T) {
|
|
backend := &fakeBackend{}
|
|
adapter := newAdapter(t, backend)
|
|
|
|
report, err := adapter.ImportTuples(context.Background(), loadRegistry(t).Snapshot())
|
|
if err != nil {
|
|
t.Fatalf("ImportTuples: %v", err)
|
|
}
|
|
if report.TuplesWritten != len(backend.imported) {
|
|
t.Fatalf("report = %+v imported=%d", report, len(backend.imported))
|
|
}
|
|
if findTuple(backend.imported, relationship.Tuple{
|
|
ObjectType: "document",
|
|
ObjectID: "document:internal-note",
|
|
Relation: "reader",
|
|
SubjectType: "group",
|
|
SubjectID: "group:platform-architecture",
|
|
SubjectRelation: "member",
|
|
}).Caring == nil {
|
|
t.Fatal("imported reader tuple lost CARING descriptor")
|
|
}
|
|
}
|
|
|
|
func newAdapter(t *testing.T, backend *fakeBackend) *relationship.Adapter {
|
|
t.Helper()
|
|
adapter, err := relationship.New(backend, relationship.Options{
|
|
BackendName: "openfga",
|
|
PolicyPackage: "relationship.boundary",
|
|
PolicyVersion: "v1",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("New: %v", err)
|
|
}
|
|
return adapter
|
|
}
|
|
|
|
func loadRegistry(t *testing.T) *registry.Store {
|
|
t.Helper()
|
|
store, err := registry.LoadFile(filepath.Join("..", "..", "..", "examples", "caring", "registry_snapshot.json"))
|
|
if err != nil {
|
|
t.Fatalf("LoadFile: %v", err)
|
|
}
|
|
return store
|
|
}
|
|
|
|
func caringDescriptor() *api.CaringAccessDescriptor {
|
|
return &api.CaringAccessDescriptor{
|
|
ID: "descriptor:tuple-reader",
|
|
Profile: api.CaringProfileCaring040RC2,
|
|
SubjectType: api.SubjectTypeGroup,
|
|
OrganizationRelation: api.OrganizationRelationCustomer,
|
|
CanonicalRole: api.CanonicalRoleDoer,
|
|
Scope: api.CaringScope{
|
|
Level: api.ScopeLevelResource,
|
|
ID: "document:internal-note",
|
|
Tenant: "tenant:alpha",
|
|
Resource: "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,
|
|
DerivedCapabilities: []api.CaringDerivedCapability{
|
|
{Capability: api.CapabilityViewCollection, Reason: "parent reader inheritance", Source: "tuple:parent"},
|
|
},
|
|
}
|
|
}
|
|
|
|
func findTuple(tuples []relationship.Tuple, want relationship.Tuple) relationship.Tuple {
|
|
for _, tuple := range tuples {
|
|
if tuple.ObjectType == want.ObjectType &&
|
|
tuple.ObjectID == want.ObjectID &&
|
|
tuple.Relation == want.Relation &&
|
|
tuple.SubjectType == want.SubjectType &&
|
|
tuple.SubjectID == want.SubjectID &&
|
|
tuple.SubjectRelation == want.SubjectRelation {
|
|
return tuple
|
|
}
|
|
}
|
|
panic("tuple not found")
|
|
}
|
|
|
|
type fakeBackend struct {
|
|
checkResult relationship.TupleCheckResult
|
|
checkErr error
|
|
batchResults []relationship.TupleCheckResult
|
|
listResult relationship.TupleListResult
|
|
imported []relationship.Tuple
|
|
}
|
|
|
|
func (b *fakeBackend) Check(context.Context, relationship.TupleCheckRequest) (relationship.TupleCheckResult, error) {
|
|
return b.checkResult, b.checkErr
|
|
}
|
|
|
|
func (b *fakeBackend) BatchCheck(_ context.Context, requests []relationship.TupleCheckRequest) ([]relationship.TupleCheckResult, error) {
|
|
if b.batchResults != nil {
|
|
return b.batchResults, nil
|
|
}
|
|
results := make([]relationship.TupleCheckResult, len(requests))
|
|
for i := range results {
|
|
results[i] = b.checkResult
|
|
}
|
|
return results, b.checkErr
|
|
}
|
|
|
|
func (b *fakeBackend) ListObjects(context.Context, relationship.TupleListRequest) (relationship.TupleListResult, error) {
|
|
return b.listResult, nil
|
|
}
|
|
|
|
func (b *fakeBackend) ImportTuples(_ context.Context, tuples []relationship.Tuple) (relationship.ImportReport, error) {
|
|
b.imported = append([]relationship.Tuple(nil), tuples...)
|
|
return relationship.ImportReport{TuplesWritten: len(tuples), ConsistencyToken: "zed:import"}, nil
|
|
}
|
|
|
|
func (b *fakeBackend) Health(context.Context) error {
|
|
return nil
|
|
}
|