From 5ef34c663747ff17d85334d0f14a269703688ff6 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Wed, 1 May 2024 15:36:47 -0400 Subject: [PATCH 1/4] Add schema-wide diffing library --- pkg/diff/diff.go | 169 ++++++++++++++++++++++++++++++++++++++++ pkg/diff/diff_test.go | 175 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 344 insertions(+) create mode 100644 pkg/diff/diff.go create mode 100644 pkg/diff/diff_test.go diff --git a/pkg/diff/diff.go b/pkg/diff/diff.go new file mode 100644 index 0000000000..4178e96323 --- /dev/null +++ b/pkg/diff/diff.go @@ -0,0 +1,169 @@ +package diff + +import ( + "github.com/authzed/spicedb/pkg/diff/caveats" + "github.com/authzed/spicedb/pkg/diff/namespace" + "github.com/authzed/spicedb/pkg/genutil/mapz" + core "github.com/authzed/spicedb/pkg/proto/core/v1" + "github.com/authzed/spicedb/pkg/schemadsl/compiler" +) + +// DiffableSchema is a schema that can be diffed. +type DiffableSchema struct { + // ObjectDefinitions holds the object definitions in the schema. + ObjectDefinitions []*core.NamespaceDefinition + + // CaveatDefinitions holds the caveat definitions in the schema. + CaveatDefinitions []*core.CaveatDefinition +} + +func (ds *DiffableSchema) GetNamespace(namespaceName string) (*core.NamespaceDefinition, bool) { + for _, ns := range ds.ObjectDefinitions { + if ns.Name == namespaceName { + return ns, true + } + } + + return nil, false +} + +func (ds *DiffableSchema) GetRelation(nsName string, relationName string) (*core.Relation, bool) { + ns, ok := ds.GetNamespace(nsName) + if !ok { + return nil, false + } + + for _, relation := range ns.Relation { + if relation.Name == relationName { + return relation, true + } + } + + return nil, false +} + +func (ds *DiffableSchema) GetCaveat(caveatName string) (*core.CaveatDefinition, bool) { + for _, caveat := range ds.CaveatDefinitions { + if caveat.Name == caveatName { + return caveat, true + } + } + + return nil, false +} + +// NewDiffableSchemaFromCompiledSchema creates a new DiffableSchema from a CompiledSchema. +func NewDiffableSchemaFromCompiledSchema(compiled *compiler.CompiledSchema) DiffableSchema { + return DiffableSchema{ + ObjectDefinitions: compiled.ObjectDefinitions, + CaveatDefinitions: compiled.CaveatDefinitions, + } +} + +// SchemaDiff holds the diff between two schemas. +type SchemaDiff struct { + // AddedNamespaces are the namespaces that were added. + AddedNamespaces []string + + // RemovedNamespaces are the namespaces that were removed. + RemovedNamespaces []string + + // AddedCaveats are the caveats that were added. + AddedCaveats []string + + // RemovedCaveats are the caveats that were removed. + RemovedCaveats []string + + // ChangedNamespaces are the namespaces that were changed. + ChangedNamespaces map[string]namespace.Diff + + // ChangedCaveats are the caveats that were changed. + ChangedCaveats map[string]caveats.Diff +} + +// DiffSchemas compares two schemas and returns the diff. +func DiffSchemas(existing DiffableSchema, comparison DiffableSchema) (*SchemaDiff, error) { + existingNamespacesByName := make(map[string]*core.NamespaceDefinition, len(existing.ObjectDefinitions)) + existingNamespaceNames := mapz.NewSet[string]() + for _, nsDef := range existing.ObjectDefinitions { + existingNamespacesByName[nsDef.Name] = nsDef + existingNamespaceNames.Add(nsDef.Name) + } + + existingCaveatsByName := make(map[string]*core.CaveatDefinition, len(existing.CaveatDefinitions)) + existingCaveatsByNames := mapz.NewSet[string]() + for _, caveatDef := range existing.CaveatDefinitions { + existingCaveatsByName[caveatDef.Name] = caveatDef + existingCaveatsByNames.Add(caveatDef.Name) + } + + comparisonNamespacesByName := make(map[string]*core.NamespaceDefinition, len(comparison.ObjectDefinitions)) + comparisonNamespaceNames := mapz.NewSet[string]() + for _, nsDef := range comparison.ObjectDefinitions { + comparisonNamespacesByName[nsDef.Name] = nsDef + comparisonNamespaceNames.Add(nsDef.Name) + } + + comparisonCaveatsByName := make(map[string]*core.CaveatDefinition, len(comparison.CaveatDefinitions)) + comparisonCaveatsByNames := mapz.NewSet[string]() + for _, caveatDef := range comparison.CaveatDefinitions { + comparisonCaveatsByName[caveatDef.Name] = caveatDef + comparisonCaveatsByNames.Add(caveatDef.Name) + } + + changedNamespaces := make(map[string]namespace.Diff, 0) + commonNamespaceNames := existingNamespaceNames.Intersect(comparisonNamespaceNames) + if err := commonNamespaceNames.ForEach(func(name string) error { + existingNamespace := existingNamespacesByName[name] + comparisonNamespace := comparisonNamespacesByName[name] + + diff, err := namespace.DiffNamespaces(existingNamespace, comparisonNamespace) + if err != nil { + return err + } + + if len(diff.Deltas()) > 0 { + changedNamespaces[name] = *diff + } + + return nil + }); err != nil { + return nil, err + } + + commonCaveatNames := existingCaveatsByNames.Intersect(comparisonCaveatsByNames) + changedCaveats := make(map[string]caveats.Diff, 0) + if err := commonCaveatNames.ForEach(func(name string) error { + existingCaveat := existingCaveatsByName[name] + comparisonCaveat := comparisonCaveatsByName[name] + + diff, err := caveats.DiffCaveats(existingCaveat, comparisonCaveat) + if err != nil { + return err + } + + if len(diff.Deltas()) > 0 { + changedCaveats[name] = *diff + } + + return nil + }); err != nil { + return nil, err + } + + if len(changedNamespaces) == 0 { + changedNamespaces = nil + } + if len(changedCaveats) == 0 { + changedCaveats = nil + } + + return &SchemaDiff{ + AddedNamespaces: comparisonNamespaceNames.Subtract(existingNamespaceNames).AsSlice(), + RemovedNamespaces: existingNamespaceNames.Subtract(comparisonNamespaceNames).AsSlice(), + AddedCaveats: comparisonCaveatsByNames.Subtract(existingCaveatsByNames).AsSlice(), + RemovedCaveats: existingCaveatsByNames.Subtract(comparisonCaveatsByNames).AsSlice(), + ChangedNamespaces: changedNamespaces, + ChangedCaveats: changedCaveats, + }, nil +} diff --git a/pkg/diff/diff_test.go b/pkg/diff/diff_test.go new file mode 100644 index 0000000000..c0cda89b0f --- /dev/null +++ b/pkg/diff/diff_test.go @@ -0,0 +1,175 @@ +package diff + +import ( + "testing" + + "github.com/authzed/spicedb/pkg/diff/caveats" + "github.com/authzed/spicedb/pkg/diff/namespace" + "github.com/authzed/spicedb/pkg/schemadsl/compiler" + "github.com/authzed/spicedb/pkg/schemadsl/input" + "github.com/stretchr/testify/require" +) + +func TestDiffSchemas(t *testing.T) { + tcs := []struct { + name string + existingSchema string + comparisonSchema string + expectedDiff SchemaDiff + }{ + { + name: "no changes", + existingSchema: `definition user {}`, + comparisonSchema: ` definition user {} `, + expectedDiff: SchemaDiff{}, + }, + { + name: "added namespace", + existingSchema: ``, + comparisonSchema: `definition user {}`, + expectedDiff: SchemaDiff{ + AddedNamespaces: []string{"user"}, + }, + }, + { + name: "removed namespace", + existingSchema: `definition user {}`, + comparisonSchema: ``, + expectedDiff: SchemaDiff{ + RemovedNamespaces: []string{"user"}, + }, + }, + { + name: "added caveat", + existingSchema: ``, + comparisonSchema: `caveat someCaveat(someparam int) { someparam < 42 }`, + expectedDiff: SchemaDiff{ + AddedCaveats: []string{"someCaveat"}, + }, + }, + { + name: "removed caveat", + existingSchema: `caveat someCaveat(someparam int) { someparam < 42 }`, + comparisonSchema: ``, + expectedDiff: SchemaDiff{ + RemovedCaveats: []string{"someCaveat"}, + }, + }, + { + name: "add and remove namespace and caveat", + existingSchema: `definition user {}`, + comparisonSchema: `caveat someCaveat(someparam int) { someparam < 42 }`, + expectedDiff: SchemaDiff{ + AddedCaveats: []string{"someCaveat"}, + RemovedNamespaces: []string{"user"}, + }, + }, + { + name: "add and remove namespaces", + existingSchema: `definition user {}`, + comparisonSchema: `definition user2 {}`, + expectedDiff: SchemaDiff{ + AddedNamespaces: []string{"user2"}, + RemovedNamespaces: []string{"user"}, + }, + }, + { + name: "add and remove caveats", + existingSchema: `caveat someCaveat(someparam int) { someparam < 42 }`, + comparisonSchema: `caveat someCaveat2(someparam int) { someparam < 42 }`, + expectedDiff: SchemaDiff{ + AddedCaveats: []string{"someCaveat2"}, + RemovedCaveats: []string{"someCaveat"}, + }, + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + existingSchema, err := compiler.Compile(compiler.InputSchema{ + Source: input.Source("schema"), + SchemaString: tc.existingSchema, + }, compiler.AllowUnprefixedObjectType()) + require.NoError(t, err) + + comparisonSchema, err := compiler.Compile(compiler.InputSchema{ + Source: input.Source("schema"), + SchemaString: tc.comparisonSchema, + }, compiler.AllowUnprefixedObjectType()) + require.NoError(t, err) + + diff, err := DiffSchemas(NewDiffableSchemaFromCompiledSchema(existingSchema), NewDiffableSchemaFromCompiledSchema(comparisonSchema)) + require.NoError(t, err) + require.Equal(t, tc.expectedDiff, *diff) + }) + } +} + +func TestDiffSchemasWithChangedNamespace(t *testing.T) { + existingSchema, err := compiler.Compile(compiler.InputSchema{ + Source: input.Source("schema"), + SchemaString: `definition user {}`, + }, compiler.AllowUnprefixedObjectType()) + require.NoError(t, err) + + comparisonSchema, err := compiler.Compile(compiler.InputSchema{ + Source: input.Source("schema"), + SchemaString: `definition user { relation somerel: user; }`, + }, compiler.AllowUnprefixedObjectType()) + require.NoError(t, err) + + diff, err := DiffSchemas(NewDiffableSchemaFromCompiledSchema(existingSchema), NewDiffableSchemaFromCompiledSchema(comparisonSchema)) + require.NoError(t, err) + + require.Len(t, diff.ChangedNamespaces, 1) + require.Contains(t, diff.ChangedNamespaces, "user") + require.Len(t, diff.ChangedNamespaces["user"].Deltas(), 1) + require.Equal(t, namespace.AddedRelation, diff.ChangedNamespaces["user"].Deltas()[0].Type) + require.Equal(t, "somerel", diff.ChangedNamespaces["user"].Deltas()[0].RelationName) +} + +func TestDiffSchemasWithChangedCaveat(t *testing.T) { + existingSchema, err := compiler.Compile(compiler.InputSchema{ + Source: input.Source("schema"), + SchemaString: `caveat someCaveat(someparam int) { someparam < 42 }`, + }, compiler.AllowUnprefixedObjectType()) + require.NoError(t, err) + + comparisonSchema, err := compiler.Compile(compiler.InputSchema{ + Source: input.Source("schema"), + SchemaString: `caveat someCaveat(someparam int) { someparam <= 42 }`, + }, compiler.AllowUnprefixedObjectType()) + require.NoError(t, err) + + diff, err := DiffSchemas(NewDiffableSchemaFromCompiledSchema(existingSchema), NewDiffableSchemaFromCompiledSchema(comparisonSchema)) + require.NoError(t, err) + + require.Len(t, diff.ChangedCaveats, 1) + require.Contains(t, diff.ChangedCaveats, "someCaveat") + require.Len(t, diff.ChangedCaveats["someCaveat"].Deltas(), 1) + require.Equal(t, caveats.CaveatExpressionChanged, diff.ChangedCaveats["someCaveat"].Deltas()[0].Type) +} + +func TestDiffSchemasWithChangedCaveatComment(t *testing.T) { + existingSchema, err := compiler.Compile(compiler.InputSchema{ + Source: input.Source("schema"), + SchemaString: `// hi there + caveat someCaveat(someparam int) { someparam < 42 }`, + }, compiler.AllowUnprefixedObjectType()) + require.NoError(t, err) + + comparisonSchema, err := compiler.Compile(compiler.InputSchema{ + Source: input.Source("schema"), + SchemaString: `// hello there + caveat someCaveat(someparam int) { someparam < 42 }`, + }, compiler.AllowUnprefixedObjectType()) + require.NoError(t, err) + + diff, err := DiffSchemas(NewDiffableSchemaFromCompiledSchema(existingSchema), NewDiffableSchemaFromCompiledSchema(comparisonSchema)) + require.NoError(t, err) + + require.Len(t, diff.ChangedCaveats, 1) + require.Contains(t, diff.ChangedCaveats, "someCaveat") + require.Len(t, diff.ChangedCaveats["someCaveat"].Deltas(), 1) + require.Equal(t, caveats.CaveatCommentsChanged, diff.ChangedCaveats["someCaveat"].Deltas()[0].Type) +} From 13e8dd817641b1c4a09fcd27ecb1e598153e8e5a Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Wed, 1 May 2024 20:27:18 -0400 Subject: [PATCH 2/4] Add support for ExperimentalSchemaDiff --- e2e/go.mod | 2 +- e2e/go.sum | 4 +- go.mod | 2 +- go.sum | 4 +- internal/services/v1/experimental.go | 19 + internal/services/v1/experimental_test.go | 91 ++++ internal/services/v1/expreflection.go | 525 +++++++++++++++++++ internal/services/v1/expreflection_test.go | 579 +++++++++++++++++++++ internal/services/v1/reflection.go | 68 +++ pkg/diff/diff_test.go | 3 +- 10 files changed, 1290 insertions(+), 7 deletions(-) create mode 100644 internal/services/v1/expreflection.go create mode 100644 internal/services/v1/expreflection_test.go create mode 100644 internal/services/v1/reflection.go diff --git a/e2e/go.mod b/e2e/go.mod index e4f6493fc5..3dc022546e 100644 --- a/e2e/go.mod +++ b/e2e/go.mod @@ -3,7 +3,7 @@ module github.com/authzed/spicedb/e2e go 1.22.2 require ( - github.com/authzed/authzed-go v0.11.2-0.20240418174337-42f221719227 + github.com/authzed/authzed-go v0.11.2-0.20240501223711-10d4e60e2a07 github.com/authzed/grpcutil v0.0.0-20240123092924-129dc0a6a6e1 github.com/authzed/spicedb v1.29.5 github.com/brianvoe/gofakeit/v6 v6.23.2 diff --git a/e2e/go.sum b/e2e/go.sum index d7fe4dff3f..272cd1059e 100644 --- a/e2e/go.sum +++ b/e2e/go.sum @@ -27,8 +27,8 @@ github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8 github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= -github.com/authzed/authzed-go v0.11.2-0.20240418174337-42f221719227 h1:VczJwysQbGiSnJeyROxmF6/u8K7GZviVbIc4XGm9u1o= -github.com/authzed/authzed-go v0.11.2-0.20240418174337-42f221719227/go.mod h1:EFCDZMQbrhJSpSRUlAooJdACESdA4VnlIkCz1s0Pw+g= +github.com/authzed/authzed-go v0.11.2-0.20240501223711-10d4e60e2a07 h1:FtbBwpHxrmGgC8p8Dl4MtiouCcIKHUzFi2E9WC9NCuc= +github.com/authzed/authzed-go v0.11.2-0.20240501223711-10d4e60e2a07/go.mod h1:0SXN/1P1G/y87PaA8cuaNxx5QC0FjN8doNk6IPqcDkY= github.com/authzed/cel-go v0.20.2 h1:GlmLecGry7Z8HU0k+hmaHHUV05ZHrsFxduXHtIePvck= github.com/authzed/cel-go v0.20.2/go.mod h1:pJHVFWbqUHV1J+klQoZubdKswlbxcsbojda3mye9kiU= github.com/authzed/grpcutil v0.0.0-20240123092924-129dc0a6a6e1 h1:zBfQzia6Hz45pJBeURTrv1b6HezmejB6UmiGuBilHZM= diff --git a/go.mod b/go.mod index 5c447b9e7f..2e517b77ee 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( contrib.go.opencensus.io/exporter/prometheus v0.4.2 github.com/IBM/pgxpoolprometheus v1.1.1 github.com/Masterminds/squirrel v1.5.4 - github.com/authzed/authzed-go v0.11.2-0.20240418174337-42f221719227 + github.com/authzed/authzed-go v0.11.2-0.20240501223711-10d4e60e2a07 // NOTE: We are using a *copy* of `cel-go` here to ensure there isn't a conflict // with the version used in Kubernetes. This is a temporary measure until we can diff --git a/go.sum b/go.sum index 32735be503..b42467d93f 100644 --- a/go.sum +++ b/go.sum @@ -692,8 +692,8 @@ github.com/ashanbrown/forbidigo v1.6.0 h1:D3aewfM37Yb3pxHujIPSpTf6oQk9sc9WZi8ger github.com/ashanbrown/forbidigo v1.6.0/go.mod h1:Y8j9jy9ZYAEHXdu723cUlraTqbzjKF1MUyfOKL+AjcU= github.com/ashanbrown/makezero v1.1.1 h1:iCQ87C0V0vSyO+M9E/FZYbu65auqH0lnsOkf5FcB28s= github.com/ashanbrown/makezero v1.1.1/go.mod h1:i1bJLCRSCHOcOa9Y6MyF2FTfMZMFdHvxKHxgO5Z1axI= -github.com/authzed/authzed-go v0.11.2-0.20240418174337-42f221719227 h1:VczJwysQbGiSnJeyROxmF6/u8K7GZviVbIc4XGm9u1o= -github.com/authzed/authzed-go v0.11.2-0.20240418174337-42f221719227/go.mod h1:EFCDZMQbrhJSpSRUlAooJdACESdA4VnlIkCz1s0Pw+g= +github.com/authzed/authzed-go v0.11.2-0.20240501223711-10d4e60e2a07 h1:FtbBwpHxrmGgC8p8Dl4MtiouCcIKHUzFi2E9WC9NCuc= +github.com/authzed/authzed-go v0.11.2-0.20240501223711-10d4e60e2a07/go.mod h1:0SXN/1P1G/y87PaA8cuaNxx5QC0FjN8doNk6IPqcDkY= github.com/authzed/cel-go v0.20.2 h1:GlmLecGry7Z8HU0k+hmaHHUV05ZHrsFxduXHtIePvck= github.com/authzed/cel-go v0.20.2/go.mod h1:pJHVFWbqUHV1J+klQoZubdKswlbxcsbojda3mye9kiU= github.com/authzed/consistent v0.1.0 h1:tlh1wvKoRbjRhMm2P+X5WQQyR54SRoS4MyjLOg17Mp8= diff --git a/internal/services/v1/experimental.go b/internal/services/v1/experimental.go index fabe522403..45fd5462e2 100644 --- a/internal/services/v1/experimental.go +++ b/internal/services/v1/experimental.go @@ -430,6 +430,25 @@ func (es *experimentalServer) BulkCheckPermission(ctx context.Context, req *v1.B return toBulkCheckPermissionResponse(res), nil } +func (es *experimentalServer) ExperimentalSchemaDiff(ctx context.Context, req *v1.ExperimentalSchemaDiffRequest) (*v1.ExperimentalSchemaDiffResponse, error) { + atRevision, _, err := consistency.RevisionFromContext(ctx) + if err != nil { + return nil, err + } + + diff, existingSchema, comparisonSchema, err := schemaDiff(ctx, req.ComparisonSchema) + if err != nil { + return nil, es.rewriteError(ctx, err) + } + + resp, err := convertDiff(diff, existingSchema, comparisonSchema, atRevision) + if err != nil { + return nil, es.rewriteError(ctx, err) + } + + return resp, nil +} + func queryForEach( ctx context.Context, reader datastore.Reader, diff --git a/internal/services/v1/experimental_test.go b/internal/services/v1/experimental_test.go index 566f05999a..a7097a9835 100644 --- a/internal/services/v1/experimental_test.go +++ b/internal/services/v1/experimental_test.go @@ -681,3 +681,94 @@ func relToBulkRequestItem(rel string) *v1.BulkCheckPermissionRequestItem { } return item } + +func TestExperimentalSchemaDiff(t *testing.T) { + conn, cleanup, _, _ := testserver.NewTestServer(require.New(t), 0, memdb.DisableGC, true, tf.EmptyDatastore) + expClient := v1.NewExperimentalServiceClient(conn) + schemaClient := v1.NewSchemaServiceClient(conn) + defer cleanup() + + testCases := []struct { + name string + existingSchema string + comparisonSchema string + expectedError string + expectedResponse *v1.ExperimentalSchemaDiffResponse + }{ + { + name: "no changes", + existingSchema: `definition user {}`, + comparisonSchema: `definition user {}`, + expectedResponse: &v1.ExperimentalSchemaDiffResponse{}, + }, + { + name: "addition from existing schema", + existingSchema: `definition user {}`, + comparisonSchema: `definition user {} definition document {}`, + expectedResponse: &v1.ExperimentalSchemaDiffResponse{ + Diffs: []*v1.ExpSchemaDiff{ + { + Diff: &v1.ExpSchemaDiff_DefinitionAdded{ + DefinitionAdded: &v1.ExpDefinition{ + Name: "document", + Comment: "", + }, + }, + }, + }, + }, + }, + { + name: "removal from existing schema", + existingSchema: `definition user {} definition document {}`, + comparisonSchema: `definition user {}`, + expectedResponse: &v1.ExperimentalSchemaDiffResponse{ + Diffs: []*v1.ExpSchemaDiff{ + { + Diff: &v1.ExpSchemaDiff_DefinitionRemoved{ + DefinitionRemoved: &v1.ExpDefinition{ + Name: "document", + Comment: "", + }, + }, + }, + }, + }, + }, + { + name: "invalid comparison schema", + existingSchema: `definition user {}`, + comparisonSchema: `definition user { invalid`, + expectedError: "Expected end of statement or definition, found: TokenTypeIdentifier", + }, + } + + for _, tt := range testCases { + tt := tt + t.Run(tt.name, func(t *testing.T) { + // Write the existing schema. + _, err := schemaClient.WriteSchema(context.Background(), &v1.WriteSchemaRequest{ + Schema: tt.existingSchema, + }) + require.NoError(t, err) + + actual, err := expClient.ExperimentalSchemaDiff(context.Background(), &v1.ExperimentalSchemaDiffRequest{ + ComparisonSchema: tt.comparisonSchema, + Consistency: &v1.Consistency{ + Requirement: &v1.Consistency_FullyConsistent{FullyConsistent: true}, + }, + }) + + if tt.expectedError != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tt.expectedError) + } else { + require.NoError(t, err) + require.NotNil(t, actual.ReadAt) + actual.ReadAt = nil + + testutil.RequireProtoEqual(t, tt.expectedResponse, actual, "mismatch in response") + } + }) + } +} diff --git a/internal/services/v1/expreflection.go b/internal/services/v1/expreflection.go new file mode 100644 index 0000000000..6e6673c93c --- /dev/null +++ b/internal/services/v1/expreflection.go @@ -0,0 +1,525 @@ +package v1 + +import ( + "fmt" + "strings" + + v1 "github.com/authzed/authzed-go/proto/authzed/api/v1" + + "github.com/authzed/spicedb/pkg/caveats" + caveattypes "github.com/authzed/spicedb/pkg/caveats/types" + "github.com/authzed/spicedb/pkg/datastore" + "github.com/authzed/spicedb/pkg/diff" + caveatdiff "github.com/authzed/spicedb/pkg/diff/caveats" + nsdiff "github.com/authzed/spicedb/pkg/diff/namespace" + "github.com/authzed/spicedb/pkg/namespace" + core "github.com/authzed/spicedb/pkg/proto/core/v1" + iv1 "github.com/authzed/spicedb/pkg/proto/impl/v1" + "github.com/authzed/spicedb/pkg/spiceerrors" + "github.com/authzed/spicedb/pkg/tuple" + "github.com/authzed/spicedb/pkg/zedtoken" +) + +// convertDiff converts a schema diff into an API response. +func convertDiff( + diff *diff.SchemaDiff, + existingSchema *diff.DiffableSchema, + comparisonSchema *diff.DiffableSchema, + atRevision datastore.Revision, +) (*v1.ExperimentalSchemaDiffResponse, error) { + size := len(diff.AddedNamespaces) + len(diff.RemovedNamespaces) + len(diff.AddedCaveats) + len(diff.RemovedCaveats) + len(diff.ChangedNamespaces) + len(diff.ChangedCaveats) + diffs := make([]*v1.ExpSchemaDiff, 0, size) + + // Add/remove namespaces. + for _, ns := range diff.AddedNamespaces { + nsDef, err := namespaceAPIRepr(ns, comparisonSchema) + if err != nil { + return nil, err + } + + diffs = append(diffs, &v1.ExpSchemaDiff{ + Diff: &v1.ExpSchemaDiff_DefinitionAdded{ + DefinitionAdded: nsDef, + }, + }) + } + + for _, ns := range diff.RemovedNamespaces { + nsDef, err := namespaceAPIRepr(ns, existingSchema) + if err != nil { + return nil, err + } + + diffs = append(diffs, &v1.ExpSchemaDiff{ + Diff: &v1.ExpSchemaDiff_DefinitionRemoved{ + DefinitionRemoved: nsDef, + }, + }) + } + + // Add/remove caveats. + for _, caveat := range diff.AddedCaveats { + caveatDef, err := caveatAPIRepr(caveat, comparisonSchema) + if err != nil { + return nil, err + } + + diffs = append(diffs, &v1.ExpSchemaDiff{ + Diff: &v1.ExpSchemaDiff_CaveatAdded{ + CaveatAdded: caveatDef, + }, + }) + } + + for _, caveat := range diff.RemovedCaveats { + caveatDef, err := caveatAPIRepr(caveat, existingSchema) + if err != nil { + return nil, err + } + + diffs = append(diffs, &v1.ExpSchemaDiff{ + Diff: &v1.ExpSchemaDiff_CaveatRemoved{ + CaveatRemoved: caveatDef, + }, + }) + } + + // Changed namespaces. + for nsName, nsDiff := range diff.ChangedNamespaces { + for _, delta := range nsDiff.Deltas() { + switch delta.Type { + case nsdiff.AddedPermission: + permission, ok := comparisonSchema.GetRelation(nsName, delta.RelationName) + if !ok { + return nil, fmt.Errorf("relation %q not found in namespace %q", delta.RelationName, nsName) + } + + perm, err := permissionAPIRepr(permission, nsName) + if err != nil { + return nil, err + } + + diffs = append(diffs, &v1.ExpSchemaDiff{ + Diff: &v1.ExpSchemaDiff_PermissionAdded{ + PermissionAdded: perm, + }, + }) + + case nsdiff.AddedRelation: + relation, ok := comparisonSchema.GetRelation(nsName, delta.RelationName) + if !ok { + return nil, fmt.Errorf("relation %q not found in namespace %q", delta.RelationName, nsName) + } + + rel, err := relationAPIRepr(relation, nsName) + if err != nil { + return nil, err + } + + diffs = append(diffs, &v1.ExpSchemaDiff{ + Diff: &v1.ExpSchemaDiff_RelationAdded{ + RelationAdded: rel, + }, + }) + + case nsdiff.ChangedPermissionComment: + permission, ok := comparisonSchema.GetRelation(nsName, delta.RelationName) + if !ok { + return nil, fmt.Errorf("relation %q not found in namespace %q", delta.RelationName, nsName) + } + + perm, err := permissionAPIRepr(permission, nsName) + if err != nil { + return nil, err + } + + diffs = append(diffs, &v1.ExpSchemaDiff{ + Diff: &v1.ExpSchemaDiff_PermissionDocCommentChanged{ + PermissionDocCommentChanged: perm, + }, + }) + + case nsdiff.ChangedPermissionImpl: + permission, ok := comparisonSchema.GetRelation(nsName, delta.RelationName) + if !ok { + return nil, fmt.Errorf("relation %q not found in namespace %q", delta.RelationName, nsName) + } + + perm, err := permissionAPIRepr(permission, nsName) + if err != nil { + return nil, err + } + + diffs = append(diffs, &v1.ExpSchemaDiff{ + Diff: &v1.ExpSchemaDiff_PermissionExprChanged{ + PermissionExprChanged: perm, + }, + }) + + case nsdiff.ChangedRelationComment: + relation, ok := comparisonSchema.GetRelation(nsName, delta.RelationName) + if !ok { + return nil, fmt.Errorf("relation %q not found in namespace %q", delta.RelationName, nsName) + } + + rel, err := relationAPIRepr(relation, nsName) + if err != nil { + return nil, err + } + + diffs = append(diffs, &v1.ExpSchemaDiff{ + Diff: &v1.ExpSchemaDiff_RelationDocCommentChanged{ + RelationDocCommentChanged: rel, + }, + }) + + case nsdiff.LegacyChangedRelationImpl: + return nil, spiceerrors.MustBugf("legacy relation implementation changes are not supported") + + case nsdiff.NamespaceCommentsChanged: + def, err := namespaceAPIRepr(nsName, comparisonSchema) + if err != nil { + return nil, err + } + + diffs = append(diffs, &v1.ExpSchemaDiff{ + Diff: &v1.ExpSchemaDiff_DefinitionDocCommentChanged{ + DefinitionDocCommentChanged: def, + }, + }) + + case nsdiff.RelationAllowedTypeRemoved: + relation, ok := comparisonSchema.GetRelation(nsName, delta.RelationName) + if !ok { + return nil, fmt.Errorf("relation %q not found in namespace %q", delta.RelationName, nsName) + } + + rel, err := relationAPIRepr(relation, nsName) + if err != nil { + return nil, err + } + + diffs = append(diffs, &v1.ExpSchemaDiff{ + Diff: &v1.ExpSchemaDiff_RelationSubjectTypeRemoved{ + RelationSubjectTypeRemoved: &v1.ExpRelationSubjectTypeChange{ + Relation: rel, + ChangedSubjectType: typeAPIRepr(delta.AllowedType), + }, + }, + }) + + case nsdiff.RelationAllowedTypeAdded: + relation, ok := comparisonSchema.GetRelation(nsName, delta.RelationName) + if !ok { + return nil, fmt.Errorf("relation %q not found in namespace %q", delta.RelationName, nsName) + } + + rel, err := relationAPIRepr(relation, nsName) + if err != nil { + return nil, err + } + + diffs = append(diffs, &v1.ExpSchemaDiff{ + Diff: &v1.ExpSchemaDiff_RelationSubjectTypeAdded{ + RelationSubjectTypeAdded: &v1.ExpRelationSubjectTypeChange{ + Relation: rel, + ChangedSubjectType: typeAPIRepr(delta.AllowedType), + }, + }, + }) + + case nsdiff.RemovedPermission: + permission, ok := existingSchema.GetRelation(nsName, delta.RelationName) + if !ok { + return nil, fmt.Errorf("relation %q not found in namespace %q", delta.RelationName, nsName) + } + + perm, err := permissionAPIRepr(permission, nsName) + if err != nil { + return nil, err + } + + diffs = append(diffs, &v1.ExpSchemaDiff{ + Diff: &v1.ExpSchemaDiff_PermissionRemoved{ + PermissionRemoved: perm, + }, + }) + + case nsdiff.RemovedRelation: + relation, ok := existingSchema.GetRelation(nsName, delta.RelationName) + if !ok { + return nil, fmt.Errorf("relation %q not found in namespace %q", delta.RelationName, nsName) + } + + rel, err := relationAPIRepr(relation, nsName) + if err != nil { + return nil, err + } + + diffs = append(diffs, &v1.ExpSchemaDiff{ + Diff: &v1.ExpSchemaDiff_RelationRemoved{ + RelationRemoved: rel, + }, + }) + + case nsdiff.NamespaceAdded: + return nil, spiceerrors.MustBugf("should be handled above") + + case nsdiff.NamespaceRemoved: + return nil, spiceerrors.MustBugf("should be handled above") + + default: + return nil, spiceerrors.MustBugf("unexpected delta type %v", delta.Type) + } + } + } + + // Changed caveats. + for caveatName, caveatDiff := range diff.ChangedCaveats { + for _, delta := range caveatDiff.Deltas() { + switch delta.Type { + case caveatdiff.CaveatCommentsChanged: + caveat, err := caveatAPIRepr(caveatName, comparisonSchema) + if err != nil { + return nil, err + } + + diffs = append(diffs, &v1.ExpSchemaDiff{ + Diff: &v1.ExpSchemaDiff_CaveatDocCommentChanged{ + CaveatDocCommentChanged: caveat, + }, + }) + + case caveatdiff.AddedParameter: + paramDef, err := caveatAPIParamRepr(delta.ParameterName, caveatName, comparisonSchema) + if err != nil { + return nil, err + } + + diffs = append(diffs, &v1.ExpSchemaDiff{ + Diff: &v1.ExpSchemaDiff_CaveatParameterAdded{ + CaveatParameterAdded: paramDef, + }, + }) + + case caveatdiff.RemovedParameter: + paramDef, err := caveatAPIParamRepr(delta.ParameterName, caveatName, existingSchema) + if err != nil { + return nil, err + } + + diffs = append(diffs, &v1.ExpSchemaDiff{ + Diff: &v1.ExpSchemaDiff_CaveatParameterRemoved{ + CaveatParameterRemoved: paramDef, + }, + }) + + case caveatdiff.ParameterTypeChanged: + previousParamDef, err := caveatAPIParamRepr(delta.ParameterName, caveatName, existingSchema) + if err != nil { + return nil, err + } + + paramDef, err := caveatAPIParamRepr(delta.ParameterName, caveatName, comparisonSchema) + if err != nil { + return nil, err + } + + diffs = append(diffs, &v1.ExpSchemaDiff{ + Diff: &v1.ExpSchemaDiff_CaveatParameterTypeChanged{ + CaveatParameterTypeChanged: &v1.ExpCaveatParameterTypeChange{ + Parameter: paramDef, + PreviousType: previousParamDef.Type, + }, + }, + }) + + case caveatdiff.CaveatExpressionChanged: + caveat, err := caveatAPIRepr(caveatName, comparisonSchema) + if err != nil { + return nil, err + } + + diffs = append(diffs, &v1.ExpSchemaDiff{ + Diff: &v1.ExpSchemaDiff_CaveatExprChanged{ + CaveatExprChanged: caveat, + }, + }) + + case caveatdiff.CaveatAdded: + return nil, spiceerrors.MustBugf("should be handled above") + + case caveatdiff.CaveatRemoved: + return nil, spiceerrors.MustBugf("should be handled above") + + default: + return nil, spiceerrors.MustBugf("unexpected delta type %v", delta.Type) + } + } + } + + return &v1.ExperimentalSchemaDiffResponse{ + Diffs: diffs, + ReadAt: zedtoken.MustNewFromRevision(atRevision), + }, nil +} + +// namespaceAPIRepr builds an API representation of a namespace. +func namespaceAPIRepr(namespaceName string, schema *diff.DiffableSchema) (*v1.ExpDefinition, error) { + nsDef, ok := schema.GetNamespace(namespaceName) + if !ok { + return nil, fmt.Errorf("namespace %q not found in schema", namespaceName) + } + + relations := make([]*v1.ExpRelation, 0, len(nsDef.Relation)) + permissions := make([]*v1.ExpPermission, 0, len(nsDef.Relation)) + + for _, rel := range nsDef.Relation { + if namespace.GetRelationKind(rel) == iv1.RelationMetadata_PERMISSION { + permission, err := permissionAPIRepr(rel, nsDef.Name) + if err != nil { + return nil, err + } + + permissions = append(permissions, permission) + continue + } + + relation, err := relationAPIRepr(rel, nsDef.Name) + if err != nil { + return nil, err + } + + relations = append(relations, relation) + } + + comments := namespace.GetComments(nsDef.Metadata) + return &v1.ExpDefinition{ + Name: namespaceName, + Comment: strings.Join(comments, "\n"), + Relations: relations, + Permissions: permissions, + }, nil +} + +// permissionAPIRepr builds an API representation of a permission. +func permissionAPIRepr(relation *core.Relation, parentDefName string) (*v1.ExpPermission, error) { + comments := namespace.GetComments(relation.Metadata) + return &v1.ExpPermission{ + Name: relation.Name, + Comment: strings.Join(comments, "\n"), + ParentDefinitionName: parentDefName, + }, nil +} + +// relationAPIRepresentation builds an API representation of a relation. +func relationAPIRepr(relation *core.Relation, parentDefName string) (*v1.ExpRelation, error) { + comments := namespace.GetComments(relation.Metadata) + + var subjectTypes []*v1.ExpTypeReference + if relation.TypeInformation != nil { + subjectTypes = make([]*v1.ExpTypeReference, 0, len(relation.TypeInformation.AllowedDirectRelations)) + for _, subjectType := range relation.TypeInformation.AllowedDirectRelations { + typeref := typeAPIRepr(subjectType) + subjectTypes = append(subjectTypes, typeref) + } + } + + return &v1.ExpRelation{ + Name: relation.Name, + Comment: strings.Join(comments, "\n"), + ParentDefinitionName: parentDefName, + SubjectTypes: subjectTypes, + }, nil +} + +// typeAPIRepr builds an API representation of a type. +func typeAPIRepr(subjectType *core.AllowedRelation) *v1.ExpTypeReference { + typeref := &v1.ExpTypeReference{ + SubjectDefinitionName: subjectType.Namespace, + Typeref: &v1.ExpTypeReference_IsTerminalSubject{}, + } + + if subjectType.GetRelation() != tuple.Ellipsis { + typeref.Typeref = &v1.ExpTypeReference_OptionalRelationName{ + OptionalRelationName: subjectType.GetRelation(), + } + } else if subjectType.GetPublicWildcard() != nil { + typeref.Typeref = &v1.ExpTypeReference_IsPublicWildcard{} + } + + if subjectType.GetRequiredCaveat() != nil { + typeref.OptionalCaveatName = subjectType.GetRequiredCaveat().CaveatName + } + + return typeref +} + +// caveatAPIRepr builds an API representation of a caveat. +func caveatAPIRepr(caveatName string, schema *diff.DiffableSchema) (*v1.ExpCaveat, error) { + caveatDef, ok := schema.GetCaveat(caveatName) + if !ok { + return nil, fmt.Errorf("caveat %q not found in schema", caveatName) + } + + parameters := make([]*v1.ExpCaveatParameter, 0, len(caveatDef.ParameterTypes)) + for paramName, paramType := range caveatDef.ParameterTypes { + decoded, err := caveattypes.DecodeParameterType(paramType) + if err != nil { + return nil, fmt.Errorf("invalid parameter type on caveat: %w", err) + } + + parameters = append(parameters, &v1.ExpCaveatParameter{ + Name: paramName, + Type: decoded.String(), + ParentCaveatName: caveatName, + }) + } + + parameterTypes, err := caveattypes.DecodeParameterTypes(caveatDef.ParameterTypes) + if err != nil { + return nil, fmt.Errorf("invalid caveat parameters: %w", err) + } + + deserializedExpression, err := caveats.DeserializeCaveat(caveatDef.SerializedExpression, parameterTypes) + if err != nil { + return nil, fmt.Errorf("invalid caveat expression bytes: %w", err) + } + + exprString, err := deserializedExpression.ExprString() + if err != nil { + return nil, fmt.Errorf("invalid caveat expression: %w", err) + } + + comments := namespace.GetComments(caveatDef.Metadata) + return &v1.ExpCaveat{ + Name: caveatName, + Comment: strings.Join(comments, "\n"), + Parameters: parameters, + Expression: exprString, + }, nil +} + +// caveatAPIParamRepresentation builds an API representation of a caveat parameter. +func caveatAPIParamRepr(paramName, parentCaveatName string, schema *diff.DiffableSchema) (*v1.ExpCaveatParameter, error) { + caveatDef, ok := schema.GetCaveat(parentCaveatName) + if !ok { + return nil, fmt.Errorf("caveat %q not found in schema", parentCaveatName) + } + + paramType, ok := caveatDef.ParameterTypes[paramName] + if !ok { + return nil, fmt.Errorf("parameter %q not found in caveat %q", paramName, parentCaveatName) + } + + decoded, err := caveattypes.DecodeParameterType(paramType) + if err != nil { + return nil, fmt.Errorf("invalid parameter type on caveat: %w", err) + } + + return &v1.ExpCaveatParameter{ + Name: paramName, + Type: decoded.String(), + ParentCaveatName: parentCaveatName, + }, nil +} diff --git a/internal/services/v1/expreflection_test.go b/internal/services/v1/expreflection_test.go new file mode 100644 index 0000000000..0d9741fe15 --- /dev/null +++ b/internal/services/v1/expreflection_test.go @@ -0,0 +1,579 @@ +package v1 + +import ( + "reflect" + "strings" + "testing" + + v1 "github.com/authzed/authzed-go/proto/authzed/api/v1" + "github.com/ettle/strcase" + "github.com/stretchr/testify/require" + + "github.com/authzed/spicedb/pkg/datastore/revisionparsing" + "github.com/authzed/spicedb/pkg/diff" + "github.com/authzed/spicedb/pkg/genutil/mapz" + "github.com/authzed/spicedb/pkg/schemadsl/compiler" + "github.com/authzed/spicedb/pkg/schemadsl/input" + "github.com/authzed/spicedb/pkg/testutil" +) + +func TestConvertDiff(t *testing.T) { + tcs := []struct { + name string + existingSchema string + comparisonSchema string + expectedResponse *v1.ExperimentalSchemaDiffResponse + }{ + { + "no diff", + `definition user {}`, + `definition user {}`, + &v1.ExperimentalSchemaDiffResponse{ + Diffs: []*v1.ExpSchemaDiff{}, + }, + }, + { + "add namespace", + ``, + `definition user {}`, + &v1.ExperimentalSchemaDiffResponse{ + Diffs: []*v1.ExpSchemaDiff{ + { + Diff: &v1.ExpSchemaDiff_DefinitionAdded{ + DefinitionAdded: &v1.ExpDefinition{ + Name: "user", + Comment: "", + }, + }, + }, + }, + }, + }, + { + "remove namespace", + `definition user {}`, + ``, + &v1.ExperimentalSchemaDiffResponse{ + Diffs: []*v1.ExpSchemaDiff{ + { + Diff: &v1.ExpSchemaDiff_DefinitionRemoved{ + DefinitionRemoved: &v1.ExpDefinition{ + Name: "user", + Comment: "", + }, + }, + }, + }, + }, + }, + { + "change namespace comment", + `definition user {}`, + `// user has a comment + definition user {}`, + &v1.ExperimentalSchemaDiffResponse{ + Diffs: []*v1.ExpSchemaDiff{ + { + Diff: &v1.ExpSchemaDiff_DefinitionDocCommentChanged{ + DefinitionDocCommentChanged: &v1.ExpDefinition{ + Name: "user", + Comment: "// user has a comment", + }, + }, + }, + }, + }, + }, + { + "add caveat", + ``, + `caveat someCaveat(someparam int) { someparam < 42 }`, + &v1.ExperimentalSchemaDiffResponse{ + Diffs: []*v1.ExpSchemaDiff{ + { + Diff: &v1.ExpSchemaDiff_CaveatAdded{ + CaveatAdded: &v1.ExpCaveat{ + Name: "someCaveat", + Comment: "", + Expression: "someparam < 42", + Parameters: []*v1.ExpCaveatParameter{ + { + Name: "someparam", + Type: "int", + ParentCaveatName: "someCaveat", + }, + }, + }, + }, + }, + }, + }, + }, + { + "remove caveat", + `caveat someCaveat(someparam int) { someparam < 42 }`, + ``, + &v1.ExperimentalSchemaDiffResponse{ + Diffs: []*v1.ExpSchemaDiff{ + { + Diff: &v1.ExpSchemaDiff_CaveatRemoved{ + CaveatRemoved: &v1.ExpCaveat{ + Name: "someCaveat", + Comment: "", + Expression: "someparam < 42", + Parameters: []*v1.ExpCaveatParameter{ + { + Name: "someparam", + Type: "int", + ParentCaveatName: "someCaveat", + }, + }, + }, + }, + }, + }, + }, + }, + { + "change caveat comment", + `// someCaveat has a comment + caveat someCaveat(someparam int) { someparam < 42 }`, + `// someCaveat has b comment + caveat someCaveat(someparam int) { someparam < 42 }`, + &v1.ExperimentalSchemaDiffResponse{ + Diffs: []*v1.ExpSchemaDiff{ + { + Diff: &v1.ExpSchemaDiff_CaveatDocCommentChanged{ + CaveatDocCommentChanged: &v1.ExpCaveat{ + Name: "someCaveat", + Comment: "// someCaveat has b comment", + Expression: "someparam < 42", + Parameters: []*v1.ExpCaveatParameter{ + { + Name: "someparam", + Type: "int", + ParentCaveatName: "someCaveat", + }, + }, + }, + }, + }, + }, + }, + }, + { + "added relation", + `definition user {}`, + `definition user { relation somerel: user; }`, + &v1.ExperimentalSchemaDiffResponse{ + Diffs: []*v1.ExpSchemaDiff{ + { + Diff: &v1.ExpSchemaDiff_RelationAdded{ + RelationAdded: &v1.ExpRelation{ + Name: "somerel", + Comment: "", + ParentDefinitionName: "user", + SubjectTypes: []*v1.ExpTypeReference{ + { + SubjectDefinitionName: "user", + Typeref: &v1.ExpTypeReference_IsTerminalSubject{}, + }, + }, + }, + }, + }, + }, + }, + }, + { + "removed relation", + `definition user { relation somerel: user; }`, + `definition user {}`, + &v1.ExperimentalSchemaDiffResponse{ + Diffs: []*v1.ExpSchemaDiff{ + { + Diff: &v1.ExpSchemaDiff_RelationRemoved{ + RelationRemoved: &v1.ExpRelation{ + Name: "somerel", + Comment: "", + ParentDefinitionName: "user", + SubjectTypes: []*v1.ExpTypeReference{ + { + SubjectDefinitionName: "user", + Typeref: &v1.ExpTypeReference_IsTerminalSubject{}, + }, + }, + }, + }, + }, + }, + }, + }, + { + "relation type added", + `definition user {} + + definition anon {} + + definition resource { + relation viewer: anon + } + `, + `definition user {} + + definition anon {} + + definition resource { + relation viewer: user | anon + } + `, + &v1.ExperimentalSchemaDiffResponse{ + Diffs: []*v1.ExpSchemaDiff{ + { + Diff: &v1.ExpSchemaDiff_RelationSubjectTypeAdded{ + RelationSubjectTypeAdded: &v1.ExpRelationSubjectTypeChange{ + Relation: &v1.ExpRelation{ + Name: "viewer", + Comment: "", + ParentDefinitionName: "resource", + SubjectTypes: []*v1.ExpTypeReference{ + { + SubjectDefinitionName: "user", + Typeref: &v1.ExpTypeReference_IsTerminalSubject{}, + }, + { + SubjectDefinitionName: "anon", + Typeref: &v1.ExpTypeReference_IsTerminalSubject{}, + }, + }, + }, + ChangedSubjectType: &v1.ExpTypeReference{ + SubjectDefinitionName: "user", + Typeref: &v1.ExpTypeReference_IsTerminalSubject{}, + }, + }, + }, + }, + }, + }, + }, + { + "relation type removed", + `definition user {} + + definition anon {} + + definition resource { + relation viewer: anon | user + } + `, + `definition user {} + + definition anon {} + + definition resource { + relation viewer: user + } + `, + &v1.ExperimentalSchemaDiffResponse{ + Diffs: []*v1.ExpSchemaDiff{ + { + Diff: &v1.ExpSchemaDiff_RelationSubjectTypeRemoved{ + RelationSubjectTypeRemoved: &v1.ExpRelationSubjectTypeChange{ + Relation: &v1.ExpRelation{ + Name: "viewer", + Comment: "", + ParentDefinitionName: "resource", + SubjectTypes: []*v1.ExpTypeReference{ + { + SubjectDefinitionName: "user", + Typeref: &v1.ExpTypeReference_IsTerminalSubject{}, + }, + }, + }, + ChangedSubjectType: &v1.ExpTypeReference{ + SubjectDefinitionName: "anon", + Typeref: &v1.ExpTypeReference_IsTerminalSubject{}, + }, + }, + }, + }, + }, + }, + }, + { + "relation comment changed", + `definition user {} + + definition resource { + relation viewer: user + }`, + `definition user {} + + definition resource { + // viewer has a comment + relation viewer: user + }`, + &v1.ExperimentalSchemaDiffResponse{ + Diffs: []*v1.ExpSchemaDiff{ + { + Diff: &v1.ExpSchemaDiff_RelationDocCommentChanged{ + RelationDocCommentChanged: &v1.ExpRelation{ + Name: "viewer", + Comment: "// viewer has a comment", + ParentDefinitionName: "resource", + SubjectTypes: []*v1.ExpTypeReference{ + { + SubjectDefinitionName: "user", + Typeref: &v1.ExpTypeReference_IsTerminalSubject{}, + }, + }, + }, + }, + }, + }, + }, + }, + { + "added permission", + `definition user {} + + definition resource { + } + `, + `definition user {} + + definition resource { + permission foo = nil + }`, + &v1.ExperimentalSchemaDiffResponse{ + Diffs: []*v1.ExpSchemaDiff{ + { + Diff: &v1.ExpSchemaDiff_PermissionAdded{ + PermissionAdded: &v1.ExpPermission{ + Name: "foo", + Comment: "", + ParentDefinitionName: "resource", + }, + }, + }, + }, + }, + }, + { + "removed permission", + `definition user {} + + definition resource { + permission foo = nil + }`, + `definition user {} + + definition resource { + }`, + &v1.ExperimentalSchemaDiffResponse{ + Diffs: []*v1.ExpSchemaDiff{ + { + Diff: &v1.ExpSchemaDiff_PermissionRemoved{ + PermissionRemoved: &v1.ExpPermission{ + Name: "foo", + Comment: "", + ParentDefinitionName: "resource", + }, + }, + }, + }, + }, + }, + { + "permission comment changed", + `definition user {} + + definition resource { + // foo has a comment + permission foo = nil + }`, + `definition user {} + + definition resource { + // foo has a new comment + permission foo = nil + }`, + &v1.ExperimentalSchemaDiffResponse{ + Diffs: []*v1.ExpSchemaDiff{ + { + Diff: &v1.ExpSchemaDiff_PermissionDocCommentChanged{ + PermissionDocCommentChanged: &v1.ExpPermission{ + Name: "foo", + Comment: "// foo has a new comment", + ParentDefinitionName: "resource", + }, + }, + }, + }, + }, + }, + { + "permission expression changed", + `definition resource { + permission foo = nil + }`, + `definition resource { + permission foo = foo + }`, + &v1.ExperimentalSchemaDiffResponse{ + Diffs: []*v1.ExpSchemaDiff{ + { + Diff: &v1.ExpSchemaDiff_PermissionExprChanged{ + PermissionExprChanged: &v1.ExpPermission{ + Name: "foo", + Comment: "", + ParentDefinitionName: "resource", + }, + }, + }, + }, + }, + }, + { + "caveat parameter added", + `caveat someCaveat(someparam int) { someparam < 42 }`, + `caveat someCaveat(someparam int, someparam2 string) { someparam < 42 }`, + &v1.ExperimentalSchemaDiffResponse{ + Diffs: []*v1.ExpSchemaDiff{ + { + Diff: &v1.ExpSchemaDiff_CaveatParameterAdded{ + CaveatParameterAdded: &v1.ExpCaveatParameter{ + Name: "someparam2", + Type: "string", + ParentCaveatName: "someCaveat", + }, + }, + }, + }, + }, + }, + { + "caveat parameter removed", + `caveat someCaveat(someparam int, someparam2 string) { someparam < 42 }`, + `caveat someCaveat(someparam int) { someparam < 42 }`, + &v1.ExperimentalSchemaDiffResponse{ + Diffs: []*v1.ExpSchemaDiff{ + { + Diff: &v1.ExpSchemaDiff_CaveatParameterRemoved{ + CaveatParameterRemoved: &v1.ExpCaveatParameter{ + Name: "someparam2", + Type: "string", + ParentCaveatName: "someCaveat", + }, + }, + }, + }, + }, + }, + { + "caveat parameter type changed", + `caveat someCaveat(someparam int) { someparam < 42 }`, + `caveat someCaveat(someparam uint) { someparam < 42 }`, + &v1.ExperimentalSchemaDiffResponse{ + Diffs: []*v1.ExpSchemaDiff{ + { + Diff: &v1.ExpSchemaDiff_CaveatParameterTypeChanged{ + CaveatParameterTypeChanged: &v1.ExpCaveatParameterTypeChange{ + Parameter: &v1.ExpCaveatParameter{ + Name: "someparam", + Type: "uint", + ParentCaveatName: "someCaveat", + }, + PreviousType: "int", + }, + }, + }, + }, + }, + }, + { + "caveat expression changes", + `caveat someCaveat(someparam int) { someparam < 42 }`, + `caveat someCaveat(someparam int) { someparam < 43 }`, + &v1.ExperimentalSchemaDiffResponse{ + Diffs: []*v1.ExpSchemaDiff{ + { + Diff: &v1.ExpSchemaDiff_CaveatExprChanged{ + CaveatExprChanged: &v1.ExpCaveat{ + Name: "someCaveat", + Comment: "", + Expression: "someparam < 43", + Parameters: []*v1.ExpCaveatParameter{ + { + Name: "someparam", + Type: "int", + ParentCaveatName: "someCaveat", + }, + }, + }, + }, + }, + }, + }, + }, + } + + encounteredDiffTypes := mapz.NewSet[string]() + casesRun := 0 + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + casesRun++ + + existingSchema, err := compiler.Compile(compiler.InputSchema{ + Source: input.Source("schema"), + SchemaString: tc.existingSchema, + }, compiler.AllowUnprefixedObjectType()) + require.NoError(t, err) + + comparisonSchema, err := compiler.Compile(compiler.InputSchema{ + Source: input.Source("schema"), + SchemaString: tc.comparisonSchema, + }, compiler.AllowUnprefixedObjectType()) + require.NoError(t, err) + + es := diff.NewDiffableSchemaFromCompiledSchema(existingSchema) + cs := diff.NewDiffableSchemaFromCompiledSchema(comparisonSchema) + + diff, err := diff.DiffSchemas(es, cs) + require.NoError(t, err) + + resp, err := convertDiff( + diff, + &es, + &cs, + revisionparsing.MustParseRevisionForTest("1"), + ) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + require.NotNil(t, resp.ReadAt) + resp.ReadAt = nil + + testutil.RequireProtoEqual(t, tc.expectedResponse, resp, "got mismatch") + + for _, diff := range resp.Diffs { + name := reflect.TypeOf(diff.GetDiff()).String() + encounteredDiffTypes.Add(strings.ToLower(strings.Split(name, "_")[1])) + } + }) + } + + if casesRun == len(tcs) { + msg := &v1.ExpSchemaDiff{} + + allDiffTypes := mapz.NewSet[string]() + fields := msg.ProtoReflect().Descriptor().Oneofs().ByName("diff").Fields() + for i := 0; i < fields.Len(); i++ { + allDiffTypes.Add(strings.ToLower(strcase.ToCamel(string(fields.Get(i).Name())))) + } + + require.Empty(t, allDiffTypes.Subtract(encounteredDiffTypes).AsSlice()) + } +} diff --git a/internal/services/v1/reflection.go b/internal/services/v1/reflection.go new file mode 100644 index 0000000000..e50ed9e007 --- /dev/null +++ b/internal/services/v1/reflection.go @@ -0,0 +1,68 @@ +package v1 + +import ( + "context" + + datastoremw "github.com/authzed/spicedb/internal/middleware/datastore" + "github.com/authzed/spicedb/pkg/diff" + "github.com/authzed/spicedb/pkg/middleware/consistency" + core "github.com/authzed/spicedb/pkg/proto/core/v1" + "github.com/authzed/spicedb/pkg/schemadsl/compiler" + "github.com/authzed/spicedb/pkg/schemadsl/input" +) + +func schemaDiff(ctx context.Context, comparisonSchemaString string) (*diff.SchemaDiff, *diff.DiffableSchema, *diff.DiffableSchema, error) { + ds := datastoremw.MustFromContext(ctx) + + // Compile the comparison schema. + compiled, err := compiler.Compile(compiler.InputSchema{ + Source: input.Source("schema"), + SchemaString: comparisonSchemaString, + }, compiler.AllowUnprefixedObjectType()) + if err != nil { + return nil, nil, nil, err + } + + // Get the schema at the requested revision. + atRevision, _, err := consistency.RevisionFromContext(ctx) + if err != nil { + return nil, nil, nil, err + } + + reader := ds.SnapshotReader(atRevision) + + namespacesAndRevs, err := reader.ListAllNamespaces(ctx) + if err != nil { + return nil, nil, nil, err + } + + caveatsAndRevs, err := reader.ListAllCaveats(ctx) + if err != nil { + return nil, nil, nil, err + } + + namespaces := make([]*core.NamespaceDefinition, 0, len(namespacesAndRevs)) + for _, namespaceAndRev := range namespacesAndRevs { + namespaces = append(namespaces, namespaceAndRev.Definition) + } + + caveats := make([]*core.CaveatDefinition, 0, len(caveatsAndRevs)) + for _, caveatAndRev := range caveatsAndRevs { + caveats = append(caveats, caveatAndRev.Definition) + } + + // Perform the diff. + existingSchema := diff.DiffableSchema{ + ObjectDefinitions: namespaces, + CaveatDefinitions: caveats, + } + comparisonSchema := diff.NewDiffableSchemaFromCompiledSchema(compiled) + + diff, err := diff.DiffSchemas(existingSchema, comparisonSchema) + if err != nil { + return nil, nil, nil, err + } + + // Return the diff. + return diff, &existingSchema, &comparisonSchema, nil +} diff --git a/pkg/diff/diff_test.go b/pkg/diff/diff_test.go index c0cda89b0f..818c4b863e 100644 --- a/pkg/diff/diff_test.go +++ b/pkg/diff/diff_test.go @@ -3,11 +3,12 @@ package diff import ( "testing" + "github.com/stretchr/testify/require" + "github.com/authzed/spicedb/pkg/diff/caveats" "github.com/authzed/spicedb/pkg/diff/namespace" "github.com/authzed/spicedb/pkg/schemadsl/compiler" "github.com/authzed/spicedb/pkg/schemadsl/input" - "github.com/stretchr/testify/require" ) func TestDiffSchemas(t *testing.T) { From 6cc9d62db393cc982501e3b7e5139acbe227e1fc Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Thu, 2 May 2024 15:01:15 -0400 Subject: [PATCH 3/4] Add experimental schema reflection API --- internal/services/v1/experimental.go | 62 +++++-- internal/services/v1/experimental_test.go | 208 ++++++++++++++++++++++ internal/services/v1/expreflection.go | 49 +++-- internal/services/v1/reflection.go | 43 +++-- 4 files changed, 315 insertions(+), 47 deletions(-) diff --git a/internal/services/v1/experimental.go b/internal/services/v1/experimental.go index 45fd5462e2..d4ec5cc1ab 100644 --- a/internal/services/v1/experimental.go +++ b/internal/services/v1/experimental.go @@ -8,6 +8,17 @@ import ( "strings" "time" + "github.com/authzed/spicedb/internal/dispatch" + log "github.com/authzed/spicedb/internal/logging" + "github.com/authzed/spicedb/internal/middleware" + "github.com/authzed/spicedb/internal/middleware/consistency" + datastoremw "github.com/authzed/spicedb/internal/middleware/datastore" + "github.com/authzed/spicedb/internal/middleware/handwrittenvalidation" + "github.com/authzed/spicedb/internal/middleware/streamtimeout" + "github.com/authzed/spicedb/internal/middleware/usagemetrics" + "github.com/authzed/spicedb/internal/relationships" + "github.com/authzed/spicedb/internal/services/shared" + "github.com/authzed/spicedb/internal/services/v1/options" "github.com/authzed/spicedb/pkg/cursor" "github.com/authzed/spicedb/pkg/datastore" dsoptions "github.com/authzed/spicedb/pkg/datastore/options" @@ -16,22 +27,11 @@ import ( implv1 "github.com/authzed/spicedb/pkg/proto/impl/v1" "github.com/authzed/spicedb/pkg/tuple" "github.com/authzed/spicedb/pkg/typesystem" + "github.com/authzed/spicedb/pkg/zedtoken" v1 "github.com/authzed/authzed-go/proto/authzed/api/v1" grpcvalidate "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/validator" "github.com/samber/lo" - - "github.com/authzed/spicedb/internal/dispatch" - log "github.com/authzed/spicedb/internal/logging" - "github.com/authzed/spicedb/internal/middleware" - "github.com/authzed/spicedb/internal/middleware/consistency" - datastoremw "github.com/authzed/spicedb/internal/middleware/datastore" - "github.com/authzed/spicedb/internal/middleware/handwrittenvalidation" - "github.com/authzed/spicedb/internal/middleware/streamtimeout" - "github.com/authzed/spicedb/internal/middleware/usagemetrics" - "github.com/authzed/spicedb/internal/relationships" - "github.com/authzed/spicedb/internal/services/shared" - "github.com/authzed/spicedb/internal/services/v1/options" ) const ( @@ -430,6 +430,40 @@ func (es *experimentalServer) BulkCheckPermission(ctx context.Context, req *v1.B return toBulkCheckPermissionResponse(res), nil } +func (es *experimentalServer) ExperimentalReflectSchema(ctx context.Context, req *v1.ExperimentalReflectSchemaRequest) (*v1.ExperimentalReflectSchemaResponse, error) { + // Get the current schema. + schema, atRevision, err := loadCurrentSchema(ctx) + if err != nil { + return nil, shared.RewriteErrorWithoutConfig(ctx, err) + } + + definitions := make([]*v1.ExpDefinition, 0, len(schema.ObjectDefinitions)) + for _, ns := range schema.ObjectDefinitions { + def, err := namespaceAPIRepr(ns) + if err != nil { + return nil, shared.RewriteErrorWithoutConfig(ctx, err) + } + + definitions = append(definitions, def) + } + + caveats := make([]*v1.ExpCaveat, 0, len(schema.CaveatDefinitions)) + for _, cd := range schema.CaveatDefinitions { + caveat, err := caveatAPIRepr(cd) + if err != nil { + return nil, shared.RewriteErrorWithoutConfig(ctx, err) + } + + caveats = append(caveats, caveat) + } + + return &v1.ExperimentalReflectSchemaResponse{ + Definitions: definitions, + Caveats: caveats, + ReadAt: zedtoken.MustNewFromRevision(atRevision), + }, nil +} + func (es *experimentalServer) ExperimentalSchemaDiff(ctx context.Context, req *v1.ExperimentalSchemaDiffRequest) (*v1.ExperimentalSchemaDiffResponse, error) { atRevision, _, err := consistency.RevisionFromContext(ctx) if err != nil { @@ -438,12 +472,12 @@ func (es *experimentalServer) ExperimentalSchemaDiff(ctx context.Context, req *v diff, existingSchema, comparisonSchema, err := schemaDiff(ctx, req.ComparisonSchema) if err != nil { - return nil, es.rewriteError(ctx, err) + return nil, shared.RewriteErrorWithoutConfig(ctx, err) } resp, err := convertDiff(diff, existingSchema, comparisonSchema, atRevision) if err != nil { - return nil, es.rewriteError(ctx, err) + return nil, shared.RewriteErrorWithoutConfig(ctx, err) } return resp, nil diff --git a/internal/services/v1/experimental_test.go b/internal/services/v1/experimental_test.go index a7097a9835..4e16ceb697 100644 --- a/internal/services/v1/experimental_test.go +++ b/internal/services/v1/experimental_test.go @@ -772,3 +772,211 @@ func TestExperimentalSchemaDiff(t *testing.T) { }) } } + +func TestExperimentalReflectSchema(t *testing.T) { + conn, cleanup, _, _ := testserver.NewTestServer(require.New(t), 0, memdb.DisableGC, true, tf.EmptyDatastore) + expClient := v1.NewExperimentalServiceClient(conn) + schemaClient := v1.NewSchemaServiceClient(conn) + defer cleanup() + + testCases := []struct { + name string + schema string + expectedResponse *v1.ExperimentalReflectSchemaResponse + }{ + { + name: "simple schema", + schema: `definition user {}`, + expectedResponse: &v1.ExperimentalReflectSchemaResponse{ + Definitions: []*v1.ExpDefinition{ + { + Name: "user", + Comment: "", + }, + }, + }, + }, + { + name: "schema with comment", + schema: `// this is a user +definition user {}`, + expectedResponse: &v1.ExperimentalReflectSchemaResponse{ + Definitions: []*v1.ExpDefinition{ + { + Name: "user", + Comment: "// this is a user", + }, + }, + }, + }, + { + name: "full schema", + schema: ` + /** user represents a user */ + definition user {} + + /** group represents a group */ + definition group { + relation direct_member: user | group#member + relation admin: user + permission member = direct_member + admin + } + + caveat somecaveat(first int, second string) { + first == 1 && second == "two" + } + + /** document is a protected document */ + definition document { + // editor is a relation + relation editor: user | group#member + relation viewer: user | user with somecaveat | group#member + + // read all the things + permission read = viewer + editor + } + `, + expectedResponse: &v1.ExperimentalReflectSchemaResponse{ + Definitions: []*v1.ExpDefinition{ + { + Name: "document", + Comment: "/** document is a protected document */", + Relations: []*v1.ExpRelation{ + { + Name: "editor", + Comment: "// editor is a relation", + ParentDefinitionName: "document", + SubjectTypes: []*v1.ExpTypeReference{ + { + SubjectDefinitionName: "user", + Typeref: &v1.ExpTypeReference_IsTerminalSubject{}, + }, + { + SubjectDefinitionName: "group", + Typeref: &v1.ExpTypeReference_OptionalRelationName{ + OptionalRelationName: "member", + }, + }, + }, + }, + { + Name: "viewer", + Comment: "", + ParentDefinitionName: "document", + SubjectTypes: []*v1.ExpTypeReference{ + { + SubjectDefinitionName: "user", + Typeref: &v1.ExpTypeReference_IsTerminalSubject{}, + }, + { + SubjectDefinitionName: "user", + OptionalCaveatName: "somecaveat", + Typeref: &v1.ExpTypeReference_IsTerminalSubject{}, + }, + { + SubjectDefinitionName: "group", + Typeref: &v1.ExpTypeReference_OptionalRelationName{ + OptionalRelationName: "member", + }, + }, + }, + }, + }, + Permissions: []*v1.ExpPermission{ + { + Name: "read", + Comment: "// read all the things", + ParentDefinitionName: "document", + }, + }, + }, + { + Name: "group", + Comment: "/** group represents a group */", + Relations: []*v1.ExpRelation{ + { + Name: "direct_member", + Comment: "", + ParentDefinitionName: "group", + SubjectTypes: []*v1.ExpTypeReference{ + { + SubjectDefinitionName: "user", + Typeref: &v1.ExpTypeReference_IsTerminalSubject{}, + }, + { + SubjectDefinitionName: "group", + Typeref: &v1.ExpTypeReference_OptionalRelationName{OptionalRelationName: "member"}, + }, + }, + }, + { + Name: "admin", + Comment: "", + ParentDefinitionName: "group", + SubjectTypes: []*v1.ExpTypeReference{ + { + SubjectDefinitionName: "user", + Typeref: &v1.ExpTypeReference_IsTerminalSubject{}, + }, + }, + }, + }, + Permissions: []*v1.ExpPermission{ + { + Name: "member", + Comment: "", + ParentDefinitionName: "group", + }, + }, + }, + { + Name: "user", + Comment: "/** user represents a user */", + }, + }, + Caveats: []*v1.ExpCaveat{ + { + Name: "somecaveat", + Comment: "", + Expression: "first == 1 && second == \"two\"", + Parameters: []*v1.ExpCaveatParameter{ + { + Name: "first", + Type: "int", + ParentCaveatName: "somecaveat", + }, + { + Name: "second", + Type: "string", + ParentCaveatName: "somecaveat", + }, + }, + }, + }, + }, + }, + } + + for _, tt := range testCases { + tt := tt + t.Run(tt.name, func(t *testing.T) { + // Write the schema. + _, err := schemaClient.WriteSchema(context.Background(), &v1.WriteSchemaRequest{ + Schema: tt.schema, + }) + require.NoError(t, err) + + actual, err := expClient.ExperimentalReflectSchema(context.Background(), &v1.ExperimentalReflectSchemaRequest{ + Consistency: &v1.Consistency{ + Requirement: &v1.Consistency_FullyConsistent{FullyConsistent: true}, + }, + }) + + require.NoError(t, err) + require.NotNil(t, actual.ReadAt) + actual.ReadAt = nil + + testutil.RequireProtoEqual(t, tt.expectedResponse, actual, "mismatch in response") + }) + } +} diff --git a/internal/services/v1/expreflection.go b/internal/services/v1/expreflection.go index 6e6673c93c..172970d5ac 100644 --- a/internal/services/v1/expreflection.go +++ b/internal/services/v1/expreflection.go @@ -2,9 +2,11 @@ package v1 import ( "fmt" + "sort" "strings" v1 "github.com/authzed/authzed-go/proto/authzed/api/v1" + "golang.org/x/exp/maps" "github.com/authzed/spicedb/pkg/caveats" caveattypes "github.com/authzed/spicedb/pkg/caveats/types" @@ -32,7 +34,7 @@ func convertDiff( // Add/remove namespaces. for _, ns := range diff.AddedNamespaces { - nsDef, err := namespaceAPIRepr(ns, comparisonSchema) + nsDef, err := namespaceAPIReprForName(ns, comparisonSchema) if err != nil { return nil, err } @@ -45,7 +47,7 @@ func convertDiff( } for _, ns := range diff.RemovedNamespaces { - nsDef, err := namespaceAPIRepr(ns, existingSchema) + nsDef, err := namespaceAPIReprForName(ns, existingSchema) if err != nil { return nil, err } @@ -59,7 +61,7 @@ func convertDiff( // Add/remove caveats. for _, caveat := range diff.AddedCaveats { - caveatDef, err := caveatAPIRepr(caveat, comparisonSchema) + caveatDef, err := caveatAPIReprForName(caveat, comparisonSchema) if err != nil { return nil, err } @@ -72,7 +74,7 @@ func convertDiff( } for _, caveat := range diff.RemovedCaveats { - caveatDef, err := caveatAPIRepr(caveat, existingSchema) + caveatDef, err := caveatAPIReprForName(caveat, existingSchema) if err != nil { return nil, err } @@ -177,7 +179,7 @@ func convertDiff( return nil, spiceerrors.MustBugf("legacy relation implementation changes are not supported") case nsdiff.NamespaceCommentsChanged: - def, err := namespaceAPIRepr(nsName, comparisonSchema) + def, err := namespaceAPIReprForName(nsName, comparisonSchema) if err != nil { return nil, err } @@ -279,7 +281,7 @@ func convertDiff( for _, delta := range caveatDiff.Deltas() { switch delta.Type { case caveatdiff.CaveatCommentsChanged: - caveat, err := caveatAPIRepr(caveatName, comparisonSchema) + caveat, err := caveatAPIReprForName(caveatName, comparisonSchema) if err != nil { return nil, err } @@ -335,7 +337,7 @@ func convertDiff( }) case caveatdiff.CaveatExpressionChanged: - caveat, err := caveatAPIRepr(caveatName, comparisonSchema) + caveat, err := caveatAPIReprForName(caveatName, comparisonSchema) if err != nil { return nil, err } @@ -364,13 +366,17 @@ func convertDiff( }, nil } -// namespaceAPIRepr builds an API representation of a namespace. -func namespaceAPIRepr(namespaceName string, schema *diff.DiffableSchema) (*v1.ExpDefinition, error) { +// namespaceAPIReprForName builds an API representation of a namespace. +func namespaceAPIReprForName(namespaceName string, schema *diff.DiffableSchema) (*v1.ExpDefinition, error) { nsDef, ok := schema.GetNamespace(namespaceName) if !ok { return nil, fmt.Errorf("namespace %q not found in schema", namespaceName) } + return namespaceAPIRepr(nsDef) +} + +func namespaceAPIRepr(nsDef *core.NamespaceDefinition) (*v1.ExpDefinition, error) { relations := make([]*v1.ExpRelation, 0, len(nsDef.Relation)) permissions := make([]*v1.ExpPermission, 0, len(nsDef.Relation)) @@ -395,7 +401,7 @@ func namespaceAPIRepr(namespaceName string, schema *diff.DiffableSchema) (*v1.Ex comments := namespace.GetComments(nsDef.Metadata) return &v1.ExpDefinition{ - Name: namespaceName, + Name: nsDef.Name, Comment: strings.Join(comments, "\n"), Relations: relations, Permissions: permissions, @@ -455,15 +461,28 @@ func typeAPIRepr(subjectType *core.AllowedRelation) *v1.ExpTypeReference { return typeref } -// caveatAPIRepr builds an API representation of a caveat. -func caveatAPIRepr(caveatName string, schema *diff.DiffableSchema) (*v1.ExpCaveat, error) { +// caveatAPIReprForName builds an API representation of a caveat. +func caveatAPIReprForName(caveatName string, schema *diff.DiffableSchema) (*v1.ExpCaveat, error) { caveatDef, ok := schema.GetCaveat(caveatName) if !ok { return nil, fmt.Errorf("caveat %q not found in schema", caveatName) } + return caveatAPIRepr(caveatDef) +} + +// caveatAPIRepr builds an API representation of a caveat. +func caveatAPIRepr(caveatDef *core.CaveatDefinition) (*v1.ExpCaveat, error) { parameters := make([]*v1.ExpCaveatParameter, 0, len(caveatDef.ParameterTypes)) - for paramName, paramType := range caveatDef.ParameterTypes { + paramNames := maps.Keys(caveatDef.ParameterTypes) + sort.Strings(paramNames) + + for _, paramName := range paramNames { + paramType, ok := caveatDef.ParameterTypes[paramName] + if !ok { + return nil, fmt.Errorf("parameter %q not found in caveat %q", paramName, caveatDef.Name) + } + decoded, err := caveattypes.DecodeParameterType(paramType) if err != nil { return nil, fmt.Errorf("invalid parameter type on caveat: %w", err) @@ -472,7 +491,7 @@ func caveatAPIRepr(caveatName string, schema *diff.DiffableSchema) (*v1.ExpCavea parameters = append(parameters, &v1.ExpCaveatParameter{ Name: paramName, Type: decoded.String(), - ParentCaveatName: caveatName, + ParentCaveatName: caveatDef.Name, }) } @@ -493,7 +512,7 @@ func caveatAPIRepr(caveatName string, schema *diff.DiffableSchema) (*v1.ExpCavea comments := namespace.GetComments(caveatDef.Metadata) return &v1.ExpCaveat{ - Name: caveatName, + Name: caveatDef.Name, Comment: strings.Join(comments, "\n"), Parameters: parameters, Expression: exprString, diff --git a/internal/services/v1/reflection.go b/internal/services/v1/reflection.go index e50ed9e007..414725c4a7 100644 --- a/internal/services/v1/reflection.go +++ b/internal/services/v1/reflection.go @@ -4,6 +4,7 @@ import ( "context" datastoremw "github.com/authzed/spicedb/internal/middleware/datastore" + "github.com/authzed/spicedb/pkg/datastore" "github.com/authzed/spicedb/pkg/diff" "github.com/authzed/spicedb/pkg/middleware/consistency" core "github.com/authzed/spicedb/pkg/proto/core/v1" @@ -11,34 +12,24 @@ import ( "github.com/authzed/spicedb/pkg/schemadsl/input" ) -func schemaDiff(ctx context.Context, comparisonSchemaString string) (*diff.SchemaDiff, *diff.DiffableSchema, *diff.DiffableSchema, error) { +func loadCurrentSchema(ctx context.Context) (*diff.DiffableSchema, datastore.Revision, error) { ds := datastoremw.MustFromContext(ctx) - // Compile the comparison schema. - compiled, err := compiler.Compile(compiler.InputSchema{ - Source: input.Source("schema"), - SchemaString: comparisonSchemaString, - }, compiler.AllowUnprefixedObjectType()) - if err != nil { - return nil, nil, nil, err - } - - // Get the schema at the requested revision. atRevision, _, err := consistency.RevisionFromContext(ctx) if err != nil { - return nil, nil, nil, err + return nil, nil, err } reader := ds.SnapshotReader(atRevision) namespacesAndRevs, err := reader.ListAllNamespaces(ctx) if err != nil { - return nil, nil, nil, err + return nil, atRevision, err } caveatsAndRevs, err := reader.ListAllCaveats(ctx) if err != nil { - return nil, nil, nil, err + return nil, atRevision, err } namespaces := make([]*core.NamespaceDefinition, 0, len(namespacesAndRevs)) @@ -51,18 +42,34 @@ func schemaDiff(ctx context.Context, comparisonSchemaString string) (*diff.Schem caveats = append(caveats, caveatAndRev.Definition) } - // Perform the diff. - existingSchema := diff.DiffableSchema{ + return &diff.DiffableSchema{ ObjectDefinitions: namespaces, CaveatDefinitions: caveats, + }, atRevision, nil +} + +func schemaDiff(ctx context.Context, comparisonSchemaString string) (*diff.SchemaDiff, *diff.DiffableSchema, *diff.DiffableSchema, error) { + existingSchema, _, err := loadCurrentSchema(ctx) + if err != nil { + return nil, nil, nil, err + } + + // Compile the comparison schema. + compiled, err := compiler.Compile(compiler.InputSchema{ + Source: input.Source("schema"), + SchemaString: comparisonSchemaString, + }, compiler.AllowUnprefixedObjectType()) + if err != nil { + return nil, nil, nil, err } + comparisonSchema := diff.NewDiffableSchemaFromCompiledSchema(compiled) - diff, err := diff.DiffSchemas(existingSchema, comparisonSchema) + diff, err := diff.DiffSchemas(*existingSchema, comparisonSchema) if err != nil { return nil, nil, nil, err } // Return the diff. - return diff, &existingSchema, &comparisonSchema, nil + return diff, existingSchema, &comparisonSchema, nil } From e8aeca080d7804e0fb1782c675065c7d5f020937 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Fri, 3 May 2024 17:27:05 -0400 Subject: [PATCH 4/4] Add filtering to the experimental schema reflection API --- e2e/go.mod | 2 +- e2e/go.sum | 4 +- go.mod | 51 +-- go.sum | 58 ++++ internal/services/v1/errors.go | 15 +- internal/services/v1/experimental.go | 39 ++- internal/services/v1/experimental_test.go | 233 ++++++++++++- internal/services/v1/expreflection.go | 261 ++++++++++++--- internal/services/v1/expreflection_test.go | 360 +++++++++++++++++++-- internal/services/v1/relationships.go | 4 +- pkg/diff/diff_test.go | 82 +++++ 11 files changed, 987 insertions(+), 122 deletions(-) diff --git a/e2e/go.mod b/e2e/go.mod index 3dc022546e..43da52909d 100644 --- a/e2e/go.mod +++ b/e2e/go.mod @@ -3,7 +3,7 @@ module github.com/authzed/spicedb/e2e go 1.22.2 require ( - github.com/authzed/authzed-go v0.11.2-0.20240501223711-10d4e60e2a07 + github.com/authzed/authzed-go v0.11.2-0.20240506164352-1e5f214fc4f5 github.com/authzed/grpcutil v0.0.0-20240123092924-129dc0a6a6e1 github.com/authzed/spicedb v1.29.5 github.com/brianvoe/gofakeit/v6 v6.23.2 diff --git a/e2e/go.sum b/e2e/go.sum index 272cd1059e..7054c63814 100644 --- a/e2e/go.sum +++ b/e2e/go.sum @@ -27,8 +27,8 @@ github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8 github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= -github.com/authzed/authzed-go v0.11.2-0.20240501223711-10d4e60e2a07 h1:FtbBwpHxrmGgC8p8Dl4MtiouCcIKHUzFi2E9WC9NCuc= -github.com/authzed/authzed-go v0.11.2-0.20240501223711-10d4e60e2a07/go.mod h1:0SXN/1P1G/y87PaA8cuaNxx5QC0FjN8doNk6IPqcDkY= +github.com/authzed/authzed-go v0.11.2-0.20240506164352-1e5f214fc4f5 h1:gsc5jhIeaqu/7XKwoACGBWAFEEJqFJK9HRh/uLdEEXw= +github.com/authzed/authzed-go v0.11.2-0.20240506164352-1e5f214fc4f5/go.mod h1:6cIxOivUQPOstQnt0jJ7sRtW91Y0e548zZpy7h8w+mU= github.com/authzed/cel-go v0.20.2 h1:GlmLecGry7Z8HU0k+hmaHHUV05ZHrsFxduXHtIePvck= github.com/authzed/cel-go v0.20.2/go.mod h1:pJHVFWbqUHV1J+klQoZubdKswlbxcsbojda3mye9kiU= github.com/authzed/grpcutil v0.0.0-20240123092924-129dc0a6a6e1 h1:zBfQzia6Hz45pJBeURTrv1b6HezmejB6UmiGuBilHZM= diff --git a/go.mod b/go.mod index 2e517b77ee..ddf00b785e 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( contrib.go.opencensus.io/exporter/prometheus v0.4.2 github.com/IBM/pgxpoolprometheus v1.1.1 github.com/Masterminds/squirrel v1.5.4 - github.com/authzed/authzed-go v0.11.2-0.20240501223711-10d4e60e2a07 + github.com/authzed/authzed-go v0.11.2-0.20240506164352-1e5f214fc4f5 // NOTE: We are using a *copy* of `cel-go` here to ensure there isn't a conflict // with the version used in Kubernetes. This is a temporary measure until we can @@ -39,7 +39,7 @@ require ( github.com/go-sql-driver/mysql v1.8.1 github.com/gogo/protobuf v1.3.2 github.com/golang/snappy v0.0.4 - github.com/golangci/golangci-lint v1.57.2 + github.com/golangci/golangci-lint v1.58.0 github.com/google/go-cmp v0.6.0 github.com/google/go-github/v43 v43.0.0 github.com/google/uuid v1.6.0 @@ -110,7 +110,9 @@ require ( require ( cloud.google.com/go/auth v0.3.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect + github.com/Crocmagnon/fatcontext v0.2.2 // indirect github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.0 // indirect + github.com/Masterminds/semver/v3 v3.2.1 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.17.11 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 // indirect @@ -125,9 +127,12 @@ require ( github.com/aws/smithy-go v1.20.2 // indirect github.com/bombsimon/wsl/v4 v4.2.1 // indirect github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect - github.com/jjti/go-spancheck v0.5.3 // indirect + github.com/golangci/modinfo v0.3.4 // indirect + github.com/jjti/go-spancheck v0.6.1 // indirect + github.com/lasiar/canonicalheader v1.0.6 // indirect + github.com/quasilyte/go-ruleguard/dsl v0.3.22 // indirect github.com/spf13/afero v1.11.0 // indirect - go-simpler.org/musttag v0.9.0 // indirect + go-simpler.org/musttag v0.12.1 // indirect ) require ( @@ -142,8 +147,8 @@ require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/4meepo/tagalign v1.3.3 // indirect github.com/Abirdcfly/dupword v0.0.14 // indirect - github.com/Antonboom/errname v0.1.12 // indirect - github.com/Antonboom/nilnil v0.1.7 // indirect + github.com/Antonboom/errname v0.1.13 // indirect + github.com/Antonboom/nilnil v0.1.8 // indirect github.com/Antonboom/testifylint v1.2.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/BurntSushi/toml v1.3.2 // indirect @@ -166,18 +171,18 @@ require ( github.com/breml/bidichk v0.2.7 // indirect github.com/breml/errchkjson v0.3.6 // indirect github.com/butuzov/ireturn v0.3.0 // indirect - github.com/butuzov/mirror v1.1.0 // indirect + github.com/butuzov/mirror v1.2.0 // indirect github.com/catenacyber/perfsprint v0.7.1 // indirect github.com/ccojocar/zxcvbn-go v1.0.2 // indirect github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d // indirect github.com/charithe/durationcheck v0.0.10 // indirect github.com/chavacava/garif v0.1.0 // indirect - github.com/ckaznocha/intrange v0.1.1 // indirect + github.com/ckaznocha/intrange v0.1.2 // indirect github.com/cncf/xds/go v0.0.0-20231128003011-0fa0005c9caa // indirect github.com/containerd/continuity v0.3.0 // indirect github.com/curioswitch/go-reassign v0.2.0 // indirect - github.com/daixiang0/gci v0.12.3 // indirect + github.com/daixiang0/gci v0.13.4 // indirect github.com/dave/jennifer v1.6.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/denis-tingaikin/go-header v0.5.0 // indirect @@ -190,11 +195,11 @@ require ( github.com/ettle/strcase v0.2.0 // indirect github.com/fatih/structtag v1.2.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/firefart/nonamedreturns v1.0.4 // indirect + github.com/firefart/nonamedreturns v1.0.5 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fzipp/gocyclo v0.6.0 // indirect github.com/ghostiam/protogetter v0.3.5 // indirect - github.com/go-critic/go-critic v0.11.2 // indirect + github.com/go-critic/go-critic v0.11.3 // indirect github.com/go-kit/log v0.2.1 // indirect github.com/go-logfmt/logfmt v0.5.1 // indirect github.com/go-logr/logr v1.4.1 // indirect @@ -216,9 +221,9 @@ require ( github.com/golang/protobuf v1.5.4 // indirect github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a // indirect github.com/golangci/gofmt v0.0.0-20231018234816-f50ced29576e // indirect - github.com/golangci/misspell v0.4.1 // indirect + github.com/golangci/misspell v0.5.1 // indirect github.com/golangci/plugin-module-register v0.1.1 // indirect - github.com/golangci/revgrep v0.5.2 // indirect + github.com/golangci/revgrep v0.5.3 // indirect github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-querystring v1.1.0 // indirect @@ -252,7 +257,7 @@ require ( github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/julz/importas v0.1.0 // indirect - github.com/karamaru-alpha/copyloopvar v1.0.10 // indirect + github.com/karamaru-alpha/copyloopvar v1.1.0 // indirect github.com/kisielk/errcheck v1.7.0 // indirect github.com/kkHAIKE/contextcheck v1.1.5 // indirect github.com/klauspost/compress v1.17.4 // indirect @@ -263,7 +268,7 @@ require ( github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/ldez/gomoddirectives v0.2.4 // indirect github.com/ldez/tagliatelle v0.5.0 // indirect - github.com/leonklingele/grouper v1.1.1 // indirect + github.com/leonklingele/grouper v1.1.2 // indirect github.com/lufeee/execinquery v1.2.1 // indirect github.com/macabu/inamedparam v0.1.3 // indirect github.com/magiconair/properties v1.8.7 // indirect @@ -292,10 +297,10 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0-rc5 // indirect github.com/opencontainers/runc v1.1.12 // indirect - github.com/pelletier/go-toml/v2 v2.2.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/polyfloyd/go-errorlint v1.4.8 // indirect + github.com/polyfloyd/go-errorlint v1.5.1 // indirect github.com/prometheus/procfs v0.12.0 // indirect github.com/prometheus/statsd_exporter v0.22.7 // indirect github.com/quasilyte/go-ruleguard v0.4.2 // indirect @@ -303,7 +308,7 @@ require ( github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/ryancurrah/gomodguard v1.3.1 // indirect + github.com/ryancurrah/gomodguard v1.3.2 // indirect github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 // indirect github.com/sagikazarmark/locafero v0.3.0 // indirect @@ -337,17 +342,17 @@ require ( github.com/tomarrell/wrapcheck/v2 v2.8.3 // indirect github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect github.com/ultraware/funlen v0.1.0 // indirect - github.com/ultraware/whitespace v0.1.0 // indirect + github.com/ultraware/whitespace v0.1.1 // indirect github.com/uudashr/gocognit v1.1.2 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xen0n/gosmopolitan v1.2.2 // indirect github.com/yagipy/maintidx v1.0.0 // indirect - github.com/yeya24/promlinter v0.2.0 // indirect + github.com/yeya24/promlinter v0.3.0 // indirect github.com/ykadowak/zerologlint v0.1.5 // indirect - gitlab.com/bosi/decorder v0.4.1 // indirect - go-simpler.org/sloglint v0.5.0 // indirect + gitlab.com/bosi/decorder v0.4.2 // indirect + go-simpler.org/sloglint v0.6.0 // indirect go.opentelemetry.io/contrib/propagators/b3 v1.20.0 // indirect go.opentelemetry.io/contrib/propagators/ot v1.20.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0 // indirect @@ -378,7 +383,7 @@ require ( k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect mvdan.cc/gofumpt v0.6.0 // indirect - mvdan.cc/unparam v0.0.0-20240104100049-c549a3470d14 // indirect + mvdan.cc/unparam v0.0.0-20240427195214-063aff900ca1 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect sigs.k8s.io/yaml v1.4.0 // indirect diff --git a/go.sum b/go.sum index b42467d93f..8b362f7735 100644 --- a/go.sum +++ b/go.sum @@ -629,8 +629,12 @@ github.com/Abirdcfly/dupword v0.0.14 h1:3U4ulkc8EUo+CaT105/GJ1BQwtgyj6+VaBVbAX11 github.com/Abirdcfly/dupword v0.0.14/go.mod h1:VKDAbxdY8YbKUByLGg8EETzYSuC4crm9WwI6Y3S0cLI= github.com/Antonboom/errname v0.1.12 h1:oh9ak2zUtsLp5oaEd/erjB4GPu9w19NyoIskZClDcQY= github.com/Antonboom/errname v0.1.12/go.mod h1:bK7todrzvlaZoQagP1orKzWXv59X/x0W0Io2XT1Ssro= +github.com/Antonboom/errname v0.1.13 h1:JHICqsewj/fNckzrfVSe+T33svwQxmjC+1ntDsHOVvM= +github.com/Antonboom/errname v0.1.13/go.mod h1:uWyefRYRN54lBg6HseYCFhs6Qjcy41Y3Jl/dVhA87Ns= github.com/Antonboom/nilnil v0.1.7 h1:ofgL+BA7vlA1K2wNQOsHzLJ2Pw5B5DpWRLdDAVvvTow= github.com/Antonboom/nilnil v0.1.7/go.mod h1:TP+ScQWVEq0eSIxqU8CbdT5DFWoHp0MbP+KMUO1BKYQ= +github.com/Antonboom/nilnil v0.1.8 h1:97QG7xrLq4TBK2U9aFq/I8Mcgz67pwMIiswnTA9gIn0= +github.com/Antonboom/nilnil v0.1.8/go.mod h1:iGe2rYwCq5/Me1khrysB4nwI7swQvjclR8/YRPl5ihQ= github.com/Antonboom/testifylint v1.2.0 h1:015bxD8zc5iY8QwTp4+RG9I4kIbqwvGX9TrBbb7jGdM= github.com/Antonboom/testifylint v1.2.0/go.mod h1:rkmEqjqVnHDRNsinyN6fPSLnoajzFwsCcguJgwADBkw= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= @@ -639,6 +643,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Crocmagnon/fatcontext v0.2.2 h1:OrFlsDdOj9hW/oBEJBNSuH7QWf+E9WPVHw+x52bXVbk= +github.com/Crocmagnon/fatcontext v0.2.2/go.mod h1:WSn/c/+MMNiD8Pri0ahRj0o9jVpeowzavOQplBJw6u0= github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 h1:sHglBQTwgx+rWPdisA5ynNEsoARbiCBOyGcJM4/OzsM= github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs= github.com/GaijinEntertainment/go-exhaustruct/v3 v3.2.0 h1:sATXp1x6/axKxz2Gjxv8MALP0bXaNRfQinEwyfMcx8c= @@ -650,6 +656,8 @@ github.com/IBM/pgxpoolprometheus v1.1.1/go.mod h1:GFJDkHbidFfB2APbhBTSy2X4PKH3bL github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= +github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= @@ -694,6 +702,10 @@ github.com/ashanbrown/makezero v1.1.1 h1:iCQ87C0V0vSyO+M9E/FZYbu65auqH0lnsOkf5Fc github.com/ashanbrown/makezero v1.1.1/go.mod h1:i1bJLCRSCHOcOa9Y6MyF2FTfMZMFdHvxKHxgO5Z1axI= github.com/authzed/authzed-go v0.11.2-0.20240501223711-10d4e60e2a07 h1:FtbBwpHxrmGgC8p8Dl4MtiouCcIKHUzFi2E9WC9NCuc= github.com/authzed/authzed-go v0.11.2-0.20240501223711-10d4e60e2a07/go.mod h1:0SXN/1P1G/y87PaA8cuaNxx5QC0FjN8doNk6IPqcDkY= +github.com/authzed/authzed-go v0.11.2-0.20240503202657-2ad9389e2cdd h1:e65sBUPk7c8f9n1KppUQLsCPD+FvIo72um0U5cLHt+4= +github.com/authzed/authzed-go v0.11.2-0.20240503202657-2ad9389e2cdd/go.mod h1:0SXN/1P1G/y87PaA8cuaNxx5QC0FjN8doNk6IPqcDkY= +github.com/authzed/authzed-go v0.11.2-0.20240506164352-1e5f214fc4f5 h1:gsc5jhIeaqu/7XKwoACGBWAFEEJqFJK9HRh/uLdEEXw= +github.com/authzed/authzed-go v0.11.2-0.20240506164352-1e5f214fc4f5/go.mod h1:6cIxOivUQPOstQnt0jJ7sRtW91Y0e548zZpy7h8w+mU= github.com/authzed/cel-go v0.20.2 h1:GlmLecGry7Z8HU0k+hmaHHUV05ZHrsFxduXHtIePvck= github.com/authzed/cel-go v0.20.2/go.mod h1:pJHVFWbqUHV1J+klQoZubdKswlbxcsbojda3mye9kiU= github.com/authzed/consistent v0.1.0 h1:tlh1wvKoRbjRhMm2P+X5WQQyR54SRoS4MyjLOg17Mp8= @@ -758,6 +770,8 @@ github.com/butuzov/ireturn v0.3.0 h1:hTjMqWw3y5JC3kpnC5vXmFJAWI/m31jaCYQqzkS6PL0 github.com/butuzov/ireturn v0.3.0/go.mod h1:A09nIiwiqzN/IoVo9ogpa0Hzi9fex1kd9PSD6edP5ZA= github.com/butuzov/mirror v1.1.0 h1:ZqX54gBVMXu78QLoiqdwpl2mgmoOJTk7s4p4o+0avZI= github.com/butuzov/mirror v1.1.0/go.mod h1:8Q0BdQU6rC6WILDiBM60DBfvV78OLJmMmixe7GF45AE= +github.com/butuzov/mirror v1.2.0 h1:9YVK1qIjNspaqWutSv8gsge2e/Xpq1eqEkslEUHy5cs= +github.com/butuzov/mirror v1.2.0/go.mod h1:DqZZDtzm42wIAIyHXeN8W/qb1EPlb9Qn/if9icBOpdQ= github.com/catenacyber/perfsprint v0.7.1 h1:PGW5G/Kxn+YrN04cRAZKC+ZuvlVwolYMrIyyTJ/rMmc= github.com/catenacyber/perfsprint v0.7.1/go.mod h1:/wclWYompEyjUD2FuIIDVKNkqz7IgBIWXIH3V0Zol50= github.com/ccojocar/zxcvbn-go v1.0.2 h1:na/czXU8RrhXO4EZme6eQJLR4PzcGsahsBOAwU6I3Vg= @@ -785,6 +799,8 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/ckaznocha/intrange v0.1.1 h1:gHe4LfqCspWkh8KpJFs20fJz3XRHFBFUV9yI7Itu83Q= github.com/ckaznocha/intrange v0.1.1/go.mod h1:RWffCw/vKBwHeOEwWdCikAtY0q4gGt8VhJZEEA5n+RE= +github.com/ckaznocha/intrange v0.1.2 h1:3Y4JAxcMntgb/wABQ6e8Q8leMd26JbX2790lIss9MTI= +github.com/ckaznocha/intrange v0.1.2/go.mod h1:RWffCw/vKBwHeOEwWdCikAtY0q4gGt8VhJZEEA5n+RE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudspannerecosystem/spanner-change-streams-tail v0.3.1 h1:76zSbhqkgwt8LXoWBzZqvnKq0gfDeDrQRwMvaLfp3bM= github.com/cloudspannerecosystem/spanner-change-streams-tail v0.3.1/go.mod h1:Fb3cQgYCLKQfjsJcw+wsalU2l/eJpbtHu2UKt12p+Mk= @@ -816,6 +832,8 @@ github.com/curioswitch/go-reassign v0.2.0 h1:G9UZyOcpk/d7Gd6mqYgd8XYWFMw/znxwGDU github.com/curioswitch/go-reassign v0.2.0/go.mod h1:x6OpXuWvgfQaMGks2BZybTngWjT84hqJfKoO8Tt/Roc= github.com/daixiang0/gci v0.12.3 h1:yOZI7VAxAGPQmkb1eqt5g/11SUlwoat1fSblGLmdiQc= github.com/daixiang0/gci v0.12.3/go.mod h1:xtHP9N7AHdNvtRNfcx9gwTDfw7FRJx4bZUsiEfiNNAI= +github.com/daixiang0/gci v0.13.4 h1:61UGkmpoAcxHM2hhNkZEf5SzwQtWJXTSws7jaPyqwlw= +github.com/daixiang0/gci v0.13.4/go.mod h1:12etP2OniiIdP4q+kjUGrC/rUagga7ODbqsom5Eo5Yk= github.com/dalzilio/rudd v1.1.1-0.20230806153452-9e08a6ea8170 h1:bHEN1z3EOO/IXHTQ8ZcmGoW4gTJt+mSrH2Sd458uo0E= github.com/dalzilio/rudd v1.1.1-0.20230806153452-9e08a6ea8170/go.mod h1:IxPC4Bdi3WqUwyGBMgLrWWGx67aRtUAZmOZrkIr7qaM= github.com/dave/jennifer v1.6.1 h1:T4T/67t6RAA5AIV6+NP8Uk/BIsXgDoqEowgycdQQLuk= @@ -885,6 +903,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/firefart/nonamedreturns v1.0.4 h1:abzI1p7mAEPYuR4A+VLKn4eNDOycjYo2phmY9sfv40Y= github.com/firefart/nonamedreturns v1.0.4/go.mod h1:TDhe/tjI1BXo48CmYbUduTV7BdIga8MAO/xbKdcVsGI= +github.com/firefart/nonamedreturns v1.0.5 h1:tM+Me2ZaXs8tfdDw3X6DOX++wMCOqzYUho6tUTYIdRA= +github.com/firefart/nonamedreturns v1.0.5/go.mod h1:gHJjDqhGM4WyPt639SOZs+G89Ko7QKH5R5BhnO6xJhw= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -898,6 +918,8 @@ github.com/ghostiam/protogetter v0.3.5 h1:+f7UiF8XNd4w3a//4DnusQ2SZjPkUjxkMEfjbx github.com/ghostiam/protogetter v0.3.5/go.mod h1:7lpeDnEJ1ZjL/YtyoN99ljO4z0pd3H0d18/t2dPBxHw= github.com/go-critic/go-critic v0.11.2 h1:81xH/2muBphEgPtcwH1p6QD+KzXl2tMSi3hXjBSxDnM= github.com/go-critic/go-critic v0.11.2/go.mod h1:OePaicfjsf+KPy33yq4gzv6CO7TEQ9Rom6ns1KsJnl8= +github.com/go-critic/go-critic v0.11.3 h1:SJbYD/egY1noYjTMNTlhGaYlfQ77rQmrNH7h+gtn0N0= +github.com/go-critic/go-critic v0.11.3/go.mod h1:Je0h5Obm1rR5hAGA9mP2PDiOOk53W+n7pyvXErFKIgI= github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= @@ -1023,12 +1045,20 @@ github.com/golangci/gofmt v0.0.0-20231018234816-f50ced29576e h1:ULcKCDV1LOZPFxGZ github.com/golangci/gofmt v0.0.0-20231018234816-f50ced29576e/go.mod h1:Pm5KhLPA8gSnQwrQ6ukebRcapGb/BG9iUkdaiCcGHJM= github.com/golangci/golangci-lint v1.57.2 h1:NNhxfZyL5He1WWDrIvl1a4n5bvWZBcgAqBwlJAAgLTw= github.com/golangci/golangci-lint v1.57.2/go.mod h1:ApiG3S3Ca23QyfGp5BmsorTiVxJpr5jGiNS0BkdSidg= +github.com/golangci/golangci-lint v1.58.0 h1:r8duFARMJ0VdSM9tDXAdt2+f57dfZQmagvYX6kmkUKQ= +github.com/golangci/golangci-lint v1.58.0/go.mod h1:WAY3BnSLvTUEv41Q0v3ZFzNybLRF+a7Vd9Da8Jx9Eqo= github.com/golangci/misspell v0.4.1 h1:+y73iSicVy2PqyX7kmUefHusENlrP9YwuHZHPLGQj/g= github.com/golangci/misspell v0.4.1/go.mod h1:9mAN1quEo3DlpbaIKKyEvRxK1pwqR9s/Sea1bJCtlNI= +github.com/golangci/misspell v0.5.1 h1:/SjR1clj5uDjNLwYzCahHwIOPmQgoH04AyQIiWGbhCM= +github.com/golangci/misspell v0.5.1/go.mod h1:keMNyY6R9isGaSAu+4Q8NMBwMPkh15Gtc8UCVoDtAWo= +github.com/golangci/modinfo v0.3.4 h1:oU5huX3fbxqQXdfspamej74DFX0kyGLkw1ppvXoJ8GA= +github.com/golangci/modinfo v0.3.4/go.mod h1:wytF1M5xl9u0ij8YSvhkEVPP3M5Mc7XLl1pxH3B2aUM= github.com/golangci/plugin-module-register v0.1.1 h1:TCmesur25LnyJkpsVrupv1Cdzo+2f7zX0H6Jkw1Ol6c= github.com/golangci/plugin-module-register v0.1.1/go.mod h1:TTpqoB6KkwOJMV8u7+NyXMrkwwESJLOkfl9TxR1DGFc= github.com/golangci/revgrep v0.5.2 h1:EndcWoRhcnfj2NHQ+28hyuXpLMF+dQmCN+YaeeIl4FU= github.com/golangci/revgrep v0.5.2/go.mod h1:bjAMA+Sh/QUfTDcHzxfyHxr4xKvllVr/0sCv2e7jJHA= +github.com/golangci/revgrep v0.5.3 h1:3tL7c1XBMtWHHqVpS5ChmiAAoe4PF/d5+ULzV9sLAzs= +github.com/golangci/revgrep v0.5.3/go.mod h1:U4R/s9dlXZsg8uJmaR1GrloUr14D7qDl8gi2iPXJH8k= github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed h1:IURFTjxeTfNFP0hTEi1YKjB/ub8zkpaOqFFMApi2EAs= github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed/go.mod h1:XLXN8bNw4CGRPaqgl3bv/lhz7bsGPh4/xSaMTbo2vkQ= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -1195,6 +1225,8 @@ github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af h1:KA9B github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af/go.mod h1:HEWGJkRDzjJY2sqdDwxccsGicWEf9BQOZsq2tV+xzM0= github.com/jjti/go-spancheck v0.5.3 h1:vfq4s2IB8T3HvbpiwDTYgVPj1Ze/ZSXrTtaZRTc7CuM= github.com/jjti/go-spancheck v0.5.3/go.mod h1:eQdOX1k3T+nAKvZDyLC3Eby0La4dZ+I19iOl5NzSPFE= +github.com/jjti/go-spancheck v0.6.1 h1:ZK/wE5Kyi1VX3PJpUO2oEgeoI4FWOUm7Shb2Gbv5obI= +github.com/jjti/go-spancheck v0.6.1/go.mod h1:vF1QkOO159prdo6mHRxak2CpzDpHAfKiPUDP/NeRnX8= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= @@ -1229,6 +1261,8 @@ github.com/jzelinskie/stringz v0.0.3/go.mod h1:hHYbgxJuNLRw91CmpuFsYEOyQqpDVFg8p github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= github.com/karamaru-alpha/copyloopvar v1.0.10 h1:8HYDy6KQYqTmD7JuhZMWS1nwPru9889XI24ROd/+WXI= github.com/karamaru-alpha/copyloopvar v1.0.10/go.mod h1:u7CIfztblY0jZLOQZgH3oYsJzpC2A7S6u/lfgSXHy0k= +github.com/karamaru-alpha/copyloopvar v1.1.0 h1:x7gNyKcC2vRBO1H2Mks5u1VxQtYvFiym7fCjIP8RPos= +github.com/karamaru-alpha/copyloopvar v1.1.0/go.mod h1:u7CIfztblY0jZLOQZgH3oYsJzpC2A7S6u/lfgSXHy0k= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/errcheck v1.7.0 h1:+SbscKmWJ5mOK/bO1zS60F5I9WwZDWOfRsC4RwfwRV0= @@ -1264,12 +1298,16 @@ github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= +github.com/lasiar/canonicalheader v1.0.6 h1:LJiiZ/MzkqibXOL2v+J8+WZM21pM0ivrBY/jbm9f5fo= +github.com/lasiar/canonicalheader v1.0.6/go.mod h1:GfXTLQb3O1qF5qcSTyXTnfNUggUNyzbkOSpzZ0dpUJo= github.com/ldez/gomoddirectives v0.2.4 h1:j3YjBIjEBbqZ0NKtBNzr8rtMHTOrLPeiwTkfUJZ3alg= github.com/ldez/gomoddirectives v0.2.4/go.mod h1:oWu9i62VcQDYp9EQ0ONTfqLNh+mDLWWDO+SO0qSQw5g= github.com/ldez/tagliatelle v0.5.0 h1:epgfuYt9v0CG3fms0pEgIMNPuFf/LpPIfjk4kyqSioo= github.com/ldez/tagliatelle v0.5.0/go.mod h1:rj1HmWiL1MiKQuOONhd09iySTEkUuE/8+5jtPYz9xa4= github.com/leonklingele/grouper v1.1.1 h1:suWXRU57D4/Enn6pXR0QVqqWWrnJ9Osrz+5rjt8ivzU= github.com/leonklingele/grouper v1.1.1/go.mod h1:uk3I3uDfi9B6PeUjsCKi6ndcf63Uy7snXgR4yDYQVDY= +github.com/leonklingele/grouper v1.1.2 h1:o1ARBDLOmmasUaNDesWqWCIFH3u7hoFlM84YrjT3mIY= +github.com/leonklingele/grouper v1.1.2/go.mod h1:6D0M/HVkhs2yRKRFZUoGjeDy7EZTfFBE9gl4kjmIGkA= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lthibault/jitterbug v2.0.0+incompatible h1:qouq51IKzlMx25+15jbxhC/d79YyTj0q6XFoptNqaUw= @@ -1381,6 +1419,8 @@ github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2D github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo= github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= @@ -1399,6 +1439,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/polyfloyd/go-errorlint v1.4.8 h1:jiEjKDH33ouFktyez7sckv6pHWif9B7SuS8cutDXFHw= github.com/polyfloyd/go-errorlint v1.4.8/go.mod h1:NNCxFcFjZcw3xNjVdCchERkEM6Oz7wta2XJVxRftwO4= +github.com/polyfloyd/go-errorlint v1.5.1 h1:5gHxDjLyyWij7fhfrjYNNlHsUNQeyx0LFQKUelO3RBo= +github.com/polyfloyd/go-errorlint v1.5.1/go.mod h1:sH1QC1pxxi0fFecsVIzBmxtrgd9IF/SkJpA6wqyKAJs= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= @@ -1441,6 +1483,8 @@ github.com/prometheus/statsd_exporter v0.22.7 h1:7Pji/i2GuhK6Lu7DHrtTkFmNBCudCPT github.com/prometheus/statsd_exporter v0.22.7/go.mod h1:N/TevpjkIh9ccs6nuzY3jQn9dFqnUakOjnEuMPJJJnI= github.com/quasilyte/go-ruleguard v0.4.2 h1:htXcXDK6/rO12kiTHKfHuqR4kr3Y4M0J0rOL6CH/BYs= github.com/quasilyte/go-ruleguard v0.4.2/go.mod h1:GJLgqsLeo4qgavUoL8JeGFNS7qcisx3awV/w9eWTmNI= +github.com/quasilyte/go-ruleguard/dsl v0.3.22 h1:wd8zkOhSNr+I+8Qeciml08ivDt1pSXe60+5DqOpCjPE= +github.com/quasilyte/go-ruleguard/dsl v0.3.22/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= github.com/quasilyte/gogrep v0.5.0 h1:eTKODPXbI8ffJMN+W2aE0+oL0z/nh8/5eNdiO34SOAo= github.com/quasilyte/gogrep v0.5.0/go.mod h1:Cm9lpz9NZjEoL1tgZ2OgeUKPIxL1meE7eo60Z6Sk+Ng= github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 h1:TCg2WBOl980XxGFEZSS6KlBGIV0diGdySzxATTWoqaU= @@ -1467,6 +1511,8 @@ github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfF github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= github.com/ryancurrah/gomodguard v1.3.1 h1:fH+fUg+ngsQO0ruZXXHnA/2aNllWA1whly4a6UvyzGE= github.com/ryancurrah/gomodguard v1.3.1/go.mod h1:DGFHzEhi6iJ0oIDfMuo3TgrS+L9gZvrEfmjjuelnRU0= +github.com/ryancurrah/gomodguard v1.3.2 h1:CuG27ulzEB1Gu5Dk5gP8PFxSOZ3ptSdP5iI/3IXxM18= +github.com/ryancurrah/gomodguard v1.3.2/go.mod h1:LqdemiFomEjcxOqirbQCb3JFvSxH2JUYMerTFd3sF2o= github.com/ryanrolds/sqlclosecheck v0.5.1 h1:dibWW826u0P8jNLsLN+En7+RqWWTYrjCB9fJfSfdyCU= github.com/ryanrolds/sqlclosecheck v0.5.1/go.mod h1:2g3dUjoS6AL4huFdv6wn55WpLIDjY7ZgUR4J8HOO/XQ= github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a8tTFrMLUcfWwyC0pnifVo2ClaLq+hP8= @@ -1589,6 +1635,8 @@ github.com/ultraware/funlen v0.1.0 h1:BuqclbkY6pO+cvxoq7OsktIXZpgBSkYTQtmwhAK81v github.com/ultraware/funlen v0.1.0/go.mod h1:XJqmOQja6DpxarLj6Jj1U7JuoS8PvL4nEqDaQhy22p4= github.com/ultraware/whitespace v0.1.0 h1:O1HKYoh0kIeqE8sFqZf1o0qbORXUCOQFrlaQyZsczZw= github.com/ultraware/whitespace v0.1.0/go.mod h1:/se4r3beMFNmewJ4Xmz0nMQ941GJt+qmSHGP9emHYe0= +github.com/ultraware/whitespace v0.1.1 h1:bTPOGejYFulW3PkcrqkeQwOd6NKOOXvmGD9bo/Gk8VQ= +github.com/ultraware/whitespace v0.1.1/go.mod h1:XcP1RLD81eV4BW8UhQlpaR+SDc2givTvyI8a586WjW8= github.com/uudashr/gocognit v1.1.2 h1:l6BAEKJqQH2UpKAPKdMfZf5kE4W/2xk8pfU1OVLvniI= github.com/uudashr/gocognit v1.1.2/go.mod h1:aAVdLURqcanke8h3vg35BC++eseDm66Z7KmchI5et4k= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= @@ -1603,6 +1651,8 @@ github.com/yagipy/maintidx v1.0.0 h1:h5NvIsCz+nRDapQ0exNv4aJ0yXSI0420omVANTv3GJM github.com/yagipy/maintidx v1.0.0/go.mod h1:0qNf/I/CCZXSMhsRsrEPDZ+DkekpKLXAJfsTACwgXLk= github.com/yeya24/promlinter v0.2.0 h1:xFKDQ82orCU5jQujdaD8stOHiv8UN68BSdn2a8u8Y3o= github.com/yeya24/promlinter v0.2.0/go.mod h1:u54lkmBOZrpEbQQ6gox2zWKKLKu2SGe+2KOiextY+IA= +github.com/yeya24/promlinter v0.3.0 h1:JVDbMp08lVCP7Y6NP3qHroGAO6z2yGKQtS5JsjqtoFs= +github.com/yeya24/promlinter v0.3.0/go.mod h1:cDfJQQYv9uYciW60QT0eeHlFodotkYZlL+YcPQN+mW4= github.com/ykadowak/zerologlint v0.1.5 h1:Gy/fMz1dFQN9JZTPjv1hxEk+sRWm05row04Yoolgdiw= github.com/ykadowak/zerologlint v0.1.5/go.mod h1:KaUskqF3e/v59oPmdq1U1DnKcuHokl2/K1U4pmIELKg= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -1616,12 +1666,18 @@ github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= gitlab.com/bosi/decorder v0.4.1 h1:VdsdfxhstabyhZovHafFw+9eJ6eU0d2CkFNJcZz/NU4= gitlab.com/bosi/decorder v0.4.1/go.mod h1:jecSqWUew6Yle1pCr2eLWTensJMmsxHsBwt+PVbkAqA= +gitlab.com/bosi/decorder v0.4.2 h1:qbQaV3zgwnBZ4zPMhGLW4KZe7A7NwxEhJx39R3shffo= +gitlab.com/bosi/decorder v0.4.2/go.mod h1:muuhHoaJkA9QLcYHq4Mj8FJUwDZ+EirSHRiaTcTf6T8= go-simpler.org/assert v0.7.0 h1:OzWWZqfNxt8cLS+MlUp6Tgk1HjPkmgdKBq9qvy8lZsA= go-simpler.org/assert v0.7.0/go.mod h1:74Eqh5eI6vCK6Y5l3PI8ZYFXG4Sa+tkr70OIPJAUr28= go-simpler.org/musttag v0.9.0 h1:Dzt6/tyP9ONr5g9h9P3cnYWCxeBFRkd0uJL/w+1Mxos= go-simpler.org/musttag v0.9.0/go.mod h1:gA9nThnalvNSKpEoyp3Ko4/vCX2xTpqKoUtNqXOnVR4= +go-simpler.org/musttag v0.12.1 h1:yaMcjl/uyVnd1z6GqIhBiFH/PoqNN9f2IgtU7bp7W/0= +go-simpler.org/musttag v0.12.1/go.mod h1:46HKu04A3Am9Lne5kKP0ssgwY3AeIlqsDzz3UxKROpY= go-simpler.org/sloglint v0.5.0 h1:2YCcd+YMuYpuqthCgubcF5lBSjb6berc5VMOYUHKrpY= go-simpler.org/sloglint v0.5.0/go.mod h1:EUknX5s8iXqf18KQxKnaBHUPVriiPnOrPjjJcsaTcSQ= +go-simpler.org/sloglint v0.6.0 h1:0YcqSVG7LI9EVBfRPhgPec79BH6X6mwjFuUR5Mr7j1M= +go-simpler.org/sloglint v0.6.0/go.mod h1:+kJJtebtPePWyG5boFwY46COydAggADDOHM22zOvzBk= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= @@ -2466,6 +2522,8 @@ mvdan.cc/gofumpt v0.6.0 h1:G3QvahNDmpD+Aek/bNOLrFR2XC6ZAdo62dZu65gmwGo= mvdan.cc/gofumpt v0.6.0/go.mod h1:4L0wf+kgIPZtcCWXynNS2e6bhmj73umwnuXSZarixzA= mvdan.cc/unparam v0.0.0-20240104100049-c549a3470d14 h1:zCr3iRRgdk5eIikZNDphGcM6KGVTx3Yu+/Uu9Es254w= mvdan.cc/unparam v0.0.0-20240104100049-c549a3470d14/go.mod h1:ZzZjEpJDOmx8TdVU6umamY3Xy0UAQUI2DHbf05USVbI= +mvdan.cc/unparam v0.0.0-20240427195214-063aff900ca1 h1:Nykk7fggxChwLK4rUPYESzeIwqsuxXXlFEAh5YhaMRo= +mvdan.cc/unparam v0.0.0-20240427195214-063aff900ca1/go.mod h1:ZzZjEpJDOmx8TdVU6umamY3Xy0UAQUI2DHbf05USVbI= resenje.org/singleflight v0.4.1 h1:ryGHRaOBwhnZLyf34LMDf4AsTSHrs4hdGPdG/I4Hmac= resenje.org/singleflight v0.4.1/go.mod h1:lAgQK7VfjG6/pgredbQfmV0RvG/uVhKo6vSuZ0vCWfk= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/internal/services/v1/errors.go b/internal/services/v1/errors.go index 3e9f50d085..10714ae32c 100644 --- a/internal/services/v1/errors.go +++ b/internal/services/v1/errors.go @@ -29,12 +29,11 @@ func (err ErrExceedsMaximumLimit) MarshalZerologObject(e *zerolog.Event) { // GRPCStatus implements retrieving the gRPC status for the error. func (err ErrExceedsMaximumLimit) GRPCStatus() *status.Status { - // TODO(jschorr): Make this a specific error. return spiceerrors.WithCodeAndDetails( err, codes.InvalidArgument, spiceerrors.ForReason( - v1.ErrorReason_ERROR_REASON_UNSPECIFIED, + v1.ErrorReason_ERROR_REASON_EXCEEDS_MAXIMUM_ALLOWABLE_LIMIT, map[string]string{ "limit_provided": strconv.FormatUint(err.providedLimit, 10), "maximum_limit_allowed": strconv.FormatUint(err.maxLimitAllowed, 10), @@ -384,27 +383,31 @@ func (err ErrInvalidCursor) GRPCStatus() *status.Status { // ErrInvalidFilter indicates the specified relationship filter was invalid. type ErrInvalidFilter struct { error + + filter string } // GRPCStatus implements retrieving the gRPC status for the error. func (err ErrInvalidFilter) GRPCStatus() *status.Status { - // TODO(jschorr): Put a proper error reason in here. return spiceerrors.WithCodeAndDetails( err, codes.InvalidArgument, spiceerrors.ForReason( - v1.ErrorReason_ERROR_REASON_UNSPECIFIED, - map[string]string{}, + v1.ErrorReason_ERROR_REASON_INVALID_FILTER, + map[string]string{ + "filter": err.filter, + }, ), ) } // NewInvalidFilterErr constructs a new invalid filter error. -func NewInvalidFilterErr(reason string) ErrInvalidFilter { +func NewInvalidFilterErr(reason string, filter string) ErrInvalidFilter { return ErrInvalidFilter{ error: fmt.Errorf( "the relationship filter provided is not valid: %s", reason, ), + filter: filter, } } diff --git a/internal/services/v1/experimental.go b/internal/services/v1/experimental.go index d4ec5cc1ab..2de405b856 100644 --- a/internal/services/v1/experimental.go +++ b/internal/services/v1/experimental.go @@ -437,24 +437,37 @@ func (es *experimentalServer) ExperimentalReflectSchema(ctx context.Context, req return nil, shared.RewriteErrorWithoutConfig(ctx, err) } + filters, err := newSchemaFilters(req.OptionalFilters) + if err != nil { + return nil, shared.RewriteErrorWithoutConfig(ctx, err) + } + definitions := make([]*v1.ExpDefinition, 0, len(schema.ObjectDefinitions)) - for _, ns := range schema.ObjectDefinitions { - def, err := namespaceAPIRepr(ns) - if err != nil { - return nil, shared.RewriteErrorWithoutConfig(ctx, err) - } + if filters.HasNamespaces() { + for _, ns := range schema.ObjectDefinitions { + def, err := namespaceAPIRepr(ns, filters) + if err != nil { + return nil, shared.RewriteErrorWithoutConfig(ctx, err) + } - definitions = append(definitions, def) + if def != nil { + definitions = append(definitions, def) + } + } } caveats := make([]*v1.ExpCaveat, 0, len(schema.CaveatDefinitions)) - for _, cd := range schema.CaveatDefinitions { - caveat, err := caveatAPIRepr(cd) - if err != nil { - return nil, shared.RewriteErrorWithoutConfig(ctx, err) - } + if filters.HasCaveats() { + for _, cd := range schema.CaveatDefinitions { + caveat, err := caveatAPIRepr(cd, filters) + if err != nil { + return nil, shared.RewriteErrorWithoutConfig(ctx, err) + } - caveats = append(caveats, caveat) + if caveat != nil { + caveats = append(caveats, caveat) + } + } } return &v1.ExperimentalReflectSchemaResponse{ @@ -464,7 +477,7 @@ func (es *experimentalServer) ExperimentalReflectSchema(ctx context.Context, req }, nil } -func (es *experimentalServer) ExperimentalSchemaDiff(ctx context.Context, req *v1.ExperimentalSchemaDiffRequest) (*v1.ExperimentalSchemaDiffResponse, error) { +func (es *experimentalServer) ExperimentalDiffSchema(ctx context.Context, req *v1.ExperimentalDiffSchemaRequest) (*v1.ExperimentalDiffSchemaResponse, error) { atRevision, _, err := consistency.RevisionFromContext(ctx) if err != nil { return nil, err diff --git a/internal/services/v1/experimental_test.go b/internal/services/v1/experimental_test.go index 4e16ceb697..9c69e7cafa 100644 --- a/internal/services/v1/experimental_test.go +++ b/internal/services/v1/experimental_test.go @@ -12,10 +12,12 @@ import ( "github.com/authzed/authzed-go/pkg/responsemeta" v1 "github.com/authzed/authzed-go/proto/authzed/api/v1" + "github.com/authzed/grpcutil" "github.com/scylladb/go-set" "github.com/stretchr/testify/require" "go.uber.org/goleak" "google.golang.org/grpc" + "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" @@ -693,19 +695,20 @@ func TestExperimentalSchemaDiff(t *testing.T) { existingSchema string comparisonSchema string expectedError string - expectedResponse *v1.ExperimentalSchemaDiffResponse + expectedCode codes.Code + expectedResponse *v1.ExperimentalDiffSchemaResponse }{ { name: "no changes", existingSchema: `definition user {}`, comparisonSchema: `definition user {}`, - expectedResponse: &v1.ExperimentalSchemaDiffResponse{}, + expectedResponse: &v1.ExperimentalDiffSchemaResponse{}, }, { name: "addition from existing schema", existingSchema: `definition user {}`, comparisonSchema: `definition user {} definition document {}`, - expectedResponse: &v1.ExperimentalSchemaDiffResponse{ + expectedResponse: &v1.ExperimentalDiffSchemaResponse{ Diffs: []*v1.ExpSchemaDiff{ { Diff: &v1.ExpSchemaDiff_DefinitionAdded{ @@ -722,7 +725,7 @@ func TestExperimentalSchemaDiff(t *testing.T) { name: "removal from existing schema", existingSchema: `definition user {} definition document {}`, comparisonSchema: `definition user {}`, - expectedResponse: &v1.ExperimentalSchemaDiffResponse{ + expectedResponse: &v1.ExperimentalDiffSchemaResponse{ Diffs: []*v1.ExpSchemaDiff{ { Diff: &v1.ExpSchemaDiff_DefinitionRemoved{ @@ -739,6 +742,7 @@ func TestExperimentalSchemaDiff(t *testing.T) { name: "invalid comparison schema", existingSchema: `definition user {}`, comparisonSchema: `definition user { invalid`, + expectedCode: codes.InvalidArgument, expectedError: "Expected end of statement or definition, found: TokenTypeIdentifier", }, } @@ -752,7 +756,7 @@ func TestExperimentalSchemaDiff(t *testing.T) { }) require.NoError(t, err) - actual, err := expClient.ExperimentalSchemaDiff(context.Background(), &v1.ExperimentalSchemaDiffRequest{ + actual, err := expClient.ExperimentalDiffSchema(context.Background(), &v1.ExperimentalDiffSchemaRequest{ ComparisonSchema: tt.comparisonSchema, Consistency: &v1.Consistency{ Requirement: &v1.Consistency_FullyConsistent{FullyConsistent: true}, @@ -762,6 +766,7 @@ func TestExperimentalSchemaDiff(t *testing.T) { if tt.expectedError != "" { require.Error(t, err) require.Contains(t, err.Error(), tt.expectedError) + grpcutil.RequireStatus(t, tt.expectedCode, err) } else { require.NoError(t, err) require.NotNil(t, actual.ReadAt) @@ -782,6 +787,9 @@ func TestExperimentalReflectSchema(t *testing.T) { testCases := []struct { name string schema string + filters []*v1.ExpSchemaFilter + expectedCode codes.Code + expectedError string expectedResponse *v1.ExperimentalReflectSchemaResponse }{ { @@ -809,6 +817,29 @@ definition user {}`, }, }, }, + { + name: "invalid filter", + schema: `definition user {}`, + filters: []*v1.ExpSchemaFilter{ + { + OptionalDefinitionNameFilter: "doc", + OptionalCaveatNameFilter: "invalid", + }, + }, + expectedCode: codes.InvalidArgument, + expectedError: "cannot filter by both definition and caveat name", + }, + { + name: "another invalid filter", + schema: `definition user {}`, + filters: []*v1.ExpSchemaFilter{ + { + OptionalRelationNameFilter: "doc", + }, + }, + expectedCode: codes.InvalidArgument, + expectedError: "relation name match requires definition name match", + }, { name: "full schema", schema: ` @@ -822,6 +853,7 @@ definition user {}`, permission member = direct_member + admin } + /** somecaveat is a caveat */ caveat somecaveat(first int, second string) { first == 1 && second == "two" } @@ -830,7 +862,7 @@ definition user {}`, definition document { // editor is a relation relation editor: user | group#member - relation viewer: user | user with somecaveat | group#member + relation viewer: user | user with somecaveat | group#member | user:* // read all the things permission read = viewer + editor @@ -879,6 +911,12 @@ definition user {}`, OptionalRelationName: "member", }, }, + { + SubjectDefinitionName: "user", + Typeref: &v1.ExpTypeReference_IsPublicWildcard{ + IsPublicWildcard: true, + }, + }, }, }, }, @@ -937,7 +975,7 @@ definition user {}`, Caveats: []*v1.ExpCaveat{ { Name: "somecaveat", - Comment: "", + Comment: "/** somecaveat is a caveat */", Expression: "first == 1 && second == \"two\"", Parameters: []*v1.ExpCaveatParameter{ { @@ -955,6 +993,172 @@ definition user {}`, }, }, }, + { + name: "full schema with definition filter", + schema: ` + /** user represents a user */ + definition user {} + + /** group represents a group */ + definition group { + relation direct_member: user | group#member + relation admin: user + permission member = direct_member + admin + } + + caveat somecaveat(first int, second string) { + first == 1 && second == "two" + } + + /** document is a protected document */ + definition document { + // editor is a relation + relation editor: user | group#member + relation viewer: user | user with somecaveat | group#member + + // read all the things + permission read = viewer + editor + } + `, + filters: []*v1.ExpSchemaFilter{ + { + OptionalDefinitionNameFilter: "doc", + }, + }, + expectedResponse: &v1.ExperimentalReflectSchemaResponse{ + Definitions: []*v1.ExpDefinition{ + { + Name: "document", + Comment: "/** document is a protected document */", + Relations: []*v1.ExpRelation{ + { + Name: "editor", + Comment: "// editor is a relation", + ParentDefinitionName: "document", + SubjectTypes: []*v1.ExpTypeReference{ + { + SubjectDefinitionName: "user", + Typeref: &v1.ExpTypeReference_IsTerminalSubject{}, + }, + { + SubjectDefinitionName: "group", + Typeref: &v1.ExpTypeReference_OptionalRelationName{ + OptionalRelationName: "member", + }, + }, + }, + }, + { + Name: "viewer", + Comment: "", + ParentDefinitionName: "document", + SubjectTypes: []*v1.ExpTypeReference{ + { + SubjectDefinitionName: "user", + Typeref: &v1.ExpTypeReference_IsTerminalSubject{}, + }, + { + SubjectDefinitionName: "user", + OptionalCaveatName: "somecaveat", + Typeref: &v1.ExpTypeReference_IsTerminalSubject{}, + }, + { + SubjectDefinitionName: "group", + Typeref: &v1.ExpTypeReference_OptionalRelationName{ + OptionalRelationName: "member", + }, + }, + }, + }, + }, + Permissions: []*v1.ExpPermission{ + { + Name: "read", + Comment: "// read all the things", + ParentDefinitionName: "document", + }, + }, + }, + }, + }, + }, + { + name: "full schema with definition, relation and permission filters", + schema: ` + /** user represents a user */ + definition user {} + + /** group represents a group */ + definition group { + relation direct_member: user | group#member + relation admin: user + permission member = direct_member + admin + } + + caveat somecaveat(first int, second string) { + first == 1 && second == "two" + } + + /** document is a protected document */ + definition document { + // editor is a relation + relation editor: user | group#member + relation viewer: user | user with somecaveat | group#member + + // read all the things + permission read = viewer + editor + } + `, + filters: []*v1.ExpSchemaFilter{ + { + OptionalDefinitionNameFilter: "doc", + OptionalRelationNameFilter: "viewer", + }, + { + OptionalDefinitionNameFilter: "doc", + OptionalPermissionNameFilter: "read", + }, + }, + expectedResponse: &v1.ExperimentalReflectSchemaResponse{ + Definitions: []*v1.ExpDefinition{ + { + Name: "document", + Comment: "/** document is a protected document */", + Relations: []*v1.ExpRelation{ + { + Name: "viewer", + Comment: "", + ParentDefinitionName: "document", + SubjectTypes: []*v1.ExpTypeReference{ + { + SubjectDefinitionName: "user", + Typeref: &v1.ExpTypeReference_IsTerminalSubject{}, + }, + { + SubjectDefinitionName: "user", + OptionalCaveatName: "somecaveat", + Typeref: &v1.ExpTypeReference_IsTerminalSubject{}, + }, + { + SubjectDefinitionName: "group", + Typeref: &v1.ExpTypeReference_OptionalRelationName{ + OptionalRelationName: "member", + }, + }, + }, + }, + }, + Permissions: []*v1.ExpPermission{ + { + Name: "read", + Comment: "// read all the things", + ParentDefinitionName: "document", + }, + }, + }, + }, + }, + }, } for _, tt := range testCases { @@ -967,16 +1171,23 @@ definition user {}`, require.NoError(t, err) actual, err := expClient.ExperimentalReflectSchema(context.Background(), &v1.ExperimentalReflectSchemaRequest{ + OptionalFilters: tt.filters, Consistency: &v1.Consistency{ Requirement: &v1.Consistency_FullyConsistent{FullyConsistent: true}, }, }) - require.NoError(t, err) - require.NotNil(t, actual.ReadAt) - actual.ReadAt = nil + if tt.expectedError != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tt.expectedError) + grpcutil.RequireStatus(t, tt.expectedCode, err) + } else { + require.NoError(t, err) + require.NotNil(t, actual.ReadAt) + actual.ReadAt = nil - testutil.RequireProtoEqual(t, tt.expectedResponse, actual, "mismatch in response") + testutil.RequireProtoEqual(t, tt.expectedResponse, actual, "mismatch in response") + } }) } } diff --git a/internal/services/v1/expreflection.go b/internal/services/v1/expreflection.go index 172970d5ac..212cfd7b37 100644 --- a/internal/services/v1/expreflection.go +++ b/internal/services/v1/expreflection.go @@ -1,7 +1,6 @@ package v1 import ( - "fmt" "sort" "strings" @@ -22,13 +21,167 @@ import ( "github.com/authzed/spicedb/pkg/zedtoken" ) +type schemaFilters struct { + filters []*v1.ExpSchemaFilter +} + +func newSchemaFilters(filters []*v1.ExpSchemaFilter) (*schemaFilters, error) { + for _, filter := range filters { + if filter.OptionalDefinitionNameFilter != "" { + if filter.OptionalCaveatNameFilter != "" { + return nil, NewInvalidFilterErr("cannot filter by both definition and caveat name", filter.String()) + } + } + + if filter.OptionalRelationNameFilter != "" { + if filter.OptionalDefinitionNameFilter == "" { + return nil, NewInvalidFilterErr("relation name match requires definition name match", filter.String()) + } + + if filter.OptionalPermissionNameFilter != "" { + return nil, NewInvalidFilterErr("cannot filter by both relation and permission name", filter.String()) + } + } + + if filter.OptionalPermissionNameFilter != "" { + if filter.OptionalDefinitionNameFilter == "" { + return nil, NewInvalidFilterErr("permission name match requires definition name match", filter.String()) + } + } + } + + return &schemaFilters{filters: filters}, nil +} + +func (sf *schemaFilters) HasNamespaces() bool { + if len(sf.filters) == 0 { + return true + } + + for _, filter := range sf.filters { + if filter.OptionalDefinitionNameFilter != "" { + return true + } + } + + return false +} + +func (sf *schemaFilters) HasCaveats() bool { + if len(sf.filters) == 0 { + return true + } + + for _, filter := range sf.filters { + if filter.OptionalCaveatNameFilter != "" { + return true + } + } + + return false +} + +func (sf *schemaFilters) HasNamespace(namespaceName string) bool { + if len(sf.filters) == 0 { + return true + } + + hasDefinitionFilter := false + for _, filter := range sf.filters { + if filter.OptionalDefinitionNameFilter == "" { + continue + } + + hasDefinitionFilter = true + isMatch := strings.HasPrefix(namespaceName, filter.OptionalDefinitionNameFilter) + if isMatch { + return true + } + } + + return !hasDefinitionFilter +} + +func (sf *schemaFilters) HasCaveat(caveatName string) bool { + if len(sf.filters) == 0 { + return true + } + + hasCaveatFilter := false + for _, filter := range sf.filters { + if filter.OptionalCaveatNameFilter == "" { + continue + } + + hasCaveatFilter = true + isMatch := strings.HasPrefix(caveatName, filter.OptionalCaveatNameFilter) + if isMatch { + return true + } + } + + return !hasCaveatFilter +} + +func (sf *schemaFilters) HasRelation(namespaceName, relationName string) bool { + if len(sf.filters) == 0 { + return true + } + + hasRelationFilter := false + for _, filter := range sf.filters { + if filter.OptionalRelationNameFilter == "" { + continue + } + + hasRelationFilter = true + isMatch := strings.HasPrefix(relationName, filter.OptionalRelationNameFilter) + if !isMatch { + continue + } + + isMatch = strings.HasPrefix(namespaceName, filter.OptionalDefinitionNameFilter) + if isMatch { + return true + } + } + + return !hasRelationFilter +} + +func (sf *schemaFilters) HasPermission(namespaceName, permissionName string) bool { + if len(sf.filters) == 0 { + return true + } + + hasPermissionFilter := false + for _, filter := range sf.filters { + if filter.OptionalPermissionNameFilter == "" { + continue + } + + hasPermissionFilter = true + isMatch := strings.HasPrefix(permissionName, filter.OptionalPermissionNameFilter) + if !isMatch { + continue + } + + isMatch = strings.HasPrefix(namespaceName, filter.OptionalDefinitionNameFilter) + if isMatch { + return true + } + } + + return !hasPermissionFilter +} + // convertDiff converts a schema diff into an API response. func convertDiff( diff *diff.SchemaDiff, existingSchema *diff.DiffableSchema, comparisonSchema *diff.DiffableSchema, atRevision datastore.Revision, -) (*v1.ExperimentalSchemaDiffResponse, error) { +) (*v1.ExperimentalDiffSchemaResponse, error) { size := len(diff.AddedNamespaces) + len(diff.RemovedNamespaces) + len(diff.AddedCaveats) + len(diff.RemovedCaveats) + len(diff.ChangedNamespaces) + len(diff.ChangedCaveats) diffs := make([]*v1.ExpSchemaDiff, 0, size) @@ -93,10 +246,10 @@ func convertDiff( case nsdiff.AddedPermission: permission, ok := comparisonSchema.GetRelation(nsName, delta.RelationName) if !ok { - return nil, fmt.Errorf("relation %q not found in namespace %q", delta.RelationName, nsName) + return nil, spiceerrors.MustBugf("permission %q not found in namespace %q", delta.RelationName, nsName) } - perm, err := permissionAPIRepr(permission, nsName) + perm, err := permissionAPIRepr(permission, nsName, nil) if err != nil { return nil, err } @@ -110,10 +263,10 @@ func convertDiff( case nsdiff.AddedRelation: relation, ok := comparisonSchema.GetRelation(nsName, delta.RelationName) if !ok { - return nil, fmt.Errorf("relation %q not found in namespace %q", delta.RelationName, nsName) + return nil, spiceerrors.MustBugf("relation %q not found in namespace %q", delta.RelationName, nsName) } - rel, err := relationAPIRepr(relation, nsName) + rel, err := relationAPIRepr(relation, nsName, nil) if err != nil { return nil, err } @@ -127,10 +280,10 @@ func convertDiff( case nsdiff.ChangedPermissionComment: permission, ok := comparisonSchema.GetRelation(nsName, delta.RelationName) if !ok { - return nil, fmt.Errorf("relation %q not found in namespace %q", delta.RelationName, nsName) + return nil, spiceerrors.MustBugf("permission %q not found in namespace %q", delta.RelationName, nsName) } - perm, err := permissionAPIRepr(permission, nsName) + perm, err := permissionAPIRepr(permission, nsName, nil) if err != nil { return nil, err } @@ -144,10 +297,10 @@ func convertDiff( case nsdiff.ChangedPermissionImpl: permission, ok := comparisonSchema.GetRelation(nsName, delta.RelationName) if !ok { - return nil, fmt.Errorf("relation %q not found in namespace %q", delta.RelationName, nsName) + return nil, spiceerrors.MustBugf("permission %q not found in namespace %q", delta.RelationName, nsName) } - perm, err := permissionAPIRepr(permission, nsName) + perm, err := permissionAPIRepr(permission, nsName, nil) if err != nil { return nil, err } @@ -161,10 +314,10 @@ func convertDiff( case nsdiff.ChangedRelationComment: relation, ok := comparisonSchema.GetRelation(nsName, delta.RelationName) if !ok { - return nil, fmt.Errorf("relation %q not found in namespace %q", delta.RelationName, nsName) + return nil, spiceerrors.MustBugf("relation %q not found in namespace %q", delta.RelationName, nsName) } - rel, err := relationAPIRepr(relation, nsName) + rel, err := relationAPIRepr(relation, nsName, nil) if err != nil { return nil, err } @@ -193,10 +346,10 @@ func convertDiff( case nsdiff.RelationAllowedTypeRemoved: relation, ok := comparisonSchema.GetRelation(nsName, delta.RelationName) if !ok { - return nil, fmt.Errorf("relation %q not found in namespace %q", delta.RelationName, nsName) + return nil, spiceerrors.MustBugf("relation %q not found in namespace %q", delta.RelationName, nsName) } - rel, err := relationAPIRepr(relation, nsName) + rel, err := relationAPIRepr(relation, nsName, nil) if err != nil { return nil, err } @@ -213,10 +366,10 @@ func convertDiff( case nsdiff.RelationAllowedTypeAdded: relation, ok := comparisonSchema.GetRelation(nsName, delta.RelationName) if !ok { - return nil, fmt.Errorf("relation %q not found in namespace %q", delta.RelationName, nsName) + return nil, spiceerrors.MustBugf("relation %q not found in namespace %q", delta.RelationName, nsName) } - rel, err := relationAPIRepr(relation, nsName) + rel, err := relationAPIRepr(relation, nsName, nil) if err != nil { return nil, err } @@ -233,10 +386,10 @@ func convertDiff( case nsdiff.RemovedPermission: permission, ok := existingSchema.GetRelation(nsName, delta.RelationName) if !ok { - return nil, fmt.Errorf("relation %q not found in namespace %q", delta.RelationName, nsName) + return nil, spiceerrors.MustBugf("relation %q not found in namespace %q", delta.RelationName, nsName) } - perm, err := permissionAPIRepr(permission, nsName) + perm, err := permissionAPIRepr(permission, nsName, nil) if err != nil { return nil, err } @@ -250,10 +403,10 @@ func convertDiff( case nsdiff.RemovedRelation: relation, ok := existingSchema.GetRelation(nsName, delta.RelationName) if !ok { - return nil, fmt.Errorf("relation %q not found in namespace %q", delta.RelationName, nsName) + return nil, spiceerrors.MustBugf("relation %q not found in namespace %q", delta.RelationName, nsName) } - rel, err := relationAPIRepr(relation, nsName) + rel, err := relationAPIRepr(relation, nsName, nil) if err != nil { return nil, err } @@ -360,7 +513,7 @@ func convertDiff( } } - return &v1.ExperimentalSchemaDiffResponse{ + return &v1.ExperimentalDiffSchemaResponse{ Diffs: diffs, ReadAt: zedtoken.MustNewFromRevision(atRevision), }, nil @@ -370,33 +523,41 @@ func convertDiff( func namespaceAPIReprForName(namespaceName string, schema *diff.DiffableSchema) (*v1.ExpDefinition, error) { nsDef, ok := schema.GetNamespace(namespaceName) if !ok { - return nil, fmt.Errorf("namespace %q not found in schema", namespaceName) + return nil, spiceerrors.MustBugf("namespace %q not found in schema", namespaceName) } - return namespaceAPIRepr(nsDef) + return namespaceAPIRepr(nsDef, nil) } -func namespaceAPIRepr(nsDef *core.NamespaceDefinition) (*v1.ExpDefinition, error) { +func namespaceAPIRepr(nsDef *core.NamespaceDefinition, schemaFilters *schemaFilters) (*v1.ExpDefinition, error) { + if schemaFilters != nil && !schemaFilters.HasNamespace(nsDef.Name) { + return nil, nil + } + relations := make([]*v1.ExpRelation, 0, len(nsDef.Relation)) permissions := make([]*v1.ExpPermission, 0, len(nsDef.Relation)) for _, rel := range nsDef.Relation { if namespace.GetRelationKind(rel) == iv1.RelationMetadata_PERMISSION { - permission, err := permissionAPIRepr(rel, nsDef.Name) + permission, err := permissionAPIRepr(rel, nsDef.Name, schemaFilters) if err != nil { return nil, err } - permissions = append(permissions, permission) + if permission != nil { + permissions = append(permissions, permission) + } continue } - relation, err := relationAPIRepr(rel, nsDef.Name) + relation, err := relationAPIRepr(rel, nsDef.Name, schemaFilters) if err != nil { return nil, err } - relations = append(relations, relation) + if relation != nil { + relations = append(relations, relation) + } } comments := namespace.GetComments(nsDef.Metadata) @@ -409,7 +570,11 @@ func namespaceAPIRepr(nsDef *core.NamespaceDefinition) (*v1.ExpDefinition, error } // permissionAPIRepr builds an API representation of a permission. -func permissionAPIRepr(relation *core.Relation, parentDefName string) (*v1.ExpPermission, error) { +func permissionAPIRepr(relation *core.Relation, parentDefName string, schemaFilters *schemaFilters) (*v1.ExpPermission, error) { + if schemaFilters != nil && !schemaFilters.HasPermission(parentDefName, relation.Name) { + return nil, nil + } + comments := namespace.GetComments(relation.Metadata) return &v1.ExpPermission{ Name: relation.Name, @@ -419,7 +584,11 @@ func permissionAPIRepr(relation *core.Relation, parentDefName string) (*v1.ExpPe } // relationAPIRepresentation builds an API representation of a relation. -func relationAPIRepr(relation *core.Relation, parentDefName string) (*v1.ExpRelation, error) { +func relationAPIRepr(relation *core.Relation, parentDefName string, schemaFilters *schemaFilters) (*v1.ExpRelation, error) { + if schemaFilters != nil && !schemaFilters.HasRelation(parentDefName, relation.Name) { + return nil, nil + } + comments := namespace.GetComments(relation.Metadata) var subjectTypes []*v1.ExpTypeReference @@ -446,12 +615,14 @@ func typeAPIRepr(subjectType *core.AllowedRelation) *v1.ExpTypeReference { Typeref: &v1.ExpTypeReference_IsTerminalSubject{}, } - if subjectType.GetRelation() != tuple.Ellipsis { + if subjectType.GetRelation() != tuple.Ellipsis && subjectType.GetRelation() != "" { typeref.Typeref = &v1.ExpTypeReference_OptionalRelationName{ OptionalRelationName: subjectType.GetRelation(), } } else if subjectType.GetPublicWildcard() != nil { - typeref.Typeref = &v1.ExpTypeReference_IsPublicWildcard{} + typeref.Typeref = &v1.ExpTypeReference_IsPublicWildcard{ + IsPublicWildcard: true, + } } if subjectType.GetRequiredCaveat() != nil { @@ -465,14 +636,18 @@ func typeAPIRepr(subjectType *core.AllowedRelation) *v1.ExpTypeReference { func caveatAPIReprForName(caveatName string, schema *diff.DiffableSchema) (*v1.ExpCaveat, error) { caveatDef, ok := schema.GetCaveat(caveatName) if !ok { - return nil, fmt.Errorf("caveat %q not found in schema", caveatName) + return nil, spiceerrors.MustBugf("caveat %q not found in schema", caveatName) } - return caveatAPIRepr(caveatDef) + return caveatAPIRepr(caveatDef, nil) } // caveatAPIRepr builds an API representation of a caveat. -func caveatAPIRepr(caveatDef *core.CaveatDefinition) (*v1.ExpCaveat, error) { +func caveatAPIRepr(caveatDef *core.CaveatDefinition, schemaFilters *schemaFilters) (*v1.ExpCaveat, error) { + if schemaFilters != nil && !schemaFilters.HasCaveat(caveatDef.Name) { + return nil, nil + } + parameters := make([]*v1.ExpCaveatParameter, 0, len(caveatDef.ParameterTypes)) paramNames := maps.Keys(caveatDef.ParameterTypes) sort.Strings(paramNames) @@ -480,12 +655,12 @@ func caveatAPIRepr(caveatDef *core.CaveatDefinition) (*v1.ExpCaveat, error) { for _, paramName := range paramNames { paramType, ok := caveatDef.ParameterTypes[paramName] if !ok { - return nil, fmt.Errorf("parameter %q not found in caveat %q", paramName, caveatDef.Name) + return nil, spiceerrors.MustBugf("parameter %q not found in caveat %q", paramName, caveatDef.Name) } decoded, err := caveattypes.DecodeParameterType(paramType) if err != nil { - return nil, fmt.Errorf("invalid parameter type on caveat: %w", err) + return nil, spiceerrors.MustBugf("invalid parameter type on caveat: %v", err) } parameters = append(parameters, &v1.ExpCaveatParameter{ @@ -497,17 +672,17 @@ func caveatAPIRepr(caveatDef *core.CaveatDefinition) (*v1.ExpCaveat, error) { parameterTypes, err := caveattypes.DecodeParameterTypes(caveatDef.ParameterTypes) if err != nil { - return nil, fmt.Errorf("invalid caveat parameters: %w", err) + return nil, spiceerrors.MustBugf("invalid caveat parameters: %v", err) } deserializedExpression, err := caveats.DeserializeCaveat(caveatDef.SerializedExpression, parameterTypes) if err != nil { - return nil, fmt.Errorf("invalid caveat expression bytes: %w", err) + return nil, spiceerrors.MustBugf("invalid caveat expression bytes: %v", err) } exprString, err := deserializedExpression.ExprString() if err != nil { - return nil, fmt.Errorf("invalid caveat expression: %w", err) + return nil, spiceerrors.MustBugf("invalid caveat expression: %v", err) } comments := namespace.GetComments(caveatDef.Metadata) @@ -523,17 +698,17 @@ func caveatAPIRepr(caveatDef *core.CaveatDefinition) (*v1.ExpCaveat, error) { func caveatAPIParamRepr(paramName, parentCaveatName string, schema *diff.DiffableSchema) (*v1.ExpCaveatParameter, error) { caveatDef, ok := schema.GetCaveat(parentCaveatName) if !ok { - return nil, fmt.Errorf("caveat %q not found in schema", parentCaveatName) + return nil, spiceerrors.MustBugf("caveat %q not found in schema", parentCaveatName) } paramType, ok := caveatDef.ParameterTypes[paramName] if !ok { - return nil, fmt.Errorf("parameter %q not found in caveat %q", paramName, parentCaveatName) + return nil, spiceerrors.MustBugf("parameter %q not found in caveat %q", paramName, parentCaveatName) } decoded, err := caveattypes.DecodeParameterType(paramType) if err != nil { - return nil, fmt.Errorf("invalid parameter type on caveat: %w", err) + return nil, spiceerrors.MustBugf("invalid parameter type on caveat: %v", err) } return &v1.ExpCaveatParameter{ diff --git a/internal/services/v1/expreflection_test.go b/internal/services/v1/expreflection_test.go index 0d9741fe15..fdc902dbc1 100644 --- a/internal/services/v1/expreflection_test.go +++ b/internal/services/v1/expreflection_test.go @@ -22,13 +22,13 @@ func TestConvertDiff(t *testing.T) { name string existingSchema string comparisonSchema string - expectedResponse *v1.ExperimentalSchemaDiffResponse + expectedResponse *v1.ExperimentalDiffSchemaResponse }{ { "no diff", `definition user {}`, `definition user {}`, - &v1.ExperimentalSchemaDiffResponse{ + &v1.ExperimentalDiffSchemaResponse{ Diffs: []*v1.ExpSchemaDiff{}, }, }, @@ -36,7 +36,7 @@ func TestConvertDiff(t *testing.T) { "add namespace", ``, `definition user {}`, - &v1.ExperimentalSchemaDiffResponse{ + &v1.ExperimentalDiffSchemaResponse{ Diffs: []*v1.ExpSchemaDiff{ { Diff: &v1.ExpSchemaDiff_DefinitionAdded{ @@ -53,7 +53,7 @@ func TestConvertDiff(t *testing.T) { "remove namespace", `definition user {}`, ``, - &v1.ExperimentalSchemaDiffResponse{ + &v1.ExperimentalDiffSchemaResponse{ Diffs: []*v1.ExpSchemaDiff{ { Diff: &v1.ExpSchemaDiff_DefinitionRemoved{ @@ -71,7 +71,7 @@ func TestConvertDiff(t *testing.T) { `definition user {}`, `// user has a comment definition user {}`, - &v1.ExperimentalSchemaDiffResponse{ + &v1.ExperimentalDiffSchemaResponse{ Diffs: []*v1.ExpSchemaDiff{ { Diff: &v1.ExpSchemaDiff_DefinitionDocCommentChanged{ @@ -88,7 +88,7 @@ func TestConvertDiff(t *testing.T) { "add caveat", ``, `caveat someCaveat(someparam int) { someparam < 42 }`, - &v1.ExperimentalSchemaDiffResponse{ + &v1.ExperimentalDiffSchemaResponse{ Diffs: []*v1.ExpSchemaDiff{ { Diff: &v1.ExpSchemaDiff_CaveatAdded{ @@ -113,7 +113,7 @@ func TestConvertDiff(t *testing.T) { "remove caveat", `caveat someCaveat(someparam int) { someparam < 42 }`, ``, - &v1.ExperimentalSchemaDiffResponse{ + &v1.ExperimentalDiffSchemaResponse{ Diffs: []*v1.ExpSchemaDiff{ { Diff: &v1.ExpSchemaDiff_CaveatRemoved{ @@ -140,7 +140,7 @@ func TestConvertDiff(t *testing.T) { caveat someCaveat(someparam int) { someparam < 42 }`, `// someCaveat has b comment caveat someCaveat(someparam int) { someparam < 42 }`, - &v1.ExperimentalSchemaDiffResponse{ + &v1.ExperimentalDiffSchemaResponse{ Diffs: []*v1.ExpSchemaDiff{ { Diff: &v1.ExpSchemaDiff_CaveatDocCommentChanged{ @@ -165,7 +165,7 @@ func TestConvertDiff(t *testing.T) { "added relation", `definition user {}`, `definition user { relation somerel: user; }`, - &v1.ExperimentalSchemaDiffResponse{ + &v1.ExperimentalDiffSchemaResponse{ Diffs: []*v1.ExpSchemaDiff{ { Diff: &v1.ExpSchemaDiff_RelationAdded{ @@ -189,7 +189,7 @@ func TestConvertDiff(t *testing.T) { "removed relation", `definition user { relation somerel: user; }`, `definition user {}`, - &v1.ExperimentalSchemaDiffResponse{ + &v1.ExperimentalDiffSchemaResponse{ Diffs: []*v1.ExpSchemaDiff{ { Diff: &v1.ExpSchemaDiff_RelationRemoved{ @@ -227,7 +227,7 @@ func TestConvertDiff(t *testing.T) { relation viewer: user | anon } `, - &v1.ExperimentalSchemaDiffResponse{ + &v1.ExperimentalDiffSchemaResponse{ Diffs: []*v1.ExpSchemaDiff{ { Diff: &v1.ExpSchemaDiff_RelationSubjectTypeAdded{ @@ -275,7 +275,7 @@ func TestConvertDiff(t *testing.T) { relation viewer: user } `, - &v1.ExperimentalSchemaDiffResponse{ + &v1.ExperimentalDiffSchemaResponse{ Diffs: []*v1.ExpSchemaDiff{ { Diff: &v1.ExpSchemaDiff_RelationSubjectTypeRemoved{ @@ -314,7 +314,7 @@ func TestConvertDiff(t *testing.T) { // viewer has a comment relation viewer: user }`, - &v1.ExperimentalSchemaDiffResponse{ + &v1.ExperimentalDiffSchemaResponse{ Diffs: []*v1.ExpSchemaDiff{ { Diff: &v1.ExpSchemaDiff_RelationDocCommentChanged{ @@ -346,7 +346,7 @@ func TestConvertDiff(t *testing.T) { definition resource { permission foo = nil }`, - &v1.ExperimentalSchemaDiffResponse{ + &v1.ExperimentalDiffSchemaResponse{ Diffs: []*v1.ExpSchemaDiff{ { Diff: &v1.ExpSchemaDiff_PermissionAdded{ @@ -371,7 +371,7 @@ func TestConvertDiff(t *testing.T) { definition resource { }`, - &v1.ExperimentalSchemaDiffResponse{ + &v1.ExperimentalDiffSchemaResponse{ Diffs: []*v1.ExpSchemaDiff{ { Diff: &v1.ExpSchemaDiff_PermissionRemoved{ @@ -399,7 +399,7 @@ func TestConvertDiff(t *testing.T) { // foo has a new comment permission foo = nil }`, - &v1.ExperimentalSchemaDiffResponse{ + &v1.ExperimentalDiffSchemaResponse{ Diffs: []*v1.ExpSchemaDiff{ { Diff: &v1.ExpSchemaDiff_PermissionDocCommentChanged{ @@ -421,7 +421,7 @@ func TestConvertDiff(t *testing.T) { `definition resource { permission foo = foo }`, - &v1.ExperimentalSchemaDiffResponse{ + &v1.ExperimentalDiffSchemaResponse{ Diffs: []*v1.ExpSchemaDiff{ { Diff: &v1.ExpSchemaDiff_PermissionExprChanged{ @@ -439,7 +439,7 @@ func TestConvertDiff(t *testing.T) { "caveat parameter added", `caveat someCaveat(someparam int) { someparam < 42 }`, `caveat someCaveat(someparam int, someparam2 string) { someparam < 42 }`, - &v1.ExperimentalSchemaDiffResponse{ + &v1.ExperimentalDiffSchemaResponse{ Diffs: []*v1.ExpSchemaDiff{ { Diff: &v1.ExpSchemaDiff_CaveatParameterAdded{ @@ -457,7 +457,7 @@ func TestConvertDiff(t *testing.T) { "caveat parameter removed", `caveat someCaveat(someparam int, someparam2 string) { someparam < 42 }`, `caveat someCaveat(someparam int) { someparam < 42 }`, - &v1.ExperimentalSchemaDiffResponse{ + &v1.ExperimentalDiffSchemaResponse{ Diffs: []*v1.ExpSchemaDiff{ { Diff: &v1.ExpSchemaDiff_CaveatParameterRemoved{ @@ -475,7 +475,7 @@ func TestConvertDiff(t *testing.T) { "caveat parameter type changed", `caveat someCaveat(someparam int) { someparam < 42 }`, `caveat someCaveat(someparam uint) { someparam < 42 }`, - &v1.ExperimentalSchemaDiffResponse{ + &v1.ExperimentalDiffSchemaResponse{ Diffs: []*v1.ExpSchemaDiff{ { Diff: &v1.ExpSchemaDiff_CaveatParameterTypeChanged{ @@ -496,7 +496,7 @@ func TestConvertDiff(t *testing.T) { "caveat expression changes", `caveat someCaveat(someparam int) { someparam < 42 }`, `caveat someCaveat(someparam int) { someparam < 43 }`, - &v1.ExperimentalSchemaDiffResponse{ + &v1.ExperimentalDiffSchemaResponse{ Diffs: []*v1.ExpSchemaDiff{ { Diff: &v1.ExpSchemaDiff_CaveatExprChanged{ @@ -577,3 +577,321 @@ func TestConvertDiff(t *testing.T) { require.Empty(t, allDiffTypes.Subtract(encounteredDiffTypes).AsSlice()) } } + +type filterCheck func(sf *schemaFilters) bool + +func TestSchemaFiltering(t *testing.T) { + tcs := []struct { + name string + filters []*v1.ExpSchemaFilter + checkers []filterCheck + }{ + { + "no filters", + []*v1.ExpSchemaFilter{}, + []filterCheck{ + func(sf *schemaFilters) bool { return sf.HasNamespaces() }, + func(sf *schemaFilters) bool { return sf.HasCaveats() }, + func(sf *schemaFilters) bool { return sf.HasNamespace("foo") }, + func(sf *schemaFilters) bool { return sf.HasCaveat("foo") }, + func(sf *schemaFilters) bool { return sf.HasRelation("document", "viewer") }, + func(sf *schemaFilters) bool { return sf.HasPermission("document", "view") }, + }, + }, + { + "namespace filter", + []*v1.ExpSchemaFilter{ + { + OptionalDefinitionNameFilter: "doc", + }, + }, + []filterCheck{ + func(sf *schemaFilters) bool { return sf.HasNamespaces() }, + func(sf *schemaFilters) bool { return !sf.HasCaveats() }, + func(sf *schemaFilters) bool { return sf.HasNamespace("document") }, + func(sf *schemaFilters) bool { return !sf.HasNamespace("foo") }, + func(sf *schemaFilters) bool { return sf.HasRelation("document", "viewer") }, + func(sf *schemaFilters) bool { return sf.HasPermission("document", "view") }, + }, + }, + { + "caveat filter", + []*v1.ExpSchemaFilter{ + { + OptionalCaveatNameFilter: "somec", + }, + }, + []filterCheck{ + func(sf *schemaFilters) bool { return !sf.HasNamespaces() }, + func(sf *schemaFilters) bool { return sf.HasCaveats() }, + func(sf *schemaFilters) bool { return sf.HasCaveat("somecaveat") }, + func(sf *schemaFilters) bool { return !sf.HasCaveat("foo") }, + }, + }, + { + "multiple namespace filters", + []*v1.ExpSchemaFilter{ + { + OptionalDefinitionNameFilter: "doc", + }, + { + OptionalDefinitionNameFilter: "user", + }, + }, + []filterCheck{ + func(sf *schemaFilters) bool { return sf.HasNamespaces() }, + func(sf *schemaFilters) bool { return !sf.HasCaveats() }, + func(sf *schemaFilters) bool { return sf.HasNamespace("document") }, + func(sf *schemaFilters) bool { return sf.HasNamespace("user") }, + func(sf *schemaFilters) bool { return !sf.HasNamespace("foo") }, + func(sf *schemaFilters) bool { return sf.HasRelation("document", "viewer") }, + func(sf *schemaFilters) bool { return sf.HasPermission("document", "view") }, + func(sf *schemaFilters) bool { return sf.HasRelation("user", "viewer") }, + func(sf *schemaFilters) bool { return sf.HasPermission("user", "view") }, + }, + }, + { + "multiple caveat filters", + []*v1.ExpSchemaFilter{ + { + OptionalCaveatNameFilter: "somec", + }, + { + OptionalCaveatNameFilter: "somec2", + }, + }, + []filterCheck{ + func(sf *schemaFilters) bool { return !sf.HasNamespaces() }, + func(sf *schemaFilters) bool { return sf.HasCaveats() }, + func(sf *schemaFilters) bool { return sf.HasCaveat("somecaveat") }, + func(sf *schemaFilters) bool { return sf.HasCaveat("somecaveat2") }, + func(sf *schemaFilters) bool { return !sf.HasCaveat("foo") }, + }, + }, + { + "namespace and caveat filters", + []*v1.ExpSchemaFilter{ + { + OptionalDefinitionNameFilter: "doc", + }, + { + OptionalCaveatNameFilter: "somec", + }, + }, + []filterCheck{ + func(sf *schemaFilters) bool { return sf.HasNamespaces() }, + func(sf *schemaFilters) bool { return sf.HasCaveats() }, + func(sf *schemaFilters) bool { return sf.HasNamespace("document") }, + func(sf *schemaFilters) bool { return sf.HasCaveat("somecaveat") }, + func(sf *schemaFilters) bool { return !sf.HasNamespace("foo") }, + func(sf *schemaFilters) bool { return !sf.HasCaveat("foo") }, + }, + }, + { + "relation filter", + []*v1.ExpSchemaFilter{ + { + OptionalDefinitionNameFilter: "doc", + OptionalRelationNameFilter: "v", + }, + }, + []filterCheck{ + func(sf *schemaFilters) bool { return sf.HasNamespaces() }, + func(sf *schemaFilters) bool { return !sf.HasCaveats() }, + func(sf *schemaFilters) bool { return sf.HasNamespace("document") }, + func(sf *schemaFilters) bool { return sf.HasRelation("document", "viewer") }, + func(sf *schemaFilters) bool { return !sf.HasRelation("document", "foo") }, + }, + }, + { + "permission filter", + []*v1.ExpSchemaFilter{ + { + OptionalDefinitionNameFilter: "doc", + OptionalPermissionNameFilter: "v", + }, + }, + []filterCheck{ + func(sf *schemaFilters) bool { return sf.HasNamespaces() }, + func(sf *schemaFilters) bool { return !sf.HasCaveats() }, + func(sf *schemaFilters) bool { return sf.HasNamespace("document") }, + func(sf *schemaFilters) bool { return sf.HasPermission("document", "view") }, + func(sf *schemaFilters) bool { return !sf.HasPermission("document", "foo") }, + }, + }, + { + "permission and relation filter", + []*v1.ExpSchemaFilter{ + { + OptionalDefinitionNameFilter: "doc", + OptionalPermissionNameFilter: "r", + }, + { + OptionalDefinitionNameFilter: "doc", + OptionalRelationNameFilter: "v", + }, + }, + []filterCheck{ + func(sf *schemaFilters) bool { return sf.HasNamespaces() }, + func(sf *schemaFilters) bool { return !sf.HasCaveats() }, + func(sf *schemaFilters) bool { return sf.HasNamespace("document") }, + func(sf *schemaFilters) bool { return sf.HasRelation("document", "viewer") }, + func(sf *schemaFilters) bool { return sf.HasPermission("document", "read") }, + func(sf *schemaFilters) bool { return !sf.HasRelation("document", "foo") }, + func(sf *schemaFilters) bool { return !sf.HasPermission("document", "foo") }, + }, + }, + { + "permission and relation filter over different definitions", + []*v1.ExpSchemaFilter{ + { + OptionalDefinitionNameFilter: "doc", + OptionalPermissionNameFilter: "r", + }, + { + OptionalDefinitionNameFilter: "user", + OptionalRelationNameFilter: "v", + }, + }, + []filterCheck{ + func(sf *schemaFilters) bool { return sf.HasNamespaces() }, + func(sf *schemaFilters) bool { return !sf.HasCaveats() }, + func(sf *schemaFilters) bool { return sf.HasNamespace("document") }, + func(sf *schemaFilters) bool { return sf.HasNamespace("user") }, + func(sf *schemaFilters) bool { return sf.HasRelation("user", "viewer") }, + func(sf *schemaFilters) bool { return sf.HasPermission("document", "read") }, + func(sf *schemaFilters) bool { return !sf.HasRelation("document", "viewer") }, + func(sf *schemaFilters) bool { return !sf.HasPermission("user", "read") }, + }, + }, + } + + for _, tc := range tcs { + tc := tc + t.Run(tc.name, func(t *testing.T) { + sf, err := newSchemaFilters(tc.filters) + require.NoError(t, err) + + for index, check := range tc.checkers { + require.True(t, check(sf), "check failed: #%d", index) + } + }) + } +} + +func TestNewSchemaFilters(t *testing.T) { + tcs := []struct { + name string + filters []*v1.ExpSchemaFilter + err string + }{ + { + "no filters", + []*v1.ExpSchemaFilter{}, + "", + }, + { + "namespace filter", + []*v1.ExpSchemaFilter{ + { + OptionalDefinitionNameFilter: "doc", + }, + }, + "", + }, + { + "caveat filter", + []*v1.ExpSchemaFilter{ + { + OptionalCaveatNameFilter: "somec", + }, + }, + "", + }, + { + "relation filter", + []*v1.ExpSchemaFilter{ + { + OptionalDefinitionNameFilter: "doc", + OptionalRelationNameFilter: "v", + }, + }, + "", + }, + { + "permission filter", + []*v1.ExpSchemaFilter{ + { + OptionalDefinitionNameFilter: "doc", + OptionalPermissionNameFilter: "v", + }, + }, + "", + }, + { + "permission and relation filter", + []*v1.ExpSchemaFilter{ + { + OptionalDefinitionNameFilter: "doc", + OptionalPermissionNameFilter: "r", + }, + { + OptionalDefinitionNameFilter: "doc", + OptionalRelationNameFilter: "v", + }, + }, + "", + }, + { + "relation filter without definition", + []*v1.ExpSchemaFilter{ + { + OptionalRelationNameFilter: "v", + }, + }, + "relation name match requires definition name match", + }, + { + "permission filter without definition", + []*v1.ExpSchemaFilter{ + { + OptionalPermissionNameFilter: "v", + }, + }, + "permission name match requires definition name match", + }, + { + "filter with both definition and caveat", + []*v1.ExpSchemaFilter{ + { + OptionalDefinitionNameFilter: "doc", + OptionalCaveatNameFilter: "somec", + }, + }, + "cannot filter by both definition and caveat name", + }, + { + "filter with both relation and permission", + []*v1.ExpSchemaFilter{ + { + OptionalDefinitionNameFilter: "doc", + OptionalRelationNameFilter: "v", + OptionalPermissionNameFilter: "r", + }, + }, + "cannot filter by both relation and permission name", + }, + } + + for _, tc := range tcs { + tc := tc + t.Run(tc.name, func(t *testing.T) { + _, err := newSchemaFilters(tc.filters) + if tc.err == "" { + require.NoError(t, err) + } else { + require.ErrorContains(t, err, tc.err) + } + }) + } +} diff --git a/internal/services/v1/relationships.go b/internal/services/v1/relationships.go index c010d92d25..adc728a216 100644 --- a/internal/services/v1/relationships.go +++ b/internal/services/v1/relationships.go @@ -499,7 +499,7 @@ func validateRelationshipsFilter(ctx context.Context, filter *v1.RelationshipFil // Ensure the resource ID and the resource ID prefix are not set at the same time. if filter.OptionalResourceId != "" && filter.OptionalResourceIdPrefix != "" { - return NewInvalidFilterErr("resource_id and resource_id_prefix cannot be set at the same time") + return NewInvalidFilterErr("resource_id and resource_id_prefix cannot be set at the same time", filter.String()) } // Ensure that at least one field is set. @@ -508,7 +508,7 @@ func validateRelationshipsFilter(ctx context.Context, filter *v1.RelationshipFil filter.OptionalResourceIdPrefix == "" && filter.OptionalRelation == "" && filter.OptionalSubjectFilter == nil { - return NewInvalidFilterErr("at least one field must be set") + return NewInvalidFilterErr("at least one field must be set", filter.String()) } return nil diff --git a/pkg/diff/diff_test.go b/pkg/diff/diff_test.go index 818c4b863e..1a7f17e149 100644 --- a/pkg/diff/diff_test.go +++ b/pkg/diff/diff_test.go @@ -1,6 +1,7 @@ package diff import ( + "fmt" "testing" "github.com/stretchr/testify/require" @@ -174,3 +175,84 @@ func TestDiffSchemasWithChangedCaveatComment(t *testing.T) { require.Len(t, diff.ChangedCaveats["someCaveat"].Deltas(), 1) require.Equal(t, caveats.CaveatCommentsChanged, diff.ChangedCaveats["someCaveat"].Deltas()[0].Type) } + +type checker func(*testing.T, *DiffableSchema) + +func TestDiffableSchema(t *testing.T) { + tcs := []struct { + name string + schema string + checkers []checker + }{ + { + name: "basic schema", + schema: ` + definition user {} + + caveat someCaveat(someparam int) { someparam < 42 } + + definition resource { + relation owner: user + relation viewer: user + permission view = owner + viewer + } + `, + checkers: []checker{ + func(t *testing.T, ds *DiffableSchema) { + ns, ok := ds.GetNamespace("user") + require.True(t, ok) + require.Equal(t, "user", ns.Name) + }, + func(t *testing.T, ds *DiffableSchema) { + ns, ok := ds.GetNamespace("resource") + require.True(t, ok) + require.Equal(t, "resource", ns.Name) + }, + func(t *testing.T, ds *DiffableSchema) { + caveat, ok := ds.GetCaveat("someCaveat") + require.True(t, ok) + require.Equal(t, "someCaveat", caveat.Name) + }, + func(t *testing.T, ds *DiffableSchema) { + _, ok := ds.GetRelation("user", "owner") + require.False(t, ok) + }, + func(t *testing.T, ds *DiffableSchema) { + _, ok := ds.GetRelation("resource", "owner") + require.True(t, ok) + }, + func(t *testing.T, ds *DiffableSchema) { + _, ok := ds.GetRelation("resource", "viewer") + require.True(t, ok) + }, + func(t *testing.T, ds *DiffableSchema) { + _, ok := ds.GetNamespace("nonexistent") + require.False(t, ok) + }, + func(t *testing.T, ds *DiffableSchema) { + _, ok := ds.GetCaveat("nonexistent") + require.False(t, ok) + }, + }, + }, + } + + for _, tc := range tcs { + tc := tc + t.Run(tc.name, func(t *testing.T) { + schema, err := compiler.Compile(compiler.InputSchema{ + Source: input.Source("schema"), + SchemaString: tc.schema, + }, compiler.AllowUnprefixedObjectType()) + require.NoError(t, err) + + diffableSchema := NewDiffableSchemaFromCompiledSchema(schema) + for index, check := range tc.checkers { + check := check + t.Run(fmt.Sprintf("check-%d", index), func(t *testing.T) { + check(t, &diffableSchema) + }) + } + }) + } +}